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