##// END OF EJS Templates
Move 'findrenames' code into its own file....
David Greenaway -
r11059:ef4aa90b default
parent child Browse files
Show More
@@ -0,0 +1,59 b''
1 # similar.py - mechanisms for finding similar files
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
7
8 from i18n import _
9 import util
10 import mdiff
11 import bdiff
12
13 def findrenames(repo, added, removed, threshold):
14 '''find renamed files -- yields (before, after, score) tuples'''
15 copies = {}
16 ctx = repo['.']
17 for i, r in enumerate(removed):
18 repo.ui.progress(_('searching'), i, total=len(removed))
19 if r not in ctx:
20 continue
21 fctx = ctx.filectx(r)
22
23 # lazily load text
24 @util.cachefunc
25 def data():
26 orig = fctx.data()
27 return orig, mdiff.splitnewlines(orig)
28
29 def score(text):
30 if not len(text):
31 return 0.0
32 if not fctx.cmp(text):
33 return 1.0
34 if threshold == 1.0:
35 return 0.0
36 orig, lines = data()
37 # bdiff.blocks() returns blocks of matching lines
38 # count the number of bytes in each
39 equal = 0
40 matches = bdiff.blocks(text, orig)
41 for x1, x2, y1, y2 in matches:
42 for line in lines[y1:y2]:
43 equal += len(line)
44
45 lengths = len(text) + len(orig)
46 return equal * 2.0 / lengths
47
48 for a in added:
49 bestscore = copies.get(a, (None, threshold))[1]
50 myscore = score(repo.wread(a))
51 if myscore >= bestscore:
52 copies[a] = (r, myscore)
53 repo.ui.progress(_('searching'), None)
54
55 for dest, v in copies.iteritems():
56 source, score = v
57 yield source, dest, score
58
59
@@ -1,1285 +1,1240 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 or any later version.
7 7
8 8 from node import hex, nullid, nullrev, short
9 9 from i18n import _
10 10 import os, sys, errno, re, glob, tempfile
11 11 import mdiff, bdiff, util, templater, patch, error, encoding, templatekw
12 12 import match as _match
13 import similar
13 14
14 15 revrangesep = ':'
15 16
16 17 def parsealiases(cmd):
17 18 return cmd.lstrip("^").split("|")
18 19
19 20 def findpossible(cmd, table, strict=False):
20 21 """
21 22 Return cmd -> (aliases, command table entry)
22 23 for each matching command.
23 24 Return debug commands (or their aliases) only if no normal command matches.
24 25 """
25 26 choice = {}
26 27 debugchoice = {}
27 28 for e in table.keys():
28 29 aliases = parsealiases(e)
29 30 found = None
30 31 if cmd in aliases:
31 32 found = cmd
32 33 elif not strict:
33 34 for a in aliases:
34 35 if a.startswith(cmd):
35 36 found = a
36 37 break
37 38 if found is not None:
38 39 if aliases[0].startswith("debug") or found.startswith("debug"):
39 40 debugchoice[found] = (aliases, table[e])
40 41 else:
41 42 choice[found] = (aliases, table[e])
42 43
43 44 if not choice and debugchoice:
44 45 choice = debugchoice
45 46
46 47 return choice
47 48
48 49 def findcmd(cmd, table, strict=True):
49 50 """Return (aliases, command table entry) for command string."""
50 51 choice = findpossible(cmd, table, strict)
51 52
52 53 if cmd in choice:
53 54 return choice[cmd]
54 55
55 56 if len(choice) > 1:
56 57 clist = choice.keys()
57 58 clist.sort()
58 59 raise error.AmbiguousCommand(cmd, clist)
59 60
60 61 if choice:
61 62 return choice.values()[0]
62 63
63 64 raise error.UnknownCommand(cmd)
64 65
65 66 def findrepo(p):
66 67 while not os.path.isdir(os.path.join(p, ".hg")):
67 68 oldp, p = p, os.path.dirname(p)
68 69 if p == oldp:
69 70 return None
70 71
71 72 return p
72 73
73 74 def bail_if_changed(repo):
74 75 if repo.dirstate.parents()[1] != nullid:
75 76 raise util.Abort(_('outstanding uncommitted merge'))
76 77 modified, added, removed, deleted = repo.status()[:4]
77 78 if modified or added or removed or deleted:
78 79 raise util.Abort(_("outstanding uncommitted changes"))
79 80
80 81 def logmessage(opts):
81 82 """ get the log message according to -m and -l option """
82 83 message = opts.get('message')
83 84 logfile = opts.get('logfile')
84 85
85 86 if message and logfile:
86 87 raise util.Abort(_('options --message and --logfile are mutually '
87 88 'exclusive'))
88 89 if not message and logfile:
89 90 try:
90 91 if logfile == '-':
91 92 message = sys.stdin.read()
92 93 else:
93 94 message = open(logfile).read()
94 95 except IOError, inst:
95 96 raise util.Abort(_("can't read commit message '%s': %s") %
96 97 (logfile, inst.strerror))
97 98 return message
98 99
99 100 def loglimit(opts):
100 101 """get the log limit according to option -l/--limit"""
101 102 limit = opts.get('limit')
102 103 if limit:
103 104 try:
104 105 limit = int(limit)
105 106 except ValueError:
106 107 raise util.Abort(_('limit must be a positive integer'))
107 108 if limit <= 0:
108 109 raise util.Abort(_('limit must be positive'))
109 110 else:
110 111 limit = None
111 112 return limit
112 113
113 114 def remoteui(src, opts):
114 115 'build a remote ui from ui or repo and opts'
115 116 if hasattr(src, 'baseui'): # looks like a repository
116 117 dst = src.baseui.copy() # drop repo-specific config
117 118 src = src.ui # copy target options from repo
118 119 else: # assume it's a global ui object
119 120 dst = src.copy() # keep all global options
120 121
121 122 # copy ssh-specific options
122 123 for o in 'ssh', 'remotecmd':
123 124 v = opts.get(o) or src.config('ui', o)
124 125 if v:
125 126 dst.setconfig("ui", o, v)
126 127
127 128 # copy bundle-specific options
128 129 r = src.config('bundle', 'mainreporoot')
129 130 if r:
130 131 dst.setconfig('bundle', 'mainreporoot', r)
131 132
132 133 # copy auth and http_proxy section settings
133 134 for sect in ('auth', 'http_proxy'):
134 135 for key, val in src.configitems(sect):
135 136 dst.setconfig(sect, key, val)
136 137
137 138 return dst
138 139
139 140 def revpair(repo, revs):
140 141 '''return pair of nodes, given list of revisions. second item can
141 142 be None, meaning use working dir.'''
142 143
143 144 def revfix(repo, val, defval):
144 145 if not val and val != 0 and defval is not None:
145 146 val = defval
146 147 return repo.lookup(val)
147 148
148 149 if not revs:
149 150 return repo.dirstate.parents()[0], None
150 151 end = None
151 152 if len(revs) == 1:
152 153 if revrangesep in revs[0]:
153 154 start, end = revs[0].split(revrangesep, 1)
154 155 start = revfix(repo, start, 0)
155 156 end = revfix(repo, end, len(repo) - 1)
156 157 else:
157 158 start = revfix(repo, revs[0], None)
158 159 elif len(revs) == 2:
159 160 if revrangesep in revs[0] or revrangesep in revs[1]:
160 161 raise util.Abort(_('too many revisions specified'))
161 162 start = revfix(repo, revs[0], None)
162 163 end = revfix(repo, revs[1], None)
163 164 else:
164 165 raise util.Abort(_('too many revisions specified'))
165 166 return start, end
166 167
167 168 def revrange(repo, revs):
168 169 """Yield revision as strings from a list of revision specifications."""
169 170
170 171 def revfix(repo, val, defval):
171 172 if not val and val != 0 and defval is not None:
172 173 return defval
173 174 return repo.changelog.rev(repo.lookup(val))
174 175
175 176 seen, l = set(), []
176 177 for spec in revs:
177 178 if revrangesep in spec:
178 179 start, end = spec.split(revrangesep, 1)
179 180 start = revfix(repo, start, 0)
180 181 end = revfix(repo, end, len(repo) - 1)
181 182 step = start > end and -1 or 1
182 183 for rev in xrange(start, end + step, step):
183 184 if rev in seen:
184 185 continue
185 186 seen.add(rev)
186 187 l.append(rev)
187 188 else:
188 189 rev = revfix(repo, spec, None)
189 190 if rev in seen:
190 191 continue
191 192 seen.add(rev)
192 193 l.append(rev)
193 194
194 195 return l
195 196
196 197 def make_filename(repo, pat, node,
197 198 total=None, seqno=None, revwidth=None, pathname=None):
198 199 node_expander = {
199 200 'H': lambda: hex(node),
200 201 'R': lambda: str(repo.changelog.rev(node)),
201 202 'h': lambda: short(node),
202 203 }
203 204 expander = {
204 205 '%': lambda: '%',
205 206 'b': lambda: os.path.basename(repo.root),
206 207 }
207 208
208 209 try:
209 210 if node:
210 211 expander.update(node_expander)
211 212 if node:
212 213 expander['r'] = (lambda:
213 214 str(repo.changelog.rev(node)).zfill(revwidth or 0))
214 215 if total is not None:
215 216 expander['N'] = lambda: str(total)
216 217 if seqno is not None:
217 218 expander['n'] = lambda: str(seqno)
218 219 if total is not None and seqno is not None:
219 220 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
220 221 if pathname is not None:
221 222 expander['s'] = lambda: os.path.basename(pathname)
222 223 expander['d'] = lambda: os.path.dirname(pathname) or '.'
223 224 expander['p'] = lambda: pathname
224 225
225 226 newname = []
226 227 patlen = len(pat)
227 228 i = 0
228 229 while i < patlen:
229 230 c = pat[i]
230 231 if c == '%':
231 232 i += 1
232 233 c = pat[i]
233 234 c = expander[c]()
234 235 newname.append(c)
235 236 i += 1
236 237 return ''.join(newname)
237 238 except KeyError, inst:
238 239 raise util.Abort(_("invalid format spec '%%%s' in output filename") %
239 240 inst.args[0])
240 241
241 242 def make_file(repo, pat, node=None,
242 243 total=None, seqno=None, revwidth=None, mode='wb', pathname=None):
243 244
244 245 writable = 'w' in mode or 'a' in mode
245 246
246 247 if not pat or pat == '-':
247 248 return writable and sys.stdout or sys.stdin
248 249 if hasattr(pat, 'write') and writable:
249 250 return pat
250 251 if hasattr(pat, 'read') and 'r' in mode:
251 252 return pat
252 253 return open(make_filename(repo, pat, node, total, seqno, revwidth,
253 254 pathname),
254 255 mode)
255 256
256 257 def expandpats(pats):
257 258 if not util.expandglobs:
258 259 return list(pats)
259 260 ret = []
260 261 for p in pats:
261 262 kind, name = _match._patsplit(p, None)
262 263 if kind is None:
263 264 try:
264 265 globbed = glob.glob(name)
265 266 except re.error:
266 267 globbed = [name]
267 268 if globbed:
268 269 ret.extend(globbed)
269 270 continue
270 271 ret.append(p)
271 272 return ret
272 273
273 274 def match(repo, pats=[], opts={}, globbed=False, default='relpath'):
274 275 if not globbed and default == 'relpath':
275 276 pats = expandpats(pats or [])
276 277 m = _match.match(repo.root, repo.getcwd(), pats,
277 278 opts.get('include'), opts.get('exclude'), default)
278 279 def badfn(f, msg):
279 280 repo.ui.warn("%s: %s\n" % (m.rel(f), msg))
280 281 m.bad = badfn
281 282 return m
282 283
283 284 def matchall(repo):
284 285 return _match.always(repo.root, repo.getcwd())
285 286
286 287 def matchfiles(repo, files):
287 288 return _match.exact(repo.root, repo.getcwd(), files)
288 289
289 def findrenames(repo, added, removed, threshold):
290 '''find renamed files -- yields (before, after, score) tuples'''
291 copies = {}
292 ctx = repo['.']
293 for i, r in enumerate(removed):
294 repo.ui.progress(_('searching'), i, total=len(removed))
295 if r not in ctx:
296 continue
297 fctx = ctx.filectx(r)
298
299 # lazily load text
300 @util.cachefunc
301 def data():
302 orig = fctx.data()
303 return orig, mdiff.splitnewlines(orig)
304
305 def score(text):
306 if not len(text):
307 return 0.0
308 if not fctx.cmp(text):
309 return 1.0
310 if threshold == 1.0:
311 return 0.0
312 orig, lines = data()
313 # bdiff.blocks() returns blocks of matching lines
314 # count the number of bytes in each
315 equal = 0
316 matches = bdiff.blocks(text, orig)
317 for x1, x2, y1, y2 in matches:
318 for line in lines[y1:y2]:
319 equal += len(line)
320
321 lengths = len(text) + len(orig)
322 return equal * 2.0 / lengths
323
324 for a in added:
325 bestscore = copies.get(a, (None, threshold))[1]
326 myscore = score(repo.wread(a))
327 if myscore >= bestscore:
328 copies[a] = (r, myscore)
329 repo.ui.progress(_('searching'), None)
330
331 for dest, v in copies.iteritems():
332 source, score = v
333 yield source, dest, score
334
335 290 def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
336 291 if dry_run is None:
337 292 dry_run = opts.get('dry_run')
338 293 if similarity is None:
339 294 similarity = float(opts.get('similarity') or 0)
340 295 # we'd use status here, except handling of symlinks and ignore is tricky
341 296 added, unknown, deleted, removed = [], [], [], []
342 297 audit_path = util.path_auditor(repo.root)
343 298 m = match(repo, pats, opts)
344 299 for abs in repo.walk(m):
345 300 target = repo.wjoin(abs)
346 301 good = True
347 302 try:
348 303 audit_path(abs)
349 304 except:
350 305 good = False
351 306 rel = m.rel(abs)
352 307 exact = m.exact(abs)
353 308 if good and abs not in repo.dirstate:
354 309 unknown.append(abs)
355 310 if repo.ui.verbose or not exact:
356 311 repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
357 312 elif repo.dirstate[abs] != 'r' and (not good or not util.lexists(target)
358 313 or (os.path.isdir(target) and not os.path.islink(target))):
359 314 deleted.append(abs)
360 315 if repo.ui.verbose or not exact:
361 316 repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
362 317 # for finding renames
363 318 elif repo.dirstate[abs] == 'r':
364 319 removed.append(abs)
365 320 elif repo.dirstate[abs] == 'a':
366 321 added.append(abs)
367 322 copies = {}
368 323 if similarity > 0:
369 for old, new, score in findrenames(repo, added + unknown,
370 removed + deleted, similarity):
324 for old, new, score in similar.findrenames(repo,
325 added + unknown, removed + deleted, similarity):
371 326 if repo.ui.verbose or not m.exact(old) or not m.exact(new):
372 327 repo.ui.status(_('recording removal of %s as rename to %s '
373 328 '(%d%% similar)\n') %
374 329 (m.rel(old), m.rel(new), score * 100))
375 330 copies[new] = old
376 331
377 332 if not dry_run:
378 333 wlock = repo.wlock()
379 334 try:
380 335 repo.remove(deleted)
381 336 repo.add(unknown)
382 337 for new, old in copies.iteritems():
383 338 repo.copy(old, new)
384 339 finally:
385 340 wlock.release()
386 341
387 342 def copy(ui, repo, pats, opts, rename=False):
388 343 # called with the repo lock held
389 344 #
390 345 # hgsep => pathname that uses "/" to separate directories
391 346 # ossep => pathname that uses os.sep to separate directories
392 347 cwd = repo.getcwd()
393 348 targets = {}
394 349 after = opts.get("after")
395 350 dryrun = opts.get("dry_run")
396 351
397 352 def walkpat(pat):
398 353 srcs = []
399 354 m = match(repo, [pat], opts, globbed=True)
400 355 for abs in repo.walk(m):
401 356 state = repo.dirstate[abs]
402 357 rel = m.rel(abs)
403 358 exact = m.exact(abs)
404 359 if state in '?r':
405 360 if exact and state == '?':
406 361 ui.warn(_('%s: not copying - file is not managed\n') % rel)
407 362 if exact and state == 'r':
408 363 ui.warn(_('%s: not copying - file has been marked for'
409 364 ' remove\n') % rel)
410 365 continue
411 366 # abs: hgsep
412 367 # rel: ossep
413 368 srcs.append((abs, rel, exact))
414 369 return srcs
415 370
416 371 # abssrc: hgsep
417 372 # relsrc: ossep
418 373 # otarget: ossep
419 374 def copyfile(abssrc, relsrc, otarget, exact):
420 375 abstarget = util.canonpath(repo.root, cwd, otarget)
421 376 reltarget = repo.pathto(abstarget, cwd)
422 377 target = repo.wjoin(abstarget)
423 378 src = repo.wjoin(abssrc)
424 379 state = repo.dirstate[abstarget]
425 380
426 381 # check for collisions
427 382 prevsrc = targets.get(abstarget)
428 383 if prevsrc is not None:
429 384 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
430 385 (reltarget, repo.pathto(abssrc, cwd),
431 386 repo.pathto(prevsrc, cwd)))
432 387 return
433 388
434 389 # check for overwrites
435 390 exists = os.path.exists(target)
436 391 if not after and exists or after and state in 'mn':
437 392 if not opts['force']:
438 393 ui.warn(_('%s: not overwriting - file exists\n') %
439 394 reltarget)
440 395 return
441 396
442 397 if after:
443 398 if not exists:
444 399 return
445 400 elif not dryrun:
446 401 try:
447 402 if exists:
448 403 os.unlink(target)
449 404 targetdir = os.path.dirname(target) or '.'
450 405 if not os.path.isdir(targetdir):
451 406 os.makedirs(targetdir)
452 407 util.copyfile(src, target)
453 408 except IOError, inst:
454 409 if inst.errno == errno.ENOENT:
455 410 ui.warn(_('%s: deleted in working copy\n') % relsrc)
456 411 else:
457 412 ui.warn(_('%s: cannot copy - %s\n') %
458 413 (relsrc, inst.strerror))
459 414 return True # report a failure
460 415
461 416 if ui.verbose or not exact:
462 417 if rename:
463 418 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
464 419 else:
465 420 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
466 421
467 422 targets[abstarget] = abssrc
468 423
469 424 # fix up dirstate
470 425 origsrc = repo.dirstate.copied(abssrc) or abssrc
471 426 if abstarget == origsrc: # copying back a copy?
472 427 if state not in 'mn' and not dryrun:
473 428 repo.dirstate.normallookup(abstarget)
474 429 else:
475 430 if repo.dirstate[origsrc] == 'a' and origsrc == abssrc:
476 431 if not ui.quiet:
477 432 ui.warn(_("%s has not been committed yet, so no copy "
478 433 "data will be stored for %s.\n")
479 434 % (repo.pathto(origsrc, cwd), reltarget))
480 435 if repo.dirstate[abstarget] in '?r' and not dryrun:
481 436 repo.add([abstarget])
482 437 elif not dryrun:
483 438 repo.copy(origsrc, abstarget)
484 439
485 440 if rename and not dryrun:
486 441 repo.remove([abssrc], not after)
487 442
488 443 # pat: ossep
489 444 # dest ossep
490 445 # srcs: list of (hgsep, hgsep, ossep, bool)
491 446 # return: function that takes hgsep and returns ossep
492 447 def targetpathfn(pat, dest, srcs):
493 448 if os.path.isdir(pat):
494 449 abspfx = util.canonpath(repo.root, cwd, pat)
495 450 abspfx = util.localpath(abspfx)
496 451 if destdirexists:
497 452 striplen = len(os.path.split(abspfx)[0])
498 453 else:
499 454 striplen = len(abspfx)
500 455 if striplen:
501 456 striplen += len(os.sep)
502 457 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
503 458 elif destdirexists:
504 459 res = lambda p: os.path.join(dest,
505 460 os.path.basename(util.localpath(p)))
506 461 else:
507 462 res = lambda p: dest
508 463 return res
509 464
510 465 # pat: ossep
511 466 # dest ossep
512 467 # srcs: list of (hgsep, hgsep, ossep, bool)
513 468 # return: function that takes hgsep and returns ossep
514 469 def targetpathafterfn(pat, dest, srcs):
515 470 if _match.patkind(pat):
516 471 # a mercurial pattern
517 472 res = lambda p: os.path.join(dest,
518 473 os.path.basename(util.localpath(p)))
519 474 else:
520 475 abspfx = util.canonpath(repo.root, cwd, pat)
521 476 if len(abspfx) < len(srcs[0][0]):
522 477 # A directory. Either the target path contains the last
523 478 # component of the source path or it does not.
524 479 def evalpath(striplen):
525 480 score = 0
526 481 for s in srcs:
527 482 t = os.path.join(dest, util.localpath(s[0])[striplen:])
528 483 if os.path.exists(t):
529 484 score += 1
530 485 return score
531 486
532 487 abspfx = util.localpath(abspfx)
533 488 striplen = len(abspfx)
534 489 if striplen:
535 490 striplen += len(os.sep)
536 491 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
537 492 score = evalpath(striplen)
538 493 striplen1 = len(os.path.split(abspfx)[0])
539 494 if striplen1:
540 495 striplen1 += len(os.sep)
541 496 if evalpath(striplen1) > score:
542 497 striplen = striplen1
543 498 res = lambda p: os.path.join(dest,
544 499 util.localpath(p)[striplen:])
545 500 else:
546 501 # a file
547 502 if destdirexists:
548 503 res = lambda p: os.path.join(dest,
549 504 os.path.basename(util.localpath(p)))
550 505 else:
551 506 res = lambda p: dest
552 507 return res
553 508
554 509
555 510 pats = expandpats(pats)
556 511 if not pats:
557 512 raise util.Abort(_('no source or destination specified'))
558 513 if len(pats) == 1:
559 514 raise util.Abort(_('no destination specified'))
560 515 dest = pats.pop()
561 516 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
562 517 if not destdirexists:
563 518 if len(pats) > 1 or _match.patkind(pats[0]):
564 519 raise util.Abort(_('with multiple sources, destination must be an '
565 520 'existing directory'))
566 521 if util.endswithsep(dest):
567 522 raise util.Abort(_('destination %s is not a directory') % dest)
568 523
569 524 tfn = targetpathfn
570 525 if after:
571 526 tfn = targetpathafterfn
572 527 copylist = []
573 528 for pat in pats:
574 529 srcs = walkpat(pat)
575 530 if not srcs:
576 531 continue
577 532 copylist.append((tfn(pat, dest, srcs), srcs))
578 533 if not copylist:
579 534 raise util.Abort(_('no files to copy'))
580 535
581 536 errors = 0
582 537 for targetpath, srcs in copylist:
583 538 for abssrc, relsrc, exact in srcs:
584 539 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
585 540 errors += 1
586 541
587 542 if errors:
588 543 ui.warn(_('(consider using --after)\n'))
589 544
590 545 return errors
591 546
592 547 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
593 548 runargs=None, appendpid=False):
594 549 '''Run a command as a service.'''
595 550
596 551 if opts['daemon'] and not opts['daemon_pipefds']:
597 552 # Signal child process startup with file removal
598 553 lockfd, lockpath = tempfile.mkstemp(prefix='hg-service-')
599 554 os.close(lockfd)
600 555 try:
601 556 if not runargs:
602 557 runargs = util.hgcmd() + sys.argv[1:]
603 558 runargs.append('--daemon-pipefds=%s' % lockpath)
604 559 # Don't pass --cwd to the child process, because we've already
605 560 # changed directory.
606 561 for i in xrange(1, len(runargs)):
607 562 if runargs[i].startswith('--cwd='):
608 563 del runargs[i]
609 564 break
610 565 elif runargs[i].startswith('--cwd'):
611 566 del runargs[i:i + 2]
612 567 break
613 568 def condfn():
614 569 return not os.path.exists(lockpath)
615 570 pid = util.rundetached(runargs, condfn)
616 571 if pid < 0:
617 572 raise util.Abort(_('child process failed to start'))
618 573 finally:
619 574 try:
620 575 os.unlink(lockpath)
621 576 except OSError, e:
622 577 if e.errno != errno.ENOENT:
623 578 raise
624 579 if parentfn:
625 580 return parentfn(pid)
626 581 else:
627 582 return
628 583
629 584 if initfn:
630 585 initfn()
631 586
632 587 if opts['pid_file']:
633 588 mode = appendpid and 'a' or 'w'
634 589 fp = open(opts['pid_file'], mode)
635 590 fp.write(str(os.getpid()) + '\n')
636 591 fp.close()
637 592
638 593 if opts['daemon_pipefds']:
639 594 lockpath = opts['daemon_pipefds']
640 595 try:
641 596 os.setsid()
642 597 except AttributeError:
643 598 pass
644 599 os.unlink(lockpath)
645 600 util.hidewindow()
646 601 sys.stdout.flush()
647 602 sys.stderr.flush()
648 603
649 604 nullfd = os.open(util.nulldev, os.O_RDWR)
650 605 logfilefd = nullfd
651 606 if logfile:
652 607 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
653 608 os.dup2(nullfd, 0)
654 609 os.dup2(logfilefd, 1)
655 610 os.dup2(logfilefd, 2)
656 611 if nullfd not in (0, 1, 2):
657 612 os.close(nullfd)
658 613 if logfile and logfilefd not in (0, 1, 2):
659 614 os.close(logfilefd)
660 615
661 616 if runfn:
662 617 return runfn()
663 618
664 619 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
665 620 opts=None):
666 621 '''export changesets as hg patches.'''
667 622
668 623 total = len(revs)
669 624 revwidth = max([len(str(rev)) for rev in revs])
670 625
671 626 def single(rev, seqno, fp):
672 627 ctx = repo[rev]
673 628 node = ctx.node()
674 629 parents = [p.node() for p in ctx.parents() if p]
675 630 branch = ctx.branch()
676 631 if switch_parent:
677 632 parents.reverse()
678 633 prev = (parents and parents[0]) or nullid
679 634
680 635 if not fp:
681 636 fp = make_file(repo, template, node, total=total, seqno=seqno,
682 637 revwidth=revwidth, mode='ab')
683 638 if fp != sys.stdout and hasattr(fp, 'name'):
684 639 repo.ui.note("%s\n" % fp.name)
685 640
686 641 fp.write("# HG changeset patch\n")
687 642 fp.write("# User %s\n" % ctx.user())
688 643 fp.write("# Date %d %d\n" % ctx.date())
689 644 if branch and (branch != 'default'):
690 645 fp.write("# Branch %s\n" % branch)
691 646 fp.write("# Node ID %s\n" % hex(node))
692 647 fp.write("# Parent %s\n" % hex(prev))
693 648 if len(parents) > 1:
694 649 fp.write("# Parent %s\n" % hex(parents[1]))
695 650 fp.write(ctx.description().rstrip())
696 651 fp.write("\n\n")
697 652
698 653 for chunk in patch.diff(repo, prev, node, opts=opts):
699 654 fp.write(chunk)
700 655
701 656 for seqno, rev in enumerate(revs):
702 657 single(rev, seqno + 1, fp)
703 658
704 659 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
705 660 changes=None, stat=False, fp=None):
706 661 '''show diff or diffstat.'''
707 662 if fp is None:
708 663 write = ui.write
709 664 else:
710 665 def write(s, **kw):
711 666 fp.write(s)
712 667
713 668 if stat:
714 669 diffopts.context = 0
715 670 width = 80
716 671 if not ui.plain():
717 672 width = util.termwidth()
718 673 chunks = patch.diff(repo, node1, node2, match, changes, diffopts)
719 674 for chunk, label in patch.diffstatui(util.iterlines(chunks),
720 675 width=width,
721 676 git=diffopts.git):
722 677 write(chunk, label=label)
723 678 else:
724 679 for chunk, label in patch.diffui(repo, node1, node2, match,
725 680 changes, diffopts):
726 681 write(chunk, label=label)
727 682
728 683 class changeset_printer(object):
729 684 '''show changeset information when templating not requested.'''
730 685
731 686 def __init__(self, ui, repo, patch, diffopts, buffered):
732 687 self.ui = ui
733 688 self.repo = repo
734 689 self.buffered = buffered
735 690 self.patch = patch
736 691 self.diffopts = diffopts
737 692 self.header = {}
738 693 self.hunk = {}
739 694 self.lastheader = None
740 695 self.footer = None
741 696
742 697 def flush(self, rev):
743 698 if rev in self.header:
744 699 h = self.header[rev]
745 700 if h != self.lastheader:
746 701 self.lastheader = h
747 702 self.ui.write(h)
748 703 del self.header[rev]
749 704 if rev in self.hunk:
750 705 self.ui.write(self.hunk[rev])
751 706 del self.hunk[rev]
752 707 return 1
753 708 return 0
754 709
755 710 def close(self):
756 711 if self.footer:
757 712 self.ui.write(self.footer)
758 713
759 714 def show(self, ctx, copies=None, **props):
760 715 if self.buffered:
761 716 self.ui.pushbuffer()
762 717 self._show(ctx, copies, props)
763 718 self.hunk[ctx.rev()] = self.ui.popbuffer(labeled=True)
764 719 else:
765 720 self._show(ctx, copies, props)
766 721
767 722 def _show(self, ctx, copies, props):
768 723 '''show a single changeset or file revision'''
769 724 changenode = ctx.node()
770 725 rev = ctx.rev()
771 726
772 727 if self.ui.quiet:
773 728 self.ui.write("%d:%s\n" % (rev, short(changenode)),
774 729 label='log.node')
775 730 return
776 731
777 732 log = self.repo.changelog
778 733 date = util.datestr(ctx.date())
779 734
780 735 hexfunc = self.ui.debugflag and hex or short
781 736
782 737 parents = [(p, hexfunc(log.node(p)))
783 738 for p in self._meaningful_parentrevs(log, rev)]
784 739
785 740 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)),
786 741 label='log.changeset')
787 742
788 743 branch = ctx.branch()
789 744 # don't show the default branch name
790 745 if branch != 'default':
791 746 branch = encoding.tolocal(branch)
792 747 self.ui.write(_("branch: %s\n") % branch,
793 748 label='log.branch')
794 749 for tag in self.repo.nodetags(changenode):
795 750 self.ui.write(_("tag: %s\n") % tag,
796 751 label='log.tag')
797 752 for parent in parents:
798 753 self.ui.write(_("parent: %d:%s\n") % parent,
799 754 label='log.parent')
800 755
801 756 if self.ui.debugflag:
802 757 mnode = ctx.manifestnode()
803 758 self.ui.write(_("manifest: %d:%s\n") %
804 759 (self.repo.manifest.rev(mnode), hex(mnode)),
805 760 label='ui.debug log.manifest')
806 761 self.ui.write(_("user: %s\n") % ctx.user(),
807 762 label='log.user')
808 763 self.ui.write(_("date: %s\n") % date,
809 764 label='log.date')
810 765
811 766 if self.ui.debugflag:
812 767 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
813 768 for key, value in zip([_("files:"), _("files+:"), _("files-:")],
814 769 files):
815 770 if value:
816 771 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
817 772 label='ui.debug log.files')
818 773 elif ctx.files() and self.ui.verbose:
819 774 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
820 775 label='ui.note log.files')
821 776 if copies and self.ui.verbose:
822 777 copies = ['%s (%s)' % c for c in copies]
823 778 self.ui.write(_("copies: %s\n") % ' '.join(copies),
824 779 label='ui.note log.copies')
825 780
826 781 extra = ctx.extra()
827 782 if extra and self.ui.debugflag:
828 783 for key, value in sorted(extra.items()):
829 784 self.ui.write(_("extra: %s=%s\n")
830 785 % (key, value.encode('string_escape')),
831 786 label='ui.debug log.extra')
832 787
833 788 description = ctx.description().strip()
834 789 if description:
835 790 if self.ui.verbose:
836 791 self.ui.write(_("description:\n"),
837 792 label='ui.note log.description')
838 793 self.ui.write(description,
839 794 label='ui.note log.description')
840 795 self.ui.write("\n\n")
841 796 else:
842 797 self.ui.write(_("summary: %s\n") %
843 798 description.splitlines()[0],
844 799 label='log.summary')
845 800 self.ui.write("\n")
846 801
847 802 self.showpatch(changenode)
848 803
849 804 def showpatch(self, node):
850 805 if self.patch:
851 806 prev = self.repo.changelog.parents(node)[0]
852 807 chunks = patch.diffui(self.repo, prev, node, match=self.patch,
853 808 opts=patch.diffopts(self.ui, self.diffopts))
854 809 for chunk, label in chunks:
855 810 self.ui.write(chunk, label=label)
856 811 self.ui.write("\n")
857 812
858 813 def _meaningful_parentrevs(self, log, rev):
859 814 """Return list of meaningful (or all if debug) parentrevs for rev.
860 815
861 816 For merges (two non-nullrev revisions) both parents are meaningful.
862 817 Otherwise the first parent revision is considered meaningful if it
863 818 is not the preceding revision.
864 819 """
865 820 parents = log.parentrevs(rev)
866 821 if not self.ui.debugflag and parents[1] == nullrev:
867 822 if parents[0] >= rev - 1:
868 823 parents = []
869 824 else:
870 825 parents = [parents[0]]
871 826 return parents
872 827
873 828
874 829 class changeset_templater(changeset_printer):
875 830 '''format changeset information.'''
876 831
877 832 def __init__(self, ui, repo, patch, diffopts, mapfile, buffered):
878 833 changeset_printer.__init__(self, ui, repo, patch, diffopts, buffered)
879 834 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
880 835 defaulttempl = {
881 836 'parent': '{rev}:{node|formatnode} ',
882 837 'manifest': '{rev}:{node|formatnode}',
883 838 'file_copy': '{name} ({source})',
884 839 'extra': '{key}={value|stringescape}'
885 840 }
886 841 # filecopy is preserved for compatibility reasons
887 842 defaulttempl['filecopy'] = defaulttempl['file_copy']
888 843 self.t = templater.templater(mapfile, {'formatnode': formatnode},
889 844 cache=defaulttempl)
890 845 self.cache = {}
891 846
892 847 def use_template(self, t):
893 848 '''set template string to use'''
894 849 self.t.cache['changeset'] = t
895 850
896 851 def _meaningful_parentrevs(self, ctx):
897 852 """Return list of meaningful (or all if debug) parentrevs for rev.
898 853 """
899 854 parents = ctx.parents()
900 855 if len(parents) > 1:
901 856 return parents
902 857 if self.ui.debugflag:
903 858 return [parents[0], self.repo['null']]
904 859 if parents[0].rev() >= ctx.rev() - 1:
905 860 return []
906 861 return parents
907 862
908 863 def _show(self, ctx, copies, props):
909 864 '''show a single changeset or file revision'''
910 865
911 866 showlist = templatekw.showlist
912 867
913 868 # showparents() behaviour depends on ui trace level which
914 869 # causes unexpected behaviours at templating level and makes
915 870 # it harder to extract it in a standalone function. Its
916 871 # behaviour cannot be changed so leave it here for now.
917 872 def showparents(**args):
918 873 ctx = args['ctx']
919 874 parents = [[('rev', p.rev()), ('node', p.hex())]
920 875 for p in self._meaningful_parentrevs(ctx)]
921 876 return showlist('parent', parents, **args)
922 877
923 878 props = props.copy()
924 879 props.update(templatekw.keywords)
925 880 props['parents'] = showparents
926 881 props['templ'] = self.t
927 882 props['ctx'] = ctx
928 883 props['repo'] = self.repo
929 884 props['revcache'] = {'copies': copies}
930 885 props['cache'] = self.cache
931 886
932 887 # find correct templates for current mode
933 888
934 889 tmplmodes = [
935 890 (True, None),
936 891 (self.ui.verbose, 'verbose'),
937 892 (self.ui.quiet, 'quiet'),
938 893 (self.ui.debugflag, 'debug'),
939 894 ]
940 895
941 896 types = {'header': '', 'footer':'', 'changeset': 'changeset'}
942 897 for mode, postfix in tmplmodes:
943 898 for type in types:
944 899 cur = postfix and ('%s_%s' % (type, postfix)) or type
945 900 if mode and cur in self.t:
946 901 types[type] = cur
947 902
948 903 try:
949 904
950 905 # write header
951 906 if types['header']:
952 907 h = templater.stringify(self.t(types['header'], **props))
953 908 if self.buffered:
954 909 self.header[ctx.rev()] = h
955 910 else:
956 911 self.ui.write(h)
957 912
958 913 # write changeset metadata, then patch if requested
959 914 key = types['changeset']
960 915 self.ui.write(templater.stringify(self.t(key, **props)))
961 916 self.showpatch(ctx.node())
962 917
963 918 if types['footer']:
964 919 if not self.footer:
965 920 self.footer = templater.stringify(self.t(types['footer'],
966 921 **props))
967 922
968 923 except KeyError, inst:
969 924 msg = _("%s: no key named '%s'")
970 925 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
971 926 except SyntaxError, inst:
972 927 raise util.Abort('%s: %s' % (self.t.mapfile, inst.args[0]))
973 928
974 929 def show_changeset(ui, repo, opts, buffered=False, matchfn=False):
975 930 """show one changeset using template or regular display.
976 931
977 932 Display format will be the first non-empty hit of:
978 933 1. option 'template'
979 934 2. option 'style'
980 935 3. [ui] setting 'logtemplate'
981 936 4. [ui] setting 'style'
982 937 If all of these values are either the unset or the empty string,
983 938 regular display via changeset_printer() is done.
984 939 """
985 940 # options
986 941 patch = False
987 942 if opts.get('patch'):
988 943 patch = matchfn or matchall(repo)
989 944
990 945 tmpl = opts.get('template')
991 946 style = None
992 947 if tmpl:
993 948 tmpl = templater.parsestring(tmpl, quoted=False)
994 949 else:
995 950 style = opts.get('style')
996 951
997 952 # ui settings
998 953 if not (tmpl or style):
999 954 tmpl = ui.config('ui', 'logtemplate')
1000 955 if tmpl:
1001 956 tmpl = templater.parsestring(tmpl)
1002 957 else:
1003 958 style = util.expandpath(ui.config('ui', 'style', ''))
1004 959
1005 960 if not (tmpl or style):
1006 961 return changeset_printer(ui, repo, patch, opts, buffered)
1007 962
1008 963 mapfile = None
1009 964 if style and not tmpl:
1010 965 mapfile = style
1011 966 if not os.path.split(mapfile)[0]:
1012 967 mapname = (templater.templatepath('map-cmdline.' + mapfile)
1013 968 or templater.templatepath(mapfile))
1014 969 if mapname:
1015 970 mapfile = mapname
1016 971
1017 972 try:
1018 973 t = changeset_templater(ui, repo, patch, opts, mapfile, buffered)
1019 974 except SyntaxError, inst:
1020 975 raise util.Abort(inst.args[0])
1021 976 if tmpl:
1022 977 t.use_template(tmpl)
1023 978 return t
1024 979
1025 980 def finddate(ui, repo, date):
1026 981 """Find the tipmost changeset that matches the given date spec"""
1027 982
1028 983 df = util.matchdate(date)
1029 984 m = matchall(repo)
1030 985 results = {}
1031 986
1032 987 def prep(ctx, fns):
1033 988 d = ctx.date()
1034 989 if df(d[0]):
1035 990 results[ctx.rev()] = d
1036 991
1037 992 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1038 993 rev = ctx.rev()
1039 994 if rev in results:
1040 995 ui.status(_("Found revision %s from %s\n") %
1041 996 (rev, util.datestr(results[rev])))
1042 997 return str(rev)
1043 998
1044 999 raise util.Abort(_("revision matching date not found"))
1045 1000
1046 1001 def walkchangerevs(repo, match, opts, prepare):
1047 1002 '''Iterate over files and the revs in which they changed.
1048 1003
1049 1004 Callers most commonly need to iterate backwards over the history
1050 1005 in which they are interested. Doing so has awful (quadratic-looking)
1051 1006 performance, so we use iterators in a "windowed" way.
1052 1007
1053 1008 We walk a window of revisions in the desired order. Within the
1054 1009 window, we first walk forwards to gather data, then in the desired
1055 1010 order (usually backwards) to display it.
1056 1011
1057 1012 This function returns an iterator yielding contexts. Before
1058 1013 yielding each context, the iterator will first call the prepare
1059 1014 function on each context in the window in forward order.'''
1060 1015
1061 1016 def increasing_windows(start, end, windowsize=8, sizelimit=512):
1062 1017 if start < end:
1063 1018 while start < end:
1064 1019 yield start, min(windowsize, end - start)
1065 1020 start += windowsize
1066 1021 if windowsize < sizelimit:
1067 1022 windowsize *= 2
1068 1023 else:
1069 1024 while start > end:
1070 1025 yield start, min(windowsize, start - end - 1)
1071 1026 start -= windowsize
1072 1027 if windowsize < sizelimit:
1073 1028 windowsize *= 2
1074 1029
1075 1030 follow = opts.get('follow') or opts.get('follow_first')
1076 1031
1077 1032 if not len(repo):
1078 1033 return []
1079 1034
1080 1035 if follow:
1081 1036 defrange = '%s:0' % repo['.'].rev()
1082 1037 else:
1083 1038 defrange = '-1:0'
1084 1039 revs = revrange(repo, opts['rev'] or [defrange])
1085 1040 wanted = set()
1086 1041 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1087 1042 fncache = {}
1088 1043 change = util.cachefunc(repo.changectx)
1089 1044
1090 1045 if not slowpath and not match.files():
1091 1046 # No files, no patterns. Display all revs.
1092 1047 wanted = set(revs)
1093 1048 copies = []
1094 1049
1095 1050 if not slowpath:
1096 1051 # Only files, no patterns. Check the history of each file.
1097 1052 def filerevgen(filelog, node):
1098 1053 cl_count = len(repo)
1099 1054 if node is None:
1100 1055 last = len(filelog) - 1
1101 1056 else:
1102 1057 last = filelog.rev(node)
1103 1058 for i, window in increasing_windows(last, nullrev):
1104 1059 revs = []
1105 1060 for j in xrange(i - window, i + 1):
1106 1061 n = filelog.node(j)
1107 1062 revs.append((filelog.linkrev(j),
1108 1063 follow and filelog.renamed(n)))
1109 1064 for rev in reversed(revs):
1110 1065 # only yield rev for which we have the changelog, it can
1111 1066 # happen while doing "hg log" during a pull or commit
1112 1067 if rev[0] < cl_count:
1113 1068 yield rev
1114 1069 def iterfiles():
1115 1070 for filename in match.files():
1116 1071 yield filename, None
1117 1072 for filename_node in copies:
1118 1073 yield filename_node
1119 1074 minrev, maxrev = min(revs), max(revs)
1120 1075 for file_, node in iterfiles():
1121 1076 filelog = repo.file(file_)
1122 1077 if not len(filelog):
1123 1078 if node is None:
1124 1079 # A zero count may be a directory or deleted file, so
1125 1080 # try to find matching entries on the slow path.
1126 1081 if follow:
1127 1082 raise util.Abort(
1128 1083 _('cannot follow nonexistent file: "%s"') % file_)
1129 1084 slowpath = True
1130 1085 break
1131 1086 else:
1132 1087 continue
1133 1088 for rev, copied in filerevgen(filelog, node):
1134 1089 if rev <= maxrev:
1135 1090 if rev < minrev:
1136 1091 break
1137 1092 fncache.setdefault(rev, [])
1138 1093 fncache[rev].append(file_)
1139 1094 wanted.add(rev)
1140 1095 if copied:
1141 1096 copies.append(copied)
1142 1097 if slowpath:
1143 1098 if follow:
1144 1099 raise util.Abort(_('can only follow copies/renames for explicit '
1145 1100 'filenames'))
1146 1101
1147 1102 # The slow path checks files modified in every changeset.
1148 1103 def changerevgen():
1149 1104 for i, window in increasing_windows(len(repo) - 1, nullrev):
1150 1105 for j in xrange(i - window, i + 1):
1151 1106 yield change(j)
1152 1107
1153 1108 for ctx in changerevgen():
1154 1109 matches = filter(match, ctx.files())
1155 1110 if matches:
1156 1111 fncache[ctx.rev()] = matches
1157 1112 wanted.add(ctx.rev())
1158 1113
1159 1114 class followfilter(object):
1160 1115 def __init__(self, onlyfirst=False):
1161 1116 self.startrev = nullrev
1162 1117 self.roots = set()
1163 1118 self.onlyfirst = onlyfirst
1164 1119
1165 1120 def match(self, rev):
1166 1121 def realparents(rev):
1167 1122 if self.onlyfirst:
1168 1123 return repo.changelog.parentrevs(rev)[0:1]
1169 1124 else:
1170 1125 return filter(lambda x: x != nullrev,
1171 1126 repo.changelog.parentrevs(rev))
1172 1127
1173 1128 if self.startrev == nullrev:
1174 1129 self.startrev = rev
1175 1130 return True
1176 1131
1177 1132 if rev > self.startrev:
1178 1133 # forward: all descendants
1179 1134 if not self.roots:
1180 1135 self.roots.add(self.startrev)
1181 1136 for parent in realparents(rev):
1182 1137 if parent in self.roots:
1183 1138 self.roots.add(rev)
1184 1139 return True
1185 1140 else:
1186 1141 # backwards: all parents
1187 1142 if not self.roots:
1188 1143 self.roots.update(realparents(self.startrev))
1189 1144 if rev in self.roots:
1190 1145 self.roots.remove(rev)
1191 1146 self.roots.update(realparents(rev))
1192 1147 return True
1193 1148
1194 1149 return False
1195 1150
1196 1151 # it might be worthwhile to do this in the iterator if the rev range
1197 1152 # is descending and the prune args are all within that range
1198 1153 for rev in opts.get('prune', ()):
1199 1154 rev = repo.changelog.rev(repo.lookup(rev))
1200 1155 ff = followfilter()
1201 1156 stop = min(revs[0], revs[-1])
1202 1157 for x in xrange(rev, stop - 1, -1):
1203 1158 if ff.match(x):
1204 1159 wanted.discard(x)
1205 1160
1206 1161 def iterate():
1207 1162 if follow and not match.files():
1208 1163 ff = followfilter(onlyfirst=opts.get('follow_first'))
1209 1164 def want(rev):
1210 1165 return ff.match(rev) and rev in wanted
1211 1166 else:
1212 1167 def want(rev):
1213 1168 return rev in wanted
1214 1169
1215 1170 for i, window in increasing_windows(0, len(revs)):
1216 1171 change = util.cachefunc(repo.changectx)
1217 1172 nrevs = [rev for rev in revs[i:i + window] if want(rev)]
1218 1173 for rev in sorted(nrevs):
1219 1174 fns = fncache.get(rev)
1220 1175 ctx = change(rev)
1221 1176 if not fns:
1222 1177 def fns_generator():
1223 1178 for f in ctx.files():
1224 1179 if match(f):
1225 1180 yield f
1226 1181 fns = fns_generator()
1227 1182 prepare(ctx, fns)
1228 1183 for rev in nrevs:
1229 1184 yield change(rev)
1230 1185 return iterate()
1231 1186
1232 1187 def commit(ui, repo, commitfunc, pats, opts):
1233 1188 '''commit the specified files or all outstanding changes'''
1234 1189 date = opts.get('date')
1235 1190 if date:
1236 1191 opts['date'] = util.parsedate(date)
1237 1192 message = logmessage(opts)
1238 1193
1239 1194 # extract addremove carefully -- this function can be called from a command
1240 1195 # that doesn't support addremove
1241 1196 if opts.get('addremove'):
1242 1197 addremove(repo, pats, opts)
1243 1198
1244 1199 return commitfunc(ui, repo, message, match(repo, pats, opts), opts)
1245 1200
1246 1201 def commiteditor(repo, ctx, subs):
1247 1202 if ctx.description():
1248 1203 return ctx.description()
1249 1204 return commitforceeditor(repo, ctx, subs)
1250 1205
1251 1206 def commitforceeditor(repo, ctx, subs):
1252 1207 edittext = []
1253 1208 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
1254 1209 if ctx.description():
1255 1210 edittext.append(ctx.description())
1256 1211 edittext.append("")
1257 1212 edittext.append("") # Empty line between message and comments.
1258 1213 edittext.append(_("HG: Enter commit message."
1259 1214 " Lines beginning with 'HG:' are removed."))
1260 1215 edittext.append(_("HG: Leave message empty to abort commit."))
1261 1216 edittext.append("HG: --")
1262 1217 edittext.append(_("HG: user: %s") % ctx.user())
1263 1218 if ctx.p2():
1264 1219 edittext.append(_("HG: branch merge"))
1265 1220 if ctx.branch():
1266 1221 edittext.append(_("HG: branch '%s'")
1267 1222 % encoding.tolocal(ctx.branch()))
1268 1223 edittext.extend([_("HG: subrepo %s") % s for s in subs])
1269 1224 edittext.extend([_("HG: added %s") % f for f in added])
1270 1225 edittext.extend([_("HG: changed %s") % f for f in modified])
1271 1226 edittext.extend([_("HG: removed %s") % f for f in removed])
1272 1227 if not added and not modified and not removed:
1273 1228 edittext.append(_("HG: no files changed"))
1274 1229 edittext.append("")
1275 1230 # run editor in the repository root
1276 1231 olddir = os.getcwd()
1277 1232 os.chdir(repo.root)
1278 1233 text = repo.ui.edit("\n".join(edittext), ctx.user())
1279 1234 text = re.sub("(?m)^HG:.*\n", "", text)
1280 1235 os.chdir(olddir)
1281 1236
1282 1237 if not text.strip():
1283 1238 raise util.Abort(_("empty commit message"))
1284 1239
1285 1240 return text
General Comments 0
You need to be logged in to leave comments. Login now