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