##// END OF EJS Templates
amend: preserve phase of amended revision (issue3602)...
Pierre-Yves David -
r17461:bacde764 stable
parent child Browse files
Show More
@@ -1,1931 +1,1936 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 import subrepo, context, repair, bookmarks, graphmod, revset
13 import subrepo, context, repair, bookmarks, graphmod, revset, phases
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(os.devnull, 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 try:
914 914 tmpl = templater.parsestring(tmpl)
915 915 except SyntaxError:
916 916 tmpl = templater.parsestring(tmpl, quoted=False)
917 917 else:
918 918 style = util.expandpath(ui.config('ui', 'style', ''))
919 919
920 920 if not (tmpl or style):
921 921 return changeset_printer(ui, repo, patch, opts, buffered)
922 922
923 923 mapfile = None
924 924 if style and not tmpl:
925 925 mapfile = style
926 926 if not os.path.split(mapfile)[0]:
927 927 mapname = (templater.templatepath('map-cmdline.' + mapfile)
928 928 or templater.templatepath(mapfile))
929 929 if mapname:
930 930 mapfile = mapname
931 931
932 932 try:
933 933 t = changeset_templater(ui, repo, patch, opts, mapfile, buffered)
934 934 except SyntaxError, inst:
935 935 raise util.Abort(inst.args[0])
936 936 if tmpl:
937 937 t.use_template(tmpl)
938 938 return t
939 939
940 940 def finddate(ui, repo, date):
941 941 """Find the tipmost changeset that matches the given date spec"""
942 942
943 943 df = util.matchdate(date)
944 944 m = scmutil.matchall(repo)
945 945 results = {}
946 946
947 947 def prep(ctx, fns):
948 948 d = ctx.date()
949 949 if df(d[0]):
950 950 results[ctx.rev()] = d
951 951
952 952 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
953 953 rev = ctx.rev()
954 954 if rev in results:
955 955 ui.status(_("found revision %s from %s\n") %
956 956 (rev, util.datestr(results[rev])))
957 957 return str(rev)
958 958
959 959 raise util.Abort(_("revision matching date not found"))
960 960
961 961 def increasingwindows(start, end, windowsize=8, sizelimit=512):
962 962 if start < end:
963 963 while start < end:
964 964 yield start, min(windowsize, end - start)
965 965 start += windowsize
966 966 if windowsize < sizelimit:
967 967 windowsize *= 2
968 968 else:
969 969 while start > end:
970 970 yield start, min(windowsize, start - end - 1)
971 971 start -= windowsize
972 972 if windowsize < sizelimit:
973 973 windowsize *= 2
974 974
975 975 def walkchangerevs(repo, match, opts, prepare):
976 976 '''Iterate over files and the revs in which they changed.
977 977
978 978 Callers most commonly need to iterate backwards over the history
979 979 in which they are interested. Doing so has awful (quadratic-looking)
980 980 performance, so we use iterators in a "windowed" way.
981 981
982 982 We walk a window of revisions in the desired order. Within the
983 983 window, we first walk forwards to gather data, then in the desired
984 984 order (usually backwards) to display it.
985 985
986 986 This function returns an iterator yielding contexts. Before
987 987 yielding each context, the iterator will first call the prepare
988 988 function on each context in the window in forward order.'''
989 989
990 990 follow = opts.get('follow') or opts.get('follow_first')
991 991
992 992 if not len(repo):
993 993 return []
994 994
995 995 if follow:
996 996 defrange = '%s:0' % repo['.'].rev()
997 997 else:
998 998 defrange = '-1:0'
999 999 revs = scmutil.revrange(repo, opts.get('rev') or [defrange])
1000 1000 if not revs:
1001 1001 return []
1002 1002 wanted = set()
1003 1003 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1004 1004 fncache = {}
1005 1005 change = repo.changectx
1006 1006
1007 1007 # First step is to fill wanted, the set of revisions that we want to yield.
1008 1008 # When it does not induce extra cost, we also fill fncache for revisions in
1009 1009 # wanted: a cache of filenames that were changed (ctx.files()) and that
1010 1010 # match the file filtering conditions.
1011 1011
1012 1012 if not slowpath and not match.files():
1013 1013 # No files, no patterns. Display all revs.
1014 1014 wanted = set(revs)
1015 1015 copies = []
1016 1016
1017 1017 if not slowpath and match.files():
1018 1018 # We only have to read through the filelog to find wanted revisions
1019 1019
1020 1020 minrev, maxrev = min(revs), max(revs)
1021 1021 def filerevgen(filelog, last):
1022 1022 """
1023 1023 Only files, no patterns. Check the history of each file.
1024 1024
1025 1025 Examines filelog entries within minrev, maxrev linkrev range
1026 1026 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1027 1027 tuples in backwards order
1028 1028 """
1029 1029 cl_count = len(repo)
1030 1030 revs = []
1031 1031 for j in xrange(0, last + 1):
1032 1032 linkrev = filelog.linkrev(j)
1033 1033 if linkrev < minrev:
1034 1034 continue
1035 1035 # only yield rev for which we have the changelog, it can
1036 1036 # happen while doing "hg log" during a pull or commit
1037 1037 if linkrev >= cl_count:
1038 1038 break
1039 1039
1040 1040 parentlinkrevs = []
1041 1041 for p in filelog.parentrevs(j):
1042 1042 if p != nullrev:
1043 1043 parentlinkrevs.append(filelog.linkrev(p))
1044 1044 n = filelog.node(j)
1045 1045 revs.append((linkrev, parentlinkrevs,
1046 1046 follow and filelog.renamed(n)))
1047 1047
1048 1048 return reversed(revs)
1049 1049 def iterfiles():
1050 1050 pctx = repo['.']
1051 1051 for filename in match.files():
1052 1052 if follow:
1053 1053 if filename not in pctx:
1054 1054 raise util.Abort(_('cannot follow file not in parent '
1055 1055 'revision: "%s"') % filename)
1056 1056 yield filename, pctx[filename].filenode()
1057 1057 else:
1058 1058 yield filename, None
1059 1059 for filename_node in copies:
1060 1060 yield filename_node
1061 1061 for file_, node in iterfiles():
1062 1062 filelog = repo.file(file_)
1063 1063 if not len(filelog):
1064 1064 if node is None:
1065 1065 # A zero count may be a directory or deleted file, so
1066 1066 # try to find matching entries on the slow path.
1067 1067 if follow:
1068 1068 raise util.Abort(
1069 1069 _('cannot follow nonexistent file: "%s"') % file_)
1070 1070 slowpath = True
1071 1071 break
1072 1072 else:
1073 1073 continue
1074 1074
1075 1075 if node is None:
1076 1076 last = len(filelog) - 1
1077 1077 else:
1078 1078 last = filelog.rev(node)
1079 1079
1080 1080
1081 1081 # keep track of all ancestors of the file
1082 1082 ancestors = set([filelog.linkrev(last)])
1083 1083
1084 1084 # iterate from latest to oldest revision
1085 1085 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1086 1086 if not follow:
1087 1087 if rev > maxrev:
1088 1088 continue
1089 1089 else:
1090 1090 # Note that last might not be the first interesting
1091 1091 # rev to us:
1092 1092 # if the file has been changed after maxrev, we'll
1093 1093 # have linkrev(last) > maxrev, and we still need
1094 1094 # to explore the file graph
1095 1095 if rev not in ancestors:
1096 1096 continue
1097 1097 # XXX insert 1327 fix here
1098 1098 if flparentlinkrevs:
1099 1099 ancestors.update(flparentlinkrevs)
1100 1100
1101 1101 fncache.setdefault(rev, []).append(file_)
1102 1102 wanted.add(rev)
1103 1103 if copied:
1104 1104 copies.append(copied)
1105 1105 if slowpath:
1106 1106 # We have to read the changelog to match filenames against
1107 1107 # changed files
1108 1108
1109 1109 if follow:
1110 1110 raise util.Abort(_('can only follow copies/renames for explicit '
1111 1111 'filenames'))
1112 1112
1113 1113 # The slow path checks files modified in every changeset.
1114 1114 for i in sorted(revs):
1115 1115 ctx = change(i)
1116 1116 matches = filter(match, ctx.files())
1117 1117 if matches:
1118 1118 fncache[i] = matches
1119 1119 wanted.add(i)
1120 1120
1121 1121 class followfilter(object):
1122 1122 def __init__(self, onlyfirst=False):
1123 1123 self.startrev = nullrev
1124 1124 self.roots = set()
1125 1125 self.onlyfirst = onlyfirst
1126 1126
1127 1127 def match(self, rev):
1128 1128 def realparents(rev):
1129 1129 if self.onlyfirst:
1130 1130 return repo.changelog.parentrevs(rev)[0:1]
1131 1131 else:
1132 1132 return filter(lambda x: x != nullrev,
1133 1133 repo.changelog.parentrevs(rev))
1134 1134
1135 1135 if self.startrev == nullrev:
1136 1136 self.startrev = rev
1137 1137 return True
1138 1138
1139 1139 if rev > self.startrev:
1140 1140 # forward: all descendants
1141 1141 if not self.roots:
1142 1142 self.roots.add(self.startrev)
1143 1143 for parent in realparents(rev):
1144 1144 if parent in self.roots:
1145 1145 self.roots.add(rev)
1146 1146 return True
1147 1147 else:
1148 1148 # backwards: all parents
1149 1149 if not self.roots:
1150 1150 self.roots.update(realparents(self.startrev))
1151 1151 if rev in self.roots:
1152 1152 self.roots.remove(rev)
1153 1153 self.roots.update(realparents(rev))
1154 1154 return True
1155 1155
1156 1156 return False
1157 1157
1158 1158 # it might be worthwhile to do this in the iterator if the rev range
1159 1159 # is descending and the prune args are all within that range
1160 1160 for rev in opts.get('prune', ()):
1161 1161 rev = repo[rev].rev()
1162 1162 ff = followfilter()
1163 1163 stop = min(revs[0], revs[-1])
1164 1164 for x in xrange(rev, stop - 1, -1):
1165 1165 if ff.match(x):
1166 1166 wanted.discard(x)
1167 1167
1168 1168 # Now that wanted is correctly initialized, we can iterate over the
1169 1169 # revision range, yielding only revisions in wanted.
1170 1170 def iterate():
1171 1171 if follow and not match.files():
1172 1172 ff = followfilter(onlyfirst=opts.get('follow_first'))
1173 1173 def want(rev):
1174 1174 return ff.match(rev) and rev in wanted
1175 1175 else:
1176 1176 def want(rev):
1177 1177 return rev in wanted
1178 1178
1179 1179 for i, window in increasingwindows(0, len(revs)):
1180 1180 nrevs = [rev for rev in revs[i:i + window] if want(rev)]
1181 1181 for rev in sorted(nrevs):
1182 1182 fns = fncache.get(rev)
1183 1183 ctx = change(rev)
1184 1184 if not fns:
1185 1185 def fns_generator():
1186 1186 for f in ctx.files():
1187 1187 if match(f):
1188 1188 yield f
1189 1189 fns = fns_generator()
1190 1190 prepare(ctx, fns)
1191 1191 for rev in nrevs:
1192 1192 yield change(rev)
1193 1193 return iterate()
1194 1194
1195 1195 def _makegraphfilematcher(repo, pats, followfirst):
1196 1196 # When displaying a revision with --patch --follow FILE, we have
1197 1197 # to know which file of the revision must be diffed. With
1198 1198 # --follow, we want the names of the ancestors of FILE in the
1199 1199 # revision, stored in "fcache". "fcache" is populated by
1200 1200 # reproducing the graph traversal already done by --follow revset
1201 1201 # and relating linkrevs to file names (which is not "correct" but
1202 1202 # good enough).
1203 1203 fcache = {}
1204 1204 fcacheready = [False]
1205 1205 pctx = repo['.']
1206 1206 wctx = repo[None]
1207 1207
1208 1208 def populate():
1209 1209 for fn in pats:
1210 1210 for i in ((pctx[fn],), pctx[fn].ancestors(followfirst=followfirst)):
1211 1211 for c in i:
1212 1212 fcache.setdefault(c.linkrev(), set()).add(c.path())
1213 1213
1214 1214 def filematcher(rev):
1215 1215 if not fcacheready[0]:
1216 1216 # Lazy initialization
1217 1217 fcacheready[0] = True
1218 1218 populate()
1219 1219 return scmutil.match(wctx, fcache.get(rev, []), default='path')
1220 1220
1221 1221 return filematcher
1222 1222
1223 1223 def _makegraphlogrevset(repo, pats, opts, revs):
1224 1224 """Return (expr, filematcher) where expr is a revset string built
1225 1225 from log options and file patterns or None. If --stat or --patch
1226 1226 are not passed filematcher is None. Otherwise it is a callable
1227 1227 taking a revision number and returning a match objects filtering
1228 1228 the files to be detailed when displaying the revision.
1229 1229 """
1230 1230 opt2revset = {
1231 1231 'no_merges': ('not merge()', None),
1232 1232 'only_merges': ('merge()', None),
1233 1233 '_ancestors': ('ancestors(%(val)s)', None),
1234 1234 '_fancestors': ('_firstancestors(%(val)s)', None),
1235 1235 '_descendants': ('descendants(%(val)s)', None),
1236 1236 '_fdescendants': ('_firstdescendants(%(val)s)', None),
1237 1237 '_matchfiles': ('_matchfiles(%(val)s)', None),
1238 1238 'date': ('date(%(val)r)', None),
1239 1239 'branch': ('branch(%(val)r)', ' or '),
1240 1240 '_patslog': ('filelog(%(val)r)', ' or '),
1241 1241 '_patsfollow': ('follow(%(val)r)', ' or '),
1242 1242 '_patsfollowfirst': ('_followfirst(%(val)r)', ' or '),
1243 1243 'keyword': ('keyword(%(val)r)', ' or '),
1244 1244 'prune': ('not (%(val)r or ancestors(%(val)r))', ' and '),
1245 1245 'user': ('user(%(val)r)', ' or '),
1246 1246 }
1247 1247
1248 1248 opts = dict(opts)
1249 1249 # follow or not follow?
1250 1250 follow = opts.get('follow') or opts.get('follow_first')
1251 1251 followfirst = opts.get('follow_first') and 1 or 0
1252 1252 # --follow with FILE behaviour depends on revs...
1253 1253 startrev = revs[0]
1254 1254 followdescendants = (len(revs) > 1 and revs[0] < revs[1]) and 1 or 0
1255 1255
1256 1256 # branch and only_branch are really aliases and must be handled at
1257 1257 # the same time
1258 1258 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
1259 1259 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
1260 1260 # pats/include/exclude are passed to match.match() directly in
1261 1261 # _matchfile() revset but walkchangerevs() builds its matcher with
1262 1262 # scmutil.match(). The difference is input pats are globbed on
1263 1263 # platforms without shell expansion (windows).
1264 1264 pctx = repo[None]
1265 1265 match, pats = scmutil.matchandpats(pctx, pats, opts)
1266 1266 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1267 1267 if not slowpath:
1268 1268 for f in match.files():
1269 1269 if follow and f not in pctx:
1270 1270 raise util.Abort(_('cannot follow file not in parent '
1271 1271 'revision: "%s"') % f)
1272 1272 filelog = repo.file(f)
1273 1273 if not len(filelog):
1274 1274 # A zero count may be a directory or deleted file, so
1275 1275 # try to find matching entries on the slow path.
1276 1276 if follow:
1277 1277 raise util.Abort(
1278 1278 _('cannot follow nonexistent file: "%s"') % f)
1279 1279 slowpath = True
1280 1280 if slowpath:
1281 1281 # See walkchangerevs() slow path.
1282 1282 #
1283 1283 if follow:
1284 1284 raise util.Abort(_('can only follow copies/renames for explicit '
1285 1285 'filenames'))
1286 1286 # pats/include/exclude cannot be represented as separate
1287 1287 # revset expressions as their filtering logic applies at file
1288 1288 # level. For instance "-I a -X a" matches a revision touching
1289 1289 # "a" and "b" while "file(a) and not file(b)" does
1290 1290 # not. Besides, filesets are evaluated against the working
1291 1291 # directory.
1292 1292 matchargs = ['r:', 'd:relpath']
1293 1293 for p in pats:
1294 1294 matchargs.append('p:' + p)
1295 1295 for p in opts.get('include', []):
1296 1296 matchargs.append('i:' + p)
1297 1297 for p in opts.get('exclude', []):
1298 1298 matchargs.append('x:' + p)
1299 1299 matchargs = ','.join(('%r' % p) for p in matchargs)
1300 1300 opts['_matchfiles'] = matchargs
1301 1301 else:
1302 1302 if follow:
1303 1303 fpats = ('_patsfollow', '_patsfollowfirst')
1304 1304 fnopats = (('_ancestors', '_fancestors'),
1305 1305 ('_descendants', '_fdescendants'))
1306 1306 if pats:
1307 1307 # follow() revset inteprets its file argument as a
1308 1308 # manifest entry, so use match.files(), not pats.
1309 1309 opts[fpats[followfirst]] = list(match.files())
1310 1310 else:
1311 1311 opts[fnopats[followdescendants][followfirst]] = str(startrev)
1312 1312 else:
1313 1313 opts['_patslog'] = list(pats)
1314 1314
1315 1315 filematcher = None
1316 1316 if opts.get('patch') or opts.get('stat'):
1317 1317 if follow:
1318 1318 filematcher = _makegraphfilematcher(repo, pats, followfirst)
1319 1319 else:
1320 1320 filematcher = lambda rev: match
1321 1321
1322 1322 expr = []
1323 1323 for op, val in opts.iteritems():
1324 1324 if not val:
1325 1325 continue
1326 1326 if op not in opt2revset:
1327 1327 continue
1328 1328 revop, andor = opt2revset[op]
1329 1329 if '%(val)' not in revop:
1330 1330 expr.append(revop)
1331 1331 else:
1332 1332 if not isinstance(val, list):
1333 1333 e = revop % {'val': val}
1334 1334 else:
1335 1335 e = '(' + andor.join((revop % {'val': v}) for v in val) + ')'
1336 1336 expr.append(e)
1337 1337
1338 1338 if expr:
1339 1339 expr = '(' + ' and '.join(expr) + ')'
1340 1340 else:
1341 1341 expr = None
1342 1342 return expr, filematcher
1343 1343
1344 1344 def getgraphlogrevs(repo, pats, opts):
1345 1345 """Return (revs, expr, filematcher) where revs is an iterable of
1346 1346 revision numbers, expr is a revset string built from log options
1347 1347 and file patterns or None, and used to filter 'revs'. If --stat or
1348 1348 --patch are not passed filematcher is None. Otherwise it is a
1349 1349 callable taking a revision number and returning a match objects
1350 1350 filtering the files to be detailed when displaying the revision.
1351 1351 """
1352 1352 def increasingrevs(repo, revs, matcher):
1353 1353 # The sorted input rev sequence is chopped in sub-sequences
1354 1354 # which are sorted in ascending order and passed to the
1355 1355 # matcher. The filtered revs are sorted again as they were in
1356 1356 # the original sub-sequence. This achieve several things:
1357 1357 #
1358 1358 # - getlogrevs() now returns a generator which behaviour is
1359 1359 # adapted to log need. First results come fast, last ones
1360 1360 # are batched for performances.
1361 1361 #
1362 1362 # - revset matchers often operate faster on revision in
1363 1363 # changelog order, because most filters deal with the
1364 1364 # changelog.
1365 1365 #
1366 1366 # - revset matchers can reorder revisions. "A or B" typically
1367 1367 # returns returns the revision matching A then the revision
1368 1368 # matching B. We want to hide this internal implementation
1369 1369 # detail from the caller, and sorting the filtered revision
1370 1370 # again achieves this.
1371 1371 for i, window in increasingwindows(0, len(revs), windowsize=1):
1372 1372 orevs = revs[i:i + window]
1373 1373 nrevs = set(matcher(repo, sorted(orevs)))
1374 1374 for rev in orevs:
1375 1375 if rev in nrevs:
1376 1376 yield rev
1377 1377
1378 1378 if not len(repo):
1379 1379 return iter([]), None, None
1380 1380 # Default --rev value depends on --follow but --follow behaviour
1381 1381 # depends on revisions resolved from --rev...
1382 1382 follow = opts.get('follow') or opts.get('follow_first')
1383 1383 if opts.get('rev'):
1384 1384 revs = scmutil.revrange(repo, opts['rev'])
1385 1385 else:
1386 1386 if follow and len(repo) > 0:
1387 1387 revs = scmutil.revrange(repo, ['.:0'])
1388 1388 else:
1389 1389 revs = range(len(repo) - 1, -1, -1)
1390 1390 if not revs:
1391 1391 return iter([]), None, None
1392 1392 expr, filematcher = _makegraphlogrevset(repo, pats, opts, revs)
1393 1393 if expr:
1394 1394 matcher = revset.match(repo.ui, expr)
1395 1395 revs = increasingrevs(repo, revs, matcher)
1396 1396 if not opts.get('hidden'):
1397 1397 # --hidden is still experimental and not worth a dedicated revset
1398 1398 # yet. Fortunately, filtering revision number is fast.
1399 1399 revs = (r for r in revs if r not in repo.hiddenrevs)
1400 1400 else:
1401 1401 revs = iter(revs)
1402 1402 return revs, expr, filematcher
1403 1403
1404 1404 def displaygraph(ui, dag, displayer, showparents, edgefn, getrenamed=None,
1405 1405 filematcher=None):
1406 1406 seen, state = [], graphmod.asciistate()
1407 1407 for rev, type, ctx, parents in dag:
1408 1408 char = 'o'
1409 1409 if ctx.node() in showparents:
1410 1410 char = '@'
1411 1411 elif ctx.obsolete():
1412 1412 char = 'x'
1413 1413 copies = None
1414 1414 if getrenamed and ctx.rev():
1415 1415 copies = []
1416 1416 for fn in ctx.files():
1417 1417 rename = getrenamed(fn, ctx.rev())
1418 1418 if rename:
1419 1419 copies.append((fn, rename[0]))
1420 1420 revmatchfn = None
1421 1421 if filematcher is not None:
1422 1422 revmatchfn = filematcher(ctx.rev())
1423 1423 displayer.show(ctx, copies=copies, matchfn=revmatchfn)
1424 1424 lines = displayer.hunk.pop(rev).split('\n')
1425 1425 if not lines[-1]:
1426 1426 del lines[-1]
1427 1427 displayer.flush(rev)
1428 1428 edges = edgefn(type, char, lines, seen, rev, parents)
1429 1429 for type, char, lines, coldata in edges:
1430 1430 graphmod.ascii(ui, state, type, char, lines, coldata)
1431 1431 displayer.close()
1432 1432
1433 1433 def graphlog(ui, repo, *pats, **opts):
1434 1434 # Parameters are identical to log command ones
1435 1435 revs, expr, filematcher = getgraphlogrevs(repo, pats, opts)
1436 1436 revs = sorted(revs, reverse=1)
1437 1437 limit = loglimit(opts)
1438 1438 if limit is not None:
1439 1439 revs = revs[:limit]
1440 1440 revdag = graphmod.dagwalker(repo, revs)
1441 1441
1442 1442 getrenamed = None
1443 1443 if opts.get('copies'):
1444 1444 endrev = None
1445 1445 if opts.get('rev'):
1446 1446 endrev = max(scmutil.revrange(repo, opts.get('rev'))) + 1
1447 1447 getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
1448 1448 displayer = show_changeset(ui, repo, opts, buffered=True)
1449 1449 showparents = [ctx.node() for ctx in repo[None].parents()]
1450 1450 displaygraph(ui, revdag, displayer, showparents,
1451 1451 graphmod.asciiedges, getrenamed, filematcher)
1452 1452
1453 1453 def checkunsupportedgraphflags(pats, opts):
1454 1454 for op in ["newest_first"]:
1455 1455 if op in opts and opts[op]:
1456 1456 raise util.Abort(_("-G/--graph option is incompatible with --%s")
1457 1457 % op.replace("_", "-"))
1458 1458
1459 1459 def graphrevs(repo, nodes, opts):
1460 1460 limit = loglimit(opts)
1461 1461 nodes.reverse()
1462 1462 if limit is not None:
1463 1463 nodes = nodes[:limit]
1464 1464 return graphmod.nodes(repo, nodes)
1465 1465
1466 1466 def add(ui, repo, match, dryrun, listsubrepos, prefix, explicitonly):
1467 1467 join = lambda f: os.path.join(prefix, f)
1468 1468 bad = []
1469 1469 oldbad = match.bad
1470 1470 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
1471 1471 names = []
1472 1472 wctx = repo[None]
1473 1473 cca = None
1474 1474 abort, warn = scmutil.checkportabilityalert(ui)
1475 1475 if abort or warn:
1476 1476 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
1477 1477 for f in repo.walk(match):
1478 1478 exact = match.exact(f)
1479 1479 if exact or not explicitonly and f not in repo.dirstate:
1480 1480 if cca:
1481 1481 cca(f)
1482 1482 names.append(f)
1483 1483 if ui.verbose or not exact:
1484 1484 ui.status(_('adding %s\n') % match.rel(join(f)))
1485 1485
1486 1486 for subpath in wctx.substate:
1487 1487 sub = wctx.sub(subpath)
1488 1488 try:
1489 1489 submatch = matchmod.narrowmatcher(subpath, match)
1490 1490 if listsubrepos:
1491 1491 bad.extend(sub.add(ui, submatch, dryrun, listsubrepos, prefix,
1492 1492 False))
1493 1493 else:
1494 1494 bad.extend(sub.add(ui, submatch, dryrun, listsubrepos, prefix,
1495 1495 True))
1496 1496 except error.LookupError:
1497 1497 ui.status(_("skipping missing subrepository: %s\n")
1498 1498 % join(subpath))
1499 1499
1500 1500 if not dryrun:
1501 1501 rejected = wctx.add(names, prefix)
1502 1502 bad.extend(f for f in rejected if f in match.files())
1503 1503 return bad
1504 1504
1505 1505 def forget(ui, repo, match, prefix, explicitonly):
1506 1506 join = lambda f: os.path.join(prefix, f)
1507 1507 bad = []
1508 1508 oldbad = match.bad
1509 1509 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
1510 1510 wctx = repo[None]
1511 1511 forgot = []
1512 1512 s = repo.status(match=match, clean=True)
1513 1513 forget = sorted(s[0] + s[1] + s[3] + s[6])
1514 1514 if explicitonly:
1515 1515 forget = [f for f in forget if match.exact(f)]
1516 1516
1517 1517 for subpath in wctx.substate:
1518 1518 sub = wctx.sub(subpath)
1519 1519 try:
1520 1520 submatch = matchmod.narrowmatcher(subpath, match)
1521 1521 subbad, subforgot = sub.forget(ui, submatch, prefix)
1522 1522 bad.extend([subpath + '/' + f for f in subbad])
1523 1523 forgot.extend([subpath + '/' + f for f in subforgot])
1524 1524 except error.LookupError:
1525 1525 ui.status(_("skipping missing subrepository: %s\n")
1526 1526 % join(subpath))
1527 1527
1528 1528 if not explicitonly:
1529 1529 for f in match.files():
1530 1530 if f not in repo.dirstate and not os.path.isdir(match.rel(join(f))):
1531 1531 if f not in forgot:
1532 1532 if os.path.exists(match.rel(join(f))):
1533 1533 ui.warn(_('not removing %s: '
1534 1534 'file is already untracked\n')
1535 1535 % match.rel(join(f)))
1536 1536 bad.append(f)
1537 1537
1538 1538 for f in forget:
1539 1539 if ui.verbose or not match.exact(f):
1540 1540 ui.status(_('removing %s\n') % match.rel(join(f)))
1541 1541
1542 1542 rejected = wctx.forget(forget, prefix)
1543 1543 bad.extend(f for f in rejected if f in match.files())
1544 1544 forgot.extend(forget)
1545 1545 return bad, forgot
1546 1546
1547 1547 def duplicatecopies(repo, rev, p1):
1548 1548 "Reproduce copies found in the source revision in the dirstate for grafts"
1549 1549 for dst, src in copies.pathcopies(repo[p1], repo[rev]).iteritems():
1550 1550 repo.dirstate.copy(src, dst)
1551 1551
1552 1552 def commit(ui, repo, commitfunc, pats, opts):
1553 1553 '''commit the specified files or all outstanding changes'''
1554 1554 date = opts.get('date')
1555 1555 if date:
1556 1556 opts['date'] = util.parsedate(date)
1557 1557 message = logmessage(ui, opts)
1558 1558
1559 1559 # extract addremove carefully -- this function can be called from a command
1560 1560 # that doesn't support addremove
1561 1561 if opts.get('addremove'):
1562 1562 scmutil.addremove(repo, pats, opts)
1563 1563
1564 1564 return commitfunc(ui, repo, message,
1565 1565 scmutil.match(repo[None], pats, opts), opts)
1566 1566
1567 1567 def amend(ui, repo, commitfunc, old, extra, pats, opts):
1568 1568 ui.note(_('amending changeset %s\n') % old)
1569 1569 base = old.p1()
1570 1570
1571 1571 wlock = repo.wlock()
1572 1572 try:
1573 1573 # First, do a regular commit to record all changes in the working
1574 1574 # directory (if there are any)
1575 1575 ui.callhooks = False
1576 1576 try:
1577 1577 node = commit(ui, repo, commitfunc, pats, opts)
1578 1578 finally:
1579 1579 ui.callhooks = True
1580 1580 ctx = repo[node]
1581 1581
1582 1582 # Participating changesets:
1583 1583 #
1584 1584 # node/ctx o - new (intermediate) commit that contains changes from
1585 1585 # | working dir to go into amending commit (or a workingctx
1586 1586 # | if there were no changes)
1587 1587 # |
1588 1588 # old o - changeset to amend
1589 1589 # |
1590 1590 # base o - parent of amending changeset
1591 1591
1592 1592 # Update extra dict from amended commit (e.g. to preserve graft source)
1593 1593 extra.update(old.extra())
1594 1594
1595 1595 # Also update it from the intermediate commit or from the wctx
1596 1596 extra.update(ctx.extra())
1597 1597
1598 1598 files = set(old.files())
1599 1599
1600 1600 # Second, we use either the commit we just did, or if there were no
1601 1601 # changes the parent of the working directory as the version of the
1602 1602 # files in the final amend commit
1603 1603 if node:
1604 1604 ui.note(_('copying changeset %s to %s\n') % (ctx, base))
1605 1605
1606 1606 user = ctx.user()
1607 1607 date = ctx.date()
1608 1608 message = ctx.description()
1609 1609 # Recompute copies (avoid recording a -> b -> a)
1610 1610 copied = copies.pathcopies(base, ctx)
1611 1611
1612 1612 # Prune files which were reverted by the updates: if old introduced
1613 1613 # file X and our intermediate commit, node, renamed that file, then
1614 1614 # those two files are the same and we can discard X from our list
1615 1615 # of files. Likewise if X was deleted, it's no longer relevant
1616 1616 files.update(ctx.files())
1617 1617
1618 1618 def samefile(f):
1619 1619 if f in ctx.manifest():
1620 1620 a = ctx.filectx(f)
1621 1621 if f in base.manifest():
1622 1622 b = base.filectx(f)
1623 1623 return (not a.cmp(b)
1624 1624 and a.flags() == b.flags())
1625 1625 else:
1626 1626 return False
1627 1627 else:
1628 1628 return f not in base.manifest()
1629 1629 files = [f for f in files if not samefile(f)]
1630 1630
1631 1631 def filectxfn(repo, ctx_, path):
1632 1632 try:
1633 1633 fctx = ctx[path]
1634 1634 flags = fctx.flags()
1635 1635 mctx = context.memfilectx(fctx.path(), fctx.data(),
1636 1636 islink='l' in flags,
1637 1637 isexec='x' in flags,
1638 1638 copied=copied.get(path))
1639 1639 return mctx
1640 1640 except KeyError:
1641 1641 raise IOError
1642 1642 else:
1643 1643 ui.note(_('copying changeset %s to %s\n') % (old, base))
1644 1644
1645 1645 # Use version of files as in the old cset
1646 1646 def filectxfn(repo, ctx_, path):
1647 1647 try:
1648 1648 return old.filectx(path)
1649 1649 except KeyError:
1650 1650 raise IOError
1651 1651
1652 1652 # See if we got a message from -m or -l, if not, open the editor
1653 1653 # with the message of the changeset to amend
1654 1654 user = opts.get('user') or old.user()
1655 1655 date = opts.get('date') or old.date()
1656 1656 message = logmessage(ui, opts)
1657 1657 if not message:
1658 1658 cctx = context.workingctx(repo, old.description(), user, date,
1659 1659 extra,
1660 1660 repo.status(base.node(), old.node()))
1661 1661 message = commitforceeditor(repo, cctx, [])
1662 1662
1663 1663 new = context.memctx(repo,
1664 1664 parents=[base.node(), nullid],
1665 1665 text=message,
1666 1666 files=files,
1667 1667 filectxfn=filectxfn,
1668 1668 user=user,
1669 1669 date=date,
1670 1670 extra=extra)
1671 newid = repo.commitctx(new)
1671 ph = repo.ui.config('phases', 'new-commit', phases.draft)
1672 try:
1673 repo.ui.setconfig('phases', 'new-commit', old.phase())
1674 newid = repo.commitctx(new)
1675 finally:
1676 repo.ui.setconfig('phases', 'new-commit', ph)
1672 1677 if newid != old.node():
1673 1678 # Reroute the working copy parent to the new changeset
1674 1679 repo.setparents(newid, nullid)
1675 1680
1676 1681 # Move bookmarks from old parent to amend commit
1677 1682 bms = repo.nodebookmarks(old.node())
1678 1683 if bms:
1679 1684 for bm in bms:
1680 1685 repo._bookmarks[bm] = newid
1681 1686 bookmarks.write(repo)
1682 1687
1683 1688 # Strip the intermediate commit (if there was one) and the amended
1684 1689 # commit
1685 1690 lock = repo.lock()
1686 1691 try:
1687 1692 if node:
1688 1693 ui.note(_('stripping intermediate changeset %s\n') % ctx)
1689 1694 ui.note(_('stripping amended changeset %s\n') % old)
1690 1695 repair.strip(ui, repo, old.node(), topic='amend-backup')
1691 1696 finally:
1692 1697 lock.release()
1693 1698 finally:
1694 1699 wlock.release()
1695 1700 return newid
1696 1701
1697 1702 def commiteditor(repo, ctx, subs):
1698 1703 if ctx.description():
1699 1704 return ctx.description()
1700 1705 return commitforceeditor(repo, ctx, subs)
1701 1706
1702 1707 def commitforceeditor(repo, ctx, subs):
1703 1708 edittext = []
1704 1709 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
1705 1710 if ctx.description():
1706 1711 edittext.append(ctx.description())
1707 1712 edittext.append("")
1708 1713 edittext.append("") # Empty line between message and comments.
1709 1714 edittext.append(_("HG: Enter commit message."
1710 1715 " Lines beginning with 'HG:' are removed."))
1711 1716 edittext.append(_("HG: Leave message empty to abort commit."))
1712 1717 edittext.append("HG: --")
1713 1718 edittext.append(_("HG: user: %s") % ctx.user())
1714 1719 if ctx.p2():
1715 1720 edittext.append(_("HG: branch merge"))
1716 1721 if ctx.branch():
1717 1722 edittext.append(_("HG: branch '%s'") % ctx.branch())
1718 1723 edittext.extend([_("HG: subrepo %s") % s for s in subs])
1719 1724 edittext.extend([_("HG: added %s") % f for f in added])
1720 1725 edittext.extend([_("HG: changed %s") % f for f in modified])
1721 1726 edittext.extend([_("HG: removed %s") % f for f in removed])
1722 1727 if not added and not modified and not removed:
1723 1728 edittext.append(_("HG: no files changed"))
1724 1729 edittext.append("")
1725 1730 # run editor in the repository root
1726 1731 olddir = os.getcwd()
1727 1732 os.chdir(repo.root)
1728 1733 text = repo.ui.edit("\n".join(edittext), ctx.user())
1729 1734 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
1730 1735 os.chdir(olddir)
1731 1736
1732 1737 if not text.strip():
1733 1738 raise util.Abort(_("empty commit message"))
1734 1739
1735 1740 return text
1736 1741
1737 1742 def revert(ui, repo, ctx, parents, *pats, **opts):
1738 1743 parent, p2 = parents
1739 1744 node = ctx.node()
1740 1745
1741 1746 mf = ctx.manifest()
1742 1747 if node == parent:
1743 1748 pmf = mf
1744 1749 else:
1745 1750 pmf = None
1746 1751
1747 1752 # need all matching names in dirstate and manifest of target rev,
1748 1753 # so have to walk both. do not print errors if files exist in one
1749 1754 # but not other.
1750 1755
1751 1756 names = {}
1752 1757
1753 1758 wlock = repo.wlock()
1754 1759 try:
1755 1760 # walk dirstate.
1756 1761
1757 1762 m = scmutil.match(repo[None], pats, opts)
1758 1763 m.bad = lambda x, y: False
1759 1764 for abs in repo.walk(m):
1760 1765 names[abs] = m.rel(abs), m.exact(abs)
1761 1766
1762 1767 # walk target manifest.
1763 1768
1764 1769 def badfn(path, msg):
1765 1770 if path in names:
1766 1771 return
1767 1772 if path in ctx.substate:
1768 1773 return
1769 1774 path_ = path + '/'
1770 1775 for f in names:
1771 1776 if f.startswith(path_):
1772 1777 return
1773 1778 ui.warn("%s: %s\n" % (m.rel(path), msg))
1774 1779
1775 1780 m = scmutil.match(ctx, pats, opts)
1776 1781 m.bad = badfn
1777 1782 for abs in ctx.walk(m):
1778 1783 if abs not in names:
1779 1784 names[abs] = m.rel(abs), m.exact(abs)
1780 1785
1781 1786 # get the list of subrepos that must be reverted
1782 1787 targetsubs = [s for s in ctx.substate if m(s)]
1783 1788 m = scmutil.matchfiles(repo, names)
1784 1789 changes = repo.status(match=m)[:4]
1785 1790 modified, added, removed, deleted = map(set, changes)
1786 1791
1787 1792 # if f is a rename, also revert the source
1788 1793 cwd = repo.getcwd()
1789 1794 for f in added:
1790 1795 src = repo.dirstate.copied(f)
1791 1796 if src and src not in names and repo.dirstate[src] == 'r':
1792 1797 removed.add(src)
1793 1798 names[src] = (repo.pathto(src, cwd), True)
1794 1799
1795 1800 def removeforget(abs):
1796 1801 if repo.dirstate[abs] == 'a':
1797 1802 return _('forgetting %s\n')
1798 1803 return _('removing %s\n')
1799 1804
1800 1805 revert = ([], _('reverting %s\n'))
1801 1806 add = ([], _('adding %s\n'))
1802 1807 remove = ([], removeforget)
1803 1808 undelete = ([], _('undeleting %s\n'))
1804 1809
1805 1810 disptable = (
1806 1811 # dispatch table:
1807 1812 # file state
1808 1813 # action if in target manifest
1809 1814 # action if not in target manifest
1810 1815 # make backup if in target manifest
1811 1816 # make backup if not in target manifest
1812 1817 (modified, revert, remove, True, True),
1813 1818 (added, revert, remove, True, False),
1814 1819 (removed, undelete, None, False, False),
1815 1820 (deleted, revert, remove, False, False),
1816 1821 )
1817 1822
1818 1823 for abs, (rel, exact) in sorted(names.items()):
1819 1824 mfentry = mf.get(abs)
1820 1825 target = repo.wjoin(abs)
1821 1826 def handle(xlist, dobackup):
1822 1827 xlist[0].append(abs)
1823 1828 if (dobackup and not opts.get('no_backup') and
1824 1829 os.path.lexists(target)):
1825 1830 bakname = "%s.orig" % rel
1826 1831 ui.note(_('saving current version of %s as %s\n') %
1827 1832 (rel, bakname))
1828 1833 if not opts.get('dry_run'):
1829 1834 util.rename(target, bakname)
1830 1835 if ui.verbose or not exact:
1831 1836 msg = xlist[1]
1832 1837 if not isinstance(msg, basestring):
1833 1838 msg = msg(abs)
1834 1839 ui.status(msg % rel)
1835 1840 for table, hitlist, misslist, backuphit, backupmiss in disptable:
1836 1841 if abs not in table:
1837 1842 continue
1838 1843 # file has changed in dirstate
1839 1844 if mfentry:
1840 1845 handle(hitlist, backuphit)
1841 1846 elif misslist is not None:
1842 1847 handle(misslist, backupmiss)
1843 1848 break
1844 1849 else:
1845 1850 if abs not in repo.dirstate:
1846 1851 if mfentry:
1847 1852 handle(add, True)
1848 1853 elif exact:
1849 1854 ui.warn(_('file not managed: %s\n') % rel)
1850 1855 continue
1851 1856 # file has not changed in dirstate
1852 1857 if node == parent:
1853 1858 if exact:
1854 1859 ui.warn(_('no changes needed to %s\n') % rel)
1855 1860 continue
1856 1861 if pmf is None:
1857 1862 # only need parent manifest in this unlikely case,
1858 1863 # so do not read by default
1859 1864 pmf = repo[parent].manifest()
1860 1865 if abs in pmf and mfentry:
1861 1866 # if version of file is same in parent and target
1862 1867 # manifests, do nothing
1863 1868 if (pmf[abs] != mfentry or
1864 1869 pmf.flags(abs) != mf.flags(abs)):
1865 1870 handle(revert, False)
1866 1871 else:
1867 1872 handle(remove, False)
1868 1873
1869 1874 if not opts.get('dry_run'):
1870 1875 def checkout(f):
1871 1876 fc = ctx[f]
1872 1877 repo.wwrite(f, fc.data(), fc.flags())
1873 1878
1874 1879 audit_path = scmutil.pathauditor(repo.root)
1875 1880 for f in remove[0]:
1876 1881 if repo.dirstate[f] == 'a':
1877 1882 repo.dirstate.drop(f)
1878 1883 continue
1879 1884 audit_path(f)
1880 1885 try:
1881 1886 util.unlinkpath(repo.wjoin(f))
1882 1887 except OSError:
1883 1888 pass
1884 1889 repo.dirstate.remove(f)
1885 1890
1886 1891 normal = None
1887 1892 if node == parent:
1888 1893 # We're reverting to our parent. If possible, we'd like status
1889 1894 # to report the file as clean. We have to use normallookup for
1890 1895 # merges to avoid losing information about merged/dirty files.
1891 1896 if p2 != nullid:
1892 1897 normal = repo.dirstate.normallookup
1893 1898 else:
1894 1899 normal = repo.dirstate.normal
1895 1900 for f in revert[0]:
1896 1901 checkout(f)
1897 1902 if normal:
1898 1903 normal(f)
1899 1904
1900 1905 for f in add[0]:
1901 1906 checkout(f)
1902 1907 repo.dirstate.add(f)
1903 1908
1904 1909 normal = repo.dirstate.normallookup
1905 1910 if node == parent and p2 == nullid:
1906 1911 normal = repo.dirstate.normal
1907 1912 for f in undelete[0]:
1908 1913 checkout(f)
1909 1914 normal(f)
1910 1915
1911 1916 if targetsubs:
1912 1917 # Revert the subrepos on the revert list
1913 1918 for sub in targetsubs:
1914 1919 ctx.sub(sub).revert(ui, ctx.substate[sub], *pats, **opts)
1915 1920 finally:
1916 1921 wlock.release()
1917 1922
1918 1923 def command(table):
1919 1924 '''returns a function object bound to table which can be used as
1920 1925 a decorator for populating table as a command table'''
1921 1926
1922 1927 def cmd(name, options, synopsis=None):
1923 1928 def decorator(func):
1924 1929 if synopsis:
1925 1930 table[name] = func, options[:], synopsis
1926 1931 else:
1927 1932 table[name] = func, options[:]
1928 1933 return func
1929 1934 return decorator
1930 1935
1931 1936 return cmd
@@ -1,357 +1,372 b''
1 1 $ hg init
2 2
3 3 Setup:
4 4
5 5 $ echo a >> a
6 6 $ hg ci -Am 'base'
7 7 adding a
8 8
9 9 Refuse to amend public csets:
10 10
11 11 $ hg phase -r . -p
12 12 $ hg ci --amend
13 13 abort: cannot amend public changesets
14 14 [255]
15 15 $ hg phase -r . -f -d
16 16
17 17 $ echo a >> a
18 18 $ hg ci -Am 'base1'
19 19
20 20 Nothing to amend:
21 21
22 22 $ hg ci --amend
23 23 nothing changed
24 24 [1]
25 25
26 26 $ cat >> $HGRCPATH <<EOF
27 27 > [hooks]
28 28 > pretxncommit.foo = sh -c "echo \"pretxncommit \$HG_NODE\"; hg id -r \$HG_NODE"
29 29 > EOF
30 30
31 31 Amending changeset with changes in working dir:
32 32
33 33 $ echo a >> a
34 34 $ hg ci --amend -m 'amend base1'
35 35 pretxncommit 9cd25b479c51be2f4ed2c38e7abdf7ce67d8e0dc
36 36 9cd25b479c51 tip
37 37 saved backup bundle to $TESTTMP/.hg/strip-backup/489edb5b847d-amend-backup.hg (glob)
38 38 $ echo 'pretxncommit.foo = ' >> $HGRCPATH
39 39 $ hg diff -c .
40 40 diff -r ad120869acf0 -r 9cd25b479c51 a
41 41 --- a/a Thu Jan 01 00:00:00 1970 +0000
42 42 +++ b/a Thu Jan 01 00:00:00 1970 +0000
43 43 @@ -1,1 +1,3 @@
44 44 a
45 45 +a
46 46 +a
47 47 $ hg log
48 48 changeset: 1:9cd25b479c51
49 49 tag: tip
50 50 user: test
51 51 date: Thu Jan 01 00:00:00 1970 +0000
52 52 summary: amend base1
53 53
54 54 changeset: 0:ad120869acf0
55 55 user: test
56 56 date: Thu Jan 01 00:00:00 1970 +0000
57 57 summary: base
58 58
59 59
60 60 Add new file:
61 61
62 62 $ echo b > b
63 63 $ hg ci --amend -Am 'amend base1 new file'
64 64 adding b
65 65 saved backup bundle to $TESTTMP/.hg/strip-backup/9cd25b479c51-amend-backup.hg (glob)
66 66
67 67 Remove file that was added in amended commit:
68 68
69 69 $ hg rm b
70 70 $ hg ci --amend -m 'amend base1 remove new file'
71 71 saved backup bundle to $TESTTMP/.hg/strip-backup/e2bb3ecffd2f-amend-backup.hg (glob)
72 72
73 73 $ hg cat b
74 74 b: no such file in rev 664a9b2d60cd
75 75 [1]
76 76
77 77 No changes, just a different message:
78 78
79 79 $ hg ci -v --amend -m 'no changes, new message'
80 80 amending changeset 664a9b2d60cd
81 81 copying changeset 664a9b2d60cd to ad120869acf0
82 82 a
83 83 stripping amended changeset 664a9b2d60cd
84 84 1 changesets found
85 85 saved backup bundle to $TESTTMP/.hg/strip-backup/664a9b2d60cd-amend-backup.hg (glob)
86 86 1 changesets found
87 87 adding branch
88 88 adding changesets
89 89 adding manifests
90 90 adding file changes
91 91 added 1 changesets with 1 changes to 1 files
92 92 committed changeset 1:ea6e356ff2ad
93 93 $ hg diff -c .
94 94 diff -r ad120869acf0 -r ea6e356ff2ad a
95 95 --- a/a Thu Jan 01 00:00:00 1970 +0000
96 96 +++ b/a Thu Jan 01 00:00:00 1970 +0000
97 97 @@ -1,1 +1,3 @@
98 98 a
99 99 +a
100 100 +a
101 101 $ hg log
102 102 changeset: 1:ea6e356ff2ad
103 103 tag: tip
104 104 user: test
105 105 date: Thu Jan 01 00:00:00 1970 +0000
106 106 summary: no changes, new message
107 107
108 108 changeset: 0:ad120869acf0
109 109 user: test
110 110 date: Thu Jan 01 00:00:00 1970 +0000
111 111 summary: base
112 112
113 113
114 114 Disable default date on commit so when -d isn't given, the old date is preserved:
115 115
116 116 $ echo '[defaults]' >> $HGRCPATH
117 117 $ echo 'commit=' >> $HGRCPATH
118 118
119 119 Test -u/-d:
120 120
121 121 $ hg ci --amend -u foo -d '1 0'
122 122 saved backup bundle to $TESTTMP/.hg/strip-backup/ea6e356ff2ad-amend-backup.hg (glob)
123 123 $ echo a >> a
124 124 $ hg ci --amend -u foo -d '1 0'
125 125 saved backup bundle to $TESTTMP/.hg/strip-backup/377b91ce8b56-amend-backup.hg (glob)
126 126 $ hg log -r .
127 127 changeset: 1:2c94e4a5756f
128 128 tag: tip
129 129 user: foo
130 130 date: Thu Jan 01 00:00:01 1970 +0000
131 131 summary: no changes, new message
132 132
133 133
134 134 Open editor with old commit message if a message isn't given otherwise:
135 135
136 136 $ cat > editor.sh << '__EOF__'
137 137 > #!/bin/sh
138 138 > cat $1
139 139 > echo "another precious commit message" > "$1"
140 140 > __EOF__
141 141 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit --amend -v
142 142 amending changeset 2c94e4a5756f
143 143 copying changeset 2c94e4a5756f to ad120869acf0
144 144 no changes, new message
145 145
146 146
147 147 HG: Enter commit message. Lines beginning with 'HG:' are removed.
148 148 HG: Leave message empty to abort commit.
149 149 HG: --
150 150 HG: user: foo
151 151 HG: branch 'default'
152 152 HG: changed a
153 153 a
154 154 stripping amended changeset 2c94e4a5756f
155 155 1 changesets found
156 156 saved backup bundle to $TESTTMP/.hg/strip-backup/2c94e4a5756f-amend-backup.hg (glob)
157 157 1 changesets found
158 158 adding branch
159 159 adding changesets
160 160 adding manifests
161 161 adding file changes
162 162 added 1 changesets with 1 changes to 1 files
163 163 committed changeset 1:ffb49186f961
164 164
165 165 Same, but with changes in working dir (different code path):
166 166
167 167 $ echo a >> a
168 168 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit --amend -v
169 169 amending changeset ffb49186f961
170 170 another precious commit message
171 171
172 172
173 173 HG: Enter commit message. Lines beginning with 'HG:' are removed.
174 174 HG: Leave message empty to abort commit.
175 175 HG: --
176 176 HG: user: foo
177 177 HG: branch 'default'
178 178 HG: changed a
179 179 a
180 180 copying changeset 27f3aacd3011 to ad120869acf0
181 181 a
182 182 stripping intermediate changeset 27f3aacd3011
183 183 stripping amended changeset ffb49186f961
184 184 2 changesets found
185 185 saved backup bundle to $TESTTMP/.hg/strip-backup/ffb49186f961-amend-backup.hg (glob)
186 186 1 changesets found
187 187 adding branch
188 188 adding changesets
189 189 adding manifests
190 190 adding file changes
191 191 added 1 changesets with 1 changes to 1 files
192 192 committed changeset 1:fb6cca43446f
193 193
194 194 $ rm editor.sh
195 195 $ hg log -r .
196 196 changeset: 1:fb6cca43446f
197 197 tag: tip
198 198 user: foo
199 199 date: Thu Jan 01 00:00:01 1970 +0000
200 200 summary: another precious commit message
201 201
202 202
203 203 Moving bookmarks, preserve active bookmark:
204 204
205 205 $ hg book book1
206 206 $ hg book book2
207 207 $ hg ci --amend -m 'move bookmarks'
208 208 saved backup bundle to $TESTTMP/.hg/strip-backup/fb6cca43446f-amend-backup.hg (glob)
209 209 $ hg book
210 210 book1 1:0cf1c7a51bcf
211 211 * book2 1:0cf1c7a51bcf
212 212 $ echo a >> a
213 213 $ hg ci --amend -m 'move bookmarks'
214 214 saved backup bundle to $TESTTMP/.hg/strip-backup/0cf1c7a51bcf-amend-backup.hg (glob)
215 215 $ hg book
216 216 book1 1:7344472bd951
217 217 * book2 1:7344472bd951
218 218
219 219 $ echo '[defaults]' >> $HGRCPATH
220 220 $ echo "commit=-d '0 0'" >> $HGRCPATH
221 221
222 222 Moving branches:
223 223
224 224 $ hg branch foo
225 225 marked working directory as branch foo
226 226 (branches are permanent and global, did you want a bookmark?)
227 227 $ echo a >> a
228 228 $ hg ci -m 'branch foo'
229 229 $ hg branch default -f
230 230 marked working directory as branch default
231 231 (branches are permanent and global, did you want a bookmark?)
232 232 $ hg ci --amend -m 'back to default'
233 233 saved backup bundle to $TESTTMP/.hg/strip-backup/1661ca36a2db-amend-backup.hg (glob)
234 234 $ hg branches
235 235 default 2:f24ee5961967
236 236
237 237 Close branch:
238 238
239 239 $ hg up -q 0
240 240 $ echo b >> b
241 241 $ hg branch foo
242 242 marked working directory as branch foo
243 243 (branches are permanent and global, did you want a bookmark?)
244 244 $ hg ci -Am 'fork'
245 245 adding b
246 246 $ echo b >> b
247 247 $ hg ci -mb
248 248 $ hg ci --amend --close-branch -m 'closing branch foo'
249 249 saved backup bundle to $TESTTMP/.hg/strip-backup/c962248fa264-amend-backup.hg (glob)
250 250
251 251 Same thing, different code path:
252 252
253 253 $ echo b >> b
254 254 $ hg ci -m 'reopen branch'
255 255 reopening closed branch head 4
256 256 $ echo b >> b
257 257 $ hg ci --amend --close-branch
258 258 saved backup bundle to $TESTTMP/.hg/strip-backup/5e302dcc12b8-amend-backup.hg (glob)
259 259 $ hg branches
260 260 default 2:f24ee5961967
261 261
262 262 Refuse to amend merges:
263 263
264 264 $ hg up -q default
265 265 $ hg merge foo
266 266 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
267 267 (branch merge, don't forget to commit)
268 268 $ hg ci --amend
269 269 abort: cannot amend while merging
270 270 [255]
271 271 $ hg ci -m 'merge'
272 272 $ hg ci --amend
273 273 abort: cannot amend merge changesets
274 274 [255]
275 275
276 276 Follow copies/renames:
277 277
278 278 $ hg mv b c
279 279 $ hg ci -m 'b -> c'
280 280 $ hg mv c d
281 281 $ hg ci --amend -m 'b -> d'
282 282 saved backup bundle to $TESTTMP/.hg/strip-backup/9c207120aa98-amend-backup.hg (glob)
283 283 $ hg st --rev '.^' --copies d
284 284 A d
285 285 b
286 286 $ hg cp d e
287 287 $ hg ci -m 'e = d'
288 288 $ hg cp e f
289 289 $ hg ci --amend -m 'f = d'
290 290 saved backup bundle to $TESTTMP/.hg/strip-backup/fda2b3b27b22-amend-backup.hg (glob)
291 291 $ hg st --rev '.^' --copies f
292 292 A f
293 293 d
294 294
295 295 $ mv f f.orig
296 296 $ hg rm -A f
297 297 $ hg ci -m removef
298 298 $ hg cp a f
299 299 $ mv f.orig f
300 300 $ hg ci --amend -m replacef
301 301 saved backup bundle to $TESTTMP/.hg/strip-backup/20a7413547f9-amend-backup.hg (glob)
302 302 $ hg st --change . --copies
303 303 $ hg log -r . --template "{file_copies}\n"
304 304
305 305
306 306 Move added file (issue3410):
307 307
308 308 $ echo g >> g
309 309 $ hg ci -Am g
310 310 adding g
311 311 $ hg mv g h
312 312 $ hg ci --amend
313 313 saved backup bundle to $TESTTMP/.hg/strip-backup/5daa77a5d616-amend-backup.hg (glob)
314 314 $ hg st --change . --copies h
315 315 A h
316 316 $ hg log -r . --template "{file_copies}\n"
317 317
318 318
319 319 Can't rollback an amend:
320 320
321 321 $ hg rollback
322 322 no rollback information available
323 323 [1]
324 324
325 325 Preserve extra dict (issue3430):
326 326
327 327 $ hg branch a
328 328 marked working directory as branch a
329 329 (branches are permanent and global, did you want a bookmark?)
330 330 $ echo a >> a
331 331 $ hg ci -ma
332 332 $ hg ci --amend -m "a'"
333 333 saved backup bundle to $TESTTMP/.hg/strip-backup/167f8e3031df-amend-backup.hg (glob)
334 334 $ hg log -r . --template "{branch}\n"
335 335 a
336 336 $ hg ci --amend -m "a''"
337 337 saved backup bundle to $TESTTMP/.hg/strip-backup/ceac1a44c806-amend-backup.hg (glob)
338 338 $ hg log -r . --template "{branch}\n"
339 339 a
340 340
341 341 Also preserve other entries in the dict that are in the old commit,
342 342 first graft something so there's an additional entry:
343 343
344 344 $ hg up 0 -q
345 345 $ echo z > z
346 346 $ hg ci -Am 'fork'
347 347 adding z
348 348 created new head
349 349 $ hg up 11
350 350 5 files updated, 0 files merged, 1 files removed, 0 files unresolved
351 351 $ hg graft 12
352 352 grafting revision 12
353 353 $ hg ci --amend -m 'graft amend'
354 354 saved backup bundle to $TESTTMP/.hg/strip-backup/18a5124daf7a-amend-backup.hg (glob)
355 355 $ hg log -r . --debug | grep extra
356 356 extra: branch=a
357 357 extra: source=2647734878ef0236dda712fae9c1651cf694ea8a
358
359 Preserve phase
360
361 $ hg phase '.^::.'
362 11: draft
363 13: draft
364 $ hg phase --secret --force .
365 $ hg phase '.^::.'
366 11: draft
367 13: secret
368 $ hg commit --amend -m 'amend for phase' -q
369 $ hg phase '.^::.'
370 11: draft
371 13: secret
372
General Comments 0
You need to be logged in to leave comments. Login now