##// END OF EJS Templates
log: move log file walk to its own function...
Durham Goode -
r19290:f21f4a1b default
parent child Browse files
Show More
@@ -1,2087 +1,2105
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 def walkchangerevs(repo, match, opts, prepare):
1009 '''Iterate over files and the revs in which they changed.
1010
1011 Callers most commonly need to iterate backwards over the history
1012 in which they are interested. Doing so has awful (quadratic-looking)
1013 performance, so we use iterators in a "windowed" way.
1008 class FileWalkError(Exception):
1009 pass
1014 1010
1015 We walk a window of revisions in the desired order. Within the
1016 window, we first walk forwards to gather data, then in the desired
1017 order (usually backwards) to display it.
1018
1019 This function returns an iterator yielding contexts. Before
1020 yielding each context, the iterator will first call the prepare
1021 function on each context in the window in forward order.'''
1022
1023 follow = opts.get('follow') or opts.get('follow_first')
1011 def walkfilerevs(repo, match, follow, revs, fncache):
1012 '''Walks the file history for the matched files.
1024 1013
1025 if opts.get('rev'):
1026 revs = scmutil.revrange(repo, opts.get('rev'))
1027 elif follow:
1028 revs = repo.revs('reverse(:.)')
1029 else:
1030 revs = list(repo)
1031 revs.reverse()
1032 if not revs:
1033 return []
1034 wanted = set()
1035 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1036 fncache = {}
1037 change = repo.changectx
1014 Returns the changeset revs that are involved in the file history.
1038 1015
1039 # First step is to fill wanted, the set of revisions that we want to yield.
1040 # When it does not induce extra cost, we also fill fncache for revisions in
1041 # wanted: a cache of filenames that were changed (ctx.files()) and that
1042 # match the file filtering conditions.
1043
1044 if not slowpath and not match.files():
1045 # No files, no patterns. Display all revs.
1046 wanted = set(revs)
1016 Throws FileWalkError if the file history can't be walked using
1017 filelogs alone.
1018 '''
1019 wanted = set()
1047 1020 copies = []
1048
1049 if not slowpath and match.files():
1050 # We only have to read through the filelog to find wanted revisions
1051
1052 1021 minrev, maxrev = min(revs), max(revs)
1053 1022 def filerevgen(filelog, last):
1054 1023 """
1055 1024 Only files, no patterns. Check the history of each file.
1056 1025
1057 1026 Examines filelog entries within minrev, maxrev linkrev range
1058 1027 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1059 1028 tuples in backwards order
1060 1029 """
1061 1030 cl_count = len(repo)
1062 1031 revs = []
1063 1032 for j in xrange(0, last + 1):
1064 1033 linkrev = filelog.linkrev(j)
1065 1034 if linkrev < minrev:
1066 1035 continue
1067 1036 # only yield rev for which we have the changelog, it can
1068 1037 # happen while doing "hg log" during a pull or commit
1069 1038 if linkrev >= cl_count:
1070 1039 break
1071 1040
1072 1041 parentlinkrevs = []
1073 1042 for p in filelog.parentrevs(j):
1074 1043 if p != nullrev:
1075 1044 parentlinkrevs.append(filelog.linkrev(p))
1076 1045 n = filelog.node(j)
1077 1046 revs.append((linkrev, parentlinkrevs,
1078 1047 follow and filelog.renamed(n)))
1079 1048
1080 1049 return reversed(revs)
1081 1050 def iterfiles():
1082 1051 pctx = repo['.']
1083 1052 for filename in match.files():
1084 1053 if follow:
1085 1054 if filename not in pctx:
1086 1055 raise util.Abort(_('cannot follow file not in parent '
1087 1056 'revision: "%s"') % filename)
1088 1057 yield filename, pctx[filename].filenode()
1089 1058 else:
1090 1059 yield filename, None
1091 1060 for filename_node in copies:
1092 1061 yield filename_node
1062
1093 1063 for file_, node in iterfiles():
1094 1064 filelog = repo.file(file_)
1095 1065 if not len(filelog):
1096 1066 if node is None:
1097 1067 # A zero count may be a directory or deleted file, so
1098 1068 # try to find matching entries on the slow path.
1099 1069 if follow:
1100 1070 raise util.Abort(
1101 1071 _('cannot follow nonexistent file: "%s"') % file_)
1102 slowpath = True
1103 break
1072 raise FileWalkError("Cannot walk via filelog")
1104 1073 else:
1105 1074 continue
1106 1075
1107 1076 if node is None:
1108 1077 last = len(filelog) - 1
1109 1078 else:
1110 1079 last = filelog.rev(node)
1111 1080
1112 1081
1113 1082 # keep track of all ancestors of the file
1114 1083 ancestors = set([filelog.linkrev(last)])
1115 1084
1116 1085 # iterate from latest to oldest revision
1117 1086 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1118 1087 if not follow:
1119 1088 if rev > maxrev:
1120 1089 continue
1121 1090 else:
1122 1091 # Note that last might not be the first interesting
1123 1092 # rev to us:
1124 1093 # if the file has been changed after maxrev, we'll
1125 1094 # have linkrev(last) > maxrev, and we still need
1126 1095 # to explore the file graph
1127 1096 if rev not in ancestors:
1128 1097 continue
1129 1098 # XXX insert 1327 fix here
1130 1099 if flparentlinkrevs:
1131 1100 ancestors.update(flparentlinkrevs)
1132 1101
1133 1102 fncache.setdefault(rev, []).append(file_)
1134 1103 wanted.add(rev)
1135 1104 if copied:
1136 1105 copies.append(copied)
1137 1106
1107 return wanted
1108
1109 def walkchangerevs(repo, match, opts, prepare):
1110 '''Iterate over files and the revs in which they changed.
1111
1112 Callers most commonly need to iterate backwards over the history
1113 in which they are interested. Doing so has awful (quadratic-looking)
1114 performance, so we use iterators in a "windowed" way.
1115
1116 We walk a window of revisions in the desired order. Within the
1117 window, we first walk forwards to gather data, then in the desired
1118 order (usually backwards) to display it.
1119
1120 This function returns an iterator yielding contexts. Before
1121 yielding each context, the iterator will first call the prepare
1122 function on each context in the window in forward order.'''
1123
1124 follow = opts.get('follow') or opts.get('follow_first')
1125
1126 if opts.get('rev'):
1127 revs = scmutil.revrange(repo, opts.get('rev'))
1128 elif follow:
1129 revs = repo.revs('reverse(:.)')
1130 else:
1131 revs = list(repo)
1132 revs.reverse()
1133 if not revs:
1134 return []
1135 wanted = set()
1136 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1137 fncache = {}
1138 change = repo.changectx
1139
1140 # First step is to fill wanted, the set of revisions that we want to yield.
1141 # When it does not induce extra cost, we also fill fncache for revisions in
1142 # wanted: a cache of filenames that were changed (ctx.files()) and that
1143 # match the file filtering conditions.
1144
1145 if not slowpath and not match.files():
1146 # No files, no patterns. Display all revs.
1147 wanted = set(revs)
1148
1149 if not slowpath and match.files():
1150 # We only have to read through the filelog to find wanted revisions
1151
1152 try:
1153 wanted = walkfilerevs(repo, match, follow, revs, fncache)
1154 except FileWalkError:
1155 slowpath = True
1156
1138 1157 # We decided to fall back to the slowpath because at least one
1139 1158 # of the paths was not a file. Check to see if at least one of them
1140 1159 # existed in history, otherwise simply return
1141 if slowpath:
1142 1160 for path in match.files():
1143 1161 if path == '.' or path in repo.store:
1144 1162 break
1145 1163 else:
1146 1164 return []
1147 1165
1148 1166 if slowpath:
1149 1167 # We have to read the changelog to match filenames against
1150 1168 # changed files
1151 1169
1152 1170 if follow:
1153 1171 raise util.Abort(_('can only follow copies/renames for explicit '
1154 1172 'filenames'))
1155 1173
1156 1174 # The slow path checks files modified in every changeset.
1157 1175 for i in sorted(revs):
1158 1176 ctx = change(i)
1159 1177 matches = filter(match, ctx.files())
1160 1178 if matches:
1161 1179 fncache[i] = matches
1162 1180 wanted.add(i)
1163 1181
1164 1182 class followfilter(object):
1165 1183 def __init__(self, onlyfirst=False):
1166 1184 self.startrev = nullrev
1167 1185 self.roots = set()
1168 1186 self.onlyfirst = onlyfirst
1169 1187
1170 1188 def match(self, rev):
1171 1189 def realparents(rev):
1172 1190 if self.onlyfirst:
1173 1191 return repo.changelog.parentrevs(rev)[0:1]
1174 1192 else:
1175 1193 return filter(lambda x: x != nullrev,
1176 1194 repo.changelog.parentrevs(rev))
1177 1195
1178 1196 if self.startrev == nullrev:
1179 1197 self.startrev = rev
1180 1198 return True
1181 1199
1182 1200 if rev > self.startrev:
1183 1201 # forward: all descendants
1184 1202 if not self.roots:
1185 1203 self.roots.add(self.startrev)
1186 1204 for parent in realparents(rev):
1187 1205 if parent in self.roots:
1188 1206 self.roots.add(rev)
1189 1207 return True
1190 1208 else:
1191 1209 # backwards: all parents
1192 1210 if not self.roots:
1193 1211 self.roots.update(realparents(self.startrev))
1194 1212 if rev in self.roots:
1195 1213 self.roots.remove(rev)
1196 1214 self.roots.update(realparents(rev))
1197 1215 return True
1198 1216
1199 1217 return False
1200 1218
1201 1219 # it might be worthwhile to do this in the iterator if the rev range
1202 1220 # is descending and the prune args are all within that range
1203 1221 for rev in opts.get('prune', ()):
1204 1222 rev = repo[rev].rev()
1205 1223 ff = followfilter()
1206 1224 stop = min(revs[0], revs[-1])
1207 1225 for x in xrange(rev, stop - 1, -1):
1208 1226 if ff.match(x):
1209 1227 wanted.discard(x)
1210 1228
1211 1229 # Choose a small initial window if we will probably only visit a
1212 1230 # few commits.
1213 1231 limit = loglimit(opts)
1214 1232 windowsize = 8
1215 1233 if limit:
1216 1234 windowsize = min(limit, windowsize)
1217 1235
1218 1236 # Now that wanted is correctly initialized, we can iterate over the
1219 1237 # revision range, yielding only revisions in wanted.
1220 1238 def iterate():
1221 1239 if follow and not match.files():
1222 1240 ff = followfilter(onlyfirst=opts.get('follow_first'))
1223 1241 def want(rev):
1224 1242 return ff.match(rev) and rev in wanted
1225 1243 else:
1226 1244 def want(rev):
1227 1245 return rev in wanted
1228 1246
1229 1247 for i, window in increasingwindows(0, len(revs), windowsize):
1230 1248 nrevs = [rev for rev in revs[i:i + window] if want(rev)]
1231 1249 for rev in sorted(nrevs):
1232 1250 fns = fncache.get(rev)
1233 1251 ctx = change(rev)
1234 1252 if not fns:
1235 1253 def fns_generator():
1236 1254 for f in ctx.files():
1237 1255 if match(f):
1238 1256 yield f
1239 1257 fns = fns_generator()
1240 1258 prepare(ctx, fns)
1241 1259 for rev in nrevs:
1242 1260 yield change(rev)
1243 1261 return iterate()
1244 1262
1245 1263 def _makegraphfilematcher(repo, pats, followfirst):
1246 1264 # When displaying a revision with --patch --follow FILE, we have
1247 1265 # to know which file of the revision must be diffed. With
1248 1266 # --follow, we want the names of the ancestors of FILE in the
1249 1267 # revision, stored in "fcache". "fcache" is populated by
1250 1268 # reproducing the graph traversal already done by --follow revset
1251 1269 # and relating linkrevs to file names (which is not "correct" but
1252 1270 # good enough).
1253 1271 fcache = {}
1254 1272 fcacheready = [False]
1255 1273 pctx = repo['.']
1256 1274 wctx = repo[None]
1257 1275
1258 1276 def populate():
1259 1277 for fn in pats:
1260 1278 for i in ((pctx[fn],), pctx[fn].ancestors(followfirst=followfirst)):
1261 1279 for c in i:
1262 1280 fcache.setdefault(c.linkrev(), set()).add(c.path())
1263 1281
1264 1282 def filematcher(rev):
1265 1283 if not fcacheready[0]:
1266 1284 # Lazy initialization
1267 1285 fcacheready[0] = True
1268 1286 populate()
1269 1287 return scmutil.match(wctx, fcache.get(rev, []), default='path')
1270 1288
1271 1289 return filematcher
1272 1290
1273 1291 def _makegraphlogrevset(repo, pats, opts, revs):
1274 1292 """Return (expr, filematcher) where expr is a revset string built
1275 1293 from log options and file patterns or None. If --stat or --patch
1276 1294 are not passed filematcher is None. Otherwise it is a callable
1277 1295 taking a revision number and returning a match objects filtering
1278 1296 the files to be detailed when displaying the revision.
1279 1297 """
1280 1298 opt2revset = {
1281 1299 'no_merges': ('not merge()', None),
1282 1300 'only_merges': ('merge()', None),
1283 1301 '_ancestors': ('ancestors(%(val)s)', None),
1284 1302 '_fancestors': ('_firstancestors(%(val)s)', None),
1285 1303 '_descendants': ('descendants(%(val)s)', None),
1286 1304 '_fdescendants': ('_firstdescendants(%(val)s)', None),
1287 1305 '_matchfiles': ('_matchfiles(%(val)s)', None),
1288 1306 'date': ('date(%(val)r)', None),
1289 1307 'branch': ('branch(%(val)r)', ' or '),
1290 1308 '_patslog': ('filelog(%(val)r)', ' or '),
1291 1309 '_patsfollow': ('follow(%(val)r)', ' or '),
1292 1310 '_patsfollowfirst': ('_followfirst(%(val)r)', ' or '),
1293 1311 'keyword': ('keyword(%(val)r)', ' or '),
1294 1312 'prune': ('not (%(val)r or ancestors(%(val)r))', ' and '),
1295 1313 'user': ('user(%(val)r)', ' or '),
1296 1314 }
1297 1315
1298 1316 opts = dict(opts)
1299 1317 # follow or not follow?
1300 1318 follow = opts.get('follow') or opts.get('follow_first')
1301 1319 followfirst = opts.get('follow_first') and 1 or 0
1302 1320 # --follow with FILE behaviour depends on revs...
1303 1321 startrev = revs[0]
1304 1322 followdescendants = (len(revs) > 1 and revs[0] < revs[1]) and 1 or 0
1305 1323
1306 1324 # branch and only_branch are really aliases and must be handled at
1307 1325 # the same time
1308 1326 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
1309 1327 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
1310 1328 # pats/include/exclude are passed to match.match() directly in
1311 1329 # _matchfiles() revset but walkchangerevs() builds its matcher with
1312 1330 # scmutil.match(). The difference is input pats are globbed on
1313 1331 # platforms without shell expansion (windows).
1314 1332 pctx = repo[None]
1315 1333 match, pats = scmutil.matchandpats(pctx, pats, opts)
1316 1334 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1317 1335 if not slowpath:
1318 1336 for f in match.files():
1319 1337 if follow and f not in pctx:
1320 1338 raise util.Abort(_('cannot follow file not in parent '
1321 1339 'revision: "%s"') % f)
1322 1340 filelog = repo.file(f)
1323 1341 if not len(filelog):
1324 1342 # A zero count may be a directory or deleted file, so
1325 1343 # try to find matching entries on the slow path.
1326 1344 if follow:
1327 1345 raise util.Abort(
1328 1346 _('cannot follow nonexistent file: "%s"') % f)
1329 1347 slowpath = True
1330 1348
1331 1349 # We decided to fall back to the slowpath because at least one
1332 1350 # of the paths was not a file. Check to see if at least one of them
1333 1351 # existed in history - in that case, we'll continue down the
1334 1352 # slowpath; otherwise, we can turn off the slowpath
1335 1353 if slowpath:
1336 1354 for path in match.files():
1337 1355 if path == '.' or path in repo.store:
1338 1356 break
1339 1357 else:
1340 1358 slowpath = False
1341 1359
1342 1360 if slowpath:
1343 1361 # See walkchangerevs() slow path.
1344 1362 #
1345 1363 if follow:
1346 1364 raise util.Abort(_('can only follow copies/renames for explicit '
1347 1365 'filenames'))
1348 1366 # pats/include/exclude cannot be represented as separate
1349 1367 # revset expressions as their filtering logic applies at file
1350 1368 # level. For instance "-I a -X a" matches a revision touching
1351 1369 # "a" and "b" while "file(a) and not file(b)" does
1352 1370 # not. Besides, filesets are evaluated against the working
1353 1371 # directory.
1354 1372 matchargs = ['r:', 'd:relpath']
1355 1373 for p in pats:
1356 1374 matchargs.append('p:' + p)
1357 1375 for p in opts.get('include', []):
1358 1376 matchargs.append('i:' + p)
1359 1377 for p in opts.get('exclude', []):
1360 1378 matchargs.append('x:' + p)
1361 1379 matchargs = ','.join(('%r' % p) for p in matchargs)
1362 1380 opts['_matchfiles'] = matchargs
1363 1381 else:
1364 1382 if follow:
1365 1383 fpats = ('_patsfollow', '_patsfollowfirst')
1366 1384 fnopats = (('_ancestors', '_fancestors'),
1367 1385 ('_descendants', '_fdescendants'))
1368 1386 if pats:
1369 1387 # follow() revset interprets its file argument as a
1370 1388 # manifest entry, so use match.files(), not pats.
1371 1389 opts[fpats[followfirst]] = list(match.files())
1372 1390 else:
1373 1391 opts[fnopats[followdescendants][followfirst]] = str(startrev)
1374 1392 else:
1375 1393 opts['_patslog'] = list(pats)
1376 1394
1377 1395 filematcher = None
1378 1396 if opts.get('patch') or opts.get('stat'):
1379 1397 if follow:
1380 1398 filematcher = _makegraphfilematcher(repo, pats, followfirst)
1381 1399 else:
1382 1400 filematcher = lambda rev: match
1383 1401
1384 1402 expr = []
1385 1403 for op, val in opts.iteritems():
1386 1404 if not val:
1387 1405 continue
1388 1406 if op not in opt2revset:
1389 1407 continue
1390 1408 revop, andor = opt2revset[op]
1391 1409 if '%(val)' not in revop:
1392 1410 expr.append(revop)
1393 1411 else:
1394 1412 if not isinstance(val, list):
1395 1413 e = revop % {'val': val}
1396 1414 else:
1397 1415 e = '(' + andor.join((revop % {'val': v}) for v in val) + ')'
1398 1416 expr.append(e)
1399 1417
1400 1418 if expr:
1401 1419 expr = '(' + ' and '.join(expr) + ')'
1402 1420 else:
1403 1421 expr = None
1404 1422 return expr, filematcher
1405 1423
1406 1424 def getgraphlogrevs(repo, pats, opts):
1407 1425 """Return (revs, expr, filematcher) where revs is an iterable of
1408 1426 revision numbers, expr is a revset string built from log options
1409 1427 and file patterns or None, and used to filter 'revs'. If --stat or
1410 1428 --patch are not passed filematcher is None. Otherwise it is a
1411 1429 callable taking a revision number and returning a match objects
1412 1430 filtering the files to be detailed when displaying the revision.
1413 1431 """
1414 1432 if not len(repo):
1415 1433 return [], None, None
1416 1434 limit = loglimit(opts)
1417 1435 # Default --rev value depends on --follow but --follow behaviour
1418 1436 # depends on revisions resolved from --rev...
1419 1437 follow = opts.get('follow') or opts.get('follow_first')
1420 1438 possiblyunsorted = False # whether revs might need sorting
1421 1439 if opts.get('rev'):
1422 1440 revs = scmutil.revrange(repo, opts['rev'])
1423 1441 # Don't sort here because _makegraphlogrevset might depend on the
1424 1442 # order of revs
1425 1443 possiblyunsorted = True
1426 1444 else:
1427 1445 if follow and len(repo) > 0:
1428 1446 revs = repo.revs('reverse(:.)')
1429 1447 else:
1430 1448 revs = list(repo.changelog)
1431 1449 revs.reverse()
1432 1450 if not revs:
1433 1451 return [], None, None
1434 1452 expr, filematcher = _makegraphlogrevset(repo, pats, opts, revs)
1435 1453 if possiblyunsorted:
1436 1454 revs.sort(reverse=True)
1437 1455 if expr:
1438 1456 # Revset matchers often operate faster on revisions in changelog
1439 1457 # order, because most filters deal with the changelog.
1440 1458 revs.reverse()
1441 1459 matcher = revset.match(repo.ui, expr)
1442 1460 # Revset matches can reorder revisions. "A or B" typically returns
1443 1461 # returns the revision matching A then the revision matching B. Sort
1444 1462 # again to fix that.
1445 1463 revs = matcher(repo, revs)
1446 1464 revs.sort(reverse=True)
1447 1465 if limit is not None:
1448 1466 revs = revs[:limit]
1449 1467
1450 1468 return revs, expr, filematcher
1451 1469
1452 1470 def displaygraph(ui, dag, displayer, showparents, edgefn, getrenamed=None,
1453 1471 filematcher=None):
1454 1472 seen, state = [], graphmod.asciistate()
1455 1473 for rev, type, ctx, parents in dag:
1456 1474 char = 'o'
1457 1475 if ctx.node() in showparents:
1458 1476 char = '@'
1459 1477 elif ctx.obsolete():
1460 1478 char = 'x'
1461 1479 copies = None
1462 1480 if getrenamed and ctx.rev():
1463 1481 copies = []
1464 1482 for fn in ctx.files():
1465 1483 rename = getrenamed(fn, ctx.rev())
1466 1484 if rename:
1467 1485 copies.append((fn, rename[0]))
1468 1486 revmatchfn = None
1469 1487 if filematcher is not None:
1470 1488 revmatchfn = filematcher(ctx.rev())
1471 1489 displayer.show(ctx, copies=copies, matchfn=revmatchfn)
1472 1490 lines = displayer.hunk.pop(rev).split('\n')
1473 1491 if not lines[-1]:
1474 1492 del lines[-1]
1475 1493 displayer.flush(rev)
1476 1494 edges = edgefn(type, char, lines, seen, rev, parents)
1477 1495 for type, char, lines, coldata in edges:
1478 1496 graphmod.ascii(ui, state, type, char, lines, coldata)
1479 1497 displayer.close()
1480 1498
1481 1499 def graphlog(ui, repo, *pats, **opts):
1482 1500 # Parameters are identical to log command ones
1483 1501 revs, expr, filematcher = getgraphlogrevs(repo, pats, opts)
1484 1502 revdag = graphmod.dagwalker(repo, revs)
1485 1503
1486 1504 getrenamed = None
1487 1505 if opts.get('copies'):
1488 1506 endrev = None
1489 1507 if opts.get('rev'):
1490 1508 endrev = max(scmutil.revrange(repo, opts.get('rev'))) + 1
1491 1509 getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
1492 1510 displayer = show_changeset(ui, repo, opts, buffered=True)
1493 1511 showparents = [ctx.node() for ctx in repo[None].parents()]
1494 1512 displaygraph(ui, revdag, displayer, showparents,
1495 1513 graphmod.asciiedges, getrenamed, filematcher)
1496 1514
1497 1515 def checkunsupportedgraphflags(pats, opts):
1498 1516 for op in ["newest_first"]:
1499 1517 if op in opts and opts[op]:
1500 1518 raise util.Abort(_("-G/--graph option is incompatible with --%s")
1501 1519 % op.replace("_", "-"))
1502 1520
1503 1521 def graphrevs(repo, nodes, opts):
1504 1522 limit = loglimit(opts)
1505 1523 nodes.reverse()
1506 1524 if limit is not None:
1507 1525 nodes = nodes[:limit]
1508 1526 return graphmod.nodes(repo, nodes)
1509 1527
1510 1528 def add(ui, repo, match, dryrun, listsubrepos, prefix, explicitonly):
1511 1529 join = lambda f: os.path.join(prefix, f)
1512 1530 bad = []
1513 1531 oldbad = match.bad
1514 1532 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
1515 1533 names = []
1516 1534 wctx = repo[None]
1517 1535 cca = None
1518 1536 abort, warn = scmutil.checkportabilityalert(ui)
1519 1537 if abort or warn:
1520 1538 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
1521 1539 for f in repo.walk(match):
1522 1540 exact = match.exact(f)
1523 1541 if exact or not explicitonly and f not in repo.dirstate:
1524 1542 if cca:
1525 1543 cca(f)
1526 1544 names.append(f)
1527 1545 if ui.verbose or not exact:
1528 1546 ui.status(_('adding %s\n') % match.rel(join(f)))
1529 1547
1530 1548 for subpath in sorted(wctx.substate):
1531 1549 sub = wctx.sub(subpath)
1532 1550 try:
1533 1551 submatch = matchmod.narrowmatcher(subpath, match)
1534 1552 if listsubrepos:
1535 1553 bad.extend(sub.add(ui, submatch, dryrun, listsubrepos, prefix,
1536 1554 False))
1537 1555 else:
1538 1556 bad.extend(sub.add(ui, submatch, dryrun, listsubrepos, prefix,
1539 1557 True))
1540 1558 except error.LookupError:
1541 1559 ui.status(_("skipping missing subrepository: %s\n")
1542 1560 % join(subpath))
1543 1561
1544 1562 if not dryrun:
1545 1563 rejected = wctx.add(names, prefix)
1546 1564 bad.extend(f for f in rejected if f in match.files())
1547 1565 return bad
1548 1566
1549 1567 def forget(ui, repo, match, prefix, explicitonly):
1550 1568 join = lambda f: os.path.join(prefix, f)
1551 1569 bad = []
1552 1570 oldbad = match.bad
1553 1571 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
1554 1572 wctx = repo[None]
1555 1573 forgot = []
1556 1574 s = repo.status(match=match, clean=True)
1557 1575 forget = sorted(s[0] + s[1] + s[3] + s[6])
1558 1576 if explicitonly:
1559 1577 forget = [f for f in forget if match.exact(f)]
1560 1578
1561 1579 for subpath in sorted(wctx.substate):
1562 1580 sub = wctx.sub(subpath)
1563 1581 try:
1564 1582 submatch = matchmod.narrowmatcher(subpath, match)
1565 1583 subbad, subforgot = sub.forget(ui, submatch, prefix)
1566 1584 bad.extend([subpath + '/' + f for f in subbad])
1567 1585 forgot.extend([subpath + '/' + f for f in subforgot])
1568 1586 except error.LookupError:
1569 1587 ui.status(_("skipping missing subrepository: %s\n")
1570 1588 % join(subpath))
1571 1589
1572 1590 if not explicitonly:
1573 1591 for f in match.files():
1574 1592 if f not in repo.dirstate and not os.path.isdir(match.rel(join(f))):
1575 1593 if f not in forgot:
1576 1594 if os.path.exists(match.rel(join(f))):
1577 1595 ui.warn(_('not removing %s: '
1578 1596 'file is already untracked\n')
1579 1597 % match.rel(join(f)))
1580 1598 bad.append(f)
1581 1599
1582 1600 for f in forget:
1583 1601 if ui.verbose or not match.exact(f):
1584 1602 ui.status(_('removing %s\n') % match.rel(join(f)))
1585 1603
1586 1604 rejected = wctx.forget(forget, prefix)
1587 1605 bad.extend(f for f in rejected if f in match.files())
1588 1606 forgot.extend(forget)
1589 1607 return bad, forgot
1590 1608
1591 1609 def duplicatecopies(repo, rev, fromrev):
1592 1610 '''reproduce copies from fromrev to rev in the dirstate'''
1593 1611 for dst, src in copies.pathcopies(repo[fromrev], repo[rev]).iteritems():
1594 1612 # copies.pathcopies returns backward renames, so dst might not
1595 1613 # actually be in the dirstate
1596 1614 if repo.dirstate[dst] in "nma":
1597 1615 repo.dirstate.copy(src, dst)
1598 1616
1599 1617 def commit(ui, repo, commitfunc, pats, opts):
1600 1618 '''commit the specified files or all outstanding changes'''
1601 1619 date = opts.get('date')
1602 1620 if date:
1603 1621 opts['date'] = util.parsedate(date)
1604 1622 message = logmessage(ui, opts)
1605 1623
1606 1624 # extract addremove carefully -- this function can be called from a command
1607 1625 # that doesn't support addremove
1608 1626 if opts.get('addremove'):
1609 1627 scmutil.addremove(repo, pats, opts)
1610 1628
1611 1629 return commitfunc(ui, repo, message,
1612 1630 scmutil.match(repo[None], pats, opts), opts)
1613 1631
1614 1632 def amend(ui, repo, commitfunc, old, extra, pats, opts):
1615 1633 ui.note(_('amending changeset %s\n') % old)
1616 1634 base = old.p1()
1617 1635
1618 1636 wlock = lock = newid = None
1619 1637 try:
1620 1638 wlock = repo.wlock()
1621 1639 lock = repo.lock()
1622 1640 tr = repo.transaction('amend')
1623 1641 try:
1624 1642 # See if we got a message from -m or -l, if not, open the editor
1625 1643 # with the message of the changeset to amend
1626 1644 message = logmessage(ui, opts)
1627 1645 # ensure logfile does not conflict with later enforcement of the
1628 1646 # message. potential logfile content has been processed by
1629 1647 # `logmessage` anyway.
1630 1648 opts.pop('logfile')
1631 1649 # First, do a regular commit to record all changes in the working
1632 1650 # directory (if there are any)
1633 1651 ui.callhooks = False
1634 1652 currentbookmark = repo._bookmarkcurrent
1635 1653 try:
1636 1654 repo._bookmarkcurrent = None
1637 1655 opts['message'] = 'temporary amend commit for %s' % old
1638 1656 node = commit(ui, repo, commitfunc, pats, opts)
1639 1657 finally:
1640 1658 repo._bookmarkcurrent = currentbookmark
1641 1659 ui.callhooks = True
1642 1660 ctx = repo[node]
1643 1661
1644 1662 # Participating changesets:
1645 1663 #
1646 1664 # node/ctx o - new (intermediate) commit that contains changes
1647 1665 # | from working dir to go into amending commit
1648 1666 # | (or a workingctx if there were no changes)
1649 1667 # |
1650 1668 # old o - changeset to amend
1651 1669 # |
1652 1670 # base o - parent of amending changeset
1653 1671
1654 1672 # Update extra dict from amended commit (e.g. to preserve graft
1655 1673 # source)
1656 1674 extra.update(old.extra())
1657 1675
1658 1676 # Also update it from the intermediate commit or from the wctx
1659 1677 extra.update(ctx.extra())
1660 1678
1661 1679 if len(old.parents()) > 1:
1662 1680 # ctx.files() isn't reliable for merges, so fall back to the
1663 1681 # slower repo.status() method
1664 1682 files = set([fn for st in repo.status(base, old)[:3]
1665 1683 for fn in st])
1666 1684 else:
1667 1685 files = set(old.files())
1668 1686
1669 1687 # Second, we use either the commit we just did, or if there were no
1670 1688 # changes the parent of the working directory as the version of the
1671 1689 # files in the final amend commit
1672 1690 if node:
1673 1691 ui.note(_('copying changeset %s to %s\n') % (ctx, base))
1674 1692
1675 1693 user = ctx.user()
1676 1694 date = ctx.date()
1677 1695 # Recompute copies (avoid recording a -> b -> a)
1678 1696 copied = copies.pathcopies(base, ctx)
1679 1697
1680 1698 # Prune files which were reverted by the updates: if old
1681 1699 # introduced file X and our intermediate commit, node,
1682 1700 # renamed that file, then those two files are the same and
1683 1701 # we can discard X from our list of files. Likewise if X
1684 1702 # was deleted, it's no longer relevant
1685 1703 files.update(ctx.files())
1686 1704
1687 1705 def samefile(f):
1688 1706 if f in ctx.manifest():
1689 1707 a = ctx.filectx(f)
1690 1708 if f in base.manifest():
1691 1709 b = base.filectx(f)
1692 1710 return (not a.cmp(b)
1693 1711 and a.flags() == b.flags())
1694 1712 else:
1695 1713 return False
1696 1714 else:
1697 1715 return f not in base.manifest()
1698 1716 files = [f for f in files if not samefile(f)]
1699 1717
1700 1718 def filectxfn(repo, ctx_, path):
1701 1719 try:
1702 1720 fctx = ctx[path]
1703 1721 flags = fctx.flags()
1704 1722 mctx = context.memfilectx(fctx.path(), fctx.data(),
1705 1723 islink='l' in flags,
1706 1724 isexec='x' in flags,
1707 1725 copied=copied.get(path))
1708 1726 return mctx
1709 1727 except KeyError:
1710 1728 raise IOError
1711 1729 else:
1712 1730 ui.note(_('copying changeset %s to %s\n') % (old, base))
1713 1731
1714 1732 # Use version of files as in the old cset
1715 1733 def filectxfn(repo, ctx_, path):
1716 1734 try:
1717 1735 return old.filectx(path)
1718 1736 except KeyError:
1719 1737 raise IOError
1720 1738
1721 1739 user = opts.get('user') or old.user()
1722 1740 date = opts.get('date') or old.date()
1723 1741 editmsg = False
1724 1742 if not message:
1725 1743 editmsg = True
1726 1744 message = old.description()
1727 1745
1728 1746 pureextra = extra.copy()
1729 1747 extra['amend_source'] = old.hex()
1730 1748
1731 1749 new = context.memctx(repo,
1732 1750 parents=[base.node(), old.p2().node()],
1733 1751 text=message,
1734 1752 files=files,
1735 1753 filectxfn=filectxfn,
1736 1754 user=user,
1737 1755 date=date,
1738 1756 extra=extra)
1739 1757 if editmsg:
1740 1758 new._text = commitforceeditor(repo, new, [])
1741 1759
1742 1760 newdesc = changelog.stripdesc(new.description())
1743 1761 if ((not node)
1744 1762 and newdesc == old.description()
1745 1763 and user == old.user()
1746 1764 and date == old.date()
1747 1765 and pureextra == old.extra()):
1748 1766 # nothing changed. continuing here would create a new node
1749 1767 # anyway because of the amend_source noise.
1750 1768 #
1751 1769 # This not what we expect from amend.
1752 1770 return old.node()
1753 1771
1754 1772 ph = repo.ui.config('phases', 'new-commit', phases.draft)
1755 1773 try:
1756 1774 repo.ui.setconfig('phases', 'new-commit', old.phase())
1757 1775 newid = repo.commitctx(new)
1758 1776 finally:
1759 1777 repo.ui.setconfig('phases', 'new-commit', ph)
1760 1778 if newid != old.node():
1761 1779 # Reroute the working copy parent to the new changeset
1762 1780 repo.setparents(newid, nullid)
1763 1781
1764 1782 # Move bookmarks from old parent to amend commit
1765 1783 bms = repo.nodebookmarks(old.node())
1766 1784 if bms:
1767 1785 marks = repo._bookmarks
1768 1786 for bm in bms:
1769 1787 marks[bm] = newid
1770 1788 marks.write()
1771 1789 #commit the whole amend process
1772 1790 if obsolete._enabled and newid != old.node():
1773 1791 # mark the new changeset as successor of the rewritten one
1774 1792 new = repo[newid]
1775 1793 obs = [(old, (new,))]
1776 1794 if node:
1777 1795 obs.append((ctx, ()))
1778 1796
1779 1797 obsolete.createmarkers(repo, obs)
1780 1798 tr.close()
1781 1799 finally:
1782 1800 tr.release()
1783 1801 if (not obsolete._enabled) and newid != old.node():
1784 1802 # Strip the intermediate commit (if there was one) and the amended
1785 1803 # commit
1786 1804 if node:
1787 1805 ui.note(_('stripping intermediate changeset %s\n') % ctx)
1788 1806 ui.note(_('stripping amended changeset %s\n') % old)
1789 1807 repair.strip(ui, repo, old.node(), topic='amend-backup')
1790 1808 finally:
1791 1809 if newid is None:
1792 1810 repo.dirstate.invalidate()
1793 1811 lockmod.release(lock, wlock)
1794 1812 return newid
1795 1813
1796 1814 def commiteditor(repo, ctx, subs):
1797 1815 if ctx.description():
1798 1816 return ctx.description()
1799 1817 return commitforceeditor(repo, ctx, subs)
1800 1818
1801 1819 def commitforceeditor(repo, ctx, subs):
1802 1820 edittext = []
1803 1821 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
1804 1822 if ctx.description():
1805 1823 edittext.append(ctx.description())
1806 1824 edittext.append("")
1807 1825 edittext.append("") # Empty line between message and comments.
1808 1826 edittext.append(_("HG: Enter commit message."
1809 1827 " Lines beginning with 'HG:' are removed."))
1810 1828 edittext.append(_("HG: Leave message empty to abort commit."))
1811 1829 edittext.append("HG: --")
1812 1830 edittext.append(_("HG: user: %s") % ctx.user())
1813 1831 if ctx.p2():
1814 1832 edittext.append(_("HG: branch merge"))
1815 1833 if ctx.branch():
1816 1834 edittext.append(_("HG: branch '%s'") % ctx.branch())
1817 1835 if bookmarks.iscurrent(repo):
1818 1836 edittext.append(_("HG: bookmark '%s'") % repo._bookmarkcurrent)
1819 1837 edittext.extend([_("HG: subrepo %s") % s for s in subs])
1820 1838 edittext.extend([_("HG: added %s") % f for f in added])
1821 1839 edittext.extend([_("HG: changed %s") % f for f in modified])
1822 1840 edittext.extend([_("HG: removed %s") % f for f in removed])
1823 1841 if not added and not modified and not removed:
1824 1842 edittext.append(_("HG: no files changed"))
1825 1843 edittext.append("")
1826 1844 # run editor in the repository root
1827 1845 olddir = os.getcwd()
1828 1846 os.chdir(repo.root)
1829 1847 text = repo.ui.edit("\n".join(edittext), ctx.user())
1830 1848 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
1831 1849 os.chdir(olddir)
1832 1850
1833 1851 if not text.strip():
1834 1852 raise util.Abort(_("empty commit message"))
1835 1853
1836 1854 return text
1837 1855
1838 1856 def commitstatus(repo, node, branch, bheads=None, opts={}):
1839 1857 ctx = repo[node]
1840 1858 parents = ctx.parents()
1841 1859
1842 1860 if (not opts.get('amend') and bheads and node not in bheads and not
1843 1861 [x for x in parents if x.node() in bheads and x.branch() == branch]):
1844 1862 repo.ui.status(_('created new head\n'))
1845 1863 # The message is not printed for initial roots. For the other
1846 1864 # changesets, it is printed in the following situations:
1847 1865 #
1848 1866 # Par column: for the 2 parents with ...
1849 1867 # N: null or no parent
1850 1868 # B: parent is on another named branch
1851 1869 # C: parent is a regular non head changeset
1852 1870 # H: parent was a branch head of the current branch
1853 1871 # Msg column: whether we print "created new head" message
1854 1872 # In the following, it is assumed that there already exists some
1855 1873 # initial branch heads of the current branch, otherwise nothing is
1856 1874 # printed anyway.
1857 1875 #
1858 1876 # Par Msg Comment
1859 1877 # N N y additional topo root
1860 1878 #
1861 1879 # B N y additional branch root
1862 1880 # C N y additional topo head
1863 1881 # H N n usual case
1864 1882 #
1865 1883 # B B y weird additional branch root
1866 1884 # C B y branch merge
1867 1885 # H B n merge with named branch
1868 1886 #
1869 1887 # C C y additional head from merge
1870 1888 # C H n merge with a head
1871 1889 #
1872 1890 # H H n head merge: head count decreases
1873 1891
1874 1892 if not opts.get('close_branch'):
1875 1893 for r in parents:
1876 1894 if r.closesbranch() and r.branch() == branch:
1877 1895 repo.ui.status(_('reopening closed branch head %d\n') % r)
1878 1896
1879 1897 if repo.ui.debugflag:
1880 1898 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx.hex()))
1881 1899 elif repo.ui.verbose:
1882 1900 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx))
1883 1901
1884 1902 def revert(ui, repo, ctx, parents, *pats, **opts):
1885 1903 parent, p2 = parents
1886 1904 node = ctx.node()
1887 1905
1888 1906 mf = ctx.manifest()
1889 1907 if node == parent:
1890 1908 pmf = mf
1891 1909 else:
1892 1910 pmf = None
1893 1911
1894 1912 # need all matching names in dirstate and manifest of target rev,
1895 1913 # so have to walk both. do not print errors if files exist in one
1896 1914 # but not other.
1897 1915
1898 1916 names = {}
1899 1917
1900 1918 wlock = repo.wlock()
1901 1919 try:
1902 1920 # walk dirstate.
1903 1921
1904 1922 m = scmutil.match(repo[None], pats, opts)
1905 1923 m.bad = lambda x, y: False
1906 1924 for abs in repo.walk(m):
1907 1925 names[abs] = m.rel(abs), m.exact(abs)
1908 1926
1909 1927 # walk target manifest.
1910 1928
1911 1929 def badfn(path, msg):
1912 1930 if path in names:
1913 1931 return
1914 1932 if path in ctx.substate:
1915 1933 return
1916 1934 path_ = path + '/'
1917 1935 for f in names:
1918 1936 if f.startswith(path_):
1919 1937 return
1920 1938 ui.warn("%s: %s\n" % (m.rel(path), msg))
1921 1939
1922 1940 m = scmutil.match(ctx, pats, opts)
1923 1941 m.bad = badfn
1924 1942 for abs in ctx.walk(m):
1925 1943 if abs not in names:
1926 1944 names[abs] = m.rel(abs), m.exact(abs)
1927 1945
1928 1946 # get the list of subrepos that must be reverted
1929 1947 targetsubs = sorted(s for s in ctx.substate if m(s))
1930 1948 m = scmutil.matchfiles(repo, names)
1931 1949 changes = repo.status(match=m)[:4]
1932 1950 modified, added, removed, deleted = map(set, changes)
1933 1951
1934 1952 # if f is a rename, also revert the source
1935 1953 cwd = repo.getcwd()
1936 1954 for f in added:
1937 1955 src = repo.dirstate.copied(f)
1938 1956 if src and src not in names and repo.dirstate[src] == 'r':
1939 1957 removed.add(src)
1940 1958 names[src] = (repo.pathto(src, cwd), True)
1941 1959
1942 1960 def removeforget(abs):
1943 1961 if repo.dirstate[abs] == 'a':
1944 1962 return _('forgetting %s\n')
1945 1963 return _('removing %s\n')
1946 1964
1947 1965 revert = ([], _('reverting %s\n'))
1948 1966 add = ([], _('adding %s\n'))
1949 1967 remove = ([], removeforget)
1950 1968 undelete = ([], _('undeleting %s\n'))
1951 1969
1952 1970 disptable = (
1953 1971 # dispatch table:
1954 1972 # file state
1955 1973 # action if in target manifest
1956 1974 # action if not in target manifest
1957 1975 # make backup if in target manifest
1958 1976 # make backup if not in target manifest
1959 1977 (modified, revert, remove, True, True),
1960 1978 (added, revert, remove, True, False),
1961 1979 (removed, undelete, None, False, False),
1962 1980 (deleted, revert, remove, False, False),
1963 1981 )
1964 1982
1965 1983 for abs, (rel, exact) in sorted(names.items()):
1966 1984 mfentry = mf.get(abs)
1967 1985 target = repo.wjoin(abs)
1968 1986 def handle(xlist, dobackup):
1969 1987 xlist[0].append(abs)
1970 1988 if (dobackup and not opts.get('no_backup') and
1971 1989 os.path.lexists(target)):
1972 1990 bakname = "%s.orig" % rel
1973 1991 ui.note(_('saving current version of %s as %s\n') %
1974 1992 (rel, bakname))
1975 1993 if not opts.get('dry_run'):
1976 1994 util.rename(target, bakname)
1977 1995 if ui.verbose or not exact:
1978 1996 msg = xlist[1]
1979 1997 if not isinstance(msg, basestring):
1980 1998 msg = msg(abs)
1981 1999 ui.status(msg % rel)
1982 2000 for table, hitlist, misslist, backuphit, backupmiss in disptable:
1983 2001 if abs not in table:
1984 2002 continue
1985 2003 # file has changed in dirstate
1986 2004 if mfentry:
1987 2005 handle(hitlist, backuphit)
1988 2006 elif misslist is not None:
1989 2007 handle(misslist, backupmiss)
1990 2008 break
1991 2009 else:
1992 2010 if abs not in repo.dirstate:
1993 2011 if mfentry:
1994 2012 handle(add, True)
1995 2013 elif exact:
1996 2014 ui.warn(_('file not managed: %s\n') % rel)
1997 2015 continue
1998 2016 # file has not changed in dirstate
1999 2017 if node == parent:
2000 2018 if exact:
2001 2019 ui.warn(_('no changes needed to %s\n') % rel)
2002 2020 continue
2003 2021 if pmf is None:
2004 2022 # only need parent manifest in this unlikely case,
2005 2023 # so do not read by default
2006 2024 pmf = repo[parent].manifest()
2007 2025 if abs in pmf and mfentry:
2008 2026 # if version of file is same in parent and target
2009 2027 # manifests, do nothing
2010 2028 if (pmf[abs] != mfentry or
2011 2029 pmf.flags(abs) != mf.flags(abs)):
2012 2030 handle(revert, False)
2013 2031 else:
2014 2032 handle(remove, False)
2015 2033
2016 2034 if not opts.get('dry_run'):
2017 2035 def checkout(f):
2018 2036 fc = ctx[f]
2019 2037 repo.wwrite(f, fc.data(), fc.flags())
2020 2038
2021 2039 audit_path = scmutil.pathauditor(repo.root)
2022 2040 for f in remove[0]:
2023 2041 if repo.dirstate[f] == 'a':
2024 2042 repo.dirstate.drop(f)
2025 2043 continue
2026 2044 audit_path(f)
2027 2045 try:
2028 2046 util.unlinkpath(repo.wjoin(f))
2029 2047 except OSError:
2030 2048 pass
2031 2049 repo.dirstate.remove(f)
2032 2050
2033 2051 normal = None
2034 2052 if node == parent:
2035 2053 # We're reverting to our parent. If possible, we'd like status
2036 2054 # to report the file as clean. We have to use normallookup for
2037 2055 # merges to avoid losing information about merged/dirty files.
2038 2056 if p2 != nullid:
2039 2057 normal = repo.dirstate.normallookup
2040 2058 else:
2041 2059 normal = repo.dirstate.normal
2042 2060 for f in revert[0]:
2043 2061 checkout(f)
2044 2062 if normal:
2045 2063 normal(f)
2046 2064
2047 2065 for f in add[0]:
2048 2066 checkout(f)
2049 2067 repo.dirstate.add(f)
2050 2068
2051 2069 normal = repo.dirstate.normallookup
2052 2070 if node == parent and p2 == nullid:
2053 2071 normal = repo.dirstate.normal
2054 2072 for f in undelete[0]:
2055 2073 checkout(f)
2056 2074 normal(f)
2057 2075
2058 2076 copied = copies.pathcopies(repo[parent], ctx)
2059 2077
2060 2078 for f in add[0] + undelete[0] + revert[0]:
2061 2079 if f in copied:
2062 2080 repo.dirstate.copy(copied[f], f)
2063 2081
2064 2082 if targetsubs:
2065 2083 # Revert the subrepos on the revert list
2066 2084 for sub in targetsubs:
2067 2085 ctx.sub(sub).revert(ui, ctx.substate[sub], *pats, **opts)
2068 2086 finally:
2069 2087 wlock.release()
2070 2088
2071 2089 def command(table):
2072 2090 '''returns a function object bound to table which can be used as
2073 2091 a decorator for populating table as a command table'''
2074 2092
2075 2093 def cmd(name, options=(), synopsis=None):
2076 2094 def decorator(func):
2077 2095 if synopsis:
2078 2096 table[name] = func, list(options), synopsis
2079 2097 else:
2080 2098 table[name] = func, list(options)
2081 2099 return func
2082 2100 return decorator
2083 2101
2084 2102 return cmd
2085 2103
2086 2104 # a list of (ui, repo) functions called by commands.summary
2087 2105 summaryhooks = util.hooks()
General Comments 0
You need to be logged in to leave comments. Login now