4 from os
import path
, chdir
5 from subprocess
import Popen
, PIPE
6 from sys
import argv
, stdout
7 from fnmatch
import fnmatch
10 class GitArchiver(object):
14 Scan a git repository and export all tracked files, and submodules.
15 Checks for .gitattributes files in each directory and uses 'export-ignore'
16 pattern entries for ignore files in the archive.
18 Automatically detects output format extension: zip, tar, bz2, or gz
21 def __init__(self
, prefix
='', verbose
=False, exclude
=True, force_sub
=False, extra
=[]):
23 self
.verbose
= verbose
24 self
.exclude
= exclude
26 self
.force_sub
= force_sub
31 def create(self
, output_file
):
33 create(str output_file) -> None
35 Creates the archive, written to the given output_file
36 Filetype may be one of: gz, zip, bz2, tar, tgz
39 # determine the format
41 _
, _
, format
= output_file
.rpartition(".")
42 format
= format
.lower()
45 from zipfile
import ZipFile
, ZIP_DEFLATED
46 output_archive
= ZipFile(path
.abspath(output_file
), 'w')
47 add
= lambda name
, arcname
: output_archive
.write(name
, self
.prefix
+ arcname
, ZIP_DEFLATED
)
49 elif format
in ['tar', 'bz2', 'gz', 'tgz']:
57 t_mode
= ('w:%s' % format
)
59 output_archive
= tarfile
.open(path
.abspath(output_file
), t_mode
)
60 add
= lambda name
, arcname
: output_archive
.add(name
, self
.prefix
+ arcname
)
62 raise RuntimeError("Unknown format: '%s'" % format
)
68 # extra files first (we may change folder later)
69 for name
in self
.extra
:
71 toPath
= '=> %s%s' % (self
.prefix
, name
) if self
.prefix
else ""
72 print 'Compressing %s %s ...' % (name
, toPath
)
77 for name
, arcname
in self
.listFiles(path
.abspath('')):
79 toPath
= '=> %s%s' % (self
.prefix
, arcname
) if self
.prefix
else ""
80 print 'Compressing %s %s ...' % (arcname
, toPath
)
83 output_archive
.close()
86 def listFiles(self
, git_repositary_path
, baselevel
=''):
88 listFiles(str git_repository_path, str baselevel='') -> iterator
90 An iterator method that yields a tuple(filepath, fullpath)
91 for each file that should be included in the archive.
92 Skips those that match the exclusion patterns found in
93 any discovered .gitattributes files along the way.
95 Recurses into submodules as well.
97 for filepath
in self
.runShell('git ls-files --cached --full-name --no-empty-directory'):
98 filepath
= filepath
.decode('string_escape').strip('"')
99 fullpath
= path
.join(baselevel
, filepath
)
100 filename
= path
.basename(filepath
)
102 if self
.exclude
and filename
== '.gitattributes':
104 fh
= open(filepath
, 'r')
107 tokens
= line
.strip().split()
108 if 'export-ignore' in tokens
[1:]:
109 self
._excludes
.append(tokens
[0])
112 # Only list symlinks and files that don't start with git
113 if not filename
.startswith('.git') \
114 and (path
.islink(filepath
) or not path
.isdir(filepath
)):
116 # check the patterns first
118 for pattern
in self
._excludes
:
119 if fnmatch(fullpath
, pattern
) or fnmatch(filename
, pattern
):
120 if self
.verbose
: print 'Exclude pattern matched (%s): %s' % (pattern
, fullpath
)
126 # baselevel is needed to tell the arhiver where it have to extract file
127 yield filepath
, fullpath
130 self
.runShell("git submodule init")
131 self
.runShell("git submodule update")
133 # get paths for every submodule
134 for submodule
in self
.runShell("git submodule --quiet foreach 'pwd'"):
136 # in order to get output path we need to exclude repository path from the submodule path
137 submodule
= submodule
[len(git_repositary_path
)+1:]
138 # recursion allows us to process repositories with more than one level of submodules
139 for git_file
in self
.listFiles(git_repositary_path
, submodule
):
146 return Popen(cmd
, shell
=True, stdout
=PIPE
).stdout
.read().splitlines()
150 if __name__
== "__main__":
151 from optparse
import OptionParser
153 parser
= OptionParser(usage
="usage: %prog [-v] [--prefix PREFIX] [--no-exclude] OUTPUT_FILE", version
="%prog 1.3")
155 parser
.add_option('--prefix', type='string', dest
='prefix',
156 default
='', help="prepend PREFIX to each filename in the archive")
158 parser
.add_option('-v', '--verbose', action
='store_true', dest
='verbose', help='enable verbose mode')
160 parser
.add_option('--no-exclude', action
='store_false', dest
='exclude',
161 default
=True, help="Don't read .gitattributes files for patterns containing export-ignore attrib")
163 parser
.add_option('--force-submodules', action
='store_true', dest
='force_sub',
164 help="Force a git submodule init && git submodule update at each level before iterating submodules.")
166 parser
.add_option('--extra', action
='append', dest
='extra', default
=[],
167 help="Any additional files to include in the archive.")
169 options
, args
= parser
.parse_args()
172 parser
.error('You must specify exactly one output file')
176 if path
.isdir(outFile
):
177 parser
.error('You cannot use directory as output')
179 archiver
= GitArchiver(options
.prefix
,
186 archiver
.create(outFile
)
188 parser
.exit(2, "%s\n" % e
)