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