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