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