##// END OF EJS Templates
largefiles: ensure lfutil.getstandinmatcher() only matches standins...
Matt Harbison -
r26025:ba808943 stable
parent child Browse files
Show More
@@ -1,610 +1,612 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 '''largefiles utility code: must not import other modules in this package.'''
10 10
11 11 import os
12 12 import platform
13 13 import shutil
14 14 import stat
15 15 import copy
16 16
17 17 from mercurial import dirstate, httpconnection, match as match_, util, scmutil
18 18 from mercurial.i18n import _
19 19 from mercurial import node
20 20
21 21 shortname = '.hglf'
22 22 shortnameslash = shortname + '/'
23 23 longname = 'largefiles'
24 24
25 25
26 26 # -- Private worker functions ------------------------------------------
27 27
28 28 def getminsize(ui, assumelfiles, opt, default=10):
29 29 lfsize = opt
30 30 if not lfsize and assumelfiles:
31 31 lfsize = ui.config(longname, 'minsize', default=default)
32 32 if lfsize:
33 33 try:
34 34 lfsize = float(lfsize)
35 35 except ValueError:
36 36 raise util.Abort(_('largefiles: size must be number (not %s)\n')
37 37 % lfsize)
38 38 if lfsize is None:
39 39 raise util.Abort(_('minimum size for largefiles must be specified'))
40 40 return lfsize
41 41
42 42 def link(src, dest):
43 43 util.makedirs(os.path.dirname(dest))
44 44 try:
45 45 util.oslink(src, dest)
46 46 except OSError:
47 47 # if hardlinks fail, fallback on atomic copy
48 48 dst = util.atomictempfile(dest)
49 49 for chunk in util.filechunkiter(open(src, 'rb')):
50 50 dst.write(chunk)
51 51 dst.close()
52 52 os.chmod(dest, os.stat(src).st_mode)
53 53
54 54 def usercachepath(ui, hash):
55 55 path = ui.configpath(longname, 'usercache', None)
56 56 if path:
57 57 path = os.path.join(path, hash)
58 58 else:
59 59 if os.name == 'nt':
60 60 appdata = os.getenv('LOCALAPPDATA', os.getenv('APPDATA'))
61 61 if appdata:
62 62 path = os.path.join(appdata, longname, hash)
63 63 elif platform.system() == 'Darwin':
64 64 home = os.getenv('HOME')
65 65 if home:
66 66 path = os.path.join(home, 'Library', 'Caches',
67 67 longname, hash)
68 68 elif os.name == 'posix':
69 69 path = os.getenv('XDG_CACHE_HOME')
70 70 if path:
71 71 path = os.path.join(path, longname, hash)
72 72 else:
73 73 home = os.getenv('HOME')
74 74 if home:
75 75 path = os.path.join(home, '.cache', longname, hash)
76 76 else:
77 77 raise util.Abort(_('unknown operating system: %s\n') % os.name)
78 78 return path
79 79
80 80 def inusercache(ui, hash):
81 81 path = usercachepath(ui, hash)
82 82 return path and os.path.exists(path)
83 83
84 84 def findfile(repo, hash):
85 85 path, exists = findstorepath(repo, hash)
86 86 if exists:
87 87 repo.ui.note(_('found %s in store\n') % hash)
88 88 return path
89 89 elif inusercache(repo.ui, hash):
90 90 repo.ui.note(_('found %s in system cache\n') % hash)
91 91 path = storepath(repo, hash)
92 92 link(usercachepath(repo.ui, hash), path)
93 93 return path
94 94 return None
95 95
96 96 class largefilesdirstate(dirstate.dirstate):
97 97 def __getitem__(self, key):
98 98 return super(largefilesdirstate, self).__getitem__(unixpath(key))
99 99 def normal(self, f):
100 100 return super(largefilesdirstate, self).normal(unixpath(f))
101 101 def remove(self, f):
102 102 return super(largefilesdirstate, self).remove(unixpath(f))
103 103 def add(self, f):
104 104 return super(largefilesdirstate, self).add(unixpath(f))
105 105 def drop(self, f):
106 106 return super(largefilesdirstate, self).drop(unixpath(f))
107 107 def forget(self, f):
108 108 return super(largefilesdirstate, self).forget(unixpath(f))
109 109 def normallookup(self, f):
110 110 return super(largefilesdirstate, self).normallookup(unixpath(f))
111 111 def _ignore(self, f):
112 112 return False
113 113
114 114 def openlfdirstate(ui, repo, create=True):
115 115 '''
116 116 Return a dirstate object that tracks largefiles: i.e. its root is
117 117 the repo root, but it is saved in .hg/largefiles/dirstate.
118 118 '''
119 119 lfstoredir = repo.join(longname)
120 120 opener = scmutil.opener(lfstoredir)
121 121 lfdirstate = largefilesdirstate(opener, ui, repo.root,
122 122 repo.dirstate._validate)
123 123
124 124 # If the largefiles dirstate does not exist, populate and create
125 125 # it. This ensures that we create it on the first meaningful
126 126 # largefiles operation in a new clone.
127 127 if create and not os.path.exists(os.path.join(lfstoredir, 'dirstate')):
128 128 matcher = getstandinmatcher(repo)
129 129 standins = repo.dirstate.walk(matcher, [], False, False)
130 130
131 131 if len(standins) > 0:
132 132 util.makedirs(lfstoredir)
133 133
134 134 for standin in standins:
135 135 lfile = splitstandin(standin)
136 136 lfdirstate.normallookup(lfile)
137 137 return lfdirstate
138 138
139 139 def lfdirstatestatus(lfdirstate, repo):
140 140 wctx = repo['.']
141 141 match = match_.always(repo.root, repo.getcwd())
142 142 unsure, s = lfdirstate.status(match, [], False, False, False)
143 143 modified, clean = s.modified, s.clean
144 144 for lfile in unsure:
145 145 try:
146 146 fctx = wctx[standin(lfile)]
147 147 except LookupError:
148 148 fctx = None
149 149 if not fctx or fctx.data().strip() != hashfile(repo.wjoin(lfile)):
150 150 modified.append(lfile)
151 151 else:
152 152 clean.append(lfile)
153 153 lfdirstate.normal(lfile)
154 154 return s
155 155
156 156 def listlfiles(repo, rev=None, matcher=None):
157 157 '''return a list of largefiles in the working copy or the
158 158 specified changeset'''
159 159
160 160 if matcher is None:
161 161 matcher = getstandinmatcher(repo)
162 162
163 163 # ignore unknown files in working directory
164 164 return [splitstandin(f)
165 165 for f in repo[rev].walk(matcher)
166 166 if rev is not None or repo.dirstate[f] != '?']
167 167
168 168 def instore(repo, hash, forcelocal=False):
169 169 return os.path.exists(storepath(repo, hash, forcelocal))
170 170
171 171 def storepath(repo, hash, forcelocal=False):
172 172 if not forcelocal and repo.shared():
173 173 return repo.vfs.reljoin(repo.sharedpath, longname, hash)
174 174 return repo.join(longname, hash)
175 175
176 176 def findstorepath(repo, hash):
177 177 '''Search through the local store path(s) to find the file for the given
178 178 hash. If the file is not found, its path in the primary store is returned.
179 179 The return value is a tuple of (path, exists(path)).
180 180 '''
181 181 # For shared repos, the primary store is in the share source. But for
182 182 # backward compatibility, force a lookup in the local store if it wasn't
183 183 # found in the share source.
184 184 path = storepath(repo, hash, False)
185 185
186 186 if instore(repo, hash):
187 187 return (path, True)
188 188 elif repo.shared() and instore(repo, hash, True):
189 189 return storepath(repo, hash, True)
190 190
191 191 return (path, False)
192 192
193 193 def copyfromcache(repo, hash, filename):
194 194 '''Copy the specified largefile from the repo or system cache to
195 195 filename in the repository. Return true on success or false if the
196 196 file was not found in either cache (which should not happened:
197 197 this is meant to be called only after ensuring that the needed
198 198 largefile exists in the cache).'''
199 199 path = findfile(repo, hash)
200 200 if path is None:
201 201 return False
202 202 util.makedirs(os.path.dirname(repo.wjoin(filename)))
203 203 # The write may fail before the file is fully written, but we
204 204 # don't use atomic writes in the working copy.
205 205 shutil.copy(path, repo.wjoin(filename))
206 206 return True
207 207
208 208 def copytostore(repo, rev, file, uploaded=False):
209 209 hash = readstandin(repo, file, rev)
210 210 if instore(repo, hash):
211 211 return
212 212 copytostoreabsolute(repo, repo.wjoin(file), hash)
213 213
214 214 def copyalltostore(repo, node):
215 215 '''Copy all largefiles in a given revision to the store'''
216 216
217 217 ctx = repo[node]
218 218 for filename in ctx.files():
219 219 if isstandin(filename) and filename in ctx.manifest():
220 220 realfile = splitstandin(filename)
221 221 copytostore(repo, ctx.node(), realfile)
222 222
223 223
224 224 def copytostoreabsolute(repo, file, hash):
225 225 if inusercache(repo.ui, hash):
226 226 link(usercachepath(repo.ui, hash), storepath(repo, hash))
227 227 else:
228 228 util.makedirs(os.path.dirname(storepath(repo, hash)))
229 229 dst = util.atomictempfile(storepath(repo, hash),
230 230 createmode=repo.store.createmode)
231 231 for chunk in util.filechunkiter(open(file, 'rb')):
232 232 dst.write(chunk)
233 233 dst.close()
234 234 linktousercache(repo, hash)
235 235
236 236 def linktousercache(repo, hash):
237 237 path = usercachepath(repo.ui, hash)
238 238 if path:
239 239 link(storepath(repo, hash), path)
240 240
241 241 def getstandinmatcher(repo, rmatcher=None):
242 242 '''Return a match object that applies rmatcher to the standin directory'''
243 243 standindir = repo.wjoin(shortname)
244 244
245 245 # no warnings about missing files or directories
246 246 badfn = lambda f, msg: None
247 247
248 248 if rmatcher and not rmatcher.always():
249 249 pats = [os.path.join(standindir, pat) for pat in rmatcher.files()]
250 if not pats:
251 pats = [standindir]
250 252 match = scmutil.match(repo[None], pats, badfn=badfn)
251 253 # if pats is empty, it would incorrectly always match, so clear _always
252 254 match._always = False
253 255 else:
254 256 # no patterns: relative to repo root
255 257 match = scmutil.match(repo[None], [standindir], badfn=badfn)
256 258 return match
257 259
258 260 def composestandinmatcher(repo, rmatcher):
259 261 '''Return a matcher that accepts standins corresponding to the
260 262 files accepted by rmatcher. Pass the list of files in the matcher
261 263 as the paths specified by the user.'''
262 264 smatcher = getstandinmatcher(repo, rmatcher)
263 265 isstandin = smatcher.matchfn
264 266 def composedmatchfn(f):
265 267 return isstandin(f) and rmatcher.matchfn(splitstandin(f))
266 268 smatcher.matchfn = composedmatchfn
267 269
268 270 return smatcher
269 271
270 272 def standin(filename):
271 273 '''Return the repo-relative path to the standin for the specified big
272 274 file.'''
273 275 # Notes:
274 276 # 1) Some callers want an absolute path, but for instance addlargefiles
275 277 # needs it repo-relative so it can be passed to repo[None].add(). So
276 278 # leave it up to the caller to use repo.wjoin() to get an absolute path.
277 279 # 2) Join with '/' because that's what dirstate always uses, even on
278 280 # Windows. Change existing separator to '/' first in case we are
279 281 # passed filenames from an external source (like the command line).
280 282 return shortnameslash + util.pconvert(filename)
281 283
282 284 def isstandin(filename):
283 285 '''Return true if filename is a big file standin. filename must be
284 286 in Mercurial's internal form (slash-separated).'''
285 287 return filename.startswith(shortnameslash)
286 288
287 289 def splitstandin(filename):
288 290 # Split on / because that's what dirstate always uses, even on Windows.
289 291 # Change local separator to / first just in case we are passed filenames
290 292 # from an external source (like the command line).
291 293 bits = util.pconvert(filename).split('/', 1)
292 294 if len(bits) == 2 and bits[0] == shortname:
293 295 return bits[1]
294 296 else:
295 297 return None
296 298
297 299 def updatestandin(repo, standin):
298 300 file = repo.wjoin(splitstandin(standin))
299 301 if os.path.exists(file):
300 302 hash = hashfile(file)
301 303 executable = getexecutable(file)
302 304 writestandin(repo, standin, hash, executable)
303 305
304 306 def readstandin(repo, filename, node=None):
305 307 '''read hex hash from standin for filename at given node, or working
306 308 directory if no node is given'''
307 309 return repo[node][standin(filename)].data().strip()
308 310
309 311 def writestandin(repo, standin, hash, executable):
310 312 '''write hash to <repo.root>/<standin>'''
311 313 repo.wwrite(standin, hash + '\n', executable and 'x' or '')
312 314
313 315 def copyandhash(instream, outfile):
314 316 '''Read bytes from instream (iterable) and write them to outfile,
315 317 computing the SHA-1 hash of the data along the way. Return the hash.'''
316 318 hasher = util.sha1('')
317 319 for data in instream:
318 320 hasher.update(data)
319 321 outfile.write(data)
320 322 return hasher.hexdigest()
321 323
322 324 def hashrepofile(repo, file):
323 325 return hashfile(repo.wjoin(file))
324 326
325 327 def hashfile(file):
326 328 if not os.path.exists(file):
327 329 return ''
328 330 hasher = util.sha1('')
329 331 fd = open(file, 'rb')
330 332 for data in util.filechunkiter(fd, 128 * 1024):
331 333 hasher.update(data)
332 334 fd.close()
333 335 return hasher.hexdigest()
334 336
335 337 def getexecutable(filename):
336 338 mode = os.stat(filename).st_mode
337 339 return ((mode & stat.S_IXUSR) and
338 340 (mode & stat.S_IXGRP) and
339 341 (mode & stat.S_IXOTH))
340 342
341 343 def urljoin(first, second, *arg):
342 344 def join(left, right):
343 345 if not left.endswith('/'):
344 346 left += '/'
345 347 if right.startswith('/'):
346 348 right = right[1:]
347 349 return left + right
348 350
349 351 url = join(first, second)
350 352 for a in arg:
351 353 url = join(url, a)
352 354 return url
353 355
354 356 def hexsha1(data):
355 357 """hexsha1 returns the hex-encoded sha1 sum of the data in the file-like
356 358 object data"""
357 359 h = util.sha1()
358 360 for chunk in util.filechunkiter(data):
359 361 h.update(chunk)
360 362 return h.hexdigest()
361 363
362 364 def httpsendfile(ui, filename):
363 365 return httpconnection.httpsendfile(ui, filename, 'rb')
364 366
365 367 def unixpath(path):
366 368 '''Return a version of path normalized for use with the lfdirstate.'''
367 369 return util.pconvert(os.path.normpath(path))
368 370
369 371 def islfilesrepo(repo):
370 372 if ('largefiles' in repo.requirements and
371 373 any(shortnameslash in f[0] for f in repo.store.datafiles())):
372 374 return True
373 375
374 376 return any(openlfdirstate(repo.ui, repo, False))
375 377
376 378 class storeprotonotcapable(Exception):
377 379 def __init__(self, storetypes):
378 380 self.storetypes = storetypes
379 381
380 382 def getstandinsstate(repo):
381 383 standins = []
382 384 matcher = getstandinmatcher(repo)
383 385 for standin in repo.dirstate.walk(matcher, [], False, False):
384 386 lfile = splitstandin(standin)
385 387 try:
386 388 hash = readstandin(repo, lfile)
387 389 except IOError:
388 390 hash = None
389 391 standins.append((lfile, hash))
390 392 return standins
391 393
392 394 def synclfdirstate(repo, lfdirstate, lfile, normallookup):
393 395 lfstandin = standin(lfile)
394 396 if lfstandin in repo.dirstate:
395 397 stat = repo.dirstate._map[lfstandin]
396 398 state, mtime = stat[0], stat[3]
397 399 else:
398 400 state, mtime = '?', -1
399 401 if state == 'n':
400 402 if normallookup or mtime < 0:
401 403 # state 'n' doesn't ensure 'clean' in this case
402 404 lfdirstate.normallookup(lfile)
403 405 else:
404 406 lfdirstate.normal(lfile)
405 407 elif state == 'm':
406 408 lfdirstate.normallookup(lfile)
407 409 elif state == 'r':
408 410 lfdirstate.remove(lfile)
409 411 elif state == 'a':
410 412 lfdirstate.add(lfile)
411 413 elif state == '?':
412 414 lfdirstate.drop(lfile)
413 415
414 416 def markcommitted(orig, ctx, node):
415 417 repo = ctx.repo()
416 418
417 419 orig(node)
418 420
419 421 # ATTENTION: "ctx.files()" may differ from "repo[node].files()"
420 422 # because files coming from the 2nd parent are omitted in the latter.
421 423 #
422 424 # The former should be used to get targets of "synclfdirstate",
423 425 # because such files:
424 426 # - are marked as "a" by "patch.patch()" (e.g. via transplant), and
425 427 # - have to be marked as "n" after commit, but
426 428 # - aren't listed in "repo[node].files()"
427 429
428 430 lfdirstate = openlfdirstate(repo.ui, repo)
429 431 for f in ctx.files():
430 432 if isstandin(f):
431 433 lfile = splitstandin(f)
432 434 synclfdirstate(repo, lfdirstate, lfile, False)
433 435 lfdirstate.write()
434 436
435 437 # As part of committing, copy all of the largefiles into the cache.
436 438 copyalltostore(repo, node)
437 439
438 440 def getlfilestoupdate(oldstandins, newstandins):
439 441 changedstandins = set(oldstandins).symmetric_difference(set(newstandins))
440 442 filelist = []
441 443 for f in changedstandins:
442 444 if f[0] not in filelist:
443 445 filelist.append(f[0])
444 446 return filelist
445 447
446 448 def getlfilestoupload(repo, missing, addfunc):
447 449 for i, n in enumerate(missing):
448 450 repo.ui.progress(_('finding outgoing largefiles'), i,
449 451 unit=_('revision'), total=len(missing))
450 452 parents = [p for p in repo.changelog.parents(n) if p != node.nullid]
451 453
452 454 oldlfstatus = repo.lfstatus
453 455 repo.lfstatus = False
454 456 try:
455 457 ctx = repo[n]
456 458 finally:
457 459 repo.lfstatus = oldlfstatus
458 460
459 461 files = set(ctx.files())
460 462 if len(parents) == 2:
461 463 mc = ctx.manifest()
462 464 mp1 = ctx.parents()[0].manifest()
463 465 mp2 = ctx.parents()[1].manifest()
464 466 for f in mp1:
465 467 if f not in mc:
466 468 files.add(f)
467 469 for f in mp2:
468 470 if f not in mc:
469 471 files.add(f)
470 472 for f in mc:
471 473 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
472 474 files.add(f)
473 475 for fn in files:
474 476 if isstandin(fn) and fn in ctx:
475 477 addfunc(fn, ctx[fn].data().strip())
476 478 repo.ui.progress(_('finding outgoing largefiles'), None)
477 479
478 480 def updatestandinsbymatch(repo, match):
479 481 '''Update standins in the working directory according to specified match
480 482
481 483 This returns (possibly modified) ``match`` object to be used for
482 484 subsequent commit process.
483 485 '''
484 486
485 487 ui = repo.ui
486 488
487 489 # Case 1: user calls commit with no specific files or
488 490 # include/exclude patterns: refresh and commit all files that
489 491 # are "dirty".
490 492 if match is None or match.always():
491 493 # Spend a bit of time here to get a list of files we know
492 494 # are modified so we can compare only against those.
493 495 # It can cost a lot of time (several seconds)
494 496 # otherwise to update all standins if the largefiles are
495 497 # large.
496 498 lfdirstate = openlfdirstate(ui, repo)
497 499 dirtymatch = match_.always(repo.root, repo.getcwd())
498 500 unsure, s = lfdirstate.status(dirtymatch, [], False, False,
499 501 False)
500 502 modifiedfiles = unsure + s.modified + s.added + s.removed
501 503 lfiles = listlfiles(repo)
502 504 # this only loops through largefiles that exist (not
503 505 # removed/renamed)
504 506 for lfile in lfiles:
505 507 if lfile in modifiedfiles:
506 508 if os.path.exists(
507 509 repo.wjoin(standin(lfile))):
508 510 # this handles the case where a rebase is being
509 511 # performed and the working copy is not updated
510 512 # yet.
511 513 if os.path.exists(repo.wjoin(lfile)):
512 514 updatestandin(repo,
513 515 standin(lfile))
514 516
515 517 return match
516 518
517 519 lfiles = listlfiles(repo)
518 520 match._files = repo._subdirlfs(match.files(), lfiles)
519 521
520 522 # Case 2: user calls commit with specified patterns: refresh
521 523 # any matching big files.
522 524 smatcher = composestandinmatcher(repo, match)
523 525 standins = repo.dirstate.walk(smatcher, [], False, False)
524 526
525 527 # No matching big files: get out of the way and pass control to
526 528 # the usual commit() method.
527 529 if not standins:
528 530 return match
529 531
530 532 # Refresh all matching big files. It's possible that the
531 533 # commit will end up failing, in which case the big files will
532 534 # stay refreshed. No harm done: the user modified them and
533 535 # asked to commit them, so sooner or later we're going to
534 536 # refresh the standins. Might as well leave them refreshed.
535 537 lfdirstate = openlfdirstate(ui, repo)
536 538 for fstandin in standins:
537 539 lfile = splitstandin(fstandin)
538 540 if lfdirstate[lfile] != 'r':
539 541 updatestandin(repo, fstandin)
540 542
541 543 # Cook up a new matcher that only matches regular files or
542 544 # standins corresponding to the big files requested by the
543 545 # user. Have to modify _files to prevent commit() from
544 546 # complaining "not tracked" for big files.
545 547 match = copy.copy(match)
546 548 origmatchfn = match.matchfn
547 549
548 550 # Check both the list of largefiles and the list of
549 551 # standins because if a largefile was removed, it
550 552 # won't be in the list of largefiles at this point
551 553 match._files += sorted(standins)
552 554
553 555 actualfiles = []
554 556 for f in match._files:
555 557 fstandin = standin(f)
556 558
557 559 # ignore known largefiles and standins
558 560 if f in lfiles or fstandin in standins:
559 561 continue
560 562
561 563 actualfiles.append(f)
562 564 match._files = actualfiles
563 565
564 566 def matchfn(f):
565 567 if origmatchfn(f):
566 568 return f not in lfiles
567 569 else:
568 570 return f in standins
569 571
570 572 match.matchfn = matchfn
571 573
572 574 return match
573 575
574 576 class automatedcommithook(object):
575 577 '''Stateful hook to update standins at the 1st commit of resuming
576 578
577 579 For efficiency, updating standins in the working directory should
578 580 be avoided while automated committing (like rebase, transplant and
579 581 so on), because they should be updated before committing.
580 582
581 583 But the 1st commit of resuming automated committing (e.g. ``rebase
582 584 --continue``) should update them, because largefiles may be
583 585 modified manually.
584 586 '''
585 587 def __init__(self, resuming):
586 588 self.resuming = resuming
587 589
588 590 def __call__(self, repo, match):
589 591 if self.resuming:
590 592 self.resuming = False # avoids updating at subsequent commits
591 593 return updatestandinsbymatch(repo, match)
592 594 else:
593 595 return match
594 596
595 597 def getstatuswriter(ui, repo, forcibly=None):
596 598 '''Return the function to write largefiles specific status out
597 599
598 600 If ``forcibly`` is ``None``, this returns the last element of
599 601 ``repo._lfstatuswriters`` as "default" writer function.
600 602
601 603 Otherwise, this returns the function to always write out (or
602 604 ignore if ``not forcibly``) status.
603 605 '''
604 606 if forcibly is None and util.safehasattr(repo, '_largefilesenabled'):
605 607 return repo._lfstatuswriters[-1]
606 608 else:
607 609 if forcibly:
608 610 return ui.status # forcibly WRITE OUT
609 611 else:
610 612 return lambda *msg, **opts: None # forcibly IGNORE
@@ -1,508 +1,512 b''
1 1 Test histedit extension: Fold commands
2 2 ======================================
3 3
4 4 This test file is dedicated to testing the fold command in non conflicting
5 5 case.
6 6
7 7 Initialization
8 8 ---------------
9 9
10 10
11 11 $ . "$TESTDIR/histedit-helpers.sh"
12 12
13 13 $ cat >> $HGRCPATH <<EOF
14 14 > [alias]
15 15 > logt = log --template '{rev}:{node|short} {desc|firstline}\n'
16 16 > [extensions]
17 17 > histedit=
18 18 > EOF
19 19
20 20
21 21 Simple folding
22 22 --------------------
23 23 $ initrepo ()
24 24 > {
25 25 > hg init r
26 26 > cd r
27 27 > for x in a b c d e f ; do
28 28 > echo $x > $x
29 29 > hg add $x
30 30 > hg ci -m $x
31 31 > done
32 32 > }
33 33
34 34 $ initrepo
35 35
36 36 log before edit
37 37 $ hg logt --graph
38 38 @ 5:652413bf663e f
39 39 |
40 40 o 4:e860deea161a e
41 41 |
42 42 o 3:055a42cdd887 d
43 43 |
44 44 o 2:177f92b77385 c
45 45 |
46 46 o 1:d2ae7f538514 b
47 47 |
48 48 o 0:cb9a9f314b8b a
49 49
50 50
51 51 $ hg histedit 177f92b77385 --commands - 2>&1 <<EOF | fixbundle
52 52 > pick e860deea161a e
53 53 > pick 652413bf663e f
54 54 > fold 177f92b77385 c
55 55 > pick 055a42cdd887 d
56 56 > EOF
57 57 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
58 58 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
59 59 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
60 60 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
61 61 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
62 62 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
63 63 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
64 64
65 65 log after edit
66 66 $ hg logt --graph
67 67 @ 4:9c277da72c9b d
68 68 |
69 69 o 3:6de59d13424a f
70 70 |
71 71 o 2:ee283cb5f2d5 e
72 72 |
73 73 o 1:d2ae7f538514 b
74 74 |
75 75 o 0:cb9a9f314b8b a
76 76
77 77
78 78 post-fold manifest
79 79 $ hg manifest
80 80 a
81 81 b
82 82 c
83 83 d
84 84 e
85 85 f
86 86
87 87
88 88 check histedit_source
89 89
90 90 $ hg log --debug --rev 3
91 91 changeset: 3:6de59d13424a8a13acd3e975514aed29dd0d9b2d
92 92 phase: draft
93 93 parent: 2:ee283cb5f2d5955443f23a27b697a04339e9a39a
94 94 parent: -1:0000000000000000000000000000000000000000
95 95 manifest: 3:81eede616954057198ead0b2c73b41d1f392829a
96 96 user: test
97 97 date: Thu Jan 01 00:00:00 1970 +0000
98 98 files+: c f
99 99 extra: branch=default
100 100 extra: histedit_source=a4f7421b80f79fcc59fff01bcbf4a53d127dd6d3,177f92b773850b59254aa5e923436f921b55483b
101 101 description:
102 102 f
103 103 ***
104 104 c
105 105
106 106
107 107
108 108 rollup will fold without preserving the folded commit's message
109 109
110 110 $ OLDHGEDITOR=$HGEDITOR
111 111 $ HGEDITOR=false
112 112 $ hg histedit d2ae7f538514 --commands - 2>&1 <<EOF | fixbundle
113 113 > pick d2ae7f538514 b
114 114 > roll ee283cb5f2d5 e
115 115 > pick 6de59d13424a f
116 116 > pick 9c277da72c9b d
117 117 > EOF
118 118 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
119 119 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
120 120 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
121 121 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
122 122 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
123 123 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
124 124
125 125 $ HGEDITOR=$OLDHGEDITOR
126 126
127 127 log after edit
128 128 $ hg logt --graph
129 129 @ 3:c4a9eb7989fc d
130 130 |
131 131 o 2:8e03a72b6f83 f
132 132 |
133 133 o 1:391ee782c689 b
134 134 |
135 135 o 0:cb9a9f314b8b a
136 136
137 137
138 138 description is taken from rollup target commit
139 139
140 140 $ hg log --debug --rev 1
141 141 changeset: 1:391ee782c68930be438ccf4c6a403daedbfbffa5
142 142 phase: draft
143 143 parent: 0:cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
144 144 parent: -1:0000000000000000000000000000000000000000
145 145 manifest: 1:b5e112a3a8354e269b1524729f0918662d847c38
146 146 user: test
147 147 date: Thu Jan 01 00:00:00 1970 +0000
148 148 files+: b e
149 149 extra: branch=default
150 150 extra: histedit_source=d2ae7f538514cd87c17547b0de4cea71fe1af9fb,ee283cb5f2d5955443f23a27b697a04339e9a39a
151 151 description:
152 152 b
153 153
154 154
155 155
156 156 check saving last-message.txt
157 157
158 158 $ cat > $TESTTMP/abortfolding.py <<EOF
159 159 > from mercurial import util
160 160 > def abortfolding(ui, repo, hooktype, **kwargs):
161 161 > ctx = repo[kwargs.get('node')]
162 162 > if set(ctx.files()) == set(['c', 'd', 'f']):
163 163 > return True # abort folding commit only
164 164 > ui.warn('allow non-folding commit\\n')
165 165 > EOF
166 166 $ cat > .hg/hgrc <<EOF
167 167 > [hooks]
168 168 > pretxncommit.abortfolding = python:$TESTTMP/abortfolding.py:abortfolding
169 169 > EOF
170 170
171 171 $ cat > $TESTTMP/editor.sh << EOF
172 172 > echo "==== before editing"
173 173 > cat \$1
174 174 > echo "===="
175 175 > echo "check saving last-message.txt" >> \$1
176 176 > EOF
177 177
178 178 $ rm -f .hg/last-message.txt
179 179 $ hg status --rev '8e03a72b6f83^1::c4a9eb7989fc'
180 180 A c
181 181 A d
182 182 A f
183 183 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit 8e03a72b6f83 --commands - 2>&1 <<EOF
184 184 > pick 8e03a72b6f83 f
185 185 > fold c4a9eb7989fc d
186 186 > EOF
187 187 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
188 188 adding d
189 189 allow non-folding commit
190 190 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
191 191 ==== before editing
192 192 f
193 193 ***
194 194 c
195 195 ***
196 196 d
197 197
198 198
199 199
200 200 HG: Enter commit message. Lines beginning with 'HG:' are removed.
201 201 HG: Leave message empty to abort commit.
202 202 HG: --
203 203 HG: user: test
204 204 HG: branch 'default'
205 205 HG: added c
206 206 HG: added d
207 207 HG: added f
208 208 ====
209 209 transaction abort!
210 210 rollback completed
211 211 abort: pretxncommit.abortfolding hook failed
212 212 [255]
213 213
214 214 $ cat .hg/last-message.txt
215 215 f
216 216 ***
217 217 c
218 218 ***
219 219 d
220 220
221 221
222 222
223 223 check saving last-message.txt
224 224
225 225 $ cd ..
226 226 $ rm -r r
227 227
228 228 folding preserves initial author
229 229 --------------------------------
230 230
231 231 $ initrepo
232 232
233 233 $ hg ci --user "someone else" --amend --quiet
234 234
235 235 tip before edit
236 236 $ hg log --rev .
237 237 changeset: 5:a00ad806cb55
238 238 tag: tip
239 239 user: someone else
240 240 date: Thu Jan 01 00:00:00 1970 +0000
241 241 summary: f
242 242
243 243
244 244 $ hg histedit e860deea161a --commands - 2>&1 <<EOF | fixbundle
245 245 > pick e860deea161a e
246 246 > fold a00ad806cb55 f
247 247 > EOF
248 248 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
249 249 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
250 250 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
251 251 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
252 252
253 253 tip after edit
254 254 $ hg log --rev .
255 255 changeset: 4:698d4e8040a1
256 256 tag: tip
257 257 user: test
258 258 date: Thu Jan 01 00:00:00 1970 +0000
259 259 summary: e
260 260
261 261
262 262 $ cd ..
263 263 $ rm -r r
264 264
265 265 folding and creating no new change doesn't break:
266 266 -------------------------------------------------
267 267
268 268 folded content is dropped during a merge. The folded commit should properly disappear.
269 269
270 270 $ mkdir fold-to-empty-test
271 271 $ cd fold-to-empty-test
272 272 $ hg init
273 273 $ printf "1\n2\n3\n" > file
274 274 $ hg add file
275 275 $ hg commit -m '1+2+3'
276 276 $ echo 4 >> file
277 277 $ hg commit -m '+4'
278 278 $ echo 5 >> file
279 279 $ hg commit -m '+5'
280 280 $ echo 6 >> file
281 281 $ hg commit -m '+6'
282 282 $ hg logt --graph
283 283 @ 3:251d831eeec5 +6
284 284 |
285 285 o 2:888f9082bf99 +5
286 286 |
287 287 o 1:617f94f13c0f +4
288 288 |
289 289 o 0:0189ba417d34 1+2+3
290 290
291 291
292 292 $ hg histedit 1 --commands - << EOF
293 293 > pick 617f94f13c0f 1 +4
294 294 > drop 888f9082bf99 2 +5
295 295 > fold 251d831eeec5 3 +6
296 296 > EOF
297 297 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
298 298 merging file
299 299 warning: conflicts during merge.
300 300 merging file incomplete! (edit conflicts, then use 'hg resolve --mark')
301 301 Fix up the change and run hg histedit --continue
302 302 [1]
303 303 There were conflicts, we keep P1 content. This
304 304 should effectively drop the changes from +6.
305 305 $ hg status
306 306 M file
307 307 ? file.orig
308 308 $ hg resolve -l
309 309 U file
310 310 $ hg revert -r 'p1()' file
311 311 $ hg resolve --mark file
312 312 (no more unresolved files)
313 313 $ hg histedit --continue
314 314 251d831eeec5: empty changeset
315 315 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
316 316 saved backup bundle to $TESTTMP/*-backup.hg (glob)
317 317 $ hg logt --graph
318 318 @ 1:617f94f13c0f +4
319 319 |
320 320 o 0:0189ba417d34 1+2+3
321 321
322 322
323 323 $ cd ..
324 324
325 325
326 326 Test fold through dropped
327 327 -------------------------
328 328
329 329
330 330 Test corner case where folded revision is separated from its parent by a
331 331 dropped revision.
332 332
333 333
334 334 $ hg init fold-with-dropped
335 335 $ cd fold-with-dropped
336 336 $ printf "1\n2\n3\n" > file
337 337 $ hg commit -Am '1+2+3'
338 338 adding file
339 339 $ echo 4 >> file
340 340 $ hg commit -m '+4'
341 341 $ echo 5 >> file
342 342 $ hg commit -m '+5'
343 343 $ echo 6 >> file
344 344 $ hg commit -m '+6'
345 345 $ hg logt -G
346 346 @ 3:251d831eeec5 +6
347 347 |
348 348 o 2:888f9082bf99 +5
349 349 |
350 350 o 1:617f94f13c0f +4
351 351 |
352 352 o 0:0189ba417d34 1+2+3
353 353
354 354 $ hg histedit 1 --commands - << EOF
355 355 > pick 617f94f13c0f 1 +4
356 356 > drop 888f9082bf99 2 +5
357 357 > fold 251d831eeec5 3 +6
358 358 > EOF
359 359 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
360 360 merging file
361 361 warning: conflicts during merge.
362 362 merging file incomplete! (edit conflicts, then use 'hg resolve --mark')
363 363 Fix up the change and run hg histedit --continue
364 364 [1]
365 365 $ cat > file << EOF
366 366 > 1
367 367 > 2
368 368 > 3
369 369 > 4
370 370 > 5
371 371 > EOF
372 372 $ hg resolve --mark file
373 373 (no more unresolved files)
374 374 $ hg commit -m '+5.2'
375 375 created new head
376 376 $ echo 6 >> file
377 377 $ HGEDITOR=cat hg histedit --continue
378 378 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
379 379 +4
380 380 ***
381 381 +5.2
382 382 ***
383 383 +6
384 384
385 385
386 386
387 387 HG: Enter commit message. Lines beginning with 'HG:' are removed.
388 388 HG: Leave message empty to abort commit.
389 389 HG: --
390 390 HG: user: test
391 391 HG: branch 'default'
392 392 HG: changed file
393 393 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
394 394 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
395 395 saved backup bundle to $TESTTMP/fold-with-dropped/.hg/strip-backup/617f94f13c0f-3d69522c-backup.hg (glob)
396 396 $ hg logt -G
397 397 @ 1:10c647b2cdd5 +4
398 398 |
399 399 o 0:0189ba417d34 1+2+3
400 400
401 401 $ hg export tip
402 402 # HG changeset patch
403 403 # User test
404 404 # Date 0 0
405 405 # Thu Jan 01 00:00:00 1970 +0000
406 406 # Node ID 10c647b2cdd54db0603ecb99b2ff5ce66d5a5323
407 407 # Parent 0189ba417d34df9dda55f88b637dcae9917b5964
408 408 +4
409 409 ***
410 410 +5.2
411 411 ***
412 412 +6
413 413
414 414 diff -r 0189ba417d34 -r 10c647b2cdd5 file
415 415 --- a/file Thu Jan 01 00:00:00 1970 +0000
416 416 +++ b/file Thu Jan 01 00:00:00 1970 +0000
417 417 @@ -1,3 +1,6 @@
418 418 1
419 419 2
420 420 3
421 421 +4
422 422 +5
423 423 +6
424 424 $ cd ..
425 425
426 426
427 427 Folding with initial rename (issue3729)
428 428 ---------------------------------------
429 429
430 430 $ hg init fold-rename
431 431 $ cd fold-rename
432 432 $ echo a > a.txt
433 433 $ hg add a.txt
434 434 $ hg commit -m a
435 435 $ hg rename a.txt b.txt
436 436 $ hg commit -m rename
437 437 $ echo b >> b.txt
438 438 $ hg commit -m b
439 439
440 440 $ hg logt --follow b.txt
441 441 2:e0371e0426bc b
442 442 1:1c4f440a8085 rename
443 443 0:6c795aa153cb a
444 444
445 445 $ hg histedit 1c4f440a8085 --commands - 2>&1 << EOF | fixbundle
446 446 > pick 1c4f440a8085 rename
447 447 > fold e0371e0426bc b
448 448 > EOF
449 449 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
450 450 reverting b.txt
451 451 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
452 452 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
453 453 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
454 454
455 455 $ hg logt --follow b.txt
456 456 1:cf858d235c76 rename
457 457 0:6c795aa153cb a
458 458
459 459 $ cd ..
460 460
461 461 Folding with swapping
462 462 ---------------------
463 463
464 464 This is an excuse to test hook with histedit temporary commit (issue4422)
465 465
466 466
467 467 $ hg init issue4422
468 468 $ cd issue4422
469 469 $ echo a > a.txt
470 470 $ hg add a.txt
471 471 $ hg commit -m a
472 472 $ echo b > b.txt
473 473 $ hg add b.txt
474 474 $ hg commit -m b
475 475 $ echo c > c.txt
476 476 $ hg add c.txt
477 477 $ hg commit -m c
478 478
479 479 $ hg logt
480 480 2:a1a953ffb4b0 c
481 481 1:199b6bb90248 b
482 482 0:6c795aa153cb a
483 483
484 484 Setup the proper environment variable symbol for the platform, to be subbed
485 485 into the hook command.
486 486 #if windows
487 487 $ NODE="%HG_NODE%"
488 488 #else
489 489 $ NODE="\$HG_NODE"
490 490 #endif
491 491 $ hg histedit 6c795aa153cb --config hooks.commit="echo commit $NODE" --commands - 2>&1 << EOF | fixbundle
492 492 > pick 199b6bb90248 b
493 493 > fold a1a953ffb4b0 c
494 494 > pick 6c795aa153cb a
495 495 > EOF
496 496 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
497 497 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
498 498 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
499 499 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
500 500 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
501 501 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
502 502 commit 9599899f62c05f4377548c32bf1c9f1a39634b0c
503 503
504 504 $ hg logt
505 505 1:9599899f62c0 a
506 506 0:79b99e9c8e49 b
507 507
508 $ echo "foo" > amended.txt
509 $ hg add amended.txt
510 $ hg ci -q --config extensions.largefiles= --amend -I amended.txt
511
508 512 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now