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