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