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