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