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