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