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