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