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