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