|
1 # archival.py - revision archival for mercurial |
|
2 # |
|
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com> |
|
4 # |
|
5 # This software may be used and distributed according to the terms of the |
|
6 # GNU General Public License version 2 or any later version. |
|
7 |
|
8 from i18n import _ |
|
9 from node import hex |
|
10 import cmdutil |
|
11 import util |
|
12 import cStringIO, os, stat, tarfile, time, zipfile |
|
13 import zlib, gzip |
|
14 |
|
15 def tidyprefix(dest, kind, prefix): |
|
16 '''choose prefix to use for names in archive. make sure prefix is |
|
17 safe for consumers.''' |
|
18 |
|
19 if prefix: |
|
20 prefix = util.normpath(prefix) |
|
21 else: |
|
22 if not isinstance(dest, str): |
|
23 raise ValueError('dest must be string if no prefix') |
|
24 prefix = os.path.basename(dest) |
|
25 lower = prefix.lower() |
|
26 for sfx in exts.get(kind, []): |
|
27 if lower.endswith(sfx): |
|
28 prefix = prefix[:-len(sfx)] |
|
29 break |
|
30 lpfx = os.path.normpath(util.localpath(prefix)) |
|
31 prefix = util.pconvert(lpfx) |
|
32 if not prefix.endswith('/'): |
|
33 prefix += '/' |
|
34 if prefix.startswith('../') or os.path.isabs(lpfx) or '/../' in prefix: |
|
35 raise util.Abort(_('archive prefix contains illegal components')) |
|
36 return prefix |
|
37 |
|
38 exts = { |
|
39 'tar': ['.tar'], |
|
40 'tbz2': ['.tbz2', '.tar.bz2'], |
|
41 'tgz': ['.tgz', '.tar.gz'], |
|
42 'zip': ['.zip'], |
|
43 } |
|
44 |
|
45 def guesskind(dest): |
|
46 for kind, extensions in exts.iteritems(): |
|
47 if util.any(dest.endswith(ext) for ext in extensions): |
|
48 return kind |
|
49 return None |
|
50 |
|
51 |
|
52 class tarit(object): |
|
53 '''write archive to tar file or stream. can write uncompressed, |
|
54 or compress with gzip or bzip2.''' |
|
55 |
|
56 class GzipFileWithTime(gzip.GzipFile): |
|
57 |
|
58 def __init__(self, *args, **kw): |
|
59 timestamp = None |
|
60 if 'timestamp' in kw: |
|
61 timestamp = kw.pop('timestamp') |
|
62 if timestamp is None: |
|
63 self.timestamp = time.time() |
|
64 else: |
|
65 self.timestamp = timestamp |
|
66 gzip.GzipFile.__init__(self, *args, **kw) |
|
67 |
|
68 def _write_gzip_header(self): |
|
69 self.fileobj.write('\037\213') # magic header |
|
70 self.fileobj.write('\010') # compression method |
|
71 # Python 2.6 deprecates self.filename |
|
72 fname = getattr(self, 'name', None) or self.filename |
|
73 if fname and fname.endswith('.gz'): |
|
74 fname = fname[:-3] |
|
75 flags = 0 |
|
76 if fname: |
|
77 flags = gzip.FNAME |
|
78 self.fileobj.write(chr(flags)) |
|
79 gzip.write32u(self.fileobj, long(self.timestamp)) |
|
80 self.fileobj.write('\002') |
|
81 self.fileobj.write('\377') |
|
82 if fname: |
|
83 self.fileobj.write(fname + '\000') |
|
84 |
|
85 def __init__(self, dest, mtime, kind=''): |
|
86 self.mtime = mtime |
|
87 |
|
88 def taropen(name, mode, fileobj=None): |
|
89 if kind == 'gz': |
|
90 mode = mode[0] |
|
91 if not fileobj: |
|
92 fileobj = open(name, mode + 'b') |
|
93 gzfileobj = self.GzipFileWithTime(name, mode + 'b', |
|
94 zlib.Z_BEST_COMPRESSION, |
|
95 fileobj, timestamp=mtime) |
|
96 return tarfile.TarFile.taropen(name, mode, gzfileobj) |
|
97 else: |
|
98 return tarfile.open(name, mode + kind, fileobj) |
|
99 |
|
100 if isinstance(dest, str): |
|
101 self.z = taropen(dest, mode='w:') |
|
102 else: |
|
103 # Python 2.5-2.5.1 have a regression that requires a name arg |
|
104 self.z = taropen(name='', mode='w|', fileobj=dest) |
|
105 |
|
106 def addfile(self, name, mode, islink, data): |
|
107 i = tarfile.TarInfo(name) |
|
108 i.mtime = self.mtime |
|
109 i.size = len(data) |
|
110 if islink: |
|
111 i.type = tarfile.SYMTYPE |
|
112 i.mode = 0777 |
|
113 i.linkname = data |
|
114 data = None |
|
115 i.size = 0 |
|
116 else: |
|
117 i.mode = mode |
|
118 data = cStringIO.StringIO(data) |
|
119 self.z.addfile(i, data) |
|
120 |
|
121 def done(self): |
|
122 self.z.close() |
|
123 |
|
124 class tellable(object): |
|
125 '''provide tell method for zipfile.ZipFile when writing to http |
|
126 response file object.''' |
|
127 |
|
128 def __init__(self, fp): |
|
129 self.fp = fp |
|
130 self.offset = 0 |
|
131 |
|
132 def __getattr__(self, key): |
|
133 return getattr(self.fp, key) |
|
134 |
|
135 def write(self, s): |
|
136 self.fp.write(s) |
|
137 self.offset += len(s) |
|
138 |
|
139 def tell(self): |
|
140 return self.offset |
|
141 |
|
142 class zipit(object): |
|
143 '''write archive to zip file or stream. can write uncompressed, |
|
144 or compressed with deflate.''' |
|
145 |
|
146 def __init__(self, dest, mtime, compress=True): |
|
147 if not isinstance(dest, str): |
|
148 try: |
|
149 dest.tell() |
|
150 except (AttributeError, IOError): |
|
151 dest = tellable(dest) |
|
152 self.z = zipfile.ZipFile(dest, 'w', |
|
153 compress and zipfile.ZIP_DEFLATED or |
|
154 zipfile.ZIP_STORED) |
|
155 |
|
156 # Python's zipfile module emits deprecation warnings if we try |
|
157 # to store files with a date before 1980. |
|
158 epoch = 315532800 # calendar.timegm((1980, 1, 1, 0, 0, 0, 1, 1, 0)) |
|
159 if mtime < epoch: |
|
160 mtime = epoch |
|
161 |
|
162 self.date_time = time.gmtime(mtime)[:6] |
|
163 |
|
164 def addfile(self, name, mode, islink, data): |
|
165 i = zipfile.ZipInfo(name, self.date_time) |
|
166 i.compress_type = self.z.compression |
|
167 # unzip will not honor unix file modes unless file creator is |
|
168 # set to unix (id 3). |
|
169 i.create_system = 3 |
|
170 ftype = stat.S_IFREG |
|
171 if islink: |
|
172 mode = 0777 |
|
173 ftype = stat.S_IFLNK |
|
174 i.external_attr = (mode | ftype) << 16L |
|
175 self.z.writestr(i, data) |
|
176 |
|
177 def done(self): |
|
178 self.z.close() |
|
179 |
|
180 class fileit(object): |
|
181 '''write archive as files in directory.''' |
|
182 |
|
183 def __init__(self, name, mtime): |
|
184 self.basedir = name |
|
185 self.opener = util.opener(self.basedir) |
|
186 |
|
187 def addfile(self, name, mode, islink, data): |
|
188 if islink: |
|
189 self.opener.symlink(data, name) |
|
190 return |
|
191 f = self.opener(name, "w", atomictemp=True) |
|
192 f.write(data) |
|
193 f.rename() |
|
194 destfile = os.path.join(self.basedir, name) |
|
195 os.chmod(destfile, mode) |
|
196 |
|
197 def done(self): |
|
198 pass |
|
199 |
|
200 archivers = { |
|
201 'files': fileit, |
|
202 'tar': tarit, |
|
203 'tbz2': lambda name, mtime: tarit(name, mtime, 'bz2'), |
|
204 'tgz': lambda name, mtime: tarit(name, mtime, 'gz'), |
|
205 'uzip': lambda name, mtime: zipit(name, mtime, False), |
|
206 'zip': zipit, |
|
207 } |
|
208 |
|
209 def archive(repo, dest, node, kind, decode=True, matchfn=None, |
|
210 prefix=None, mtime=None, subrepos=False): |
|
211 '''create archive of repo as it was at node. |
|
212 |
|
213 dest can be name of directory, name of archive file, or file |
|
214 object to write archive to. |
|
215 |
|
216 kind is type of archive to create. |
|
217 |
|
218 decode tells whether to put files through decode filters from |
|
219 hgrc. |
|
220 |
|
221 matchfn is function to filter names of files to write to archive. |
|
222 |
|
223 prefix is name of path to put before every archive member.''' |
|
224 |
|
225 if kind == 'files': |
|
226 if prefix: |
|
227 raise util.Abort(_('cannot give prefix when archiving to files')) |
|
228 else: |
|
229 prefix = tidyprefix(dest, kind, prefix) |
|
230 |
|
231 def write(name, mode, islink, getdata): |
|
232 if matchfn and not matchfn(name): |
|
233 return |
|
234 data = getdata() |
|
235 if decode: |
|
236 data = repo.wwritedata(name, data) |
|
237 archiver.addfile(prefix + name, mode, islink, data) |
|
238 |
|
239 if kind not in archivers: |
|
240 raise util.Abort(_("unknown archive type '%s'") % kind) |
|
241 |
|
242 ctx = repo[node] |
|
243 archiver = archivers[kind](dest, mtime or ctx.date()[0]) |
|
244 |
|
245 if repo.ui.configbool("ui", "archivemeta", True): |
|
246 def metadata(): |
|
247 base = 'repo: %s\nnode: %s\nbranch: %s\n' % ( |
|
248 repo[0].hex(), hex(node), ctx.branch()) |
|
249 |
|
250 tags = ''.join('tag: %s\n' % t for t in ctx.tags() |
|
251 if repo.tagtype(t) == 'global') |
|
252 if not tags: |
|
253 repo.ui.pushbuffer() |
|
254 opts = {'template': '{latesttag}\n{latesttagdistance}', |
|
255 'style': '', 'patch': None, 'git': None} |
|
256 cmdutil.show_changeset(repo.ui, repo, opts).show(ctx) |
|
257 ltags, dist = repo.ui.popbuffer().split('\n') |
|
258 tags = ''.join('latesttag: %s\n' % t for t in ltags.split(':')) |
|
259 tags += 'latesttagdistance: %s\n' % dist |
|
260 |
|
261 return base + tags |
|
262 |
|
263 write('.hg_archival.txt', 0644, False, metadata) |
|
264 |
|
265 for f in ctx: |
|
266 ff = ctx.flags(f) |
|
267 write(f, 'x' in ff and 0755 or 0644, 'l' in ff, ctx[f].data) |
|
268 |
|
269 if subrepos: |
|
270 for subpath in ctx.substate: |
|
271 sub = ctx.sub(subpath) |
|
272 sub.archive(archiver, prefix) |
|
273 |
|
274 archiver.done() |