##// END OF EJS Templates
largefiles: remove pasted code...
Levi Bard -
r15811:b9886dde default
parent child Browse files
Show More
@@ -1,502 +1,491 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
11 11 import os
12 12 import shutil
13 13
14 14 from mercurial import util, match as match_, hg, node, context, error
15 15 from mercurial.i18n import _
16 16
17 17 import lfutil
18 18 import basestore
19 19
20 20 # -- Commands ----------------------------------------------------------
21 21
22 22 def lfconvert(ui, src, dest, *pats, **opts):
23 23 '''convert a normal repository to a largefiles repository
24 24
25 25 Convert repository SOURCE to a new repository DEST, identical to
26 26 SOURCE except that certain files will be converted as largefiles:
27 27 specifically, any file that matches any PATTERN *or* whose size is
28 28 above the minimum size threshold is converted as a largefile. The
29 29 size used to determine whether or not to track a file as a
30 30 largefile is the size of the first version of the file. The
31 31 minimum size can be specified either with --size or in
32 32 configuration as ``largefiles.size``.
33 33
34 34 After running this command you will need to make sure that
35 35 largefiles is enabled anywhere you intend to push the new
36 36 repository.
37 37
38 38 Use --to-normal to convert largefiles back to normal files; after
39 39 this, the DEST repository can be used without largefiles at all.'''
40 40
41 41 if opts['to_normal']:
42 42 tolfile = False
43 43 else:
44 44 tolfile = True
45 45 size = lfutil.getminsize(ui, True, opts.get('size'), default=None)
46 46
47 47 if not hg.islocal(src):
48 48 raise util.Abort(_('%s is not a local Mercurial repo') % src)
49 49 if not hg.islocal(dest):
50 50 raise util.Abort(_('%s is not a local Mercurial repo') % dest)
51 51
52 52 rsrc = hg.repository(ui, src)
53 53 ui.status(_('initializing destination %s\n') % dest)
54 54 rdst = hg.repository(ui, dest, create=True)
55 55
56 56 success = False
57 57 try:
58 58 # Lock destination to prevent modification while it is converted to.
59 59 # Don't need to lock src because we are just reading from its history
60 60 # which can't change.
61 61 dst_lock = rdst.lock()
62 62
63 63 # Get a list of all changesets in the source. The easy way to do this
64 64 # is to simply walk the changelog, using changelog.nodesbewteen().
65 65 # Take a look at mercurial/revlog.py:639 for more details.
66 66 # Use a generator instead of a list to decrease memory usage
67 67 ctxs = (rsrc[ctx] for ctx in rsrc.changelog.nodesbetween(None,
68 68 rsrc.heads())[0])
69 69 revmap = {node.nullid: node.nullid}
70 70 if tolfile:
71 71 lfiles = set()
72 72 normalfiles = set()
73 73 if not pats:
74 74 pats = ui.configlist(lfutil.longname, 'patterns', default=[])
75 75 if pats:
76 76 matcher = match_.match(rsrc.root, '', list(pats))
77 77 else:
78 78 matcher = None
79 79
80 80 lfiletohash = {}
81 81 for ctx in ctxs:
82 82 ui.progress(_('converting revisions'), ctx.rev(),
83 83 unit=_('revision'), total=rsrc['tip'].rev())
84 84 _lfconvert_addchangeset(rsrc, rdst, ctx, revmap,
85 85 lfiles, normalfiles, matcher, size, lfiletohash)
86 86 ui.progress(_('converting revisions'), None)
87 87
88 88 if os.path.exists(rdst.wjoin(lfutil.shortname)):
89 89 shutil.rmtree(rdst.wjoin(lfutil.shortname))
90 90
91 91 for f in lfiletohash.keys():
92 92 if os.path.isfile(rdst.wjoin(f)):
93 93 os.unlink(rdst.wjoin(f))
94 94 try:
95 95 os.removedirs(os.path.dirname(rdst.wjoin(f)))
96 96 except OSError:
97 97 pass
98 98
99 99 # If there were any files converted to largefiles, add largefiles
100 100 # to the destination repository's requirements.
101 101 if lfiles:
102 102 rdst.requirements.add('largefiles')
103 103 rdst._writerequirements()
104 104 else:
105 105 for ctx in ctxs:
106 106 ui.progress(_('converting revisions'), ctx.rev(),
107 107 unit=_('revision'), total=rsrc['tip'].rev())
108 108 _addchangeset(ui, rsrc, rdst, ctx, revmap)
109 109
110 110 ui.progress(_('converting revisions'), None)
111 111 success = True
112 112 finally:
113 113 if not success:
114 114 # we failed, remove the new directory
115 115 shutil.rmtree(rdst.root)
116 116 dst_lock.release()
117 117
118 118 def _addchangeset(ui, rsrc, rdst, ctx, revmap):
119 119 # Convert src parents to dst parents
120 parents = []
121 for p in ctx.parents():
122 parents.append(revmap[p.node()])
123 while len(parents) < 2:
124 parents.append(node.nullid)
120 parents = _convertparents(ctx, revmap)
125 121
126 122 # Generate list of changed files
127 files = set(ctx.files())
128 if node.nullid not in parents:
129 mc = ctx.manifest()
130 mp1 = ctx.parents()[0].manifest()
131 mp2 = ctx.parents()[1].manifest()
132 files |= (set(mp1) | set(mp2)) - set(mc)
133 for f in mc:
134 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
135 files.add(f)
123 files = _getchangedfiles(ctx, parents)
136 124
137 125 def getfilectx(repo, memctx, f):
138 126 if lfutil.standin(f) in files:
139 127 # if the file isn't in the manifest then it was removed
140 128 # or renamed, raise IOError to indicate this
141 129 try:
142 130 fctx = ctx.filectx(lfutil.standin(f))
143 131 except error.LookupError:
144 132 raise IOError()
145 133 renamed = fctx.renamed()
146 134 if renamed:
147 135 renamed = lfutil.splitstandin(renamed[0])
148 136
149 137 hash = fctx.data().strip()
150 138 path = lfutil.findfile(rsrc, hash)
151 139 ### TODO: What if the file is not cached?
152 140 data = ''
153 141 fd = None
154 142 try:
155 143 fd = open(path, 'rb')
156 144 data = fd.read()
157 145 finally:
158 146 if fd:
159 147 fd.close()
160 148 return context.memfilectx(f, data, 'l' in fctx.flags(),
161 149 'x' in fctx.flags(), renamed)
162 150 else:
163 try:
164 fctx = ctx.filectx(f)
165 except error.LookupError:
166 raise IOError()
167 renamed = fctx.renamed()
168 if renamed:
169 renamed = renamed[0]
170 data = fctx.data()
171 if f == '.hgtags':
172 newdata = []
173 for line in data.splitlines():
174 id, name = line.split(' ', 1)
175 newdata.append('%s %s\n' % (node.hex(revmap[node.bin(id)]),
176 name))
177 data = ''.join(newdata)
178 return context.memfilectx(f, data, 'l' in fctx.flags(),
179 'x' in fctx.flags(), renamed)
151 return _getnormalcontext(repo.ui, ctx, f, revmap)
180 152
181 153 dstfiles = []
182 154 for file in files:
183 155 if lfutil.isstandin(file):
184 156 dstfiles.append(lfutil.splitstandin(file))
185 157 else:
186 158 dstfiles.append(file)
187 159 # Commit
188 mctx = context.memctx(rdst, parents, ctx.description(), dstfiles,
189 getfilectx, ctx.user(), ctx.date(), ctx.extra())
190 ret = rdst.commitctx(mctx)
191 rdst.dirstate.setparents(ret)
192 revmap[ctx.node()] = rdst.changelog.tip()
160 _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap)
193 161
194 162 def _lfconvert_addchangeset(rsrc, rdst, ctx, revmap, lfiles, normalfiles,
195 163 matcher, size, lfiletohash):
196 164 # Convert src parents to dst parents
197 parents = []
198 for p in ctx.parents():
199 parents.append(revmap[p.node()])
200 while len(parents) < 2:
201 parents.append(node.nullid)
165 parents = _convertparents(ctx, revmap)
202 166
203 167 # Generate list of changed files
204 files = set(ctx.files())
205 if node.nullid not in parents:
206 mc = ctx.manifest()
207 mp1 = ctx.parents()[0].manifest()
208 mp2 = ctx.parents()[1].manifest()
209 files |= (set(mp1) | set(mp2)) - set(mc)
210 for f in mc:
211 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
212 files.add(f)
168 files = _getchangedfiles(ctx, parents)
213 169
214 170 dstfiles = []
215 171 for f in files:
216 172 if f not in lfiles and f not in normalfiles:
217 173 islfile = _islfile(f, ctx, matcher, size)
218 174 # If this file was renamed or copied then copy
219 175 # the lfileness of its predecessor
220 176 if f in ctx.manifest():
221 177 fctx = ctx.filectx(f)
222 178 renamed = fctx.renamed()
223 179 renamedlfile = renamed and renamed[0] in lfiles
224 180 islfile |= renamedlfile
225 181 if 'l' in fctx.flags():
226 182 if renamedlfile:
227 183 raise util.Abort(
228 184 _('renamed/copied largefile %s becomes symlink')
229 185 % f)
230 186 islfile = False
231 187 if islfile:
232 188 lfiles.add(f)
233 189 else:
234 190 normalfiles.add(f)
235 191
236 192 if f in lfiles:
237 193 dstfiles.append(lfutil.standin(f))
238 194 # largefile in manifest if it has not been removed/renamed
239 195 if f in ctx.manifest():
240 196 fctx = ctx.filectx(f)
241 197 if 'l' in fctx.flags():
242 198 renamed = fctx.renamed()
243 199 if renamed and renamed[0] in lfiles:
244 200 raise util.Abort(_('largefile %s becomes symlink') % f)
245 201
246 202 # largefile was modified, update standins
247 203 fullpath = rdst.wjoin(f)
248 204 util.makedirs(os.path.dirname(fullpath))
249 205 m = util.sha1('')
250 206 m.update(ctx[f].data())
251 207 hash = m.hexdigest()
252 208 if f not in lfiletohash or lfiletohash[f] != hash:
253 209 try:
254 210 fd = open(fullpath, 'wb')
255 211 fd.write(ctx[f].data())
256 212 finally:
257 213 if fd:
258 214 fd.close()
259 215 executable = 'x' in ctx[f].flags()
260 216 os.chmod(fullpath, lfutil.getmode(executable))
261 217 lfutil.writestandin(rdst, lfutil.standin(f), hash,
262 218 executable)
263 219 lfiletohash[f] = hash
264 220 else:
265 221 # normal file
266 222 dstfiles.append(f)
267 223
268 224 def getfilectx(repo, memctx, f):
269 225 if lfutil.isstandin(f):
270 226 # if the file isn't in the manifest then it was removed
271 227 # or renamed, raise IOError to indicate this
272 228 srcfname = lfutil.splitstandin(f)
273 229 try:
274 230 fctx = ctx.filectx(srcfname)
275 231 except error.LookupError:
276 232 raise IOError()
277 233 renamed = fctx.renamed()
278 234 if renamed:
279 235 # standin is always a largefile because largefile-ness
280 236 # doesn't change after rename or copy
281 237 renamed = lfutil.standin(renamed[0])
282 238
283 239 return context.memfilectx(f, lfiletohash[srcfname] + '\n', 'l' in
284 240 fctx.flags(), 'x' in fctx.flags(), renamed)
285 241 else:
286 try:
287 fctx = ctx.filectx(f)
288 except error.LookupError:
289 raise IOError()
290 renamed = fctx.renamed()
291 if renamed:
292 renamed = renamed[0]
293
294 data = fctx.data()
295 if f == '.hgtags':
296 newdata = []
297 for line in data.splitlines():
298 try:
299 id, name = line.split(' ', 1)
300 except ValueError:
301 repo.ui.warn(_('skipping incorrectly formatted tag %s\n'
302 % line))
303 continue
304 try:
305 newid = node.bin(id)
306 except TypeError:
307 repo.ui.warn(_('skipping incorrectly formatted id %s\n'
308 % id))
309 continue
310 try:
311 newdata.append('%s %s\n' % (node.hex(revmap[newid]),
312 name))
313 except KeyError:
314 repo.ui.warn(_('no mapping for id %s\n' % id))
315 continue
316 data = ''.join(newdata)
317 return context.memfilectx(f, data, 'l' in fctx.flags(),
318 'x' in fctx.flags(), renamed)
242 return _getnormalcontext(repo.ui, ctx, f, revmap)
319 243
320 244 # Commit
245 _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap)
246
247 def _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap):
321 248 mctx = context.memctx(rdst, parents, ctx.description(), dstfiles,
322 249 getfilectx, ctx.user(), ctx.date(), ctx.extra())
323 250 ret = rdst.commitctx(mctx)
324 251 rdst.dirstate.setparents(ret)
325 252 revmap[ctx.node()] = rdst.changelog.tip()
326 253
254 # Generate list of changed files
255 def _getchangedfiles(ctx, parents):
256 files = set(ctx.files())
257 if node.nullid not in parents:
258 mc = ctx.manifest()
259 mp1 = ctx.parents()[0].manifest()
260 mp2 = ctx.parents()[1].manifest()
261 files |= (set(mp1) | set(mp2)) - set(mc)
262 for f in mc:
263 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
264 files.add(f)
265 return files
266
267 # Convert src parents to dst parents
268 def _convertparents(ctx, revmap):
269 parents = []
270 for p in ctx.parents():
271 parents.append(revmap[p.node()])
272 while len(parents) < 2:
273 parents.append(node.nullid)
274 return parents
275
276 # Get memfilectx for a normal file
277 def _getnormalcontext(ui, ctx, f, revmap):
278 try:
279 fctx = ctx.filectx(f)
280 except error.LookupError:
281 raise IOError()
282 renamed = fctx.renamed()
283 if renamed:
284 renamed = renamed[0]
285
286 data = fctx.data()
287 if f == '.hgtags':
288 data = _converttags (ui, revmap, data)
289 return context.memfilectx(f, data, 'l' in fctx.flags(),
290 'x' in fctx.flags(), renamed)
291
292 # Remap tag data using a revision map
293 def _converttags(ui, revmap, data):
294 newdata = []
295 for line in data.splitlines():
296 try:
297 id, name = line.split(' ', 1)
298 except ValueError:
299 ui.warn(_('skipping incorrectly formatted tag %s\n'
300 % line))
301 continue
302 try:
303 newid = node.bin(id)
304 except TypeError:
305 ui.warn(_('skipping incorrectly formatted id %s\n'
306 % id))
307 continue
308 try:
309 newdata.append('%s %s\n' % (node.hex(revmap[newid]),
310 name))
311 except KeyError:
312 ui.warn(_('no mapping for id %s\n' % id))
313 continue
314 return ''.join(newdata)
315
327 316 def _islfile(file, ctx, matcher, size):
328 317 '''Return true if file should be considered a largefile, i.e.
329 318 matcher matches it or it is larger than size.'''
330 319 # never store special .hg* files as largefiles
331 320 if file == '.hgtags' or file == '.hgignore' or file == '.hgsigs':
332 321 return False
333 322 if matcher and matcher(file):
334 323 return True
335 324 try:
336 325 return ctx.filectx(file).size() >= size * 1024 * 1024
337 326 except error.LookupError:
338 327 return False
339 328
340 329 def uploadlfiles(ui, rsrc, rdst, files):
341 330 '''upload largefiles to the central store'''
342 331
343 332 if not files:
344 333 return
345 334
346 335 store = basestore._openstore(rsrc, rdst, put=True)
347 336
348 337 at = 0
349 338 files = filter(lambda h: not store.exists(h), files)
350 339 for hash in files:
351 340 ui.progress(_('uploading largefiles'), at, unit='largefile',
352 341 total=len(files))
353 342 source = lfutil.findfile(rsrc, hash)
354 343 if not source:
355 344 raise util.Abort(_('largefile %s missing from store'
356 345 ' (needs to be uploaded)') % hash)
357 346 # XXX check for errors here
358 347 store.put(source, hash)
359 348 at += 1
360 349 ui.progress(_('uploading largefiles'), None)
361 350
362 351 def verifylfiles(ui, repo, all=False, contents=False):
363 352 '''Verify that every big file revision in the current changeset
364 353 exists in the central store. With --contents, also verify that
365 354 the contents of each big file revision are correct (SHA-1 hash
366 355 matches the revision ID). With --all, check every changeset in
367 356 this repository.'''
368 357 if all:
369 358 # Pass a list to the function rather than an iterator because we know a
370 359 # list will work.
371 360 revs = range(len(repo))
372 361 else:
373 362 revs = ['.']
374 363
375 364 store = basestore._openstore(repo)
376 365 return store.verify(revs, contents=contents)
377 366
378 367 def cachelfiles(ui, repo, node):
379 368 '''cachelfiles ensures that all largefiles needed by the specified revision
380 369 are present in the repository's largefile cache.
381 370
382 371 returns a tuple (cached, missing). cached is the list of files downloaded
383 372 by this operation; missing is the list of files that were needed but could
384 373 not be found.'''
385 374 lfiles = lfutil.listlfiles(repo, node)
386 375 toget = []
387 376
388 377 for lfile in lfiles:
389 378 expectedhash = repo[node][lfutil.standin(lfile)].data().strip()
390 379 # if it exists and its hash matches, it might have been locally
391 380 # modified before updating and the user chose 'local'. in this case,
392 381 # it will not be in any store, so don't look for it.
393 382 if ((not os.path.exists(repo.wjoin(lfile)) or
394 383 expectedhash != lfutil.hashfile(repo.wjoin(lfile))) and
395 384 not lfutil.findfile(repo, expectedhash)):
396 385 toget.append((lfile, expectedhash))
397 386
398 387 if toget:
399 388 store = basestore._openstore(repo)
400 389 ret = store.get(toget)
401 390 return ret
402 391
403 392 return ([], [])
404 393
405 394 def updatelfiles(ui, repo, filelist=None, printmessage=True):
406 395 wlock = repo.wlock()
407 396 try:
408 397 lfdirstate = lfutil.openlfdirstate(ui, repo)
409 398 lfiles = set(lfutil.listlfiles(repo)) | set(lfdirstate)
410 399
411 400 if filelist is not None:
412 401 lfiles = [f for f in lfiles if f in filelist]
413 402
414 403 printed = False
415 404 if printmessage and lfiles:
416 405 ui.status(_('getting changed largefiles\n'))
417 406 printed = True
418 407 cachelfiles(ui, repo, '.')
419 408
420 409 updated, removed = 0, 0
421 410 for i in map(lambda f: _updatelfile(repo, lfdirstate, f), lfiles):
422 411 # increment the appropriate counter according to _updatelfile's
423 412 # return value
424 413 updated += i > 0 and i or 0
425 414 removed -= i < 0 and i or 0
426 415 if printmessage and (removed or updated) and not printed:
427 416 ui.status(_('getting changed largefiles\n'))
428 417 printed = True
429 418
430 419 lfdirstate.write()
431 420 if printed and printmessage:
432 421 ui.status(_('%d largefiles updated, %d removed\n') % (updated,
433 422 removed))
434 423 finally:
435 424 wlock.release()
436 425
437 426 def _updatelfile(repo, lfdirstate, lfile):
438 427 '''updates a single largefile and copies the state of its standin from
439 428 the repository's dirstate to its state in the lfdirstate.
440 429
441 430 returns 1 if the file was modified, -1 if the file was removed, 0 if the
442 431 file was unchanged, and None if the needed largefile was missing from the
443 432 cache.'''
444 433 ret = 0
445 434 abslfile = repo.wjoin(lfile)
446 435 absstandin = repo.wjoin(lfutil.standin(lfile))
447 436 if os.path.exists(absstandin):
448 437 if os.path.exists(absstandin+'.orig'):
449 438 shutil.copyfile(abslfile, abslfile+'.orig')
450 439 expecthash = lfutil.readstandin(repo, lfile)
451 440 if (expecthash != '' and
452 441 (not os.path.exists(abslfile) or
453 442 expecthash != lfutil.hashfile(abslfile))):
454 443 if not lfutil.copyfromcache(repo, expecthash, lfile):
455 444 # use normallookup() to allocate entry in largefiles dirstate,
456 445 # because lack of it misleads lfiles_repo.status() into
457 446 # recognition that such cache missing files are REMOVED.
458 447 lfdirstate.normallookup(lfile)
459 448 return None # don't try to set the mode
460 449 ret = 1
461 450 mode = os.stat(absstandin).st_mode
462 451 if mode != os.stat(abslfile).st_mode:
463 452 os.chmod(abslfile, mode)
464 453 ret = 1
465 454 else:
466 455 # Remove lfiles for which the standin is deleted, unless the
467 456 # lfile is added to the repository again. This happens when a
468 457 # largefile is converted back to a normal file: the standin
469 458 # disappears, but a new (normal) file appears as the lfile.
470 459 if os.path.exists(abslfile) and lfile not in repo[None]:
471 460 os.unlink(abslfile)
472 461 ret = -1
473 462 state = repo.dirstate[lfutil.standin(lfile)]
474 463 if state == 'n':
475 464 # When rebasing, we need to synchronize the standin and the largefile,
476 465 # because otherwise the largefile will get reverted. But for commit's
477 466 # sake, we have to mark the file as unclean.
478 467 if getattr(repo, "_isrebasing", False):
479 468 lfdirstate.normallookup(lfile)
480 469 else:
481 470 lfdirstate.normal(lfile)
482 471 elif state == 'r':
483 472 lfdirstate.remove(lfile)
484 473 elif state == 'a':
485 474 lfdirstate.add(lfile)
486 475 elif state == '?':
487 476 lfdirstate.drop(lfile)
488 477 return ret
489 478
490 479 # -- hg commands declarations ------------------------------------------------
491 480
492 481 cmdtable = {
493 482 'lfconvert': (lfconvert,
494 483 [('s', 'size', '',
495 484 _('minimum size (MB) for files to be converted '
496 485 'as largefiles'),
497 486 'SIZE'),
498 487 ('', 'to-normal', False,
499 488 _('convert from a largefiles repo to a normal repo')),
500 489 ],
501 490 _('hg lfconvert SOURCE DEST [FILE ...]')),
502 491 }
General Comments 0
You need to be logged in to leave comments. Login now