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