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