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