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