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