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