##// END OF EJS Templates
largefiles: add a 'debuglfput' command to put largefile into the store...
Boris Feld -
r35579:4aa6ed59 default
parent child Browse files
Show More
@@ -1,595 +1,604 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 hg,
24 24 lock,
25 25 match as matchmod,
26 26 node,
27 27 pycompat,
28 28 registrar,
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 cmdtable = {}
48 48 command = registrar.command(cmdtable)
49 49
50 50 @command('lfconvert',
51 51 [('s', 'size', '',
52 52 _('minimum size (MB) for files to be converted as largefiles'), 'SIZE'),
53 53 ('', 'to-normal', False,
54 54 _('convert from a largefiles repo to a normal repo')),
55 55 ],
56 56 _('hg lfconvert SOURCE DEST [FILE ...]'),
57 57 norepo=True,
58 58 inferrepo=True)
59 59 def lfconvert(ui, src, dest, *pats, **opts):
60 60 '''convert a normal repository to a largefiles repository
61 61
62 62 Convert repository SOURCE to a new repository DEST, identical to
63 63 SOURCE except that certain files will be converted as largefiles:
64 64 specifically, any file that matches any PATTERN *or* whose size is
65 65 above the minimum size threshold is converted as a largefile. The
66 66 size used to determine whether or not to track a file as a
67 67 largefile is the size of the first version of the file. The
68 68 minimum size can be specified either with --size or in
69 69 configuration as ``largefiles.size``.
70 70
71 71 After running this command you will need to make sure that
72 72 largefiles is enabled anywhere you intend to push the new
73 73 repository.
74 74
75 75 Use --to-normal to convert largefiles back to normal files; after
76 76 this, the DEST repository can be used without largefiles at all.'''
77 77
78 78 opts = pycompat.byteskwargs(opts)
79 79 if opts['to_normal']:
80 80 tolfile = False
81 81 else:
82 82 tolfile = True
83 83 size = lfutil.getminsize(ui, True, opts.get('size'), default=None)
84 84
85 85 if not hg.islocal(src):
86 86 raise error.Abort(_('%s is not a local Mercurial repo') % src)
87 87 if not hg.islocal(dest):
88 88 raise error.Abort(_('%s is not a local Mercurial repo') % dest)
89 89
90 90 rsrc = hg.repository(ui, src)
91 91 ui.status(_('initializing destination %s\n') % dest)
92 92 rdst = hg.repository(ui, dest, create=True)
93 93
94 94 success = False
95 95 dstwlock = dstlock = None
96 96 try:
97 97 # Get a list of all changesets in the source. The easy way to do this
98 98 # is to simply walk the changelog, using changelog.nodesbetween().
99 99 # Take a look at mercurial/revlog.py:639 for more details.
100 100 # Use a generator instead of a list to decrease memory usage
101 101 ctxs = (rsrc[ctx] for ctx in rsrc.changelog.nodesbetween(None,
102 102 rsrc.heads())[0])
103 103 revmap = {node.nullid: node.nullid}
104 104 if tolfile:
105 105 # Lock destination to prevent modification while it is converted to.
106 106 # Don't need to lock src because we are just reading from its
107 107 # history which can't change.
108 108 dstwlock = rdst.wlock()
109 109 dstlock = rdst.lock()
110 110
111 111 lfiles = set()
112 112 normalfiles = set()
113 113 if not pats:
114 114 pats = ui.configlist(lfutil.longname, 'patterns')
115 115 if pats:
116 116 matcher = matchmod.match(rsrc.root, '', list(pats))
117 117 else:
118 118 matcher = None
119 119
120 120 lfiletohash = {}
121 121 for ctx in ctxs:
122 122 ui.progress(_('converting revisions'), ctx.rev(),
123 123 unit=_('revisions'), total=rsrc['tip'].rev())
124 124 _lfconvert_addchangeset(rsrc, rdst, ctx, revmap,
125 125 lfiles, normalfiles, matcher, size, lfiletohash)
126 126 ui.progress(_('converting revisions'), None)
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 renamedlfile = renamed and renamed[0] in lfiles
212 212 islfile |= renamedlfile
213 213 if 'l' in fctx.flags():
214 214 if renamedlfile:
215 215 raise error.Abort(
216 216 _('renamed/copied largefile %s becomes symlink')
217 217 % f)
218 218 islfile = False
219 219 if islfile:
220 220 lfiles.add(f)
221 221 else:
222 222 normalfiles.add(f)
223 223
224 224 if f in lfiles:
225 225 fstandin = lfutil.standin(f)
226 226 dstfiles.append(fstandin)
227 227 # largefile in manifest if it has not been removed/renamed
228 228 if f in ctx.manifest():
229 229 fctx = ctx.filectx(f)
230 230 if 'l' in fctx.flags():
231 231 renamed = fctx.renamed()
232 232 if renamed and renamed[0] in lfiles:
233 233 raise error.Abort(_('largefile %s becomes symlink') % f)
234 234
235 235 # largefile was modified, update standins
236 236 m = hashlib.sha1('')
237 237 m.update(ctx[f].data())
238 238 hash = m.hexdigest()
239 239 if f not in lfiletohash or lfiletohash[f] != hash:
240 240 rdst.wwrite(f, ctx[f].data(), ctx[f].flags())
241 241 executable = 'x' in ctx[f].flags()
242 242 lfutil.writestandin(rdst, fstandin, hash,
243 243 executable)
244 244 lfiletohash[f] = hash
245 245 else:
246 246 # normal file
247 247 dstfiles.append(f)
248 248
249 249 def getfilectx(repo, memctx, f):
250 250 srcfname = lfutil.splitstandin(f)
251 251 if srcfname is not None:
252 252 # if the file isn't in the manifest then it was removed
253 253 # or renamed, return None to indicate this
254 254 try:
255 255 fctx = ctx.filectx(srcfname)
256 256 except error.LookupError:
257 257 return None
258 258 renamed = fctx.renamed()
259 259 if renamed:
260 260 # standin is always a largefile because largefile-ness
261 261 # doesn't change after rename or copy
262 262 renamed = lfutil.standin(renamed[0])
263 263
264 264 return context.memfilectx(repo, memctx, f,
265 265 lfiletohash[srcfname] + '\n',
266 266 'l' in fctx.flags(), 'x' in fctx.flags(),
267 267 renamed)
268 268 else:
269 269 return _getnormalcontext(repo, ctx, f, revmap)
270 270
271 271 # Commit
272 272 _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap)
273 273
274 274 def _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap):
275 275 mctx = context.memctx(rdst, parents, ctx.description(), dstfiles,
276 276 getfilectx, ctx.user(), ctx.date(), ctx.extra())
277 277 ret = rdst.commitctx(mctx)
278 278 lfutil.copyalltostore(rdst, ret)
279 279 rdst.setparents(ret)
280 280 revmap[ctx.node()] = rdst.changelog.tip()
281 281
282 282 # Generate list of changed files
283 283 def _getchangedfiles(ctx, parents):
284 284 files = set(ctx.files())
285 285 if node.nullid not in parents:
286 286 mc = ctx.manifest()
287 287 mp1 = ctx.parents()[0].manifest()
288 288 mp2 = ctx.parents()[1].manifest()
289 289 files |= (set(mp1) | set(mp2)) - set(mc)
290 290 for f in mc:
291 291 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
292 292 files.add(f)
293 293 return files
294 294
295 295 # Convert src parents to dst parents
296 296 def _convertparents(ctx, revmap):
297 297 parents = []
298 298 for p in ctx.parents():
299 299 parents.append(revmap[p.node()])
300 300 while len(parents) < 2:
301 301 parents.append(node.nullid)
302 302 return parents
303 303
304 304 # Get memfilectx for a normal file
305 305 def _getnormalcontext(repo, ctx, f, revmap):
306 306 try:
307 307 fctx = ctx.filectx(f)
308 308 except error.LookupError:
309 309 return None
310 310 renamed = fctx.renamed()
311 311 if renamed:
312 312 renamed = renamed[0]
313 313
314 314 data = fctx.data()
315 315 if f == '.hgtags':
316 316 data = _converttags (repo.ui, revmap, data)
317 317 return context.memfilectx(repo, ctx, f, data, 'l' in fctx.flags(),
318 318 'x' in fctx.flags(), renamed)
319 319
320 320 # Remap tag data using a revision map
321 321 def _converttags(ui, revmap, data):
322 322 newdata = []
323 323 for line in data.splitlines():
324 324 try:
325 325 id, name = line.split(' ', 1)
326 326 except ValueError:
327 327 ui.warn(_('skipping incorrectly formatted tag %s\n')
328 328 % line)
329 329 continue
330 330 try:
331 331 newid = node.bin(id)
332 332 except TypeError:
333 333 ui.warn(_('skipping incorrectly formatted id %s\n')
334 334 % id)
335 335 continue
336 336 try:
337 337 newdata.append('%s %s\n' % (node.hex(revmap[newid]),
338 338 name))
339 339 except KeyError:
340 340 ui.warn(_('no mapping for id %s\n') % id)
341 341 continue
342 342 return ''.join(newdata)
343 343
344 344 def _islfile(file, ctx, matcher, size):
345 345 '''Return true if file should be considered a largefile, i.e.
346 346 matcher matches it or it is larger than size.'''
347 347 # never store special .hg* files as largefiles
348 348 if file == '.hgtags' or file == '.hgignore' or file == '.hgsigs':
349 349 return False
350 350 if matcher and matcher(file):
351 351 return True
352 352 try:
353 353 return ctx.filectx(file).size() >= size * 1024 * 1024
354 354 except error.LookupError:
355 355 return False
356 356
357 357 def uploadlfiles(ui, rsrc, rdst, files):
358 358 '''upload largefiles to the central store'''
359 359
360 360 if not files:
361 361 return
362 362
363 363 store = storefactory.openstore(rsrc, rdst, put=True)
364 364
365 365 at = 0
366 366 ui.debug("sending statlfile command for %d largefiles\n" % len(files))
367 367 retval = store.exists(files)
368 368 files = filter(lambda h: not retval[h], files)
369 369 ui.debug("%d largefiles need to be uploaded\n" % len(files))
370 370
371 371 for hash in files:
372 372 ui.progress(_('uploading largefiles'), at, unit=_('files'),
373 373 total=len(files))
374 374 source = lfutil.findfile(rsrc, hash)
375 375 if not source:
376 376 raise error.Abort(_('largefile %s missing from store'
377 377 ' (needs to be uploaded)') % hash)
378 378 # XXX check for errors here
379 379 store.put(source, hash)
380 380 at += 1
381 381 ui.progress(_('uploading largefiles'), None)
382 382
383 383 def verifylfiles(ui, repo, all=False, contents=False):
384 384 '''Verify that every largefile revision in the current changeset
385 385 exists in the central store. With --contents, also verify that
386 386 the contents of each local largefile file revision are correct (SHA-1 hash
387 387 matches the revision ID). With --all, check every changeset in
388 388 this repository.'''
389 389 if all:
390 390 revs = repo.revs('all()')
391 391 else:
392 392 revs = ['.']
393 393
394 394 store = storefactory.openstore(repo)
395 395 return store.verify(revs, contents=contents)
396 396
397 397 def cachelfiles(ui, repo, node, filelist=None):
398 398 '''cachelfiles ensures that all largefiles needed by the specified revision
399 399 are present in the repository's largefile cache.
400 400
401 401 returns a tuple (cached, missing). cached is the list of files downloaded
402 402 by this operation; missing is the list of files that were needed but could
403 403 not be found.'''
404 404 lfiles = lfutil.listlfiles(repo, node)
405 405 if filelist:
406 406 lfiles = set(lfiles) & set(filelist)
407 407 toget = []
408 408
409 409 ctx = repo[node]
410 410 for lfile in lfiles:
411 411 try:
412 412 expectedhash = lfutil.readasstandin(ctx[lfutil.standin(lfile)])
413 413 except IOError as err:
414 414 if err.errno == errno.ENOENT:
415 415 continue # node must be None and standin wasn't found in wctx
416 416 raise
417 417 if not lfutil.findfile(repo, expectedhash):
418 418 toget.append((lfile, expectedhash))
419 419
420 420 if toget:
421 421 store = storefactory.openstore(repo)
422 422 ret = store.get(toget)
423 423 return ret
424 424
425 425 return ([], [])
426 426
427 427 def downloadlfiles(ui, repo, rev=None):
428 428 match = scmutil.match(repo[None], [repo.wjoin(lfutil.shortname)], {})
429 429 def prepare(ctx, fns):
430 430 pass
431 431 totalsuccess = 0
432 432 totalmissing = 0
433 433 if rev != []: # walkchangerevs on empty list would return all revs
434 434 for ctx in cmdutil.walkchangerevs(repo, match, {'rev' : rev},
435 435 prepare):
436 436 success, missing = cachelfiles(ui, repo, ctx.node())
437 437 totalsuccess += len(success)
438 438 totalmissing += len(missing)
439 439 ui.status(_("%d additional largefiles cached\n") % totalsuccess)
440 440 if totalmissing > 0:
441 441 ui.status(_("%d largefiles failed to download\n") % totalmissing)
442 442 return totalsuccess, totalmissing
443 443
444 444 def updatelfiles(ui, repo, filelist=None, printmessage=None,
445 445 normallookup=False):
446 446 '''Update largefiles according to standins in the working directory
447 447
448 448 If ``printmessage`` is other than ``None``, it means "print (or
449 449 ignore, for false) message forcibly".
450 450 '''
451 451 statuswriter = lfutil.getstatuswriter(ui, repo, printmessage)
452 452 with repo.wlock():
453 453 lfdirstate = lfutil.openlfdirstate(ui, repo)
454 454 lfiles = set(lfutil.listlfiles(repo)) | set(lfdirstate)
455 455
456 456 if filelist is not None:
457 457 filelist = set(filelist)
458 458 lfiles = [f for f in lfiles if f in filelist]
459 459
460 460 update = {}
461 461 dropped = set()
462 462 updated, removed = 0, 0
463 463 wvfs = repo.wvfs
464 464 wctx = repo[None]
465 465 for lfile in lfiles:
466 466 rellfile = lfile
467 467 rellfileorig = os.path.relpath(
468 468 scmutil.origpath(ui, repo, wvfs.join(rellfile)),
469 469 start=repo.root)
470 470 relstandin = lfutil.standin(lfile)
471 471 relstandinorig = os.path.relpath(
472 472 scmutil.origpath(ui, repo, wvfs.join(relstandin)),
473 473 start=repo.root)
474 474 if wvfs.exists(relstandin):
475 475 if (wvfs.exists(relstandinorig) and
476 476 wvfs.exists(rellfile)):
477 477 shutil.copyfile(wvfs.join(rellfile),
478 478 wvfs.join(rellfileorig))
479 479 wvfs.unlinkpath(relstandinorig)
480 480 expecthash = lfutil.readasstandin(wctx[relstandin])
481 481 if expecthash != '':
482 482 if lfile not in wctx: # not switched to normal file
483 483 if repo.dirstate[relstandin] != '?':
484 484 wvfs.unlinkpath(rellfile, ignoremissing=True)
485 485 else:
486 486 dropped.add(rellfile)
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(rellfile) and
499 499 repo.dirstate.normalize(lfile) not in wctx):
500 500 wvfs.unlinkpath(rellfile)
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 rellfile = lfile
535 535 relstandin = lfutil.standin(lfile)
536 536 if wvfs.exists(relstandin):
537 537 # exec is decided by the users permissions using mask 0o100
538 538 standinexec = wvfs.stat(relstandin).st_mode & 0o100
539 539 st = wvfs.stat(rellfile)
540 540 mode = st.st_mode
541 541 if standinexec != mode & 0o100:
542 542 # first remove all X bits, then shift all R bits to X
543 543 mode &= ~0o111
544 544 if standinexec:
545 545 mode |= (mode >> 2) & 0o111 & ~util.umask
546 546 wvfs.chmod(rellfile, mode)
547 547 update1 = 1
548 548
549 549 updated += update1
550 550
551 551 lfutil.synclfdirstate(repo, lfdirstate, lfile, normallookup)
552 552
553 553 lfdirstate.write()
554 554 if lfiles:
555 555 statuswriter(_('%d largefiles updated, %d removed\n') % (updated,
556 556 removed))
557 557
558 558 @command('lfpull',
559 559 [('r', 'rev', [], _('pull largefiles for these revisions'))
560 560 ] + cmdutil.remoteopts,
561 561 _('-r REV... [-e CMD] [--remotecmd CMD] [SOURCE]'))
562 562 def lfpull(ui, repo, source="default", **opts):
563 563 """pull largefiles for the specified revisions from the specified source
564 564
565 565 Pull largefiles that are referenced from local changesets but missing
566 566 locally, pulling from a remote repository to the local cache.
567 567
568 568 If SOURCE is omitted, the 'default' path will be used.
569 569 See :hg:`help urls` for more information.
570 570
571 571 .. container:: verbose
572 572
573 573 Some examples:
574 574
575 575 - pull largefiles for all branch heads::
576 576
577 577 hg lfpull -r "head() and not closed()"
578 578
579 579 - pull largefiles on the default branch::
580 580
581 581 hg lfpull -r "branch(default)"
582 582 """
583 583 repo.lfpullsource = source
584 584
585 585 revs = opts.get(r'rev', [])
586 586 if not revs:
587 587 raise error.Abort(_('no revisions specified'))
588 588 revs = scmutil.revrange(repo, revs)
589 589
590 590 numcached = 0
591 591 for rev in revs:
592 592 ui.note(_('pulling largefiles for revision %s\n') % rev)
593 593 (cached, missing) = cachelfiles(ui, repo, rev)
594 594 numcached += len(cached)
595 595 ui.status(_("%d largefiles cached\n") % numcached)
596
597 @command('debuglfput',
598 [] + cmdutil.remoteopts,
599 _('FILE'))
600 def debuglfput(ui, repo, filepath, **kwargs):
601 hash = lfutil.hashfile(filepath)
602 storefactory.openstore(repo).put(filepath, hash)
603 ui.write('%s\n' % hash)
604 return 0
General Comments 0
You need to be logged in to leave comments. Login now