##// END OF EJS Templates
amend: fix copy records handling (issue3410)...
Patrick Mezard -
r16553:9224cc2e stable
parent child Browse files
Show More
@@ -1,1644 +1,1648 b''
1 1 # cmdutil.py - help for command processing in mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from node import hex, nullid, nullrev, short
9 9 from i18n import _
10 10 import os, sys, errno, re, tempfile
11 11 import util, scmutil, templater, patch, error, templatekw, revlog, copies
12 12 import match as matchmod
13 13 import subrepo, context, repair, bookmarks
14 14
15 15 def parsealiases(cmd):
16 16 return cmd.lstrip("^").split("|")
17 17
18 18 def findpossible(cmd, table, strict=False):
19 19 """
20 20 Return cmd -> (aliases, command table entry)
21 21 for each matching command.
22 22 Return debug commands (or their aliases) only if no normal command matches.
23 23 """
24 24 choice = {}
25 25 debugchoice = {}
26 26
27 27 if cmd in table:
28 28 # short-circuit exact matches, "log" alias beats "^log|history"
29 29 keys = [cmd]
30 30 else:
31 31 keys = table.keys()
32 32
33 33 for e in keys:
34 34 aliases = parsealiases(e)
35 35 found = None
36 36 if cmd in aliases:
37 37 found = cmd
38 38 elif not strict:
39 39 for a in aliases:
40 40 if a.startswith(cmd):
41 41 found = a
42 42 break
43 43 if found is not None:
44 44 if aliases[0].startswith("debug") or found.startswith("debug"):
45 45 debugchoice[found] = (aliases, table[e])
46 46 else:
47 47 choice[found] = (aliases, table[e])
48 48
49 49 if not choice and debugchoice:
50 50 choice = debugchoice
51 51
52 52 return choice
53 53
54 54 def findcmd(cmd, table, strict=True):
55 55 """Return (aliases, command table entry) for command string."""
56 56 choice = findpossible(cmd, table, strict)
57 57
58 58 if cmd in choice:
59 59 return choice[cmd]
60 60
61 61 if len(choice) > 1:
62 62 clist = choice.keys()
63 63 clist.sort()
64 64 raise error.AmbiguousCommand(cmd, clist)
65 65
66 66 if choice:
67 67 return choice.values()[0]
68 68
69 69 raise error.UnknownCommand(cmd)
70 70
71 71 def findrepo(p):
72 72 while not os.path.isdir(os.path.join(p, ".hg")):
73 73 oldp, p = p, os.path.dirname(p)
74 74 if p == oldp:
75 75 return None
76 76
77 77 return p
78 78
79 79 def bailifchanged(repo):
80 80 if repo.dirstate.p2() != nullid:
81 81 raise util.Abort(_('outstanding uncommitted merge'))
82 82 modified, added, removed, deleted = repo.status()[:4]
83 83 if modified or added or removed or deleted:
84 84 raise util.Abort(_("outstanding uncommitted changes"))
85 85 ctx = repo[None]
86 86 for s in ctx.substate:
87 87 if ctx.sub(s).dirty():
88 88 raise util.Abort(_("uncommitted changes in subrepo %s") % s)
89 89
90 90 def logmessage(ui, opts):
91 91 """ get the log message according to -m and -l option """
92 92 message = opts.get('message')
93 93 logfile = opts.get('logfile')
94 94
95 95 if message and logfile:
96 96 raise util.Abort(_('options --message and --logfile are mutually '
97 97 'exclusive'))
98 98 if not message and logfile:
99 99 try:
100 100 if logfile == '-':
101 101 message = ui.fin.read()
102 102 else:
103 103 message = '\n'.join(util.readfile(logfile).splitlines())
104 104 except IOError, inst:
105 105 raise util.Abort(_("can't read commit message '%s': %s") %
106 106 (logfile, inst.strerror))
107 107 return message
108 108
109 109 def loglimit(opts):
110 110 """get the log limit according to option -l/--limit"""
111 111 limit = opts.get('limit')
112 112 if limit:
113 113 try:
114 114 limit = int(limit)
115 115 except ValueError:
116 116 raise util.Abort(_('limit must be a positive integer'))
117 117 if limit <= 0:
118 118 raise util.Abort(_('limit must be positive'))
119 119 else:
120 120 limit = None
121 121 return limit
122 122
123 123 def makefilename(repo, pat, node, desc=None,
124 124 total=None, seqno=None, revwidth=None, pathname=None):
125 125 node_expander = {
126 126 'H': lambda: hex(node),
127 127 'R': lambda: str(repo.changelog.rev(node)),
128 128 'h': lambda: short(node),
129 129 'm': lambda: re.sub('[^\w]', '_', str(desc))
130 130 }
131 131 expander = {
132 132 '%': lambda: '%',
133 133 'b': lambda: os.path.basename(repo.root),
134 134 }
135 135
136 136 try:
137 137 if node:
138 138 expander.update(node_expander)
139 139 if node:
140 140 expander['r'] = (lambda:
141 141 str(repo.changelog.rev(node)).zfill(revwidth or 0))
142 142 if total is not None:
143 143 expander['N'] = lambda: str(total)
144 144 if seqno is not None:
145 145 expander['n'] = lambda: str(seqno)
146 146 if total is not None and seqno is not None:
147 147 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
148 148 if pathname is not None:
149 149 expander['s'] = lambda: os.path.basename(pathname)
150 150 expander['d'] = lambda: os.path.dirname(pathname) or '.'
151 151 expander['p'] = lambda: pathname
152 152
153 153 newname = []
154 154 patlen = len(pat)
155 155 i = 0
156 156 while i < patlen:
157 157 c = pat[i]
158 158 if c == '%':
159 159 i += 1
160 160 c = pat[i]
161 161 c = expander[c]()
162 162 newname.append(c)
163 163 i += 1
164 164 return ''.join(newname)
165 165 except KeyError, inst:
166 166 raise util.Abort(_("invalid format spec '%%%s' in output filename") %
167 167 inst.args[0])
168 168
169 169 def makefileobj(repo, pat, node=None, desc=None, total=None,
170 170 seqno=None, revwidth=None, mode='wb', pathname=None):
171 171
172 172 writable = mode not in ('r', 'rb')
173 173
174 174 if not pat or pat == '-':
175 175 fp = writable and repo.ui.fout or repo.ui.fin
176 176 if util.safehasattr(fp, 'fileno'):
177 177 return os.fdopen(os.dup(fp.fileno()), mode)
178 178 else:
179 179 # if this fp can't be duped properly, return
180 180 # a dummy object that can be closed
181 181 class wrappedfileobj(object):
182 182 noop = lambda x: None
183 183 def __init__(self, f):
184 184 self.f = f
185 185 def __getattr__(self, attr):
186 186 if attr == 'close':
187 187 return self.noop
188 188 else:
189 189 return getattr(self.f, attr)
190 190
191 191 return wrappedfileobj(fp)
192 192 if util.safehasattr(pat, 'write') and writable:
193 193 return pat
194 194 if util.safehasattr(pat, 'read') and 'r' in mode:
195 195 return pat
196 196 return open(makefilename(repo, pat, node, desc, total, seqno, revwidth,
197 197 pathname),
198 198 mode)
199 199
200 200 def openrevlog(repo, cmd, file_, opts):
201 201 """opens the changelog, manifest, a filelog or a given revlog"""
202 202 cl = opts['changelog']
203 203 mf = opts['manifest']
204 204 msg = None
205 205 if cl and mf:
206 206 msg = _('cannot specify --changelog and --manifest at the same time')
207 207 elif cl or mf:
208 208 if file_:
209 209 msg = _('cannot specify filename with --changelog or --manifest')
210 210 elif not repo:
211 211 msg = _('cannot specify --changelog or --manifest '
212 212 'without a repository')
213 213 if msg:
214 214 raise util.Abort(msg)
215 215
216 216 r = None
217 217 if repo:
218 218 if cl:
219 219 r = repo.changelog
220 220 elif mf:
221 221 r = repo.manifest
222 222 elif file_:
223 223 filelog = repo.file(file_)
224 224 if len(filelog):
225 225 r = filelog
226 226 if not r:
227 227 if not file_:
228 228 raise error.CommandError(cmd, _('invalid arguments'))
229 229 if not os.path.isfile(file_):
230 230 raise util.Abort(_("revlog '%s' not found") % file_)
231 231 r = revlog.revlog(scmutil.opener(os.getcwd(), audit=False),
232 232 file_[:-2] + ".i")
233 233 return r
234 234
235 235 def copy(ui, repo, pats, opts, rename=False):
236 236 # called with the repo lock held
237 237 #
238 238 # hgsep => pathname that uses "/" to separate directories
239 239 # ossep => pathname that uses os.sep to separate directories
240 240 cwd = repo.getcwd()
241 241 targets = {}
242 242 after = opts.get("after")
243 243 dryrun = opts.get("dry_run")
244 244 wctx = repo[None]
245 245
246 246 def walkpat(pat):
247 247 srcs = []
248 248 badstates = after and '?' or '?r'
249 249 m = scmutil.match(repo[None], [pat], opts, globbed=True)
250 250 for abs in repo.walk(m):
251 251 state = repo.dirstate[abs]
252 252 rel = m.rel(abs)
253 253 exact = m.exact(abs)
254 254 if state in badstates:
255 255 if exact and state == '?':
256 256 ui.warn(_('%s: not copying - file is not managed\n') % rel)
257 257 if exact and state == 'r':
258 258 ui.warn(_('%s: not copying - file has been marked for'
259 259 ' remove\n') % rel)
260 260 continue
261 261 # abs: hgsep
262 262 # rel: ossep
263 263 srcs.append((abs, rel, exact))
264 264 return srcs
265 265
266 266 # abssrc: hgsep
267 267 # relsrc: ossep
268 268 # otarget: ossep
269 269 def copyfile(abssrc, relsrc, otarget, exact):
270 270 abstarget = scmutil.canonpath(repo.root, cwd, otarget)
271 271 if '/' in abstarget:
272 272 # We cannot normalize abstarget itself, this would prevent
273 273 # case only renames, like a => A.
274 274 abspath, absname = abstarget.rsplit('/', 1)
275 275 abstarget = repo.dirstate.normalize(abspath) + '/' + absname
276 276 reltarget = repo.pathto(abstarget, cwd)
277 277 target = repo.wjoin(abstarget)
278 278 src = repo.wjoin(abssrc)
279 279 state = repo.dirstate[abstarget]
280 280
281 281 scmutil.checkportable(ui, abstarget)
282 282
283 283 # check for collisions
284 284 prevsrc = targets.get(abstarget)
285 285 if prevsrc is not None:
286 286 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
287 287 (reltarget, repo.pathto(abssrc, cwd),
288 288 repo.pathto(prevsrc, cwd)))
289 289 return
290 290
291 291 # check for overwrites
292 292 exists = os.path.lexists(target)
293 293 samefile = False
294 294 if exists and abssrc != abstarget:
295 295 if (repo.dirstate.normalize(abssrc) ==
296 296 repo.dirstate.normalize(abstarget)):
297 297 if not rename:
298 298 ui.warn(_("%s: can't copy - same file\n") % reltarget)
299 299 return
300 300 exists = False
301 301 samefile = True
302 302
303 303 if not after and exists or after and state in 'mn':
304 304 if not opts['force']:
305 305 ui.warn(_('%s: not overwriting - file exists\n') %
306 306 reltarget)
307 307 return
308 308
309 309 if after:
310 310 if not exists:
311 311 if rename:
312 312 ui.warn(_('%s: not recording move - %s does not exist\n') %
313 313 (relsrc, reltarget))
314 314 else:
315 315 ui.warn(_('%s: not recording copy - %s does not exist\n') %
316 316 (relsrc, reltarget))
317 317 return
318 318 elif not dryrun:
319 319 try:
320 320 if exists:
321 321 os.unlink(target)
322 322 targetdir = os.path.dirname(target) or '.'
323 323 if not os.path.isdir(targetdir):
324 324 os.makedirs(targetdir)
325 325 if samefile:
326 326 tmp = target + "~hgrename"
327 327 os.rename(src, tmp)
328 328 os.rename(tmp, target)
329 329 else:
330 330 util.copyfile(src, target)
331 331 srcexists = True
332 332 except IOError, inst:
333 333 if inst.errno == errno.ENOENT:
334 334 ui.warn(_('%s: deleted in working copy\n') % relsrc)
335 335 srcexists = False
336 336 else:
337 337 ui.warn(_('%s: cannot copy - %s\n') %
338 338 (relsrc, inst.strerror))
339 339 return True # report a failure
340 340
341 341 if ui.verbose or not exact:
342 342 if rename:
343 343 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
344 344 else:
345 345 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
346 346
347 347 targets[abstarget] = abssrc
348 348
349 349 # fix up dirstate
350 350 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
351 351 dryrun=dryrun, cwd=cwd)
352 352 if rename and not dryrun:
353 353 if not after and srcexists and not samefile:
354 354 util.unlinkpath(repo.wjoin(abssrc))
355 355 wctx.forget([abssrc])
356 356
357 357 # pat: ossep
358 358 # dest ossep
359 359 # srcs: list of (hgsep, hgsep, ossep, bool)
360 360 # return: function that takes hgsep and returns ossep
361 361 def targetpathfn(pat, dest, srcs):
362 362 if os.path.isdir(pat):
363 363 abspfx = scmutil.canonpath(repo.root, cwd, pat)
364 364 abspfx = util.localpath(abspfx)
365 365 if destdirexists:
366 366 striplen = len(os.path.split(abspfx)[0])
367 367 else:
368 368 striplen = len(abspfx)
369 369 if striplen:
370 370 striplen += len(os.sep)
371 371 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
372 372 elif destdirexists:
373 373 res = lambda p: os.path.join(dest,
374 374 os.path.basename(util.localpath(p)))
375 375 else:
376 376 res = lambda p: dest
377 377 return res
378 378
379 379 # pat: ossep
380 380 # dest ossep
381 381 # srcs: list of (hgsep, hgsep, ossep, bool)
382 382 # return: function that takes hgsep and returns ossep
383 383 def targetpathafterfn(pat, dest, srcs):
384 384 if matchmod.patkind(pat):
385 385 # a mercurial pattern
386 386 res = lambda p: os.path.join(dest,
387 387 os.path.basename(util.localpath(p)))
388 388 else:
389 389 abspfx = scmutil.canonpath(repo.root, cwd, pat)
390 390 if len(abspfx) < len(srcs[0][0]):
391 391 # A directory. Either the target path contains the last
392 392 # component of the source path or it does not.
393 393 def evalpath(striplen):
394 394 score = 0
395 395 for s in srcs:
396 396 t = os.path.join(dest, util.localpath(s[0])[striplen:])
397 397 if os.path.lexists(t):
398 398 score += 1
399 399 return score
400 400
401 401 abspfx = util.localpath(abspfx)
402 402 striplen = len(abspfx)
403 403 if striplen:
404 404 striplen += len(os.sep)
405 405 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
406 406 score = evalpath(striplen)
407 407 striplen1 = len(os.path.split(abspfx)[0])
408 408 if striplen1:
409 409 striplen1 += len(os.sep)
410 410 if evalpath(striplen1) > score:
411 411 striplen = striplen1
412 412 res = lambda p: os.path.join(dest,
413 413 util.localpath(p)[striplen:])
414 414 else:
415 415 # a file
416 416 if destdirexists:
417 417 res = lambda p: os.path.join(dest,
418 418 os.path.basename(util.localpath(p)))
419 419 else:
420 420 res = lambda p: dest
421 421 return res
422 422
423 423
424 424 pats = scmutil.expandpats(pats)
425 425 if not pats:
426 426 raise util.Abort(_('no source or destination specified'))
427 427 if len(pats) == 1:
428 428 raise util.Abort(_('no destination specified'))
429 429 dest = pats.pop()
430 430 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
431 431 if not destdirexists:
432 432 if len(pats) > 1 or matchmod.patkind(pats[0]):
433 433 raise util.Abort(_('with multiple sources, destination must be an '
434 434 'existing directory'))
435 435 if util.endswithsep(dest):
436 436 raise util.Abort(_('destination %s is not a directory') % dest)
437 437
438 438 tfn = targetpathfn
439 439 if after:
440 440 tfn = targetpathafterfn
441 441 copylist = []
442 442 for pat in pats:
443 443 srcs = walkpat(pat)
444 444 if not srcs:
445 445 continue
446 446 copylist.append((tfn(pat, dest, srcs), srcs))
447 447 if not copylist:
448 448 raise util.Abort(_('no files to copy'))
449 449
450 450 errors = 0
451 451 for targetpath, srcs in copylist:
452 452 for abssrc, relsrc, exact in srcs:
453 453 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
454 454 errors += 1
455 455
456 456 if errors:
457 457 ui.warn(_('(consider using --after)\n'))
458 458
459 459 return errors != 0
460 460
461 461 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
462 462 runargs=None, appendpid=False):
463 463 '''Run a command as a service.'''
464 464
465 465 if opts['daemon'] and not opts['daemon_pipefds']:
466 466 # Signal child process startup with file removal
467 467 lockfd, lockpath = tempfile.mkstemp(prefix='hg-service-')
468 468 os.close(lockfd)
469 469 try:
470 470 if not runargs:
471 471 runargs = util.hgcmd() + sys.argv[1:]
472 472 runargs.append('--daemon-pipefds=%s' % lockpath)
473 473 # Don't pass --cwd to the child process, because we've already
474 474 # changed directory.
475 475 for i in xrange(1, len(runargs)):
476 476 if runargs[i].startswith('--cwd='):
477 477 del runargs[i]
478 478 break
479 479 elif runargs[i].startswith('--cwd'):
480 480 del runargs[i:i + 2]
481 481 break
482 482 def condfn():
483 483 return not os.path.exists(lockpath)
484 484 pid = util.rundetached(runargs, condfn)
485 485 if pid < 0:
486 486 raise util.Abort(_('child process failed to start'))
487 487 finally:
488 488 try:
489 489 os.unlink(lockpath)
490 490 except OSError, e:
491 491 if e.errno != errno.ENOENT:
492 492 raise
493 493 if parentfn:
494 494 return parentfn(pid)
495 495 else:
496 496 return
497 497
498 498 if initfn:
499 499 initfn()
500 500
501 501 if opts['pid_file']:
502 502 mode = appendpid and 'a' or 'w'
503 503 fp = open(opts['pid_file'], mode)
504 504 fp.write(str(os.getpid()) + '\n')
505 505 fp.close()
506 506
507 507 if opts['daemon_pipefds']:
508 508 lockpath = opts['daemon_pipefds']
509 509 try:
510 510 os.setsid()
511 511 except AttributeError:
512 512 pass
513 513 os.unlink(lockpath)
514 514 util.hidewindow()
515 515 sys.stdout.flush()
516 516 sys.stderr.flush()
517 517
518 518 nullfd = os.open(util.nulldev, os.O_RDWR)
519 519 logfilefd = nullfd
520 520 if logfile:
521 521 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
522 522 os.dup2(nullfd, 0)
523 523 os.dup2(logfilefd, 1)
524 524 os.dup2(logfilefd, 2)
525 525 if nullfd not in (0, 1, 2):
526 526 os.close(nullfd)
527 527 if logfile and logfilefd not in (0, 1, 2):
528 528 os.close(logfilefd)
529 529
530 530 if runfn:
531 531 return runfn()
532 532
533 533 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
534 534 opts=None):
535 535 '''export changesets as hg patches.'''
536 536
537 537 total = len(revs)
538 538 revwidth = max([len(str(rev)) for rev in revs])
539 539
540 540 def single(rev, seqno, fp):
541 541 ctx = repo[rev]
542 542 node = ctx.node()
543 543 parents = [p.node() for p in ctx.parents() if p]
544 544 branch = ctx.branch()
545 545 if switch_parent:
546 546 parents.reverse()
547 547 prev = (parents and parents[0]) or nullid
548 548
549 549 shouldclose = False
550 550 if not fp:
551 551 desc_lines = ctx.description().rstrip().split('\n')
552 552 desc = desc_lines[0] #Commit always has a first line.
553 553 fp = makefileobj(repo, template, node, desc=desc, total=total,
554 554 seqno=seqno, revwidth=revwidth, mode='ab')
555 555 if fp != template:
556 556 shouldclose = True
557 557 if fp != sys.stdout and util.safehasattr(fp, 'name'):
558 558 repo.ui.note("%s\n" % fp.name)
559 559
560 560 fp.write("# HG changeset patch\n")
561 561 fp.write("# User %s\n" % ctx.user())
562 562 fp.write("# Date %d %d\n" % ctx.date())
563 563 if branch and branch != 'default':
564 564 fp.write("# Branch %s\n" % branch)
565 565 fp.write("# Node ID %s\n" % hex(node))
566 566 fp.write("# Parent %s\n" % hex(prev))
567 567 if len(parents) > 1:
568 568 fp.write("# Parent %s\n" % hex(parents[1]))
569 569 fp.write(ctx.description().rstrip())
570 570 fp.write("\n\n")
571 571
572 572 for chunk in patch.diff(repo, prev, node, opts=opts):
573 573 fp.write(chunk)
574 574
575 575 if shouldclose:
576 576 fp.close()
577 577
578 578 for seqno, rev in enumerate(revs):
579 579 single(rev, seqno + 1, fp)
580 580
581 581 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
582 582 changes=None, stat=False, fp=None, prefix='',
583 583 listsubrepos=False):
584 584 '''show diff or diffstat.'''
585 585 if fp is None:
586 586 write = ui.write
587 587 else:
588 588 def write(s, **kw):
589 589 fp.write(s)
590 590
591 591 if stat:
592 592 diffopts = diffopts.copy(context=0)
593 593 width = 80
594 594 if not ui.plain():
595 595 width = ui.termwidth()
596 596 chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
597 597 prefix=prefix)
598 598 for chunk, label in patch.diffstatui(util.iterlines(chunks),
599 599 width=width,
600 600 git=diffopts.git):
601 601 write(chunk, label=label)
602 602 else:
603 603 for chunk, label in patch.diffui(repo, node1, node2, match,
604 604 changes, diffopts, prefix=prefix):
605 605 write(chunk, label=label)
606 606
607 607 if listsubrepos:
608 608 ctx1 = repo[node1]
609 609 ctx2 = repo[node2]
610 610 for subpath, sub in subrepo.itersubrepos(ctx1, ctx2):
611 611 tempnode2 = node2
612 612 try:
613 613 if node2 is not None:
614 614 tempnode2 = ctx2.substate[subpath][1]
615 615 except KeyError:
616 616 # A subrepo that existed in node1 was deleted between node1 and
617 617 # node2 (inclusive). Thus, ctx2's substate won't contain that
618 618 # subpath. The best we can do is to ignore it.
619 619 tempnode2 = None
620 620 submatch = matchmod.narrowmatcher(subpath, match)
621 621 sub.diff(diffopts, tempnode2, submatch, changes=changes,
622 622 stat=stat, fp=fp, prefix=prefix)
623 623
624 624 class changeset_printer(object):
625 625 '''show changeset information when templating not requested.'''
626 626
627 627 def __init__(self, ui, repo, patch, diffopts, buffered):
628 628 self.ui = ui
629 629 self.repo = repo
630 630 self.buffered = buffered
631 631 self.patch = patch
632 632 self.diffopts = diffopts
633 633 self.header = {}
634 634 self.hunk = {}
635 635 self.lastheader = None
636 636 self.footer = None
637 637
638 638 def flush(self, rev):
639 639 if rev in self.header:
640 640 h = self.header[rev]
641 641 if h != self.lastheader:
642 642 self.lastheader = h
643 643 self.ui.write(h)
644 644 del self.header[rev]
645 645 if rev in self.hunk:
646 646 self.ui.write(self.hunk[rev])
647 647 del self.hunk[rev]
648 648 return 1
649 649 return 0
650 650
651 651 def close(self):
652 652 if self.footer:
653 653 self.ui.write(self.footer)
654 654
655 655 def show(self, ctx, copies=None, matchfn=None, **props):
656 656 if self.buffered:
657 657 self.ui.pushbuffer()
658 658 self._show(ctx, copies, matchfn, props)
659 659 self.hunk[ctx.rev()] = self.ui.popbuffer(labeled=True)
660 660 else:
661 661 self._show(ctx, copies, matchfn, props)
662 662
663 663 def _show(self, ctx, copies, matchfn, props):
664 664 '''show a single changeset or file revision'''
665 665 changenode = ctx.node()
666 666 rev = ctx.rev()
667 667
668 668 if self.ui.quiet:
669 669 self.ui.write("%d:%s\n" % (rev, short(changenode)),
670 670 label='log.node')
671 671 return
672 672
673 673 log = self.repo.changelog
674 674 date = util.datestr(ctx.date())
675 675
676 676 hexfunc = self.ui.debugflag and hex or short
677 677
678 678 parents = [(p, hexfunc(log.node(p)))
679 679 for p in self._meaningful_parentrevs(log, rev)]
680 680
681 681 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)),
682 682 label='log.changeset')
683 683
684 684 branch = ctx.branch()
685 685 # don't show the default branch name
686 686 if branch != 'default':
687 687 self.ui.write(_("branch: %s\n") % branch,
688 688 label='log.branch')
689 689 for bookmark in self.repo.nodebookmarks(changenode):
690 690 self.ui.write(_("bookmark: %s\n") % bookmark,
691 691 label='log.bookmark')
692 692 for tag in self.repo.nodetags(changenode):
693 693 self.ui.write(_("tag: %s\n") % tag,
694 694 label='log.tag')
695 695 if self.ui.debugflag and ctx.phase():
696 696 self.ui.write(_("phase: %s\n") % _(ctx.phasestr()),
697 697 label='log.phase')
698 698 for parent in parents:
699 699 self.ui.write(_("parent: %d:%s\n") % parent,
700 700 label='log.parent')
701 701
702 702 if self.ui.debugflag:
703 703 mnode = ctx.manifestnode()
704 704 self.ui.write(_("manifest: %d:%s\n") %
705 705 (self.repo.manifest.rev(mnode), hex(mnode)),
706 706 label='ui.debug log.manifest')
707 707 self.ui.write(_("user: %s\n") % ctx.user(),
708 708 label='log.user')
709 709 self.ui.write(_("date: %s\n") % date,
710 710 label='log.date')
711 711
712 712 if self.ui.debugflag:
713 713 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
714 714 for key, value in zip([_("files:"), _("files+:"), _("files-:")],
715 715 files):
716 716 if value:
717 717 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
718 718 label='ui.debug log.files')
719 719 elif ctx.files() and self.ui.verbose:
720 720 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
721 721 label='ui.note log.files')
722 722 if copies and self.ui.verbose:
723 723 copies = ['%s (%s)' % c for c in copies]
724 724 self.ui.write(_("copies: %s\n") % ' '.join(copies),
725 725 label='ui.note log.copies')
726 726
727 727 extra = ctx.extra()
728 728 if extra and self.ui.debugflag:
729 729 for key, value in sorted(extra.items()):
730 730 self.ui.write(_("extra: %s=%s\n")
731 731 % (key, value.encode('string_escape')),
732 732 label='ui.debug log.extra')
733 733
734 734 description = ctx.description().strip()
735 735 if description:
736 736 if self.ui.verbose:
737 737 self.ui.write(_("description:\n"),
738 738 label='ui.note log.description')
739 739 self.ui.write(description,
740 740 label='ui.note log.description')
741 741 self.ui.write("\n\n")
742 742 else:
743 743 self.ui.write(_("summary: %s\n") %
744 744 description.splitlines()[0],
745 745 label='log.summary')
746 746 self.ui.write("\n")
747 747
748 748 self.showpatch(changenode, matchfn)
749 749
750 750 def showpatch(self, node, matchfn):
751 751 if not matchfn:
752 752 matchfn = self.patch
753 753 if matchfn:
754 754 stat = self.diffopts.get('stat')
755 755 diff = self.diffopts.get('patch')
756 756 diffopts = patch.diffopts(self.ui, self.diffopts)
757 757 prev = self.repo.changelog.parents(node)[0]
758 758 if stat:
759 759 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
760 760 match=matchfn, stat=True)
761 761 if diff:
762 762 if stat:
763 763 self.ui.write("\n")
764 764 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
765 765 match=matchfn, stat=False)
766 766 self.ui.write("\n")
767 767
768 768 def _meaningful_parentrevs(self, log, rev):
769 769 """Return list of meaningful (or all if debug) parentrevs for rev.
770 770
771 771 For merges (two non-nullrev revisions) both parents are meaningful.
772 772 Otherwise the first parent revision is considered meaningful if it
773 773 is not the preceding revision.
774 774 """
775 775 parents = log.parentrevs(rev)
776 776 if not self.ui.debugflag and parents[1] == nullrev:
777 777 if parents[0] >= rev - 1:
778 778 parents = []
779 779 else:
780 780 parents = [parents[0]]
781 781 return parents
782 782
783 783
784 784 class changeset_templater(changeset_printer):
785 785 '''format changeset information.'''
786 786
787 787 def __init__(self, ui, repo, patch, diffopts, mapfile, buffered):
788 788 changeset_printer.__init__(self, ui, repo, patch, diffopts, buffered)
789 789 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
790 790 defaulttempl = {
791 791 'parent': '{rev}:{node|formatnode} ',
792 792 'manifest': '{rev}:{node|formatnode}',
793 793 'file_copy': '{name} ({source})',
794 794 'extra': '{key}={value|stringescape}'
795 795 }
796 796 # filecopy is preserved for compatibility reasons
797 797 defaulttempl['filecopy'] = defaulttempl['file_copy']
798 798 self.t = templater.templater(mapfile, {'formatnode': formatnode},
799 799 cache=defaulttempl)
800 800 self.cache = {}
801 801
802 802 def use_template(self, t):
803 803 '''set template string to use'''
804 804 self.t.cache['changeset'] = t
805 805
806 806 def _meaningful_parentrevs(self, ctx):
807 807 """Return list of meaningful (or all if debug) parentrevs for rev.
808 808 """
809 809 parents = ctx.parents()
810 810 if len(parents) > 1:
811 811 return parents
812 812 if self.ui.debugflag:
813 813 return [parents[0], self.repo['null']]
814 814 if parents[0].rev() >= ctx.rev() - 1:
815 815 return []
816 816 return parents
817 817
818 818 def _show(self, ctx, copies, matchfn, props):
819 819 '''show a single changeset or file revision'''
820 820
821 821 showlist = templatekw.showlist
822 822
823 823 # showparents() behaviour depends on ui trace level which
824 824 # causes unexpected behaviours at templating level and makes
825 825 # it harder to extract it in a standalone function. Its
826 826 # behaviour cannot be changed so leave it here for now.
827 827 def showparents(**args):
828 828 ctx = args['ctx']
829 829 parents = [[('rev', p.rev()), ('node', p.hex())]
830 830 for p in self._meaningful_parentrevs(ctx)]
831 831 return showlist('parent', parents, **args)
832 832
833 833 props = props.copy()
834 834 props.update(templatekw.keywords)
835 835 props['parents'] = showparents
836 836 props['templ'] = self.t
837 837 props['ctx'] = ctx
838 838 props['repo'] = self.repo
839 839 props['revcache'] = {'copies': copies}
840 840 props['cache'] = self.cache
841 841
842 842 # find correct templates for current mode
843 843
844 844 tmplmodes = [
845 845 (True, None),
846 846 (self.ui.verbose, 'verbose'),
847 847 (self.ui.quiet, 'quiet'),
848 848 (self.ui.debugflag, 'debug'),
849 849 ]
850 850
851 851 types = {'header': '', 'footer':'', 'changeset': 'changeset'}
852 852 for mode, postfix in tmplmodes:
853 853 for type in types:
854 854 cur = postfix and ('%s_%s' % (type, postfix)) or type
855 855 if mode and cur in self.t:
856 856 types[type] = cur
857 857
858 858 try:
859 859
860 860 # write header
861 861 if types['header']:
862 862 h = templater.stringify(self.t(types['header'], **props))
863 863 if self.buffered:
864 864 self.header[ctx.rev()] = h
865 865 else:
866 866 if self.lastheader != h:
867 867 self.lastheader = h
868 868 self.ui.write(h)
869 869
870 870 # write changeset metadata, then patch if requested
871 871 key = types['changeset']
872 872 self.ui.write(templater.stringify(self.t(key, **props)))
873 873 self.showpatch(ctx.node(), matchfn)
874 874
875 875 if types['footer']:
876 876 if not self.footer:
877 877 self.footer = templater.stringify(self.t(types['footer'],
878 878 **props))
879 879
880 880 except KeyError, inst:
881 881 msg = _("%s: no key named '%s'")
882 882 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
883 883 except SyntaxError, inst:
884 884 raise util.Abort('%s: %s' % (self.t.mapfile, inst.args[0]))
885 885
886 886 def show_changeset(ui, repo, opts, buffered=False):
887 887 """show one changeset using template or regular display.
888 888
889 889 Display format will be the first non-empty hit of:
890 890 1. option 'template'
891 891 2. option 'style'
892 892 3. [ui] setting 'logtemplate'
893 893 4. [ui] setting 'style'
894 894 If all of these values are either the unset or the empty string,
895 895 regular display via changeset_printer() is done.
896 896 """
897 897 # options
898 898 patch = False
899 899 if opts.get('patch') or opts.get('stat'):
900 900 patch = scmutil.matchall(repo)
901 901
902 902 tmpl = opts.get('template')
903 903 style = None
904 904 if tmpl:
905 905 tmpl = templater.parsestring(tmpl, quoted=False)
906 906 else:
907 907 style = opts.get('style')
908 908
909 909 # ui settings
910 910 if not (tmpl or style):
911 911 tmpl = ui.config('ui', 'logtemplate')
912 912 if tmpl:
913 913 tmpl = templater.parsestring(tmpl)
914 914 else:
915 915 style = util.expandpath(ui.config('ui', 'style', ''))
916 916
917 917 if not (tmpl or style):
918 918 return changeset_printer(ui, repo, patch, opts, buffered)
919 919
920 920 mapfile = None
921 921 if style and not tmpl:
922 922 mapfile = style
923 923 if not os.path.split(mapfile)[0]:
924 924 mapname = (templater.templatepath('map-cmdline.' + mapfile)
925 925 or templater.templatepath(mapfile))
926 926 if mapname:
927 927 mapfile = mapname
928 928
929 929 try:
930 930 t = changeset_templater(ui, repo, patch, opts, mapfile, buffered)
931 931 except SyntaxError, inst:
932 932 raise util.Abort(inst.args[0])
933 933 if tmpl:
934 934 t.use_template(tmpl)
935 935 return t
936 936
937 937 def finddate(ui, repo, date):
938 938 """Find the tipmost changeset that matches the given date spec"""
939 939
940 940 df = util.matchdate(date)
941 941 m = scmutil.matchall(repo)
942 942 results = {}
943 943
944 944 def prep(ctx, fns):
945 945 d = ctx.date()
946 946 if df(d[0]):
947 947 results[ctx.rev()] = d
948 948
949 949 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
950 950 rev = ctx.rev()
951 951 if rev in results:
952 952 ui.status(_("Found revision %s from %s\n") %
953 953 (rev, util.datestr(results[rev])))
954 954 return str(rev)
955 955
956 956 raise util.Abort(_("revision matching date not found"))
957 957
958 958 def walkchangerevs(repo, match, opts, prepare):
959 959 '''Iterate over files and the revs in which they changed.
960 960
961 961 Callers most commonly need to iterate backwards over the history
962 962 in which they are interested. Doing so has awful (quadratic-looking)
963 963 performance, so we use iterators in a "windowed" way.
964 964
965 965 We walk a window of revisions in the desired order. Within the
966 966 window, we first walk forwards to gather data, then in the desired
967 967 order (usually backwards) to display it.
968 968
969 969 This function returns an iterator yielding contexts. Before
970 970 yielding each context, the iterator will first call the prepare
971 971 function on each context in the window in forward order.'''
972 972
973 973 def increasing_windows(start, end, windowsize=8, sizelimit=512):
974 974 if start < end:
975 975 while start < end:
976 976 yield start, min(windowsize, end - start)
977 977 start += windowsize
978 978 if windowsize < sizelimit:
979 979 windowsize *= 2
980 980 else:
981 981 while start > end:
982 982 yield start, min(windowsize, start - end - 1)
983 983 start -= windowsize
984 984 if windowsize < sizelimit:
985 985 windowsize *= 2
986 986
987 987 follow = opts.get('follow') or opts.get('follow_first')
988 988
989 989 if not len(repo):
990 990 return []
991 991
992 992 if follow:
993 993 defrange = '%s:0' % repo['.'].rev()
994 994 else:
995 995 defrange = '-1:0'
996 996 revs = scmutil.revrange(repo, opts['rev'] or [defrange])
997 997 if not revs:
998 998 return []
999 999 wanted = set()
1000 1000 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1001 1001 fncache = {}
1002 1002 change = repo.changectx
1003 1003
1004 1004 # First step is to fill wanted, the set of revisions that we want to yield.
1005 1005 # When it does not induce extra cost, we also fill fncache for revisions in
1006 1006 # wanted: a cache of filenames that were changed (ctx.files()) and that
1007 1007 # match the file filtering conditions.
1008 1008
1009 1009 if not slowpath and not match.files():
1010 1010 # No files, no patterns. Display all revs.
1011 1011 wanted = set(revs)
1012 1012 copies = []
1013 1013
1014 1014 if not slowpath and match.files():
1015 1015 # We only have to read through the filelog to find wanted revisions
1016 1016
1017 1017 minrev, maxrev = min(revs), max(revs)
1018 1018 def filerevgen(filelog, last):
1019 1019 """
1020 1020 Only files, no patterns. Check the history of each file.
1021 1021
1022 1022 Examines filelog entries within minrev, maxrev linkrev range
1023 1023 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1024 1024 tuples in backwards order
1025 1025 """
1026 1026 cl_count = len(repo)
1027 1027 revs = []
1028 1028 for j in xrange(0, last + 1):
1029 1029 linkrev = filelog.linkrev(j)
1030 1030 if linkrev < minrev:
1031 1031 continue
1032 1032 # only yield rev for which we have the changelog, it can
1033 1033 # happen while doing "hg log" during a pull or commit
1034 1034 if linkrev >= cl_count:
1035 1035 break
1036 1036
1037 1037 parentlinkrevs = []
1038 1038 for p in filelog.parentrevs(j):
1039 1039 if p != nullrev:
1040 1040 parentlinkrevs.append(filelog.linkrev(p))
1041 1041 n = filelog.node(j)
1042 1042 revs.append((linkrev, parentlinkrevs,
1043 1043 follow and filelog.renamed(n)))
1044 1044
1045 1045 return reversed(revs)
1046 1046 def iterfiles():
1047 1047 pctx = repo['.']
1048 1048 for filename in match.files():
1049 1049 if follow:
1050 1050 if filename not in pctx:
1051 1051 raise util.Abort(_('cannot follow file not in parent '
1052 1052 'revision: "%s"') % filename)
1053 1053 yield filename, pctx[filename].filenode()
1054 1054 else:
1055 1055 yield filename, None
1056 1056 for filename_node in copies:
1057 1057 yield filename_node
1058 1058 for file_, node in iterfiles():
1059 1059 filelog = repo.file(file_)
1060 1060 if not len(filelog):
1061 1061 if node is None:
1062 1062 # A zero count may be a directory or deleted file, so
1063 1063 # try to find matching entries on the slow path.
1064 1064 if follow:
1065 1065 raise util.Abort(
1066 1066 _('cannot follow nonexistent file: "%s"') % file_)
1067 1067 slowpath = True
1068 1068 break
1069 1069 else:
1070 1070 continue
1071 1071
1072 1072 if node is None:
1073 1073 last = len(filelog) - 1
1074 1074 else:
1075 1075 last = filelog.rev(node)
1076 1076
1077 1077
1078 1078 # keep track of all ancestors of the file
1079 1079 ancestors = set([filelog.linkrev(last)])
1080 1080
1081 1081 # iterate from latest to oldest revision
1082 1082 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1083 1083 if not follow:
1084 1084 if rev > maxrev:
1085 1085 continue
1086 1086 else:
1087 1087 # Note that last might not be the first interesting
1088 1088 # rev to us:
1089 1089 # if the file has been changed after maxrev, we'll
1090 1090 # have linkrev(last) > maxrev, and we still need
1091 1091 # to explore the file graph
1092 1092 if rev not in ancestors:
1093 1093 continue
1094 1094 # XXX insert 1327 fix here
1095 1095 if flparentlinkrevs:
1096 1096 ancestors.update(flparentlinkrevs)
1097 1097
1098 1098 fncache.setdefault(rev, []).append(file_)
1099 1099 wanted.add(rev)
1100 1100 if copied:
1101 1101 copies.append(copied)
1102 1102 if slowpath:
1103 1103 # We have to read the changelog to match filenames against
1104 1104 # changed files
1105 1105
1106 1106 if follow:
1107 1107 raise util.Abort(_('can only follow copies/renames for explicit '
1108 1108 'filenames'))
1109 1109
1110 1110 # The slow path checks files modified in every changeset.
1111 1111 for i in sorted(revs):
1112 1112 ctx = change(i)
1113 1113 matches = filter(match, ctx.files())
1114 1114 if matches:
1115 1115 fncache[i] = matches
1116 1116 wanted.add(i)
1117 1117
1118 1118 class followfilter(object):
1119 1119 def __init__(self, onlyfirst=False):
1120 1120 self.startrev = nullrev
1121 1121 self.roots = set()
1122 1122 self.onlyfirst = onlyfirst
1123 1123
1124 1124 def match(self, rev):
1125 1125 def realparents(rev):
1126 1126 if self.onlyfirst:
1127 1127 return repo.changelog.parentrevs(rev)[0:1]
1128 1128 else:
1129 1129 return filter(lambda x: x != nullrev,
1130 1130 repo.changelog.parentrevs(rev))
1131 1131
1132 1132 if self.startrev == nullrev:
1133 1133 self.startrev = rev
1134 1134 return True
1135 1135
1136 1136 if rev > self.startrev:
1137 1137 # forward: all descendants
1138 1138 if not self.roots:
1139 1139 self.roots.add(self.startrev)
1140 1140 for parent in realparents(rev):
1141 1141 if parent in self.roots:
1142 1142 self.roots.add(rev)
1143 1143 return True
1144 1144 else:
1145 1145 # backwards: all parents
1146 1146 if not self.roots:
1147 1147 self.roots.update(realparents(self.startrev))
1148 1148 if rev in self.roots:
1149 1149 self.roots.remove(rev)
1150 1150 self.roots.update(realparents(rev))
1151 1151 return True
1152 1152
1153 1153 return False
1154 1154
1155 1155 # it might be worthwhile to do this in the iterator if the rev range
1156 1156 # is descending and the prune args are all within that range
1157 1157 for rev in opts.get('prune', ()):
1158 1158 rev = repo[rev].rev()
1159 1159 ff = followfilter()
1160 1160 stop = min(revs[0], revs[-1])
1161 1161 for x in xrange(rev, stop - 1, -1):
1162 1162 if ff.match(x):
1163 1163 wanted.discard(x)
1164 1164
1165 1165 # Now that wanted is correctly initialized, we can iterate over the
1166 1166 # revision range, yielding only revisions in wanted.
1167 1167 def iterate():
1168 1168 if follow and not match.files():
1169 1169 ff = followfilter(onlyfirst=opts.get('follow_first'))
1170 1170 def want(rev):
1171 1171 return ff.match(rev) and rev in wanted
1172 1172 else:
1173 1173 def want(rev):
1174 1174 return rev in wanted
1175 1175
1176 1176 for i, window in increasing_windows(0, len(revs)):
1177 1177 nrevs = [rev for rev in revs[i:i + window] if want(rev)]
1178 1178 for rev in sorted(nrevs):
1179 1179 fns = fncache.get(rev)
1180 1180 ctx = change(rev)
1181 1181 if not fns:
1182 1182 def fns_generator():
1183 1183 for f in ctx.files():
1184 1184 if match(f):
1185 1185 yield f
1186 1186 fns = fns_generator()
1187 1187 prepare(ctx, fns)
1188 1188 for rev in nrevs:
1189 1189 yield change(rev)
1190 1190 return iterate()
1191 1191
1192 1192 def add(ui, repo, match, dryrun, listsubrepos, prefix, explicitonly):
1193 1193 join = lambda f: os.path.join(prefix, f)
1194 1194 bad = []
1195 1195 oldbad = match.bad
1196 1196 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
1197 1197 names = []
1198 1198 wctx = repo[None]
1199 1199 cca = None
1200 1200 abort, warn = scmutil.checkportabilityalert(ui)
1201 1201 if abort or warn:
1202 1202 cca = scmutil.casecollisionauditor(ui, abort, wctx)
1203 1203 for f in repo.walk(match):
1204 1204 exact = match.exact(f)
1205 1205 if exact or not explicitonly and f not in repo.dirstate:
1206 1206 if cca:
1207 1207 cca(f)
1208 1208 names.append(f)
1209 1209 if ui.verbose or not exact:
1210 1210 ui.status(_('adding %s\n') % match.rel(join(f)))
1211 1211
1212 1212 for subpath in wctx.substate:
1213 1213 sub = wctx.sub(subpath)
1214 1214 try:
1215 1215 submatch = matchmod.narrowmatcher(subpath, match)
1216 1216 if listsubrepos:
1217 1217 bad.extend(sub.add(ui, submatch, dryrun, listsubrepos, prefix,
1218 1218 False))
1219 1219 else:
1220 1220 bad.extend(sub.add(ui, submatch, dryrun, listsubrepos, prefix,
1221 1221 True))
1222 1222 except error.LookupError:
1223 1223 ui.status(_("skipping missing subrepository: %s\n")
1224 1224 % join(subpath))
1225 1225
1226 1226 if not dryrun:
1227 1227 rejected = wctx.add(names, prefix)
1228 1228 bad.extend(f for f in rejected if f in match.files())
1229 1229 return bad
1230 1230
1231 1231 def forget(ui, repo, match, prefix, explicitonly):
1232 1232 join = lambda f: os.path.join(prefix, f)
1233 1233 bad = []
1234 1234 oldbad = match.bad
1235 1235 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
1236 1236 wctx = repo[None]
1237 1237 forgot = []
1238 1238 s = repo.status(match=match, clean=True)
1239 1239 forget = sorted(s[0] + s[1] + s[3] + s[6])
1240 1240 if explicitonly:
1241 1241 forget = [f for f in forget if match.exact(f)]
1242 1242
1243 1243 for subpath in wctx.substate:
1244 1244 sub = wctx.sub(subpath)
1245 1245 try:
1246 1246 submatch = matchmod.narrowmatcher(subpath, match)
1247 1247 subbad, subforgot = sub.forget(ui, submatch, prefix)
1248 1248 bad.extend([subpath + '/' + f for f in subbad])
1249 1249 forgot.extend([subpath + '/' + f for f in subforgot])
1250 1250 except error.LookupError:
1251 1251 ui.status(_("skipping missing subrepository: %s\n")
1252 1252 % join(subpath))
1253 1253
1254 1254 if not explicitonly:
1255 1255 for f in match.files():
1256 1256 if f not in repo.dirstate and not os.path.isdir(match.rel(join(f))):
1257 1257 if f not in forgot:
1258 1258 if os.path.exists(match.rel(join(f))):
1259 1259 ui.warn(_('not removing %s: '
1260 1260 'file is already untracked\n')
1261 1261 % match.rel(join(f)))
1262 1262 bad.append(f)
1263 1263
1264 1264 for f in forget:
1265 1265 if ui.verbose or not match.exact(f):
1266 1266 ui.status(_('removing %s\n') % match.rel(join(f)))
1267 1267
1268 1268 rejected = wctx.forget(forget, prefix)
1269 1269 bad.extend(f for f in rejected if f in match.files())
1270 1270 forgot.extend(forget)
1271 1271 return bad, forgot
1272 1272
1273 1273 def duplicatecopies(repo, rev, p1):
1274 1274 "Reproduce copies found in the source revision in the dirstate for grafts"
1275 1275 for dst, src in copies.pathcopies(repo[p1], repo[rev]).iteritems():
1276 1276 repo.dirstate.copy(src, dst)
1277 1277
1278 1278 def commit(ui, repo, commitfunc, pats, opts):
1279 1279 '''commit the specified files or all outstanding changes'''
1280 1280 date = opts.get('date')
1281 1281 if date:
1282 1282 opts['date'] = util.parsedate(date)
1283 1283 message = logmessage(ui, opts)
1284 1284
1285 1285 # extract addremove carefully -- this function can be called from a command
1286 1286 # that doesn't support addremove
1287 1287 if opts.get('addremove'):
1288 1288 scmutil.addremove(repo, pats, opts)
1289 1289
1290 1290 return commitfunc(ui, repo, message,
1291 1291 scmutil.match(repo[None], pats, opts), opts)
1292 1292
1293 1293 def amend(ui, repo, commitfunc, old, extra, pats, opts):
1294 1294 ui.note(_('amending changeset %s\n') % old)
1295 1295 base = old.p1()
1296 1296
1297 1297 wlock = repo.wlock()
1298 1298 try:
1299 # Fix up dirstate for copies and renames
1300 duplicatecopies(repo, None, base.node())
1301
1302 1299 # First, do a regular commit to record all changes in the working
1303 1300 # directory (if there are any)
1304 1301 node = commit(ui, repo, commitfunc, pats, opts)
1305 1302 ctx = repo[node]
1306 1303
1307 1304 # Participating changesets:
1308 1305 #
1309 1306 # node/ctx o - new (intermediate) commit that contains changes from
1310 1307 # | working dir to go into amending commit (or a workingctx
1311 1308 # | if there were no changes)
1312 1309 # |
1313 1310 # old o - changeset to amend
1314 1311 # |
1315 1312 # base o - parent of amending changeset
1316 1313
1317 1314 files = set(old.files())
1318 1315
1319 1316 # Second, we use either the commit we just did, or if there were no
1320 1317 # changes the parent of the working directory as the version of the
1321 1318 # files in the final amend commit
1322 1319 if node:
1323 1320 ui.note(_('copying changeset %s to %s\n') % (ctx, base))
1324 1321
1325 1322 user = ctx.user()
1326 1323 date = ctx.date()
1327 1324 message = ctx.description()
1328 1325 extra = ctx.extra()
1326 # Recompute copies (avoid recording a -> b -> a)
1327 copied = copies.pathcopies(base, ctx)
1329 1328
1330 1329 # Prune files which were reverted by the updates: if old introduced
1331 1330 # file X and our intermediate commit, node, renamed that file, then
1332 1331 # those two files are the same and we can discard X from our list
1333 1332 # of files. Likewise if X was deleted, it's no longer relevant
1334 1333 files.update(ctx.files())
1335 1334
1336 1335 def samefile(f):
1337 1336 if f in ctx.manifest():
1338 1337 a = ctx.filectx(f)
1339 1338 if f in base.manifest():
1340 1339 b = base.filectx(f)
1341 1340 return (a.data() == b.data()
1342 and a.flags() == b.flags()
1343 and a.renamed() == b.renamed())
1341 and a.flags() == b.flags())
1344 1342 else:
1345 1343 return False
1346 1344 else:
1347 1345 return f not in base.manifest()
1348 1346 files = [f for f in files if not samefile(f)]
1349 1347
1350 1348 def filectxfn(repo, ctx_, path):
1351 1349 try:
1352 return ctx.filectx(path)
1350 fctx = ctx[path]
1351 flags = fctx.flags()
1352 mctx = context.memfilectx(fctx.path(), fctx.data(),
1353 islink='l' in flags,
1354 isexec='x' in flags,
1355 copied=copied.get(path))
1356 return mctx
1353 1357 except KeyError:
1354 1358 raise IOError()
1355 1359 else:
1356 1360 ui.note(_('copying changeset %s to %s\n') % (old, base))
1357 1361
1358 1362 # Use version of files as in the old cset
1359 1363 def filectxfn(repo, ctx_, path):
1360 1364 try:
1361 1365 return old.filectx(path)
1362 1366 except KeyError:
1363 1367 raise IOError()
1364 1368
1365 1369 # See if we got a message from -m or -l, if not, open the editor
1366 1370 # with the message of the changeset to amend
1367 1371 user = opts.get('user') or old.user()
1368 1372 date = opts.get('date') or old.date()
1369 1373 message = logmessage(ui, opts)
1370 1374 if not message:
1371 1375 cctx = context.workingctx(repo, old.description(), user, date,
1372 1376 extra,
1373 1377 repo.status(base.node(), old.node()))
1374 1378 message = commitforceeditor(repo, cctx, [])
1375 1379
1376 1380 new = context.memctx(repo,
1377 1381 parents=[base.node(), nullid],
1378 1382 text=message,
1379 1383 files=files,
1380 1384 filectxfn=filectxfn,
1381 1385 user=user,
1382 1386 date=date,
1383 1387 extra=extra)
1384 1388 newid = repo.commitctx(new)
1385 1389 if newid != old.node():
1386 1390 # Reroute the working copy parent to the new changeset
1387 1391 repo.setparents(newid, nullid)
1388 1392
1389 1393 # Move bookmarks from old parent to amend commit
1390 1394 bms = repo.nodebookmarks(old.node())
1391 1395 if bms:
1392 1396 for bm in bms:
1393 1397 repo._bookmarks[bm] = newid
1394 1398 bookmarks.write(repo)
1395 1399
1396 1400 # Strip the intermediate commit (if there was one) and the amended
1397 1401 # commit
1398 1402 lock = repo.lock()
1399 1403 try:
1400 1404 if node:
1401 1405 ui.note(_('stripping intermediate changeset %s\n') % ctx)
1402 1406 ui.note(_('stripping amended changeset %s\n') % old)
1403 1407 repair.strip(ui, repo, old.node(), topic='amend-backup')
1404 1408 finally:
1405 1409 lock.release()
1406 1410 finally:
1407 1411 wlock.release()
1408 1412 return newid
1409 1413
1410 1414 def commiteditor(repo, ctx, subs):
1411 1415 if ctx.description():
1412 1416 return ctx.description()
1413 1417 return commitforceeditor(repo, ctx, subs)
1414 1418
1415 1419 def commitforceeditor(repo, ctx, subs):
1416 1420 edittext = []
1417 1421 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
1418 1422 if ctx.description():
1419 1423 edittext.append(ctx.description())
1420 1424 edittext.append("")
1421 1425 edittext.append("") # Empty line between message and comments.
1422 1426 edittext.append(_("HG: Enter commit message."
1423 1427 " Lines beginning with 'HG:' are removed."))
1424 1428 edittext.append(_("HG: Leave message empty to abort commit."))
1425 1429 edittext.append("HG: --")
1426 1430 edittext.append(_("HG: user: %s") % ctx.user())
1427 1431 if ctx.p2():
1428 1432 edittext.append(_("HG: branch merge"))
1429 1433 if ctx.branch():
1430 1434 edittext.append(_("HG: branch '%s'") % ctx.branch())
1431 1435 edittext.extend([_("HG: subrepo %s") % s for s in subs])
1432 1436 edittext.extend([_("HG: added %s") % f for f in added])
1433 1437 edittext.extend([_("HG: changed %s") % f for f in modified])
1434 1438 edittext.extend([_("HG: removed %s") % f for f in removed])
1435 1439 if not added and not modified and not removed:
1436 1440 edittext.append(_("HG: no files changed"))
1437 1441 edittext.append("")
1438 1442 # run editor in the repository root
1439 1443 olddir = os.getcwd()
1440 1444 os.chdir(repo.root)
1441 1445 text = repo.ui.edit("\n".join(edittext), ctx.user())
1442 1446 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
1443 1447 os.chdir(olddir)
1444 1448
1445 1449 if not text.strip():
1446 1450 raise util.Abort(_("empty commit message"))
1447 1451
1448 1452 return text
1449 1453
1450 1454 def revert(ui, repo, ctx, parents, *pats, **opts):
1451 1455 parent, p2 = parents
1452 1456 node = ctx.node()
1453 1457
1454 1458 mf = ctx.manifest()
1455 1459 if node == parent:
1456 1460 pmf = mf
1457 1461 else:
1458 1462 pmf = None
1459 1463
1460 1464 # need all matching names in dirstate and manifest of target rev,
1461 1465 # so have to walk both. do not print errors if files exist in one
1462 1466 # but not other.
1463 1467
1464 1468 names = {}
1465 1469
1466 1470 wlock = repo.wlock()
1467 1471 try:
1468 1472 # walk dirstate.
1469 1473
1470 1474 m = scmutil.match(repo[None], pats, opts)
1471 1475 m.bad = lambda x, y: False
1472 1476 for abs in repo.walk(m):
1473 1477 names[abs] = m.rel(abs), m.exact(abs)
1474 1478
1475 1479 # walk target manifest.
1476 1480
1477 1481 def badfn(path, msg):
1478 1482 if path in names:
1479 1483 return
1480 1484 if path in repo[node].substate:
1481 1485 return
1482 1486 path_ = path + '/'
1483 1487 for f in names:
1484 1488 if f.startswith(path_):
1485 1489 return
1486 1490 ui.warn("%s: %s\n" % (m.rel(path), msg))
1487 1491
1488 1492 m = scmutil.match(repo[node], pats, opts)
1489 1493 m.bad = badfn
1490 1494 for abs in repo[node].walk(m):
1491 1495 if abs not in names:
1492 1496 names[abs] = m.rel(abs), m.exact(abs)
1493 1497
1494 1498 # get the list of subrepos that must be reverted
1495 1499 targetsubs = [s for s in repo[node].substate if m(s)]
1496 1500 m = scmutil.matchfiles(repo, names)
1497 1501 changes = repo.status(match=m)[:4]
1498 1502 modified, added, removed, deleted = map(set, changes)
1499 1503
1500 1504 # if f is a rename, also revert the source
1501 1505 cwd = repo.getcwd()
1502 1506 for f in added:
1503 1507 src = repo.dirstate.copied(f)
1504 1508 if src and src not in names and repo.dirstate[src] == 'r':
1505 1509 removed.add(src)
1506 1510 names[src] = (repo.pathto(src, cwd), True)
1507 1511
1508 1512 def removeforget(abs):
1509 1513 if repo.dirstate[abs] == 'a':
1510 1514 return _('forgetting %s\n')
1511 1515 return _('removing %s\n')
1512 1516
1513 1517 revert = ([], _('reverting %s\n'))
1514 1518 add = ([], _('adding %s\n'))
1515 1519 remove = ([], removeforget)
1516 1520 undelete = ([], _('undeleting %s\n'))
1517 1521
1518 1522 disptable = (
1519 1523 # dispatch table:
1520 1524 # file state
1521 1525 # action if in target manifest
1522 1526 # action if not in target manifest
1523 1527 # make backup if in target manifest
1524 1528 # make backup if not in target manifest
1525 1529 (modified, revert, remove, True, True),
1526 1530 (added, revert, remove, True, False),
1527 1531 (removed, undelete, None, False, False),
1528 1532 (deleted, revert, remove, False, False),
1529 1533 )
1530 1534
1531 1535 for abs, (rel, exact) in sorted(names.items()):
1532 1536 mfentry = mf.get(abs)
1533 1537 target = repo.wjoin(abs)
1534 1538 def handle(xlist, dobackup):
1535 1539 xlist[0].append(abs)
1536 1540 if (dobackup and not opts.get('no_backup') and
1537 1541 os.path.lexists(target)):
1538 1542 bakname = "%s.orig" % rel
1539 1543 ui.note(_('saving current version of %s as %s\n') %
1540 1544 (rel, bakname))
1541 1545 if not opts.get('dry_run'):
1542 1546 util.rename(target, bakname)
1543 1547 if ui.verbose or not exact:
1544 1548 msg = xlist[1]
1545 1549 if not isinstance(msg, basestring):
1546 1550 msg = msg(abs)
1547 1551 ui.status(msg % rel)
1548 1552 for table, hitlist, misslist, backuphit, backupmiss in disptable:
1549 1553 if abs not in table:
1550 1554 continue
1551 1555 # file has changed in dirstate
1552 1556 if mfentry:
1553 1557 handle(hitlist, backuphit)
1554 1558 elif misslist is not None:
1555 1559 handle(misslist, backupmiss)
1556 1560 break
1557 1561 else:
1558 1562 if abs not in repo.dirstate:
1559 1563 if mfentry:
1560 1564 handle(add, True)
1561 1565 elif exact:
1562 1566 ui.warn(_('file not managed: %s\n') % rel)
1563 1567 continue
1564 1568 # file has not changed in dirstate
1565 1569 if node == parent:
1566 1570 if exact:
1567 1571 ui.warn(_('no changes needed to %s\n') % rel)
1568 1572 continue
1569 1573 if pmf is None:
1570 1574 # only need parent manifest in this unlikely case,
1571 1575 # so do not read by default
1572 1576 pmf = repo[parent].manifest()
1573 1577 if abs in pmf and mfentry:
1574 1578 # if version of file is same in parent and target
1575 1579 # manifests, do nothing
1576 1580 if (pmf[abs] != mfentry or
1577 1581 pmf.flags(abs) != mf.flags(abs)):
1578 1582 handle(revert, False)
1579 1583 else:
1580 1584 handle(remove, False)
1581 1585
1582 1586 if not opts.get('dry_run'):
1583 1587 def checkout(f):
1584 1588 fc = ctx[f]
1585 1589 repo.wwrite(f, fc.data(), fc.flags())
1586 1590
1587 1591 audit_path = scmutil.pathauditor(repo.root)
1588 1592 for f in remove[0]:
1589 1593 if repo.dirstate[f] == 'a':
1590 1594 repo.dirstate.drop(f)
1591 1595 continue
1592 1596 audit_path(f)
1593 1597 try:
1594 1598 util.unlinkpath(repo.wjoin(f))
1595 1599 except OSError:
1596 1600 pass
1597 1601 repo.dirstate.remove(f)
1598 1602
1599 1603 normal = None
1600 1604 if node == parent:
1601 1605 # We're reverting to our parent. If possible, we'd like status
1602 1606 # to report the file as clean. We have to use normallookup for
1603 1607 # merges to avoid losing information about merged/dirty files.
1604 1608 if p2 != nullid:
1605 1609 normal = repo.dirstate.normallookup
1606 1610 else:
1607 1611 normal = repo.dirstate.normal
1608 1612 for f in revert[0]:
1609 1613 checkout(f)
1610 1614 if normal:
1611 1615 normal(f)
1612 1616
1613 1617 for f in add[0]:
1614 1618 checkout(f)
1615 1619 repo.dirstate.add(f)
1616 1620
1617 1621 normal = repo.dirstate.normallookup
1618 1622 if node == parent and p2 == nullid:
1619 1623 normal = repo.dirstate.normal
1620 1624 for f in undelete[0]:
1621 1625 checkout(f)
1622 1626 normal(f)
1623 1627
1624 1628 if targetsubs:
1625 1629 # Revert the subrepos on the revert list
1626 1630 for sub in targetsubs:
1627 1631 ctx.sub(sub).revert(ui, ctx.substate[sub], *pats, **opts)
1628 1632 finally:
1629 1633 wlock.release()
1630 1634
1631 1635 def command(table):
1632 1636 '''returns a function object bound to table which can be used as
1633 1637 a decorator for populating table as a command table'''
1634 1638
1635 1639 def cmd(name, options, synopsis=None):
1636 1640 def decorator(func):
1637 1641 if synopsis:
1638 1642 table[name] = func, options[:], synopsis
1639 1643 else:
1640 1644 table[name] = func, options[:]
1641 1645 return func
1642 1646 return decorator
1643 1647
1644 1648 return cmd
@@ -1,306 +1,318 b''
1 1 $ "$TESTDIR/hghave" execbit || exit 80
2 2
3 3 $ hg init
4 4
5 5 Setup:
6 6
7 7 $ echo a >> a
8 8 $ hg ci -Am 'base'
9 9 adding a
10 10
11 11 Refuse to amend public csets:
12 12
13 13 $ hg phase -r . -p
14 14 $ hg ci --amend
15 15 abort: cannot amend public changesets
16 16 [255]
17 17 $ hg phase -r . -f -d
18 18
19 19 $ echo a >> a
20 20 $ hg ci -Am 'base1'
21 21
22 22 Nothing to amend:
23 23
24 24 $ hg ci --amend
25 25 nothing changed
26 26 [1]
27 27
28 28 Amending changeset with changes in working dir:
29 29
30 30 $ echo a >> a
31 31 $ hg ci --amend -m 'amend base1'
32 32 saved backup bundle to $TESTTMP/.hg/strip-backup/489edb5b847d-amend-backup.hg
33 33 $ hg diff -c .
34 34 diff -r ad120869acf0 -r 9cd25b479c51 a
35 35 --- a/a Thu Jan 01 00:00:00 1970 +0000
36 36 +++ b/a Thu Jan 01 00:00:00 1970 +0000
37 37 @@ -1,1 +1,3 @@
38 38 a
39 39 +a
40 40 +a
41 41 $ hg log
42 42 changeset: 1:9cd25b479c51
43 43 tag: tip
44 44 user: test
45 45 date: Thu Jan 01 00:00:00 1970 +0000
46 46 summary: amend base1
47 47
48 48 changeset: 0:ad120869acf0
49 49 user: test
50 50 date: Thu Jan 01 00:00:00 1970 +0000
51 51 summary: base
52 52
53 53
54 54 Add new file:
55 55
56 56 $ echo b > b
57 57 $ hg ci --amend -Am 'amend base1 new file'
58 58 adding b
59 59 saved backup bundle to $TESTTMP/.hg/strip-backup/9cd25b479c51-amend-backup.hg
60 60
61 61 Remove file that was added in amended commit:
62 62
63 63 $ hg rm b
64 64 $ hg ci --amend -m 'amend base1 remove new file'
65 65 saved backup bundle to $TESTTMP/.hg/strip-backup/e2bb3ecffd2f-amend-backup.hg
66 66
67 67 $ hg cat b
68 68 b: no such file in rev 664a9b2d60cd
69 69 [1]
70 70
71 71 No changes, just a different message:
72 72
73 73 $ hg ci -v --amend -m 'no changes, new message'
74 74 amending changeset 664a9b2d60cd
75 75 copying changeset 664a9b2d60cd to ad120869acf0
76 76 a
77 77 stripping amended changeset 664a9b2d60cd
78 78 1 changesets found
79 79 saved backup bundle to $TESTTMP/.hg/strip-backup/664a9b2d60cd-amend-backup.hg
80 80 1 changesets found
81 81 adding branch
82 82 adding changesets
83 83 adding manifests
84 84 adding file changes
85 85 added 1 changesets with 1 changes to 1 files
86 86 committed changeset 1:ea6e356ff2ad
87 87 $ hg diff -c .
88 88 diff -r ad120869acf0 -r ea6e356ff2ad a
89 89 --- a/a Thu Jan 01 00:00:00 1970 +0000
90 90 +++ b/a Thu Jan 01 00:00:00 1970 +0000
91 91 @@ -1,1 +1,3 @@
92 92 a
93 93 +a
94 94 +a
95 95 $ hg log
96 96 changeset: 1:ea6e356ff2ad
97 97 tag: tip
98 98 user: test
99 99 date: Thu Jan 01 00:00:00 1970 +0000
100 100 summary: no changes, new message
101 101
102 102 changeset: 0:ad120869acf0
103 103 user: test
104 104 date: Thu Jan 01 00:00:00 1970 +0000
105 105 summary: base
106 106
107 107
108 108 Disable default date on commit so when -d isn't given, the old date is preserved:
109 109
110 110 $ echo '[defaults]' >> $HGRCPATH
111 111 $ echo 'commit=' >> $HGRCPATH
112 112
113 113 Test -u/-d:
114 114
115 115 $ hg ci --amend -u foo -d '1 0'
116 116 saved backup bundle to $TESTTMP/.hg/strip-backup/ea6e356ff2ad-amend-backup.hg
117 117 $ echo a >> a
118 118 $ hg ci --amend -u foo -d '1 0'
119 119 saved backup bundle to $TESTTMP/.hg/strip-backup/377b91ce8b56-amend-backup.hg
120 120 $ hg log -r .
121 121 changeset: 1:2c94e4a5756f
122 122 tag: tip
123 123 user: foo
124 124 date: Thu Jan 01 00:00:01 1970 +0000
125 125 summary: no changes, new message
126 126
127 127
128 128 Open editor with old commit message if a message isn't given otherwise:
129 129
130 130 $ cat > editor << '__EOF__'
131 131 > #!/bin/sh
132 132 > cat $1
133 133 > echo "another precious commit message" > "$1"
134 134 > __EOF__
135 135 $ chmod +x editor
136 136 $ HGEDITOR="'`pwd`'"/editor hg commit --amend -v
137 137 amending changeset 2c94e4a5756f
138 138 copying changeset 2c94e4a5756f to ad120869acf0
139 139 no changes, new message
140 140
141 141
142 142 HG: Enter commit message. Lines beginning with 'HG:' are removed.
143 143 HG: Leave message empty to abort commit.
144 144 HG: --
145 145 HG: user: foo
146 146 HG: branch 'default'
147 147 HG: changed a
148 148 a
149 149 stripping amended changeset 2c94e4a5756f
150 150 1 changesets found
151 151 saved backup bundle to $TESTTMP/.hg/strip-backup/2c94e4a5756f-amend-backup.hg
152 152 1 changesets found
153 153 adding branch
154 154 adding changesets
155 155 adding manifests
156 156 adding file changes
157 157 added 1 changesets with 1 changes to 1 files
158 158 committed changeset 1:ffb49186f961
159 159
160 160 Same, but with changes in working dir (different code path):
161 161
162 162 $ echo a >> a
163 163 $ HGEDITOR="'`pwd`'"/editor hg commit --amend -v
164 164 amending changeset ffb49186f961
165 165 another precious commit message
166 166
167 167
168 168 HG: Enter commit message. Lines beginning with 'HG:' are removed.
169 169 HG: Leave message empty to abort commit.
170 170 HG: --
171 171 HG: user: foo
172 172 HG: branch 'default'
173 173 HG: changed a
174 174 a
175 175 copying changeset 27f3aacd3011 to ad120869acf0
176 176 a
177 177 stripping intermediate changeset 27f3aacd3011
178 178 stripping amended changeset ffb49186f961
179 179 2 changesets found
180 180 saved backup bundle to $TESTTMP/.hg/strip-backup/ffb49186f961-amend-backup.hg
181 181 1 changesets found
182 182 adding branch
183 183 adding changesets
184 184 adding manifests
185 185 adding file changes
186 186 added 1 changesets with 1 changes to 1 files
187 187 committed changeset 1:fb6cca43446f
188 188
189 189 $ rm editor
190 190 $ hg log -r .
191 191 changeset: 1:fb6cca43446f
192 192 tag: tip
193 193 user: foo
194 194 date: Thu Jan 01 00:00:01 1970 +0000
195 195 summary: another precious commit message
196 196
197 197
198 198 Moving bookmarks, preserve active bookmark:
199 199
200 200 $ hg book book1
201 201 $ hg book book2
202 202 $ hg ci --amend -m 'move bookmarks'
203 203 saved backup bundle to $TESTTMP/.hg/strip-backup/fb6cca43446f-amend-backup.hg
204 204 $ hg book
205 205 book1 1:0cf1c7a51bcf
206 206 * book2 1:0cf1c7a51bcf
207 207 $ echo a >> a
208 208 $ hg ci --amend -m 'move bookmarks'
209 209 saved backup bundle to $TESTTMP/.hg/strip-backup/0cf1c7a51bcf-amend-backup.hg
210 210 $ hg book
211 211 book1 1:7344472bd951
212 212 * book2 1:7344472bd951
213 213
214 214 $ echo '[defaults]' >> $HGRCPATH
215 215 $ echo "commit=-d '0 0'" >> $HGRCPATH
216 216
217 217 Moving branches:
218 218
219 219 $ hg branch foo
220 220 marked working directory as branch foo
221 221 (branches are permanent and global, did you want a bookmark?)
222 222 $ echo a >> a
223 223 $ hg ci -m 'branch foo'
224 224 $ hg branch default -f
225 225 marked working directory as branch default
226 226 (branches are permanent and global, did you want a bookmark?)
227 227 $ hg ci --amend -m 'back to default'
228 228 saved backup bundle to $TESTTMP/.hg/strip-backup/1661ca36a2db-amend-backup.hg
229 229 $ hg branches
230 230 default 2:f24ee5961967
231 231
232 232 Close branch:
233 233
234 234 $ hg up -q 0
235 235 $ echo b >> b
236 236 $ hg branch foo
237 237 marked working directory as branch foo
238 238 (branches are permanent and global, did you want a bookmark?)
239 239 $ hg ci -Am 'fork'
240 240 adding b
241 241 $ echo b >> b
242 242 $ hg ci -mb
243 243 $ hg ci --amend --close-branch -m 'closing branch foo'
244 244 saved backup bundle to $TESTTMP/.hg/strip-backup/c962248fa264-amend-backup.hg
245 245
246 246 Same thing, different code path:
247 247
248 248 $ echo b >> b
249 249 $ hg ci -m 'reopen branch'
250 250 reopening closed branch head 4
251 251 $ echo b >> b
252 252 $ hg ci --amend --close-branch
253 253 saved backup bundle to $TESTTMP/.hg/strip-backup/5e302dcc12b8-amend-backup.hg
254 254 $ hg branches
255 255 default 2:f24ee5961967
256 256
257 257 Refuse to amend merges:
258 258
259 259 $ hg up -q default
260 260 $ hg merge foo
261 261 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
262 262 (branch merge, don't forget to commit)
263 263 $ hg ci --amend
264 264 abort: cannot amend while merging
265 265 [255]
266 266 $ hg ci -m 'merge'
267 267 $ hg ci --amend
268 268 abort: cannot amend merge changesets
269 269 [255]
270 270
271 271 Follow copies/renames:
272 272
273 273 $ hg mv b c
274 274 $ hg ci -m 'b -> c'
275 275 $ hg mv c d
276 276 $ hg ci --amend -m 'b -> d'
277 277 saved backup bundle to $TESTTMP/.hg/strip-backup/9c207120aa98-amend-backup.hg
278 278 $ hg st --rev '.^' --copies d
279 279 A d
280 280 b
281 281 $ hg cp d e
282 282 $ hg ci -m 'e = d'
283 283 $ hg cp e f
284 284 $ hg ci --amend -m 'f = d'
285 285 saved backup bundle to $TESTTMP/.hg/strip-backup/fda2b3b27b22-amend-backup.hg
286 286 $ hg st --rev '.^' --copies f
287 287 A f
288 288 d
289 289
290 290 $ mv f f.orig
291 291 $ hg rm -A f
292 292 $ hg ci -m removef
293 293 $ hg cp a f
294 294 $ mv f.orig f
295 295 $ hg ci --amend -m replacef
296 saved backup bundle to $TESTTMP/.hg/strip-backup/0ce2c92dc50d-amend-backup.hg
296 saved backup bundle to $TESTTMP/.hg/strip-backup/20a7413547f9-amend-backup.hg
297 297 $ hg st --change . --copies
298 M f
299 298 $ hg log -r . --template "{file_copies}\n"
300 f (a)
299
300
301 Move added file (issue3410):
302
303 $ echo g >> g
304 $ hg ci -Am g
305 adding g
306 $ hg mv g h
307 $ hg ci --amend
308 saved backup bundle to $TESTTMP/.hg/strip-backup/5daa77a5d616-amend-backup.hg
309 $ hg st --change . --copies h
310 A h
311 $ hg log -r . --template "{file_copies}\n"
312
301 313
302 314 Can't rollback an amend:
303 315
304 316 $ hg rollback
305 317 no rollback information available
306 318 [1]
General Comments 0
You need to be logged in to leave comments. Login now