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