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