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