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