##// END OF EJS Templates
largefiles: when setting/clearing x bit on largefiles, don't change other bits...
Mads Kiilerich -
r30141:c01acee3 default
parent child Browse files
Show More
@@ -1,569 +1,573 b''
1 1 # Copyright 2009-2010 Gregory P. Ward
2 2 # Copyright 2009-2010 Intelerad Medical Systems Incorporated
3 3 # Copyright 2010-2011 Fog Creek Software
4 4 # Copyright 2010-2011 Unity Technologies
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 '''High-level command function for lfconvert, plus the cmdtable.'''
10 10 from __future__ import absolute_import
11 11
12 12 import errno
13 13 import hashlib
14 14 import os
15 15 import shutil
16 16
17 17 from mercurial.i18n import _
18 18
19 19 from mercurial import (
20 20 cmdutil,
21 21 commands,
22 22 context,
23 23 error,
24 24 hg,
25 25 lock,
26 26 match as matchmod,
27 27 node,
28 28 scmutil,
29 29 util,
30 30 )
31 31
32 32 from ..convert import (
33 33 convcmd,
34 34 filemap,
35 35 )
36 36
37 37 from . import (
38 38 lfutil,
39 39 storefactory
40 40 )
41 41
42 42 release = lock.release
43 43
44 44 # -- Commands ----------------------------------------------------------
45 45
46 46 cmdtable = {}
47 47 command = cmdutil.command(cmdtable)
48 48
49 49 @command('lfconvert',
50 50 [('s', 'size', '',
51 51 _('minimum size (MB) for files to be converted as largefiles'), 'SIZE'),
52 52 ('', 'to-normal', False,
53 53 _('convert from a largefiles repo to a normal repo')),
54 54 ],
55 55 _('hg lfconvert SOURCE DEST [FILE ...]'),
56 56 norepo=True,
57 57 inferrepo=True)
58 58 def lfconvert(ui, src, dest, *pats, **opts):
59 59 '''convert a normal repository to a largefiles repository
60 60
61 61 Convert repository SOURCE to a new repository DEST, identical to
62 62 SOURCE except that certain files will be converted as largefiles:
63 63 specifically, any file that matches any PATTERN *or* whose size is
64 64 above the minimum size threshold is converted as a largefile. The
65 65 size used to determine whether or not to track a file as a
66 66 largefile is the size of the first version of the file. The
67 67 minimum size can be specified either with --size or in
68 68 configuration as ``largefiles.size``.
69 69
70 70 After running this command you will need to make sure that
71 71 largefiles is enabled anywhere you intend to push the new
72 72 repository.
73 73
74 74 Use --to-normal to convert largefiles back to normal files; after
75 75 this, the DEST repository can be used without largefiles at all.'''
76 76
77 77 if opts['to_normal']:
78 78 tolfile = False
79 79 else:
80 80 tolfile = True
81 81 size = lfutil.getminsize(ui, True, opts.get('size'), default=None)
82 82
83 83 if not hg.islocal(src):
84 84 raise error.Abort(_('%s is not a local Mercurial repo') % src)
85 85 if not hg.islocal(dest):
86 86 raise error.Abort(_('%s is not a local Mercurial repo') % dest)
87 87
88 88 rsrc = hg.repository(ui, src)
89 89 ui.status(_('initializing destination %s\n') % dest)
90 90 rdst = hg.repository(ui, dest, create=True)
91 91
92 92 success = False
93 93 dstwlock = dstlock = None
94 94 try:
95 95 # Get a list of all changesets in the source. The easy way to do this
96 96 # is to simply walk the changelog, using changelog.nodesbetween().
97 97 # Take a look at mercurial/revlog.py:639 for more details.
98 98 # Use a generator instead of a list to decrease memory usage
99 99 ctxs = (rsrc[ctx] for ctx in rsrc.changelog.nodesbetween(None,
100 100 rsrc.heads())[0])
101 101 revmap = {node.nullid: node.nullid}
102 102 if tolfile:
103 103 # Lock destination to prevent modification while it is converted to.
104 104 # Don't need to lock src because we are just reading from its
105 105 # history which can't change.
106 106 dstwlock = rdst.wlock()
107 107 dstlock = rdst.lock()
108 108
109 109 lfiles = set()
110 110 normalfiles = set()
111 111 if not pats:
112 112 pats = ui.configlist(lfutil.longname, 'patterns', default=[])
113 113 if pats:
114 114 matcher = matchmod.match(rsrc.root, '', list(pats))
115 115 else:
116 116 matcher = None
117 117
118 118 lfiletohash = {}
119 119 for ctx in ctxs:
120 120 ui.progress(_('converting revisions'), ctx.rev(),
121 121 unit=_('revisions'), total=rsrc['tip'].rev())
122 122 _lfconvert_addchangeset(rsrc, rdst, ctx, revmap,
123 123 lfiles, normalfiles, matcher, size, lfiletohash)
124 124 ui.progress(_('converting revisions'), None)
125 125
126 126 if rdst.wvfs.exists(lfutil.shortname):
127 127 rdst.wvfs.rmtree(lfutil.shortname)
128 128
129 129 for f in lfiletohash.keys():
130 130 if rdst.wvfs.isfile(f):
131 131 rdst.wvfs.unlink(f)
132 132 try:
133 133 rdst.wvfs.removedirs(rdst.wvfs.dirname(f))
134 134 except OSError:
135 135 pass
136 136
137 137 # If there were any files converted to largefiles, add largefiles
138 138 # to the destination repository's requirements.
139 139 if lfiles:
140 140 rdst.requirements.add('largefiles')
141 141 rdst._writerequirements()
142 142 else:
143 143 class lfsource(filemap.filemap_source):
144 144 def __init__(self, ui, source):
145 145 super(lfsource, self).__init__(ui, source, None)
146 146 self.filemapper.rename[lfutil.shortname] = '.'
147 147
148 148 def getfile(self, name, rev):
149 149 realname, realrev = rev
150 150 f = super(lfsource, self).getfile(name, rev)
151 151
152 152 if (not realname.startswith(lfutil.shortnameslash)
153 153 or f[0] is None):
154 154 return f
155 155
156 156 # Substitute in the largefile data for the hash
157 157 hash = f[0].strip()
158 158 path = lfutil.findfile(rsrc, hash)
159 159
160 160 if path is None:
161 161 raise error.Abort(_("missing largefile for '%s' in %s")
162 162 % (realname, realrev))
163 163 return util.readfile(path), f[1]
164 164
165 165 class converter(convcmd.converter):
166 166 def __init__(self, ui, source, dest, revmapfile, opts):
167 167 src = lfsource(ui, source)
168 168
169 169 super(converter, self).__init__(ui, src, dest, revmapfile,
170 170 opts)
171 171
172 172 found, missing = downloadlfiles(ui, rsrc)
173 173 if missing != 0:
174 174 raise error.Abort(_("all largefiles must be present locally"))
175 175
176 176 orig = convcmd.converter
177 177 convcmd.converter = converter
178 178
179 179 try:
180 180 convcmd.convert(ui, src, dest)
181 181 finally:
182 182 convcmd.converter = orig
183 183 success = True
184 184 finally:
185 185 if tolfile:
186 186 rdst.dirstate.clear()
187 187 release(dstlock, dstwlock)
188 188 if not success:
189 189 # we failed, remove the new directory
190 190 shutil.rmtree(rdst.root)
191 191
192 192 def _lfconvert_addchangeset(rsrc, rdst, ctx, revmap, lfiles, normalfiles,
193 193 matcher, size, lfiletohash):
194 194 # Convert src parents to dst parents
195 195 parents = _convertparents(ctx, revmap)
196 196
197 197 # Generate list of changed files
198 198 files = _getchangedfiles(ctx, parents)
199 199
200 200 dstfiles = []
201 201 for f in files:
202 202 if f not in lfiles and f not in normalfiles:
203 203 islfile = _islfile(f, ctx, matcher, size)
204 204 # If this file was renamed or copied then copy
205 205 # the largefile-ness of its predecessor
206 206 if f in ctx.manifest():
207 207 fctx = ctx.filectx(f)
208 208 renamed = fctx.renamed()
209 209 renamedlfile = renamed and renamed[0] in lfiles
210 210 islfile |= renamedlfile
211 211 if 'l' in fctx.flags():
212 212 if renamedlfile:
213 213 raise error.Abort(
214 214 _('renamed/copied largefile %s becomes symlink')
215 215 % f)
216 216 islfile = False
217 217 if islfile:
218 218 lfiles.add(f)
219 219 else:
220 220 normalfiles.add(f)
221 221
222 222 if f in lfiles:
223 223 dstfiles.append(lfutil.standin(f))
224 224 # largefile in manifest if it has not been removed/renamed
225 225 if f in ctx.manifest():
226 226 fctx = ctx.filectx(f)
227 227 if 'l' in fctx.flags():
228 228 renamed = fctx.renamed()
229 229 if renamed and renamed[0] in lfiles:
230 230 raise error.Abort(_('largefile %s becomes symlink') % f)
231 231
232 232 # largefile was modified, update standins
233 233 m = hashlib.sha1('')
234 234 m.update(ctx[f].data())
235 235 hash = m.hexdigest()
236 236 if f not in lfiletohash or lfiletohash[f] != hash:
237 237 rdst.wwrite(f, ctx[f].data(), ctx[f].flags())
238 238 executable = 'x' in ctx[f].flags()
239 239 lfutil.writestandin(rdst, lfutil.standin(f), hash,
240 240 executable)
241 241 lfiletohash[f] = hash
242 242 else:
243 243 # normal file
244 244 dstfiles.append(f)
245 245
246 246 def getfilectx(repo, memctx, f):
247 247 if lfutil.isstandin(f):
248 248 # if the file isn't in the manifest then it was removed
249 249 # or renamed, raise IOError to indicate this
250 250 srcfname = lfutil.splitstandin(f)
251 251 try:
252 252 fctx = ctx.filectx(srcfname)
253 253 except error.LookupError:
254 254 return None
255 255 renamed = fctx.renamed()
256 256 if renamed:
257 257 # standin is always a largefile because largefile-ness
258 258 # doesn't change after rename or copy
259 259 renamed = lfutil.standin(renamed[0])
260 260
261 261 return context.memfilectx(repo, f, lfiletohash[srcfname] + '\n',
262 262 'l' in fctx.flags(), 'x' in fctx.flags(),
263 263 renamed)
264 264 else:
265 265 return _getnormalcontext(repo, ctx, f, revmap)
266 266
267 267 # Commit
268 268 _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap)
269 269
270 270 def _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap):
271 271 mctx = context.memctx(rdst, parents, ctx.description(), dstfiles,
272 272 getfilectx, ctx.user(), ctx.date(), ctx.extra())
273 273 ret = rdst.commitctx(mctx)
274 274 lfutil.copyalltostore(rdst, ret)
275 275 rdst.setparents(ret)
276 276 revmap[ctx.node()] = rdst.changelog.tip()
277 277
278 278 # Generate list of changed files
279 279 def _getchangedfiles(ctx, parents):
280 280 files = set(ctx.files())
281 281 if node.nullid not in parents:
282 282 mc = ctx.manifest()
283 283 mp1 = ctx.parents()[0].manifest()
284 284 mp2 = ctx.parents()[1].manifest()
285 285 files |= (set(mp1) | set(mp2)) - set(mc)
286 286 for f in mc:
287 287 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
288 288 files.add(f)
289 289 return files
290 290
291 291 # Convert src parents to dst parents
292 292 def _convertparents(ctx, revmap):
293 293 parents = []
294 294 for p in ctx.parents():
295 295 parents.append(revmap[p.node()])
296 296 while len(parents) < 2:
297 297 parents.append(node.nullid)
298 298 return parents
299 299
300 300 # Get memfilectx for a normal file
301 301 def _getnormalcontext(repo, ctx, f, revmap):
302 302 try:
303 303 fctx = ctx.filectx(f)
304 304 except error.LookupError:
305 305 return None
306 306 renamed = fctx.renamed()
307 307 if renamed:
308 308 renamed = renamed[0]
309 309
310 310 data = fctx.data()
311 311 if f == '.hgtags':
312 312 data = _converttags (repo.ui, revmap, data)
313 313 return context.memfilectx(repo, f, data, 'l' in fctx.flags(),
314 314 'x' in fctx.flags(), renamed)
315 315
316 316 # Remap tag data using a revision map
317 317 def _converttags(ui, revmap, data):
318 318 newdata = []
319 319 for line in data.splitlines():
320 320 try:
321 321 id, name = line.split(' ', 1)
322 322 except ValueError:
323 323 ui.warn(_('skipping incorrectly formatted tag %s\n')
324 324 % line)
325 325 continue
326 326 try:
327 327 newid = node.bin(id)
328 328 except TypeError:
329 329 ui.warn(_('skipping incorrectly formatted id %s\n')
330 330 % id)
331 331 continue
332 332 try:
333 333 newdata.append('%s %s\n' % (node.hex(revmap[newid]),
334 334 name))
335 335 except KeyError:
336 336 ui.warn(_('no mapping for id %s\n') % id)
337 337 continue
338 338 return ''.join(newdata)
339 339
340 340 def _islfile(file, ctx, matcher, size):
341 341 '''Return true if file should be considered a largefile, i.e.
342 342 matcher matches it or it is larger than size.'''
343 343 # never store special .hg* files as largefiles
344 344 if file == '.hgtags' or file == '.hgignore' or file == '.hgsigs':
345 345 return False
346 346 if matcher and matcher(file):
347 347 return True
348 348 try:
349 349 return ctx.filectx(file).size() >= size * 1024 * 1024
350 350 except error.LookupError:
351 351 return False
352 352
353 353 def uploadlfiles(ui, rsrc, rdst, files):
354 354 '''upload largefiles to the central store'''
355 355
356 356 if not files:
357 357 return
358 358
359 359 store = storefactory.openstore(rsrc, rdst, put=True)
360 360
361 361 at = 0
362 362 ui.debug("sending statlfile command for %d largefiles\n" % len(files))
363 363 retval = store.exists(files)
364 364 files = filter(lambda h: not retval[h], files)
365 365 ui.debug("%d largefiles need to be uploaded\n" % len(files))
366 366
367 367 for hash in files:
368 368 ui.progress(_('uploading largefiles'), at, unit=_('files'),
369 369 total=len(files))
370 370 source = lfutil.findfile(rsrc, hash)
371 371 if not source:
372 372 raise error.Abort(_('largefile %s missing from store'
373 373 ' (needs to be uploaded)') % hash)
374 374 # XXX check for errors here
375 375 store.put(source, hash)
376 376 at += 1
377 377 ui.progress(_('uploading largefiles'), None)
378 378
379 379 def verifylfiles(ui, repo, all=False, contents=False):
380 380 '''Verify that every largefile revision in the current changeset
381 381 exists in the central store. With --contents, also verify that
382 382 the contents of each local largefile file revision are correct (SHA-1 hash
383 383 matches the revision ID). With --all, check every changeset in
384 384 this repository.'''
385 385 if all:
386 386 revs = repo.revs('all()')
387 387 else:
388 388 revs = ['.']
389 389
390 390 store = storefactory.openstore(repo)
391 391 return store.verify(revs, contents=contents)
392 392
393 393 def cachelfiles(ui, repo, node, filelist=None):
394 394 '''cachelfiles ensures that all largefiles needed by the specified revision
395 395 are present in the repository's largefile cache.
396 396
397 397 returns a tuple (cached, missing). cached is the list of files downloaded
398 398 by this operation; missing is the list of files that were needed but could
399 399 not be found.'''
400 400 lfiles = lfutil.listlfiles(repo, node)
401 401 if filelist:
402 402 lfiles = set(lfiles) & set(filelist)
403 403 toget = []
404 404
405 405 for lfile in lfiles:
406 406 try:
407 407 expectedhash = repo[node][lfutil.standin(lfile)].data().strip()
408 408 except IOError as err:
409 409 if err.errno == errno.ENOENT:
410 410 continue # node must be None and standin wasn't found in wctx
411 411 raise
412 412 if not lfutil.findfile(repo, expectedhash):
413 413 toget.append((lfile, expectedhash))
414 414
415 415 if toget:
416 416 store = storefactory.openstore(repo)
417 417 ret = store.get(toget)
418 418 return ret
419 419
420 420 return ([], [])
421 421
422 422 def downloadlfiles(ui, repo, rev=None):
423 423 matchfn = scmutil.match(repo[None],
424 424 [repo.wjoin(lfutil.shortname)], {})
425 425 def prepare(ctx, fns):
426 426 pass
427 427 totalsuccess = 0
428 428 totalmissing = 0
429 429 if rev != []: # walkchangerevs on empty list would return all revs
430 430 for ctx in cmdutil.walkchangerevs(repo, matchfn, {'rev' : rev},
431 431 prepare):
432 432 success, missing = cachelfiles(ui, repo, ctx.node())
433 433 totalsuccess += len(success)
434 434 totalmissing += len(missing)
435 435 ui.status(_("%d additional largefiles cached\n") % totalsuccess)
436 436 if totalmissing > 0:
437 437 ui.status(_("%d largefiles failed to download\n") % totalmissing)
438 438 return totalsuccess, totalmissing
439 439
440 440 def updatelfiles(ui, repo, filelist=None, printmessage=None,
441 441 normallookup=False):
442 442 '''Update largefiles according to standins in the working directory
443 443
444 444 If ``printmessage`` is other than ``None``, it means "print (or
445 445 ignore, for false) message forcibly".
446 446 '''
447 447 statuswriter = lfutil.getstatuswriter(ui, repo, printmessage)
448 448 with repo.wlock():
449 449 lfdirstate = lfutil.openlfdirstate(ui, repo)
450 450 lfiles = set(lfutil.listlfiles(repo)) | set(lfdirstate)
451 451
452 452 if filelist is not None:
453 453 filelist = set(filelist)
454 454 lfiles = [f for f in lfiles if f in filelist]
455 455
456 456 update = {}
457 457 updated, removed = 0, 0
458 458 wvfs = repo.wvfs
459 459 for lfile in lfiles:
460 460 rellfile = lfile
461 461 rellfileorig = os.path.relpath(
462 462 scmutil.origpath(ui, repo, wvfs.join(rellfile)),
463 463 start=repo.root)
464 464 relstandin = lfutil.standin(lfile)
465 465 relstandinorig = os.path.relpath(
466 466 scmutil.origpath(ui, repo, wvfs.join(relstandin)),
467 467 start=repo.root)
468 468 if wvfs.exists(relstandin):
469 469 if (wvfs.exists(relstandinorig) and
470 470 wvfs.exists(rellfile)):
471 471 shutil.copyfile(wvfs.join(rellfile),
472 472 wvfs.join(rellfileorig))
473 473 wvfs.unlinkpath(relstandinorig)
474 474 expecthash = lfutil.readstandin(repo, lfile)
475 475 if expecthash != '':
476 476 if lfile not in repo[None]: # not switched to normal file
477 477 wvfs.unlinkpath(rellfile, ignoremissing=True)
478 478 # use normallookup() to allocate an entry in largefiles
479 479 # dirstate to prevent lfilesrepo.status() from reporting
480 480 # missing files as removed.
481 481 lfdirstate.normallookup(lfile)
482 482 update[lfile] = expecthash
483 483 else:
484 484 # Remove lfiles for which the standin is deleted, unless the
485 485 # lfile is added to the repository again. This happens when a
486 486 # largefile is converted back to a normal file: the standin
487 487 # disappears, but a new (normal) file appears as the lfile.
488 488 if (wvfs.exists(rellfile) and
489 489 repo.dirstate.normalize(lfile) not in repo[None]):
490 490 wvfs.unlinkpath(rellfile)
491 491 removed += 1
492 492
493 493 # largefile processing might be slow and be interrupted - be prepared
494 494 lfdirstate.write()
495 495
496 496 if lfiles:
497 497 statuswriter(_('getting changed largefiles\n'))
498 498 cachelfiles(ui, repo, None, lfiles)
499 499
500 500 for lfile in lfiles:
501 501 update1 = 0
502 502
503 503 expecthash = update.get(lfile)
504 504 if expecthash:
505 505 if not lfutil.copyfromcache(repo, expecthash, lfile):
506 506 # failed ... but already removed and set to normallookup
507 507 continue
508 508 # Synchronize largefile dirstate to the last modified
509 509 # time of the file
510 510 lfdirstate.normal(lfile)
511 511 update1 = 1
512 512
513 513 # copy the state of largefile standin from the repository's
514 514 # dirstate to its state in the lfdirstate.
515 515 rellfile = lfile
516 516 relstandin = lfutil.standin(lfile)
517 517 if wvfs.exists(relstandin):
518 mode = wvfs.stat(relstandin).st_mode
519 if mode != wvfs.stat(rellfile).st_mode:
520 wvfs.chmod(rellfile, mode)
518 standinexec = wvfs.stat(relstandin).st_mode & 0o100
519 st = wvfs.stat(rellfile).st_mode
520 if standinexec != st & 0o100:
521 st &= ~0o111
522 if standinexec:
523 st |= (st >> 2) & 0o111 & ~util.umask
524 wvfs.chmod(rellfile, st)
521 525 update1 = 1
522 526
523 527 updated += update1
524 528
525 529 lfutil.synclfdirstate(repo, lfdirstate, lfile, normallookup)
526 530
527 531 lfdirstate.write()
528 532 if lfiles:
529 533 statuswriter(_('%d largefiles updated, %d removed\n') % (updated,
530 534 removed))
531 535
532 536 @command('lfpull',
533 537 [('r', 'rev', [], _('pull largefiles for these revisions'))
534 538 ] + commands.remoteopts,
535 539 _('-r REV... [-e CMD] [--remotecmd CMD] [SOURCE]'))
536 540 def lfpull(ui, repo, source="default", **opts):
537 541 """pull largefiles for the specified revisions from the specified source
538 542
539 543 Pull largefiles that are referenced from local changesets but missing
540 544 locally, pulling from a remote repository to the local cache.
541 545
542 546 If SOURCE is omitted, the 'default' path will be used.
543 547 See :hg:`help urls` for more information.
544 548
545 549 .. container:: verbose
546 550
547 551 Some examples:
548 552
549 553 - pull largefiles for all branch heads::
550 554
551 555 hg lfpull -r "head() and not closed()"
552 556
553 557 - pull largefiles on the default branch::
554 558
555 559 hg lfpull -r "branch(default)"
556 560 """
557 561 repo.lfpullsource = source
558 562
559 563 revs = opts.get('rev', [])
560 564 if not revs:
561 565 raise error.Abort(_('no revisions specified'))
562 566 revs = scmutil.revrange(repo, revs)
563 567
564 568 numcached = 0
565 569 for rev in revs:
566 570 ui.note(_('pulling largefiles for revision %s\n') % rev)
567 571 (cached, missing) = cachelfiles(ui, repo, rev)
568 572 numcached += len(cached)
569 573 ui.status(_("%d largefiles cached\n") % numcached)
General Comments 0
You need to be logged in to leave comments. Login now