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