14 import time |
14 import time |
15 import zipfile |
15 import zipfile |
16 import zlib |
16 import zlib |
17 |
17 |
18 from .i18n import _ |
18 from .i18n import _ |
19 from .node import ( |
19 from .node import nullrev |
20 nullrev, |
|
21 ) |
|
22 |
20 |
23 from . import ( |
21 from . import ( |
24 error, |
22 error, |
25 formatter, |
23 formatter, |
26 match as matchmod, |
24 match as matchmod, |
27 pycompat, |
25 pycompat, |
28 scmutil, |
26 scmutil, |
29 util, |
27 util, |
30 vfs as vfsmod, |
28 vfs as vfsmod, |
31 ) |
29 ) |
|
30 |
32 stringio = util.stringio |
31 stringio = util.stringio |
33 |
32 |
34 # from unzip source code: |
33 # from unzip source code: |
35 _UNX_IFREG = 0x8000 |
34 _UNX_IFREG = 0x8000 |
36 _UNX_IFLNK = 0xa000 |
35 _UNX_IFLNK = 0xA000 |
|
36 |
37 |
37 |
38 def tidyprefix(dest, kind, prefix): |
38 def tidyprefix(dest, kind, prefix): |
39 '''choose prefix to use for names in archive. make sure prefix is |
39 '''choose prefix to use for names in archive. make sure prefix is |
40 safe for consumers.''' |
40 safe for consumers.''' |
41 |
41 |
46 raise ValueError('dest must be string if no prefix') |
46 raise ValueError('dest must be string if no prefix') |
47 prefix = os.path.basename(dest) |
47 prefix = os.path.basename(dest) |
48 lower = prefix.lower() |
48 lower = prefix.lower() |
49 for sfx in exts.get(kind, []): |
49 for sfx in exts.get(kind, []): |
50 if lower.endswith(sfx): |
50 if lower.endswith(sfx): |
51 prefix = prefix[:-len(sfx)] |
51 prefix = prefix[: -len(sfx)] |
52 break |
52 break |
53 lpfx = os.path.normpath(util.localpath(prefix)) |
53 lpfx = os.path.normpath(util.localpath(prefix)) |
54 prefix = util.pconvert(lpfx) |
54 prefix = util.pconvert(lpfx) |
55 if not prefix.endswith('/'): |
55 if not prefix.endswith('/'): |
56 prefix += '/' |
56 prefix += '/' |
60 prefix = prefix[2:] |
60 prefix = prefix[2:] |
61 if prefix.startswith('../') or os.path.isabs(lpfx) or '/../' in prefix: |
61 if prefix.startswith('../') or os.path.isabs(lpfx) or '/../' in prefix: |
62 raise error.Abort(_('archive prefix contains illegal components')) |
62 raise error.Abort(_('archive prefix contains illegal components')) |
63 return prefix |
63 return prefix |
64 |
64 |
|
65 |
65 exts = { |
66 exts = { |
66 'tar': ['.tar'], |
67 'tar': ['.tar'], |
67 'tbz2': ['.tbz2', '.tar.bz2'], |
68 'tbz2': ['.tbz2', '.tar.bz2'], |
68 'tgz': ['.tgz', '.tar.gz'], |
69 'tgz': ['.tgz', '.tar.gz'], |
69 'zip': ['.zip'], |
70 'zip': ['.zip'], |
70 'txz': ['.txz', '.tar.xz'] |
71 'txz': ['.txz', '.tar.xz'], |
71 } |
72 } |
|
73 |
72 |
74 |
73 def guesskind(dest): |
75 def guesskind(dest): |
74 for kind, extensions in exts.iteritems(): |
76 for kind, extensions in exts.iteritems(): |
75 if any(dest.endswith(ext) for ext in extensions): |
77 if any(dest.endswith(ext) for ext in extensions): |
76 return kind |
78 return kind |
77 return None |
79 return None |
78 |
80 |
|
81 |
79 def _rootctx(repo): |
82 def _rootctx(repo): |
80 # repo[0] may be hidden |
83 # repo[0] may be hidden |
81 for rev in repo: |
84 for rev in repo: |
82 return repo[rev] |
85 return repo[rev] |
83 return repo[nullrev] |
86 return repo[nullrev] |
|
87 |
84 |
88 |
85 # {tags} on ctx includes local tags and 'tip', with no current way to limit |
89 # {tags} on ctx includes local tags and 'tip', with no current way to limit |
86 # that to global tags. Therefore, use {latesttag} as a substitute when |
90 # that to global tags. Therefore, use {latesttag} as a substitute when |
87 # the distance is 0, since that will be the list of global tags on ctx. |
91 # the distance is 0, since that will be the list of global tags on ctx. |
88 _defaultmetatemplate = br''' |
92 _defaultmetatemplate = br''' |
92 {ifeq(latesttagdistance, 0, join(latesttag % "tag: {tag}", "\n"), |
96 {ifeq(latesttagdistance, 0, join(latesttag % "tag: {tag}", "\n"), |
93 separate("\n", |
97 separate("\n", |
94 join(latesttag % "latesttag: {tag}", "\n"), |
98 join(latesttag % "latesttag: {tag}", "\n"), |
95 "latesttagdistance: {latesttagdistance}", |
99 "latesttagdistance: {latesttagdistance}", |
96 "changessincelatesttag: {changessincelatesttag}"))} |
100 "changessincelatesttag: {changessincelatesttag}"))} |
97 '''[1:] # drop leading '\n' |
101 '''[ |
|
102 1: |
|
103 ] # drop leading '\n' |
|
104 |
98 |
105 |
99 def buildmetadata(ctx): |
106 def buildmetadata(ctx): |
100 '''build content of .hg_archival.txt''' |
107 '''build content of .hg_archival.txt''' |
101 repo = ctx.repo() |
108 repo = ctx.repo() |
102 |
109 |
103 opts = { |
110 opts = { |
104 'template': repo.ui.config('experimental', 'archivemetatemplate', |
111 'template': repo.ui.config( |
105 _defaultmetatemplate) |
112 'experimental', 'archivemetatemplate', _defaultmetatemplate |
|
113 ) |
106 } |
114 } |
107 |
115 |
108 out = util.stringio() |
116 out = util.stringio() |
109 |
117 |
110 fm = formatter.formatter(repo.ui, out, 'archive', opts) |
118 fm = formatter.formatter(repo.ui, out, 'archive', opts) |
119 fm.data(dirty=dirty) |
127 fm.data(dirty=dirty) |
120 fm.end() |
128 fm.end() |
121 |
129 |
122 return out.getvalue() |
130 return out.getvalue() |
123 |
131 |
|
132 |
124 class tarit(object): |
133 class tarit(object): |
125 '''write archive to tar file or stream. can write uncompressed, |
134 '''write archive to tar file or stream. can write uncompressed, |
126 or compress with gzip or bzip2.''' |
135 or compress with gzip or bzip2.''' |
127 |
136 |
128 class GzipFileWithTime(gzip.GzipFile): |
137 class GzipFileWithTime(gzip.GzipFile): |
129 |
|
130 def __init__(self, *args, **kw): |
138 def __init__(self, *args, **kw): |
131 timestamp = None |
139 timestamp = None |
132 if r'timestamp' in kw: |
140 if r'timestamp' in kw: |
133 timestamp = kw.pop(r'timestamp') |
141 timestamp = kw.pop(r'timestamp') |
134 if timestamp is None: |
142 if timestamp is None: |
136 else: |
144 else: |
137 self.timestamp = timestamp |
145 self.timestamp = timestamp |
138 gzip.GzipFile.__init__(self, *args, **kw) |
146 gzip.GzipFile.__init__(self, *args, **kw) |
139 |
147 |
140 def _write_gzip_header(self): |
148 def _write_gzip_header(self): |
141 self.fileobj.write('\037\213') # magic header |
149 self.fileobj.write('\037\213') # magic header |
142 self.fileobj.write('\010') # compression method |
150 self.fileobj.write('\010') # compression method |
143 fname = self.name |
151 fname = self.name |
144 if fname and fname.endswith('.gz'): |
152 if fname and fname.endswith('.gz'): |
145 fname = fname[:-3] |
153 fname = fname[:-3] |
146 flags = 0 |
154 flags = 0 |
147 if fname: |
155 if fname: |
160 def taropen(mode, name='', fileobj=None): |
168 def taropen(mode, name='', fileobj=None): |
161 if kind == 'gz': |
169 if kind == 'gz': |
162 mode = mode[0:1] |
170 mode = mode[0:1] |
163 if not fileobj: |
171 if not fileobj: |
164 fileobj = open(name, mode + 'b') |
172 fileobj = open(name, mode + 'b') |
165 gzfileobj = self.GzipFileWithTime(name, |
173 gzfileobj = self.GzipFileWithTime( |
166 pycompat.sysstr(mode + 'b'), |
174 name, |
167 zlib.Z_BEST_COMPRESSION, |
175 pycompat.sysstr(mode + 'b'), |
168 fileobj, timestamp=mtime) |
176 zlib.Z_BEST_COMPRESSION, |
|
177 fileobj, |
|
178 timestamp=mtime, |
|
179 ) |
169 self.fileobj = gzfileobj |
180 self.fileobj = gzfileobj |
170 return tarfile.TarFile.taropen( |
181 return tarfile.TarFile.taropen( |
171 name, pycompat.sysstr(mode), gzfileobj) |
182 name, pycompat.sysstr(mode), gzfileobj |
|
183 ) |
172 else: |
184 else: |
173 return tarfile.open( |
185 return tarfile.open(name, pycompat.sysstr(mode + kind), fileobj) |
174 name, pycompat.sysstr(mode + kind), fileobj) |
|
175 |
186 |
176 if isinstance(dest, bytes): |
187 if isinstance(dest, bytes): |
177 self.z = taropen('w:', name=dest) |
188 self.z = taropen('w:', name=dest) |
178 else: |
189 else: |
179 self.z = taropen('w|', fileobj=dest) |
190 self.z = taropen('w|', fileobj=dest) |
197 def done(self): |
208 def done(self): |
198 self.z.close() |
209 self.z.close() |
199 if self.fileobj: |
210 if self.fileobj: |
200 self.fileobj.close() |
211 self.fileobj.close() |
201 |
212 |
|
213 |
202 class zipit(object): |
214 class zipit(object): |
203 '''write archive to zip file or stream. can write uncompressed, |
215 '''write archive to zip file or stream. can write uncompressed, |
204 or compressed with deflate.''' |
216 or compressed with deflate.''' |
205 |
217 |
206 def __init__(self, dest, mtime, compress=True): |
218 def __init__(self, dest, mtime, compress=True): |
207 if isinstance(dest, bytes): |
219 if isinstance(dest, bytes): |
208 dest = pycompat.fsdecode(dest) |
220 dest = pycompat.fsdecode(dest) |
209 self.z = zipfile.ZipFile(dest, r'w', |
221 self.z = zipfile.ZipFile( |
210 compress and zipfile.ZIP_DEFLATED or |
222 dest, r'w', compress and zipfile.ZIP_DEFLATED or zipfile.ZIP_STORED |
211 zipfile.ZIP_STORED) |
223 ) |
212 |
224 |
213 # Python's zipfile module emits deprecation warnings if we try |
225 # Python's zipfile module emits deprecation warnings if we try |
214 # to store files with a date before 1980. |
226 # to store files with a date before 1980. |
215 epoch = 315532800 # calendar.timegm((1980, 1, 1, 0, 0, 0, 1, 1, 0)) |
227 epoch = 315532800 # calendar.timegm((1980, 1, 1, 0, 0, 0, 1, 1, 0)) |
216 if mtime < epoch: |
228 if mtime < epoch: |
217 mtime = epoch |
229 mtime = epoch |
218 |
230 |
219 self.mtime = mtime |
231 self.mtime = mtime |
220 self.date_time = time.gmtime(mtime)[:6] |
232 self.date_time = time.gmtime(mtime)[:6] |
231 ftype = _UNX_IFLNK |
243 ftype = _UNX_IFLNK |
232 i.external_attr = (mode | ftype) << 16 |
244 i.external_attr = (mode | ftype) << 16 |
233 # add "extended-timestamp" extra block, because zip archives |
245 # add "extended-timestamp" extra block, because zip archives |
234 # without this will be extracted with unexpected timestamp, |
246 # without this will be extracted with unexpected timestamp, |
235 # if TZ is not configured as GMT |
247 # if TZ is not configured as GMT |
236 i.extra += struct.pack('<hhBl', |
248 i.extra += struct.pack( |
237 0x5455, # block type: "extended-timestamp" |
249 '<hhBl', |
238 1 + 4, # size of this block |
250 0x5455, # block type: "extended-timestamp" |
239 1, # "modification time is present" |
251 1 + 4, # size of this block |
240 int(self.mtime)) # last modification (UTC) |
252 1, # "modification time is present" |
|
253 int(self.mtime), |
|
254 ) # last modification (UTC) |
241 self.z.writestr(i, data) |
255 self.z.writestr(i, data) |
242 |
256 |
243 def done(self): |
257 def done(self): |
244 self.z.close() |
258 self.z.close() |
|
259 |
245 |
260 |
246 class fileit(object): |
261 class fileit(object): |
247 '''write archive as files in directory.''' |
262 '''write archive as files in directory.''' |
248 |
263 |
249 def __init__(self, name, mtime): |
264 def __init__(self, name, mtime): |
264 os.utime(destfile, (self.mtime, self.mtime)) |
279 os.utime(destfile, (self.mtime, self.mtime)) |
265 |
280 |
266 def done(self): |
281 def done(self): |
267 pass |
282 pass |
268 |
283 |
|
284 |
269 archivers = { |
285 archivers = { |
270 'files': fileit, |
286 'files': fileit, |
271 'tar': tarit, |
287 'tar': tarit, |
272 'tbz2': lambda name, mtime: tarit(name, mtime, 'bz2'), |
288 'tbz2': lambda name, mtime: tarit(name, mtime, 'bz2'), |
273 'tgz': lambda name, mtime: tarit(name, mtime, 'gz'), |
289 'tgz': lambda name, mtime: tarit(name, mtime, 'gz'), |
274 'txz': lambda name, mtime: tarit(name, mtime, 'xz'), |
290 'txz': lambda name, mtime: tarit(name, mtime, 'xz'), |
275 'uzip': lambda name, mtime: zipit(name, mtime, False), |
291 'uzip': lambda name, mtime: zipit(name, mtime, False), |
276 'zip': zipit, |
292 'zip': zipit, |
277 } |
293 } |
278 |
294 |
279 def archive(repo, dest, node, kind, decode=True, match=None, |
295 |
280 prefix='', mtime=None, subrepos=False): |
296 def archive( |
|
297 repo, |
|
298 dest, |
|
299 node, |
|
300 kind, |
|
301 decode=True, |
|
302 match=None, |
|
303 prefix='', |
|
304 mtime=None, |
|
305 subrepos=False, |
|
306 ): |
281 '''create archive of repo as it was at node. |
307 '''create archive of repo as it was at node. |
282 |
308 |
283 dest can be name of directory, name of archive file, or file |
309 dest can be name of directory, name of archive file, or file |
284 object to write archive to. |
310 object to write archive to. |
285 |
311 |
328 |
354 |
329 files = [f for f in ctx.manifest().matches(match)] |
355 files = [f for f in ctx.manifest().matches(match)] |
330 total = len(files) |
356 total = len(files) |
331 if total: |
357 if total: |
332 files.sort() |
358 files.sort() |
333 scmutil.prefetchfiles(repo, [ctx.rev()], |
359 scmutil.prefetchfiles( |
334 scmutil.matchfiles(repo, files)) |
360 repo, [ctx.rev()], scmutil.matchfiles(repo, files) |
335 progress = repo.ui.makeprogress(_('archiving'), unit=_('files'), |
361 ) |
336 total=total) |
362 progress = repo.ui.makeprogress( |
|
363 _('archiving'), unit=_('files'), total=total |
|
364 ) |
337 progress.update(0) |
365 progress.update(0) |
338 for f in files: |
366 for f in files: |
339 ff = ctx.flags(f) |
367 ff = ctx.flags(f) |
340 write(f, 'x' in ff and 0o755 or 0o644, 'l' in ff, ctx[f].data) |
368 write(f, 'x' in ff and 0o755 or 0o644, 'l' in ff, ctx[f].data) |
341 progress.increment(item=f) |
369 progress.increment(item=f) |