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