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