##// END OF EJS Templates
archival: use walk() instead of matches() on manifest...
Augie Fackler -
r44766:e786d69c default
parent child Browse files
Show More
@@ -1,386 +1,386 b''
1 1 # archival.py - revision archival for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import gzip
11 11 import os
12 12 import struct
13 13 import tarfile
14 14 import time
15 15 import zipfile
16 16 import zlib
17 17
18 18 from .i18n import _
19 19 from .node import nullrev
20 20 from .pycompat import open
21 21
22 22 from . import (
23 23 error,
24 24 formatter,
25 25 match as matchmod,
26 26 pycompat,
27 27 scmutil,
28 28 util,
29 29 vfs as vfsmod,
30 30 )
31 31
32 32 stringio = util.stringio
33 33
34 34 # from unzip source code:
35 35 _UNX_IFREG = 0x8000
36 36 _UNX_IFLNK = 0xA000
37 37
38 38
39 39 def tidyprefix(dest, kind, prefix):
40 40 '''choose prefix to use for names in archive. make sure prefix is
41 41 safe for consumers.'''
42 42
43 43 if prefix:
44 44 prefix = util.normpath(prefix)
45 45 else:
46 46 if not isinstance(dest, bytes):
47 47 raise ValueError(b'dest must be string if no prefix')
48 48 prefix = os.path.basename(dest)
49 49 lower = prefix.lower()
50 50 for sfx in exts.get(kind, []):
51 51 if lower.endswith(sfx):
52 52 prefix = prefix[: -len(sfx)]
53 53 break
54 54 lpfx = os.path.normpath(util.localpath(prefix))
55 55 prefix = util.pconvert(lpfx)
56 56 if not prefix.endswith(b'/'):
57 57 prefix += b'/'
58 58 # Drop the leading '.' path component if present, so Windows can read the
59 59 # zip files (issue4634)
60 60 if prefix.startswith(b'./'):
61 61 prefix = prefix[2:]
62 62 if prefix.startswith(b'../') or os.path.isabs(lpfx) or b'/../' in prefix:
63 63 raise error.Abort(_(b'archive prefix contains illegal components'))
64 64 return prefix
65 65
66 66
67 67 exts = {
68 68 b'tar': [b'.tar'],
69 69 b'tbz2': [b'.tbz2', b'.tar.bz2'],
70 70 b'tgz': [b'.tgz', b'.tar.gz'],
71 71 b'zip': [b'.zip'],
72 72 b'txz': [b'.txz', b'.tar.xz'],
73 73 }
74 74
75 75
76 76 def guesskind(dest):
77 77 for kind, extensions in pycompat.iteritems(exts):
78 78 if any(dest.endswith(ext) for ext in extensions):
79 79 return kind
80 80 return None
81 81
82 82
83 83 def _rootctx(repo):
84 84 # repo[0] may be hidden
85 85 for rev in repo:
86 86 return repo[rev]
87 87 return repo[nullrev]
88 88
89 89
90 90 # {tags} on ctx includes local tags and 'tip', with no current way to limit
91 91 # that to global tags. Therefore, use {latesttag} as a substitute when
92 92 # the distance is 0, since that will be the list of global tags on ctx.
93 93 _defaultmetatemplate = br'''
94 94 repo: {root}
95 95 node: {ifcontains(rev, revset("wdir()"), "{p1node}{dirty}", "{node}")}
96 96 branch: {branch|utf8}
97 97 {ifeq(latesttagdistance, 0, join(latesttag % "tag: {tag}", "\n"),
98 98 separate("\n",
99 99 join(latesttag % "latesttag: {tag}", "\n"),
100 100 "latesttagdistance: {latesttagdistance}",
101 101 "changessincelatesttag: {changessincelatesttag}"))}
102 102 '''[
103 103 1:
104 104 ] # drop leading '\n'
105 105
106 106
107 107 def buildmetadata(ctx):
108 108 '''build content of .hg_archival.txt'''
109 109 repo = ctx.repo()
110 110
111 111 opts = {
112 112 b'template': repo.ui.config(
113 113 b'experimental', b'archivemetatemplate', _defaultmetatemplate
114 114 )
115 115 }
116 116
117 117 out = util.stringio()
118 118
119 119 fm = formatter.formatter(repo.ui, out, b'archive', opts)
120 120 fm.startitem()
121 121 fm.context(ctx=ctx)
122 122 fm.data(root=_rootctx(repo).hex())
123 123
124 124 if ctx.rev() is None:
125 125 dirty = b''
126 126 if ctx.dirty(missing=True):
127 127 dirty = b'+'
128 128 fm.data(dirty=dirty)
129 129 fm.end()
130 130
131 131 return out.getvalue()
132 132
133 133
134 134 class tarit(object):
135 135 '''write archive to tar file or stream. can write uncompressed,
136 136 or compress with gzip or bzip2.'''
137 137
138 138 class GzipFileWithTime(gzip.GzipFile):
139 139 def __init__(self, *args, **kw):
140 140 timestamp = None
141 141 if 'timestamp' in kw:
142 142 timestamp = kw.pop('timestamp')
143 143 if timestamp is None:
144 144 self.timestamp = time.time()
145 145 else:
146 146 self.timestamp = timestamp
147 147 gzip.GzipFile.__init__(self, *args, **kw)
148 148
149 149 def _write_gzip_header(self):
150 150 self.fileobj.write(b'\037\213') # magic header
151 151 self.fileobj.write(b'\010') # compression method
152 152 fname = self.name
153 153 if fname and fname.endswith(b'.gz'):
154 154 fname = fname[:-3]
155 155 flags = 0
156 156 if fname:
157 157 flags = gzip.FNAME # pytype: disable=module-attr
158 158 self.fileobj.write(pycompat.bytechr(flags))
159 159 gzip.write32u( # pytype: disable=module-attr
160 160 self.fileobj, int(self.timestamp)
161 161 )
162 162 self.fileobj.write(b'\002')
163 163 self.fileobj.write(b'\377')
164 164 if fname:
165 165 self.fileobj.write(fname + b'\000')
166 166
167 167 def __init__(self, dest, mtime, kind=b''):
168 168 self.mtime = mtime
169 169 self.fileobj = None
170 170
171 171 def taropen(mode, name=b'', fileobj=None):
172 172 if kind == b'gz':
173 173 mode = mode[0:1]
174 174 if not fileobj:
175 175 fileobj = open(name, mode + b'b')
176 176 gzfileobj = self.GzipFileWithTime(
177 177 name,
178 178 pycompat.sysstr(mode + b'b'),
179 179 zlib.Z_BEST_COMPRESSION,
180 180 fileobj,
181 181 timestamp=mtime,
182 182 )
183 183 self.fileobj = gzfileobj
184 184 return tarfile.TarFile.taropen( # pytype: disable=attribute-error
185 185 name, pycompat.sysstr(mode), gzfileobj
186 186 )
187 187 else:
188 188 return tarfile.open(name, pycompat.sysstr(mode + kind), fileobj)
189 189
190 190 if isinstance(dest, bytes):
191 191 self.z = taropen(b'w:', name=dest)
192 192 else:
193 193 self.z = taropen(b'w|', fileobj=dest)
194 194
195 195 def addfile(self, name, mode, islink, data):
196 196 name = pycompat.fsdecode(name)
197 197 i = tarfile.TarInfo(name)
198 198 i.mtime = self.mtime
199 199 i.size = len(data)
200 200 if islink:
201 201 i.type = tarfile.SYMTYPE
202 202 i.mode = 0o777
203 203 i.linkname = pycompat.fsdecode(data)
204 204 data = None
205 205 i.size = 0
206 206 else:
207 207 i.mode = mode
208 208 data = stringio(data)
209 209 self.z.addfile(i, data)
210 210
211 211 def done(self):
212 212 self.z.close()
213 213 if self.fileobj:
214 214 self.fileobj.close()
215 215
216 216
217 217 class zipit(object):
218 218 '''write archive to zip file or stream. can write uncompressed,
219 219 or compressed with deflate.'''
220 220
221 221 def __init__(self, dest, mtime, compress=True):
222 222 if isinstance(dest, bytes):
223 223 dest = pycompat.fsdecode(dest)
224 224 self.z = zipfile.ZipFile(
225 225 dest, 'w', compress and zipfile.ZIP_DEFLATED or zipfile.ZIP_STORED
226 226 )
227 227
228 228 # Python's zipfile module emits deprecation warnings if we try
229 229 # to store files with a date before 1980.
230 230 epoch = 315532800 # calendar.timegm((1980, 1, 1, 0, 0, 0, 1, 1, 0))
231 231 if mtime < epoch:
232 232 mtime = epoch
233 233
234 234 self.mtime = mtime
235 235 self.date_time = time.gmtime(mtime)[:6]
236 236
237 237 def addfile(self, name, mode, islink, data):
238 238 i = zipfile.ZipInfo(pycompat.fsdecode(name), self.date_time)
239 239 i.compress_type = self.z.compression # pytype: disable=attribute-error
240 240 # unzip will not honor unix file modes unless file creator is
241 241 # set to unix (id 3).
242 242 i.create_system = 3
243 243 ftype = _UNX_IFREG
244 244 if islink:
245 245 mode = 0o777
246 246 ftype = _UNX_IFLNK
247 247 i.external_attr = (mode | ftype) << 16
248 248 # add "extended-timestamp" extra block, because zip archives
249 249 # without this will be extracted with unexpected timestamp,
250 250 # if TZ is not configured as GMT
251 251 i.extra += struct.pack(
252 252 b'<hhBl',
253 253 0x5455, # block type: "extended-timestamp"
254 254 1 + 4, # size of this block
255 255 1, # "modification time is present"
256 256 int(self.mtime),
257 257 ) # last modification (UTC)
258 258 self.z.writestr(i, data)
259 259
260 260 def done(self):
261 261 self.z.close()
262 262
263 263
264 264 class fileit(object):
265 265 '''write archive as files in directory.'''
266 266
267 267 def __init__(self, name, mtime):
268 268 self.basedir = name
269 269 self.opener = vfsmod.vfs(self.basedir)
270 270 self.mtime = mtime
271 271
272 272 def addfile(self, name, mode, islink, data):
273 273 if islink:
274 274 self.opener.symlink(data, name)
275 275 return
276 276 f = self.opener(name, b"w", atomictemp=False)
277 277 f.write(data)
278 278 f.close()
279 279 destfile = os.path.join(self.basedir, name)
280 280 os.chmod(destfile, mode)
281 281 if self.mtime is not None:
282 282 os.utime(destfile, (self.mtime, self.mtime))
283 283
284 284 def done(self):
285 285 pass
286 286
287 287
288 288 archivers = {
289 289 b'files': fileit,
290 290 b'tar': tarit,
291 291 b'tbz2': lambda name, mtime: tarit(name, mtime, b'bz2'),
292 292 b'tgz': lambda name, mtime: tarit(name, mtime, b'gz'),
293 293 b'txz': lambda name, mtime: tarit(name, mtime, b'xz'),
294 294 b'uzip': lambda name, mtime: zipit(name, mtime, False),
295 295 b'zip': zipit,
296 296 }
297 297
298 298
299 299 def archive(
300 300 repo,
301 301 dest,
302 302 node,
303 303 kind,
304 304 decode=True,
305 305 match=None,
306 306 prefix=b'',
307 307 mtime=None,
308 308 subrepos=False,
309 309 ):
310 310 '''create archive of repo as it was at node.
311 311
312 312 dest can be name of directory, name of archive file, or file
313 313 object to write archive to.
314 314
315 315 kind is type of archive to create.
316 316
317 317 decode tells whether to put files through decode filters from
318 318 hgrc.
319 319
320 320 match is a matcher to filter names of files to write to archive.
321 321
322 322 prefix is name of path to put before every archive member.
323 323
324 324 mtime is the modified time, in seconds, or None to use the changeset time.
325 325
326 326 subrepos tells whether to include subrepos.
327 327 '''
328 328
329 329 if kind == b'txz' and not pycompat.ispy3:
330 330 raise error.Abort(_(b'xz compression is only available in Python 3'))
331 331
332 332 if kind == b'files':
333 333 if prefix:
334 334 raise error.Abort(_(b'cannot give prefix when archiving to files'))
335 335 else:
336 336 prefix = tidyprefix(dest, kind, prefix)
337 337
338 338 def write(name, mode, islink, getdata):
339 339 data = getdata()
340 340 if decode:
341 341 data = repo.wwritedata(name, data)
342 342 archiver.addfile(prefix + name, mode, islink, data)
343 343
344 344 if kind not in archivers:
345 345 raise error.Abort(_(b"unknown archive type '%s'") % kind)
346 346
347 347 ctx = repo[node]
348 348 archiver = archivers[kind](dest, mtime or ctx.date()[0])
349 349
350 350 if not match:
351 351 match = scmutil.matchall(repo)
352 352
353 353 if repo.ui.configbool(b"ui", b"archivemeta"):
354 354 name = b'.hg_archival.txt'
355 355 if match(name):
356 356 write(name, 0o644, False, lambda: buildmetadata(ctx))
357 357
358 files = [f for f in ctx.manifest().matches(match)]
358 files = list(ctx.manifest().walk(match))
359 359 total = len(files)
360 360 if total:
361 361 files.sort()
362 362 scmutil.prefetchfiles(
363 363 repo, [ctx.rev()], scmutil.matchfiles(repo, files)
364 364 )
365 365 progress = repo.ui.makeprogress(
366 366 _(b'archiving'), unit=_(b'files'), total=total
367 367 )
368 368 progress.update(0)
369 369 for f in files:
370 370 ff = ctx.flags(f)
371 371 write(f, b'x' in ff and 0o755 or 0o644, b'l' in ff, ctx[f].data)
372 372 progress.increment(item=f)
373 373 progress.complete()
374 374
375 375 if subrepos:
376 376 for subpath in sorted(ctx.substate):
377 377 sub = ctx.workingsub(subpath)
378 378 submatch = matchmod.subdirmatcher(subpath, match)
379 379 subprefix = prefix + subpath + b'/'
380 380 total += sub.archive(archiver, subprefix, submatch, decode)
381 381
382 382 if total == 0:
383 383 raise error.Abort(_(b'no files match the archive pattern'))
384 384
385 385 archiver.done()
386 386 return total
General Comments 0
You need to be logged in to leave comments. Login now