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