##// END OF EJS Templates
cmdutil: make private copies of option lists to avoid sharing monkeypatches
Matt Mackall -
r14442:5b48ad1e default
parent child Browse files
Show More
@@ -1,1224 +1,1224 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
12 12 import match as matchmod
13 13 import subrepo
14 14
15 15 def parsealiases(cmd):
16 16 return cmd.lstrip("^").split("|")
17 17
18 18 def findpossible(cmd, table, strict=False):
19 19 """
20 20 Return cmd -> (aliases, command table entry)
21 21 for each matching command.
22 22 Return debug commands (or their aliases) only if no normal command matches.
23 23 """
24 24 choice = {}
25 25 debugchoice = {}
26 26 for e in table.keys():
27 27 aliases = parsealiases(e)
28 28 found = None
29 29 if cmd in aliases:
30 30 found = cmd
31 31 elif not strict:
32 32 for a in aliases:
33 33 if a.startswith(cmd):
34 34 found = a
35 35 break
36 36 if found is not None:
37 37 if aliases[0].startswith("debug") or found.startswith("debug"):
38 38 debugchoice[found] = (aliases, table[e])
39 39 else:
40 40 choice[found] = (aliases, table[e])
41 41
42 42 if not choice and debugchoice:
43 43 choice = debugchoice
44 44
45 45 return choice
46 46
47 47 def findcmd(cmd, table, strict=True):
48 48 """Return (aliases, command table entry) for command string."""
49 49 choice = findpossible(cmd, table, strict)
50 50
51 51 if cmd in choice:
52 52 return choice[cmd]
53 53
54 54 if len(choice) > 1:
55 55 clist = choice.keys()
56 56 clist.sort()
57 57 raise error.AmbiguousCommand(cmd, clist)
58 58
59 59 if choice:
60 60 return choice.values()[0]
61 61
62 62 raise error.UnknownCommand(cmd)
63 63
64 64 def findrepo(p):
65 65 while not os.path.isdir(os.path.join(p, ".hg")):
66 66 oldp, p = p, os.path.dirname(p)
67 67 if p == oldp:
68 68 return None
69 69
70 70 return p
71 71
72 72 def bailifchanged(repo):
73 73 if repo.dirstate.p2() != nullid:
74 74 raise util.Abort(_('outstanding uncommitted merge'))
75 75 modified, added, removed, deleted = repo.status()[:4]
76 76 if modified or added or removed or deleted:
77 77 raise util.Abort(_("outstanding uncommitted changes"))
78 78
79 79 def logmessage(opts):
80 80 """ get the log message according to -m and -l option """
81 81 message = opts.get('message')
82 82 logfile = opts.get('logfile')
83 83
84 84 if message and logfile:
85 85 raise util.Abort(_('options --message and --logfile are mutually '
86 86 'exclusive'))
87 87 if not message and logfile:
88 88 try:
89 89 if logfile == '-':
90 90 message = sys.stdin.read()
91 91 else:
92 92 message = '\n'.join(util.readfile(logfile).splitlines())
93 93 except IOError, inst:
94 94 raise util.Abort(_("can't read commit message '%s': %s") %
95 95 (logfile, inst.strerror))
96 96 return message
97 97
98 98 def loglimit(opts):
99 99 """get the log limit according to option -l/--limit"""
100 100 limit = opts.get('limit')
101 101 if limit:
102 102 try:
103 103 limit = int(limit)
104 104 except ValueError:
105 105 raise util.Abort(_('limit must be a positive integer'))
106 106 if limit <= 0:
107 107 raise util.Abort(_('limit must be positive'))
108 108 else:
109 109 limit = None
110 110 return limit
111 111
112 112 def makefilename(repo, pat, node,
113 113 total=None, seqno=None, revwidth=None, pathname=None):
114 114 node_expander = {
115 115 'H': lambda: hex(node),
116 116 'R': lambda: str(repo.changelog.rev(node)),
117 117 'h': lambda: short(node),
118 118 }
119 119 expander = {
120 120 '%': lambda: '%',
121 121 'b': lambda: os.path.basename(repo.root),
122 122 }
123 123
124 124 try:
125 125 if node:
126 126 expander.update(node_expander)
127 127 if node:
128 128 expander['r'] = (lambda:
129 129 str(repo.changelog.rev(node)).zfill(revwidth or 0))
130 130 if total is not None:
131 131 expander['N'] = lambda: str(total)
132 132 if seqno is not None:
133 133 expander['n'] = lambda: str(seqno)
134 134 if total is not None and seqno is not None:
135 135 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
136 136 if pathname is not None:
137 137 expander['s'] = lambda: os.path.basename(pathname)
138 138 expander['d'] = lambda: os.path.dirname(pathname) or '.'
139 139 expander['p'] = lambda: pathname
140 140
141 141 newname = []
142 142 patlen = len(pat)
143 143 i = 0
144 144 while i < patlen:
145 145 c = pat[i]
146 146 if c == '%':
147 147 i += 1
148 148 c = pat[i]
149 149 c = expander[c]()
150 150 newname.append(c)
151 151 i += 1
152 152 return ''.join(newname)
153 153 except KeyError, inst:
154 154 raise util.Abort(_("invalid format spec '%%%s' in output filename") %
155 155 inst.args[0])
156 156
157 157 def makefileobj(repo, pat, node=None, total=None,
158 158 seqno=None, revwidth=None, mode='wb', pathname=None):
159 159
160 160 writable = mode not in ('r', 'rb')
161 161
162 162 if not pat or pat == '-':
163 163 fp = writable and sys.stdout or sys.stdin
164 164 return os.fdopen(os.dup(fp.fileno()), mode)
165 165 if hasattr(pat, 'write') and writable:
166 166 return pat
167 167 if hasattr(pat, 'read') and 'r' in mode:
168 168 return pat
169 169 return open(makefilename(repo, pat, node, total, seqno, revwidth,
170 170 pathname),
171 171 mode)
172 172
173 173 def openrevlog(repo, cmd, file_, opts):
174 174 """opens the changelog, manifest, a filelog or a given revlog"""
175 175 cl = opts['changelog']
176 176 mf = opts['manifest']
177 177 msg = None
178 178 if cl and mf:
179 179 msg = _('cannot specify --changelog and --manifest at the same time')
180 180 elif cl or mf:
181 181 if file_:
182 182 msg = _('cannot specify filename with --changelog or --manifest')
183 183 elif not repo:
184 184 msg = _('cannot specify --changelog or --manifest '
185 185 'without a repository')
186 186 if msg:
187 187 raise util.Abort(msg)
188 188
189 189 r = None
190 190 if repo:
191 191 if cl:
192 192 r = repo.changelog
193 193 elif mf:
194 194 r = repo.manifest
195 195 elif file_:
196 196 filelog = repo.file(file_)
197 197 if len(filelog):
198 198 r = filelog
199 199 if not r:
200 200 if not file_:
201 201 raise error.CommandError(cmd, _('invalid arguments'))
202 202 if not os.path.isfile(file_):
203 203 raise util.Abort(_("revlog '%s' not found") % file_)
204 204 r = revlog.revlog(scmutil.opener(os.getcwd(), audit=False),
205 205 file_[:-2] + ".i")
206 206 return r
207 207
208 208 def copy(ui, repo, pats, opts, rename=False):
209 209 # called with the repo lock held
210 210 #
211 211 # hgsep => pathname that uses "/" to separate directories
212 212 # ossep => pathname that uses os.sep to separate directories
213 213 cwd = repo.getcwd()
214 214 targets = {}
215 215 after = opts.get("after")
216 216 dryrun = opts.get("dry_run")
217 217 wctx = repo[None]
218 218
219 219 def walkpat(pat):
220 220 srcs = []
221 221 badstates = after and '?' or '?r'
222 222 m = scmutil.match(repo, [pat], opts, globbed=True)
223 223 for abs in repo.walk(m):
224 224 state = repo.dirstate[abs]
225 225 rel = m.rel(abs)
226 226 exact = m.exact(abs)
227 227 if state in badstates:
228 228 if exact and state == '?':
229 229 ui.warn(_('%s: not copying - file is not managed\n') % rel)
230 230 if exact and state == 'r':
231 231 ui.warn(_('%s: not copying - file has been marked for'
232 232 ' remove\n') % rel)
233 233 continue
234 234 # abs: hgsep
235 235 # rel: ossep
236 236 srcs.append((abs, rel, exact))
237 237 return srcs
238 238
239 239 # abssrc: hgsep
240 240 # relsrc: ossep
241 241 # otarget: ossep
242 242 def copyfile(abssrc, relsrc, otarget, exact):
243 243 abstarget = scmutil.canonpath(repo.root, cwd, otarget)
244 244 reltarget = repo.pathto(abstarget, cwd)
245 245 target = repo.wjoin(abstarget)
246 246 src = repo.wjoin(abssrc)
247 247 state = repo.dirstate[abstarget]
248 248
249 249 scmutil.checkportable(ui, abstarget)
250 250
251 251 # check for collisions
252 252 prevsrc = targets.get(abstarget)
253 253 if prevsrc is not None:
254 254 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
255 255 (reltarget, repo.pathto(abssrc, cwd),
256 256 repo.pathto(prevsrc, cwd)))
257 257 return
258 258
259 259 # check for overwrites
260 260 exists = os.path.lexists(target)
261 261 if not after and exists or after and state in 'mn':
262 262 if not opts['force']:
263 263 ui.warn(_('%s: not overwriting - file exists\n') %
264 264 reltarget)
265 265 return
266 266
267 267 if after:
268 268 if not exists:
269 269 if rename:
270 270 ui.warn(_('%s: not recording move - %s does not exist\n') %
271 271 (relsrc, reltarget))
272 272 else:
273 273 ui.warn(_('%s: not recording copy - %s does not exist\n') %
274 274 (relsrc, reltarget))
275 275 return
276 276 elif not dryrun:
277 277 try:
278 278 if exists:
279 279 os.unlink(target)
280 280 targetdir = os.path.dirname(target) or '.'
281 281 if not os.path.isdir(targetdir):
282 282 os.makedirs(targetdir)
283 283 util.copyfile(src, target)
284 284 except IOError, inst:
285 285 if inst.errno == errno.ENOENT:
286 286 ui.warn(_('%s: deleted in working copy\n') % relsrc)
287 287 else:
288 288 ui.warn(_('%s: cannot copy - %s\n') %
289 289 (relsrc, inst.strerror))
290 290 return True # report a failure
291 291
292 292 if ui.verbose or not exact:
293 293 if rename:
294 294 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
295 295 else:
296 296 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
297 297
298 298 targets[abstarget] = abssrc
299 299
300 300 # fix up dirstate
301 301 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
302 302 dryrun=dryrun, cwd=cwd)
303 303 if rename and not dryrun:
304 304 wctx.remove([abssrc], not after)
305 305
306 306 # pat: ossep
307 307 # dest ossep
308 308 # srcs: list of (hgsep, hgsep, ossep, bool)
309 309 # return: function that takes hgsep and returns ossep
310 310 def targetpathfn(pat, dest, srcs):
311 311 if os.path.isdir(pat):
312 312 abspfx = scmutil.canonpath(repo.root, cwd, pat)
313 313 abspfx = util.localpath(abspfx)
314 314 if destdirexists:
315 315 striplen = len(os.path.split(abspfx)[0])
316 316 else:
317 317 striplen = len(abspfx)
318 318 if striplen:
319 319 striplen += len(os.sep)
320 320 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
321 321 elif destdirexists:
322 322 res = lambda p: os.path.join(dest,
323 323 os.path.basename(util.localpath(p)))
324 324 else:
325 325 res = lambda p: dest
326 326 return res
327 327
328 328 # pat: ossep
329 329 # dest ossep
330 330 # srcs: list of (hgsep, hgsep, ossep, bool)
331 331 # return: function that takes hgsep and returns ossep
332 332 def targetpathafterfn(pat, dest, srcs):
333 333 if matchmod.patkind(pat):
334 334 # a mercurial pattern
335 335 res = lambda p: os.path.join(dest,
336 336 os.path.basename(util.localpath(p)))
337 337 else:
338 338 abspfx = scmutil.canonpath(repo.root, cwd, pat)
339 339 if len(abspfx) < len(srcs[0][0]):
340 340 # A directory. Either the target path contains the last
341 341 # component of the source path or it does not.
342 342 def evalpath(striplen):
343 343 score = 0
344 344 for s in srcs:
345 345 t = os.path.join(dest, util.localpath(s[0])[striplen:])
346 346 if os.path.lexists(t):
347 347 score += 1
348 348 return score
349 349
350 350 abspfx = util.localpath(abspfx)
351 351 striplen = len(abspfx)
352 352 if striplen:
353 353 striplen += len(os.sep)
354 354 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
355 355 score = evalpath(striplen)
356 356 striplen1 = len(os.path.split(abspfx)[0])
357 357 if striplen1:
358 358 striplen1 += len(os.sep)
359 359 if evalpath(striplen1) > score:
360 360 striplen = striplen1
361 361 res = lambda p: os.path.join(dest,
362 362 util.localpath(p)[striplen:])
363 363 else:
364 364 # a file
365 365 if destdirexists:
366 366 res = lambda p: os.path.join(dest,
367 367 os.path.basename(util.localpath(p)))
368 368 else:
369 369 res = lambda p: dest
370 370 return res
371 371
372 372
373 373 pats = scmutil.expandpats(pats)
374 374 if not pats:
375 375 raise util.Abort(_('no source or destination specified'))
376 376 if len(pats) == 1:
377 377 raise util.Abort(_('no destination specified'))
378 378 dest = pats.pop()
379 379 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
380 380 if not destdirexists:
381 381 if len(pats) > 1 or matchmod.patkind(pats[0]):
382 382 raise util.Abort(_('with multiple sources, destination must be an '
383 383 'existing directory'))
384 384 if util.endswithsep(dest):
385 385 raise util.Abort(_('destination %s is not a directory') % dest)
386 386
387 387 tfn = targetpathfn
388 388 if after:
389 389 tfn = targetpathafterfn
390 390 copylist = []
391 391 for pat in pats:
392 392 srcs = walkpat(pat)
393 393 if not srcs:
394 394 continue
395 395 copylist.append((tfn(pat, dest, srcs), srcs))
396 396 if not copylist:
397 397 raise util.Abort(_('no files to copy'))
398 398
399 399 errors = 0
400 400 for targetpath, srcs in copylist:
401 401 for abssrc, relsrc, exact in srcs:
402 402 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
403 403 errors += 1
404 404
405 405 if errors:
406 406 ui.warn(_('(consider using --after)\n'))
407 407
408 408 return errors != 0
409 409
410 410 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
411 411 runargs=None, appendpid=False):
412 412 '''Run a command as a service.'''
413 413
414 414 if opts['daemon'] and not opts['daemon_pipefds']:
415 415 # Signal child process startup with file removal
416 416 lockfd, lockpath = tempfile.mkstemp(prefix='hg-service-')
417 417 os.close(lockfd)
418 418 try:
419 419 if not runargs:
420 420 runargs = util.hgcmd() + sys.argv[1:]
421 421 runargs.append('--daemon-pipefds=%s' % lockpath)
422 422 # Don't pass --cwd to the child process, because we've already
423 423 # changed directory.
424 424 for i in xrange(1, len(runargs)):
425 425 if runargs[i].startswith('--cwd='):
426 426 del runargs[i]
427 427 break
428 428 elif runargs[i].startswith('--cwd'):
429 429 del runargs[i:i + 2]
430 430 break
431 431 def condfn():
432 432 return not os.path.exists(lockpath)
433 433 pid = util.rundetached(runargs, condfn)
434 434 if pid < 0:
435 435 raise util.Abort(_('child process failed to start'))
436 436 finally:
437 437 try:
438 438 os.unlink(lockpath)
439 439 except OSError, e:
440 440 if e.errno != errno.ENOENT:
441 441 raise
442 442 if parentfn:
443 443 return parentfn(pid)
444 444 else:
445 445 return
446 446
447 447 if initfn:
448 448 initfn()
449 449
450 450 if opts['pid_file']:
451 451 mode = appendpid and 'a' or 'w'
452 452 fp = open(opts['pid_file'], mode)
453 453 fp.write(str(os.getpid()) + '\n')
454 454 fp.close()
455 455
456 456 if opts['daemon_pipefds']:
457 457 lockpath = opts['daemon_pipefds']
458 458 try:
459 459 os.setsid()
460 460 except AttributeError:
461 461 pass
462 462 os.unlink(lockpath)
463 463 util.hidewindow()
464 464 sys.stdout.flush()
465 465 sys.stderr.flush()
466 466
467 467 nullfd = os.open(util.nulldev, os.O_RDWR)
468 468 logfilefd = nullfd
469 469 if logfile:
470 470 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
471 471 os.dup2(nullfd, 0)
472 472 os.dup2(logfilefd, 1)
473 473 os.dup2(logfilefd, 2)
474 474 if nullfd not in (0, 1, 2):
475 475 os.close(nullfd)
476 476 if logfile and logfilefd not in (0, 1, 2):
477 477 os.close(logfilefd)
478 478
479 479 if runfn:
480 480 return runfn()
481 481
482 482 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
483 483 opts=None):
484 484 '''export changesets as hg patches.'''
485 485
486 486 total = len(revs)
487 487 revwidth = max([len(str(rev)) for rev in revs])
488 488
489 489 def single(rev, seqno, fp):
490 490 ctx = repo[rev]
491 491 node = ctx.node()
492 492 parents = [p.node() for p in ctx.parents() if p]
493 493 branch = ctx.branch()
494 494 if switch_parent:
495 495 parents.reverse()
496 496 prev = (parents and parents[0]) or nullid
497 497
498 498 shouldclose = False
499 499 if not fp:
500 500 fp = makefileobj(repo, template, node, total=total, seqno=seqno,
501 501 revwidth=revwidth, mode='ab')
502 502 if fp != template:
503 503 shouldclose = True
504 504 if fp != sys.stdout and hasattr(fp, 'name'):
505 505 repo.ui.note("%s\n" % fp.name)
506 506
507 507 fp.write("# HG changeset patch\n")
508 508 fp.write("# User %s\n" % ctx.user())
509 509 fp.write("# Date %d %d\n" % ctx.date())
510 510 if branch and branch != 'default':
511 511 fp.write("# Branch %s\n" % branch)
512 512 fp.write("# Node ID %s\n" % hex(node))
513 513 fp.write("# Parent %s\n" % hex(prev))
514 514 if len(parents) > 1:
515 515 fp.write("# Parent %s\n" % hex(parents[1]))
516 516 fp.write(ctx.description().rstrip())
517 517 fp.write("\n\n")
518 518
519 519 for chunk in patch.diff(repo, prev, node, opts=opts):
520 520 fp.write(chunk)
521 521
522 522 if shouldclose:
523 523 fp.close()
524 524
525 525 for seqno, rev in enumerate(revs):
526 526 single(rev, seqno + 1, fp)
527 527
528 528 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
529 529 changes=None, stat=False, fp=None, prefix='',
530 530 listsubrepos=False):
531 531 '''show diff or diffstat.'''
532 532 if fp is None:
533 533 write = ui.write
534 534 else:
535 535 def write(s, **kw):
536 536 fp.write(s)
537 537
538 538 if stat:
539 539 diffopts = diffopts.copy(context=0)
540 540 width = 80
541 541 if not ui.plain():
542 542 width = ui.termwidth()
543 543 chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
544 544 prefix=prefix)
545 545 for chunk, label in patch.diffstatui(util.iterlines(chunks),
546 546 width=width,
547 547 git=diffopts.git):
548 548 write(chunk, label=label)
549 549 else:
550 550 for chunk, label in patch.diffui(repo, node1, node2, match,
551 551 changes, diffopts, prefix=prefix):
552 552 write(chunk, label=label)
553 553
554 554 if listsubrepos:
555 555 ctx1 = repo[node1]
556 556 ctx2 = repo[node2]
557 557 for subpath, sub in subrepo.itersubrepos(ctx1, ctx2):
558 558 if node2 is not None:
559 559 node2 = ctx2.substate[subpath][1]
560 560 submatch = matchmod.narrowmatcher(subpath, match)
561 561 sub.diff(diffopts, node2, submatch, changes=changes,
562 562 stat=stat, fp=fp, prefix=prefix)
563 563
564 564 class changeset_printer(object):
565 565 '''show changeset information when templating not requested.'''
566 566
567 567 def __init__(self, ui, repo, patch, diffopts, buffered):
568 568 self.ui = ui
569 569 self.repo = repo
570 570 self.buffered = buffered
571 571 self.patch = patch
572 572 self.diffopts = diffopts
573 573 self.header = {}
574 574 self.hunk = {}
575 575 self.lastheader = None
576 576 self.footer = None
577 577
578 578 def flush(self, rev):
579 579 if rev in self.header:
580 580 h = self.header[rev]
581 581 if h != self.lastheader:
582 582 self.lastheader = h
583 583 self.ui.write(h)
584 584 del self.header[rev]
585 585 if rev in self.hunk:
586 586 self.ui.write(self.hunk[rev])
587 587 del self.hunk[rev]
588 588 return 1
589 589 return 0
590 590
591 591 def close(self):
592 592 if self.footer:
593 593 self.ui.write(self.footer)
594 594
595 595 def show(self, ctx, copies=None, matchfn=None, **props):
596 596 if self.buffered:
597 597 self.ui.pushbuffer()
598 598 self._show(ctx, copies, matchfn, props)
599 599 self.hunk[ctx.rev()] = self.ui.popbuffer(labeled=True)
600 600 else:
601 601 self._show(ctx, copies, matchfn, props)
602 602
603 603 def _show(self, ctx, copies, matchfn, props):
604 604 '''show a single changeset or file revision'''
605 605 changenode = ctx.node()
606 606 rev = ctx.rev()
607 607
608 608 if self.ui.quiet:
609 609 self.ui.write("%d:%s\n" % (rev, short(changenode)),
610 610 label='log.node')
611 611 return
612 612
613 613 log = self.repo.changelog
614 614 date = util.datestr(ctx.date())
615 615
616 616 hexfunc = self.ui.debugflag and hex or short
617 617
618 618 parents = [(p, hexfunc(log.node(p)))
619 619 for p in self._meaningful_parentrevs(log, rev)]
620 620
621 621 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)),
622 622 label='log.changeset')
623 623
624 624 branch = ctx.branch()
625 625 # don't show the default branch name
626 626 if branch != 'default':
627 627 self.ui.write(_("branch: %s\n") % branch,
628 628 label='log.branch')
629 629 for bookmark in self.repo.nodebookmarks(changenode):
630 630 self.ui.write(_("bookmark: %s\n") % bookmark,
631 631 label='log.bookmark')
632 632 for tag in self.repo.nodetags(changenode):
633 633 self.ui.write(_("tag: %s\n") % tag,
634 634 label='log.tag')
635 635 for parent in parents:
636 636 self.ui.write(_("parent: %d:%s\n") % parent,
637 637 label='log.parent')
638 638
639 639 if self.ui.debugflag:
640 640 mnode = ctx.manifestnode()
641 641 self.ui.write(_("manifest: %d:%s\n") %
642 642 (self.repo.manifest.rev(mnode), hex(mnode)),
643 643 label='ui.debug log.manifest')
644 644 self.ui.write(_("user: %s\n") % ctx.user(),
645 645 label='log.user')
646 646 self.ui.write(_("date: %s\n") % date,
647 647 label='log.date')
648 648
649 649 if self.ui.debugflag:
650 650 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
651 651 for key, value in zip([_("files:"), _("files+:"), _("files-:")],
652 652 files):
653 653 if value:
654 654 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
655 655 label='ui.debug log.files')
656 656 elif ctx.files() and self.ui.verbose:
657 657 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
658 658 label='ui.note log.files')
659 659 if copies and self.ui.verbose:
660 660 copies = ['%s (%s)' % c for c in copies]
661 661 self.ui.write(_("copies: %s\n") % ' '.join(copies),
662 662 label='ui.note log.copies')
663 663
664 664 extra = ctx.extra()
665 665 if extra and self.ui.debugflag:
666 666 for key, value in sorted(extra.items()):
667 667 self.ui.write(_("extra: %s=%s\n")
668 668 % (key, value.encode('string_escape')),
669 669 label='ui.debug log.extra')
670 670
671 671 description = ctx.description().strip()
672 672 if description:
673 673 if self.ui.verbose:
674 674 self.ui.write(_("description:\n"),
675 675 label='ui.note log.description')
676 676 self.ui.write(description,
677 677 label='ui.note log.description')
678 678 self.ui.write("\n\n")
679 679 else:
680 680 self.ui.write(_("summary: %s\n") %
681 681 description.splitlines()[0],
682 682 label='log.summary')
683 683 self.ui.write("\n")
684 684
685 685 self.showpatch(changenode, matchfn)
686 686
687 687 def showpatch(self, node, matchfn):
688 688 if not matchfn:
689 689 matchfn = self.patch
690 690 if matchfn:
691 691 stat = self.diffopts.get('stat')
692 692 diff = self.diffopts.get('patch')
693 693 diffopts = patch.diffopts(self.ui, self.diffopts)
694 694 prev = self.repo.changelog.parents(node)[0]
695 695 if stat:
696 696 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
697 697 match=matchfn, stat=True)
698 698 if diff:
699 699 if stat:
700 700 self.ui.write("\n")
701 701 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
702 702 match=matchfn, stat=False)
703 703 self.ui.write("\n")
704 704
705 705 def _meaningful_parentrevs(self, log, rev):
706 706 """Return list of meaningful (or all if debug) parentrevs for rev.
707 707
708 708 For merges (two non-nullrev revisions) both parents are meaningful.
709 709 Otherwise the first parent revision is considered meaningful if it
710 710 is not the preceding revision.
711 711 """
712 712 parents = log.parentrevs(rev)
713 713 if not self.ui.debugflag and parents[1] == nullrev:
714 714 if parents[0] >= rev - 1:
715 715 parents = []
716 716 else:
717 717 parents = [parents[0]]
718 718 return parents
719 719
720 720
721 721 class changeset_templater(changeset_printer):
722 722 '''format changeset information.'''
723 723
724 724 def __init__(self, ui, repo, patch, diffopts, mapfile, buffered):
725 725 changeset_printer.__init__(self, ui, repo, patch, diffopts, buffered)
726 726 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
727 727 defaulttempl = {
728 728 'parent': '{rev}:{node|formatnode} ',
729 729 'manifest': '{rev}:{node|formatnode}',
730 730 'file_copy': '{name} ({source})',
731 731 'extra': '{key}={value|stringescape}'
732 732 }
733 733 # filecopy is preserved for compatibility reasons
734 734 defaulttempl['filecopy'] = defaulttempl['file_copy']
735 735 self.t = templater.templater(mapfile, {'formatnode': formatnode},
736 736 cache=defaulttempl)
737 737 self.cache = {}
738 738
739 739 def use_template(self, t):
740 740 '''set template string to use'''
741 741 self.t.cache['changeset'] = t
742 742
743 743 def _meaningful_parentrevs(self, ctx):
744 744 """Return list of meaningful (or all if debug) parentrevs for rev.
745 745 """
746 746 parents = ctx.parents()
747 747 if len(parents) > 1:
748 748 return parents
749 749 if self.ui.debugflag:
750 750 return [parents[0], self.repo['null']]
751 751 if parents[0].rev() >= ctx.rev() - 1:
752 752 return []
753 753 return parents
754 754
755 755 def _show(self, ctx, copies, matchfn, props):
756 756 '''show a single changeset or file revision'''
757 757
758 758 showlist = templatekw.showlist
759 759
760 760 # showparents() behaviour depends on ui trace level which
761 761 # causes unexpected behaviours at templating level and makes
762 762 # it harder to extract it in a standalone function. Its
763 763 # behaviour cannot be changed so leave it here for now.
764 764 def showparents(**args):
765 765 ctx = args['ctx']
766 766 parents = [[('rev', p.rev()), ('node', p.hex())]
767 767 for p in self._meaningful_parentrevs(ctx)]
768 768 return showlist('parent', parents, **args)
769 769
770 770 props = props.copy()
771 771 props.update(templatekw.keywords)
772 772 props['parents'] = showparents
773 773 props['templ'] = self.t
774 774 props['ctx'] = ctx
775 775 props['repo'] = self.repo
776 776 props['revcache'] = {'copies': copies}
777 777 props['cache'] = self.cache
778 778
779 779 # find correct templates for current mode
780 780
781 781 tmplmodes = [
782 782 (True, None),
783 783 (self.ui.verbose, 'verbose'),
784 784 (self.ui.quiet, 'quiet'),
785 785 (self.ui.debugflag, 'debug'),
786 786 ]
787 787
788 788 types = {'header': '', 'footer':'', 'changeset': 'changeset'}
789 789 for mode, postfix in tmplmodes:
790 790 for type in types:
791 791 cur = postfix and ('%s_%s' % (type, postfix)) or type
792 792 if mode and cur in self.t:
793 793 types[type] = cur
794 794
795 795 try:
796 796
797 797 # write header
798 798 if types['header']:
799 799 h = templater.stringify(self.t(types['header'], **props))
800 800 if self.buffered:
801 801 self.header[ctx.rev()] = h
802 802 else:
803 803 if self.lastheader != h:
804 804 self.lastheader = h
805 805 self.ui.write(h)
806 806
807 807 # write changeset metadata, then patch if requested
808 808 key = types['changeset']
809 809 self.ui.write(templater.stringify(self.t(key, **props)))
810 810 self.showpatch(ctx.node(), matchfn)
811 811
812 812 if types['footer']:
813 813 if not self.footer:
814 814 self.footer = templater.stringify(self.t(types['footer'],
815 815 **props))
816 816
817 817 except KeyError, inst:
818 818 msg = _("%s: no key named '%s'")
819 819 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
820 820 except SyntaxError, inst:
821 821 raise util.Abort('%s: %s' % (self.t.mapfile, inst.args[0]))
822 822
823 823 def show_changeset(ui, repo, opts, buffered=False):
824 824 """show one changeset using template or regular display.
825 825
826 826 Display format will be the first non-empty hit of:
827 827 1. option 'template'
828 828 2. option 'style'
829 829 3. [ui] setting 'logtemplate'
830 830 4. [ui] setting 'style'
831 831 If all of these values are either the unset or the empty string,
832 832 regular display via changeset_printer() is done.
833 833 """
834 834 # options
835 835 patch = False
836 836 if opts.get('patch') or opts.get('stat'):
837 837 patch = scmutil.matchall(repo)
838 838
839 839 tmpl = opts.get('template')
840 840 style = None
841 841 if tmpl:
842 842 tmpl = templater.parsestring(tmpl, quoted=False)
843 843 else:
844 844 style = opts.get('style')
845 845
846 846 # ui settings
847 847 if not (tmpl or style):
848 848 tmpl = ui.config('ui', 'logtemplate')
849 849 if tmpl:
850 850 tmpl = templater.parsestring(tmpl)
851 851 else:
852 852 style = util.expandpath(ui.config('ui', 'style', ''))
853 853
854 854 if not (tmpl or style):
855 855 return changeset_printer(ui, repo, patch, opts, buffered)
856 856
857 857 mapfile = None
858 858 if style and not tmpl:
859 859 mapfile = style
860 860 if not os.path.split(mapfile)[0]:
861 861 mapname = (templater.templatepath('map-cmdline.' + mapfile)
862 862 or templater.templatepath(mapfile))
863 863 if mapname:
864 864 mapfile = mapname
865 865
866 866 try:
867 867 t = changeset_templater(ui, repo, patch, opts, mapfile, buffered)
868 868 except SyntaxError, inst:
869 869 raise util.Abort(inst.args[0])
870 870 if tmpl:
871 871 t.use_template(tmpl)
872 872 return t
873 873
874 874 def finddate(ui, repo, date):
875 875 """Find the tipmost changeset that matches the given date spec"""
876 876
877 877 df = util.matchdate(date)
878 878 m = scmutil.matchall(repo)
879 879 results = {}
880 880
881 881 def prep(ctx, fns):
882 882 d = ctx.date()
883 883 if df(d[0]):
884 884 results[ctx.rev()] = d
885 885
886 886 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
887 887 rev = ctx.rev()
888 888 if rev in results:
889 889 ui.status(_("Found revision %s from %s\n") %
890 890 (rev, util.datestr(results[rev])))
891 891 return str(rev)
892 892
893 893 raise util.Abort(_("revision matching date not found"))
894 894
895 895 def walkchangerevs(repo, match, opts, prepare):
896 896 '''Iterate over files and the revs in which they changed.
897 897
898 898 Callers most commonly need to iterate backwards over the history
899 899 in which they are interested. Doing so has awful (quadratic-looking)
900 900 performance, so we use iterators in a "windowed" way.
901 901
902 902 We walk a window of revisions in the desired order. Within the
903 903 window, we first walk forwards to gather data, then in the desired
904 904 order (usually backwards) to display it.
905 905
906 906 This function returns an iterator yielding contexts. Before
907 907 yielding each context, the iterator will first call the prepare
908 908 function on each context in the window in forward order.'''
909 909
910 910 def increasing_windows(start, end, windowsize=8, sizelimit=512):
911 911 if start < end:
912 912 while start < end:
913 913 yield start, min(windowsize, end - start)
914 914 start += windowsize
915 915 if windowsize < sizelimit:
916 916 windowsize *= 2
917 917 else:
918 918 while start > end:
919 919 yield start, min(windowsize, start - end - 1)
920 920 start -= windowsize
921 921 if windowsize < sizelimit:
922 922 windowsize *= 2
923 923
924 924 follow = opts.get('follow') or opts.get('follow_first')
925 925
926 926 if not len(repo):
927 927 return []
928 928
929 929 if follow:
930 930 defrange = '%s:0' % repo['.'].rev()
931 931 else:
932 932 defrange = '-1:0'
933 933 revs = scmutil.revrange(repo, opts['rev'] or [defrange])
934 934 if not revs:
935 935 return []
936 936 wanted = set()
937 937 slowpath = match.anypats() or (match.files() and opts.get('removed'))
938 938 fncache = {}
939 939 change = util.cachefunc(repo.changectx)
940 940
941 941 # First step is to fill wanted, the set of revisions that we want to yield.
942 942 # When it does not induce extra cost, we also fill fncache for revisions in
943 943 # wanted: a cache of filenames that were changed (ctx.files()) and that
944 944 # match the file filtering conditions.
945 945
946 946 if not slowpath and not match.files():
947 947 # No files, no patterns. Display all revs.
948 948 wanted = set(revs)
949 949 copies = []
950 950
951 951 if not slowpath:
952 952 # We only have to read through the filelog to find wanted revisions
953 953
954 954 minrev, maxrev = min(revs), max(revs)
955 955 def filerevgen(filelog, last):
956 956 """
957 957 Only files, no patterns. Check the history of each file.
958 958
959 959 Examines filelog entries within minrev, maxrev linkrev range
960 960 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
961 961 tuples in backwards order
962 962 """
963 963 cl_count = len(repo)
964 964 revs = []
965 965 for j in xrange(0, last + 1):
966 966 linkrev = filelog.linkrev(j)
967 967 if linkrev < minrev:
968 968 continue
969 969 # only yield rev for which we have the changelog, it can
970 970 # happen while doing "hg log" during a pull or commit
971 971 if linkrev >= cl_count:
972 972 break
973 973
974 974 parentlinkrevs = []
975 975 for p in filelog.parentrevs(j):
976 976 if p != nullrev:
977 977 parentlinkrevs.append(filelog.linkrev(p))
978 978 n = filelog.node(j)
979 979 revs.append((linkrev, parentlinkrevs,
980 980 follow and filelog.renamed(n)))
981 981
982 982 return reversed(revs)
983 983 def iterfiles():
984 984 for filename in match.files():
985 985 yield filename, None
986 986 for filename_node in copies:
987 987 yield filename_node
988 988 for file_, node in iterfiles():
989 989 filelog = repo.file(file_)
990 990 if not len(filelog):
991 991 if node is None:
992 992 # A zero count may be a directory or deleted file, so
993 993 # try to find matching entries on the slow path.
994 994 if follow:
995 995 raise util.Abort(
996 996 _('cannot follow nonexistent file: "%s"') % file_)
997 997 slowpath = True
998 998 break
999 999 else:
1000 1000 continue
1001 1001
1002 1002 if node is None:
1003 1003 last = len(filelog) - 1
1004 1004 else:
1005 1005 last = filelog.rev(node)
1006 1006
1007 1007
1008 1008 # keep track of all ancestors of the file
1009 1009 ancestors = set([filelog.linkrev(last)])
1010 1010
1011 1011 # iterate from latest to oldest revision
1012 1012 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1013 1013 if not follow:
1014 1014 if rev > maxrev:
1015 1015 continue
1016 1016 else:
1017 1017 # Note that last might not be the first interesting
1018 1018 # rev to us:
1019 1019 # if the file has been changed after maxrev, we'll
1020 1020 # have linkrev(last) > maxrev, and we still need
1021 1021 # to explore the file graph
1022 1022 if rev not in ancestors:
1023 1023 continue
1024 1024 # XXX insert 1327 fix here
1025 1025 if flparentlinkrevs:
1026 1026 ancestors.update(flparentlinkrevs)
1027 1027
1028 1028 fncache.setdefault(rev, []).append(file_)
1029 1029 wanted.add(rev)
1030 1030 if copied:
1031 1031 copies.append(copied)
1032 1032 if slowpath:
1033 1033 # We have to read the changelog to match filenames against
1034 1034 # changed files
1035 1035
1036 1036 if follow:
1037 1037 raise util.Abort(_('can only follow copies/renames for explicit '
1038 1038 'filenames'))
1039 1039
1040 1040 # The slow path checks files modified in every changeset.
1041 1041 for i in sorted(revs):
1042 1042 ctx = change(i)
1043 1043 matches = filter(match, ctx.files())
1044 1044 if matches:
1045 1045 fncache[i] = matches
1046 1046 wanted.add(i)
1047 1047
1048 1048 class followfilter(object):
1049 1049 def __init__(self, onlyfirst=False):
1050 1050 self.startrev = nullrev
1051 1051 self.roots = set()
1052 1052 self.onlyfirst = onlyfirst
1053 1053
1054 1054 def match(self, rev):
1055 1055 def realparents(rev):
1056 1056 if self.onlyfirst:
1057 1057 return repo.changelog.parentrevs(rev)[0:1]
1058 1058 else:
1059 1059 return filter(lambda x: x != nullrev,
1060 1060 repo.changelog.parentrevs(rev))
1061 1061
1062 1062 if self.startrev == nullrev:
1063 1063 self.startrev = rev
1064 1064 return True
1065 1065
1066 1066 if rev > self.startrev:
1067 1067 # forward: all descendants
1068 1068 if not self.roots:
1069 1069 self.roots.add(self.startrev)
1070 1070 for parent in realparents(rev):
1071 1071 if parent in self.roots:
1072 1072 self.roots.add(rev)
1073 1073 return True
1074 1074 else:
1075 1075 # backwards: all parents
1076 1076 if not self.roots:
1077 1077 self.roots.update(realparents(self.startrev))
1078 1078 if rev in self.roots:
1079 1079 self.roots.remove(rev)
1080 1080 self.roots.update(realparents(rev))
1081 1081 return True
1082 1082
1083 1083 return False
1084 1084
1085 1085 # it might be worthwhile to do this in the iterator if the rev range
1086 1086 # is descending and the prune args are all within that range
1087 1087 for rev in opts.get('prune', ()):
1088 1088 rev = repo.changelog.rev(repo.lookup(rev))
1089 1089 ff = followfilter()
1090 1090 stop = min(revs[0], revs[-1])
1091 1091 for x in xrange(rev, stop - 1, -1):
1092 1092 if ff.match(x):
1093 1093 wanted.discard(x)
1094 1094
1095 1095 # Now that wanted is correctly initialized, we can iterate over the
1096 1096 # revision range, yielding only revisions in wanted.
1097 1097 def iterate():
1098 1098 if follow and not match.files():
1099 1099 ff = followfilter(onlyfirst=opts.get('follow_first'))
1100 1100 def want(rev):
1101 1101 return ff.match(rev) and rev in wanted
1102 1102 else:
1103 1103 def want(rev):
1104 1104 return rev in wanted
1105 1105
1106 1106 for i, window in increasing_windows(0, len(revs)):
1107 1107 nrevs = [rev for rev in revs[i:i + window] if want(rev)]
1108 1108 for rev in sorted(nrevs):
1109 1109 fns = fncache.get(rev)
1110 1110 ctx = change(rev)
1111 1111 if not fns:
1112 1112 def fns_generator():
1113 1113 for f in ctx.files():
1114 1114 if match(f):
1115 1115 yield f
1116 1116 fns = fns_generator()
1117 1117 prepare(ctx, fns)
1118 1118 for rev in nrevs:
1119 1119 yield change(rev)
1120 1120 return iterate()
1121 1121
1122 1122 def add(ui, repo, match, dryrun, listsubrepos, prefix):
1123 1123 join = lambda f: os.path.join(prefix, f)
1124 1124 bad = []
1125 1125 oldbad = match.bad
1126 1126 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
1127 1127 names = []
1128 1128 wctx = repo[None]
1129 1129 cca = None
1130 1130 abort, warn = scmutil.checkportabilityalert(ui)
1131 1131 if abort or warn:
1132 1132 cca = scmutil.casecollisionauditor(ui, abort, wctx)
1133 1133 for f in repo.walk(match):
1134 1134 exact = match.exact(f)
1135 1135 if exact or f not in repo.dirstate:
1136 1136 if cca:
1137 1137 cca(f)
1138 1138 names.append(f)
1139 1139 if ui.verbose or not exact:
1140 1140 ui.status(_('adding %s\n') % match.rel(join(f)))
1141 1141
1142 1142 if listsubrepos:
1143 1143 for subpath in wctx.substate:
1144 1144 sub = wctx.sub(subpath)
1145 1145 try:
1146 1146 submatch = matchmod.narrowmatcher(subpath, match)
1147 1147 bad.extend(sub.add(ui, submatch, dryrun, prefix))
1148 1148 except error.LookupError:
1149 1149 ui.status(_("skipping missing subrepository: %s\n")
1150 1150 % join(subpath))
1151 1151
1152 1152 if not dryrun:
1153 1153 rejected = wctx.add(names, prefix)
1154 1154 bad.extend(f for f in rejected if f in match.files())
1155 1155 return bad
1156 1156
1157 1157 def commit(ui, repo, commitfunc, pats, opts):
1158 1158 '''commit the specified files or all outstanding changes'''
1159 1159 date = opts.get('date')
1160 1160 if date:
1161 1161 opts['date'] = util.parsedate(date)
1162 1162 message = logmessage(opts)
1163 1163
1164 1164 # extract addremove carefully -- this function can be called from a command
1165 1165 # that doesn't support addremove
1166 1166 if opts.get('addremove'):
1167 1167 scmutil.addremove(repo, pats, opts)
1168 1168
1169 1169 return commitfunc(ui, repo, message, scmutil.match(repo, pats, opts), opts)
1170 1170
1171 1171 def commiteditor(repo, ctx, subs):
1172 1172 if ctx.description():
1173 1173 return ctx.description()
1174 1174 return commitforceeditor(repo, ctx, subs)
1175 1175
1176 1176 def commitforceeditor(repo, ctx, subs):
1177 1177 edittext = []
1178 1178 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
1179 1179 if ctx.description():
1180 1180 edittext.append(ctx.description())
1181 1181 edittext.append("")
1182 1182 edittext.append("") # Empty line between message and comments.
1183 1183 edittext.append(_("HG: Enter commit message."
1184 1184 " Lines beginning with 'HG:' are removed."))
1185 1185 edittext.append(_("HG: Leave message empty to abort commit."))
1186 1186 edittext.append("HG: --")
1187 1187 edittext.append(_("HG: user: %s") % ctx.user())
1188 1188 if ctx.p2():
1189 1189 edittext.append(_("HG: branch merge"))
1190 1190 if ctx.branch():
1191 1191 edittext.append(_("HG: branch '%s'") % ctx.branch())
1192 1192 edittext.extend([_("HG: subrepo %s") % s for s in subs])
1193 1193 edittext.extend([_("HG: added %s") % f for f in added])
1194 1194 edittext.extend([_("HG: changed %s") % f for f in modified])
1195 1195 edittext.extend([_("HG: removed %s") % f for f in removed])
1196 1196 if not added and not modified and not removed:
1197 1197 edittext.append(_("HG: no files changed"))
1198 1198 edittext.append("")
1199 1199 # run editor in the repository root
1200 1200 olddir = os.getcwd()
1201 1201 os.chdir(repo.root)
1202 1202 text = repo.ui.edit("\n".join(edittext), ctx.user())
1203 1203 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
1204 1204 os.chdir(olddir)
1205 1205
1206 1206 if not text.strip():
1207 1207 raise util.Abort(_("empty commit message"))
1208 1208
1209 1209 return text
1210 1210
1211 1211 def command(table):
1212 1212 '''returns a function object bound to table which can be used as
1213 1213 a decorator for populating table as a command table'''
1214 1214
1215 1215 def cmd(name, options, synopsis=None):
1216 1216 def decorator(func):
1217 1217 if synopsis:
1218 table[name] = func, options, synopsis
1218 table[name] = func, options[:], synopsis
1219 1219 else:
1220 table[name] = func, options
1220 table[name] = func, options[:]
1221 1221 return func
1222 1222 return decorator
1223 1223
1224 1224 return cmd
General Comments 0
You need to be logged in to leave comments. Login now