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