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