comparison hgext/graphlog.py @ 7426:df0962f6c54e

Graphlog extension adds a --graph option to log/in/out The --graph option shows the ascii revision graph when used in conjunction with the incoming, outgoing or log commands. It also makes sure that incompatible options (e.g. --newest-first) are not used.
author Alpar Juttner <alpar@cs.elte.hu>
date Fri, 21 Nov 2008 22:27:11 +0000
parents b501c7f3c2ad
children 5e13df32fb74
comparison
equal deleted inserted replaced
7425:14ed6662af48 7426:df0962f6c54e
2 # 2 #
3 # Copyright 2007 Joel Rosdahl <joel@rosdahl.net> 3 # Copyright 2007 Joel Rosdahl <joel@rosdahl.net>
4 # 4 #
5 # This software may be used and distributed according to the terms of 5 # This software may be used and distributed according to the terms of
6 # the GNU General Public License, incorporated herein by reference. 6 # the GNU General Public License, incorporated herein by reference.
7 '''show revision graphs in terminal windows''' 7 '''show revision graphs in terminal windows
8
9 This extension adds a --graph option to the incoming, outgoing and log
10 commands. When this options is given, an ascii representation of the
11 revision graph is also shown.
12 '''
8 13
9 import os 14 import os
10 import sys 15 import sys
11 from mercurial.cmdutil import revrange, show_changeset 16 from mercurial.cmdutil import revrange, show_changeset
12 from mercurial.commands import templateopts 17 from mercurial.commands import templateopts, logopts, remoteopts
13 from mercurial.i18n import _ 18 from mercurial.i18n import _
14 from mercurial.node import nullrev 19 from mercurial.node import nullrev
15 from mercurial.util import Abort, canonpath 20 from mercurial.util import Abort, canonpath
21 from mercurial import bundlerepo, changegroup, cmdutil, commands, extensions
22 from mercurial import hg, ui, url
16 23
17 def revisions(repo, start, stop): 24 def revisions(repo, start, stop):
18 """cset DAG generator yielding (rev, node, [parents]) tuples 25 """cset DAG generator yielding (rev, node, [parents]) tuples
19 26
20 This generator function walks through the revision history from revision 27 This generator function walks through the revision history from revision
255 revs = revrange(repo, rev_opt) 262 revs = revrange(repo, rev_opt)
256 return (max(revs), min(revs)) 263 return (max(revs), min(revs))
257 else: 264 else:
258 return (len(repo) - 1, 0) 265 return (len(repo) - 1, 0)
259 266
267 def check_unsupported_flags(opts):
268 for op in ["follow", "follow_first", "date", "copies", "keyword", "remove",
269 "only_merges", "user", "only_branch", "prune", "newest_first",
270 "no_merges", "include", "exclude"]:
271 if op in opts and opts[op]:
272 raise Abort(_("--graph option is incompatible with --%s") % op)
273
274
260 def graphlog(ui, repo, path=None, **opts): 275 def graphlog(ui, repo, path=None, **opts):
261 """show revision history alongside an ASCII revision graph 276 """show revision history alongside an ASCII revision graph
262 277
263 Print a revision history alongside a revision graph drawn with 278 Print a revision history alongside a revision graph drawn with
264 ASCII characters. 279 ASCII characters.
265 280
266 Nodes printed as an @ character are parents of the working 281 Nodes printed as an @ character are parents of the working
267 directory. 282 directory.
268 """ 283 """
269 284
285 check_unsupported_flags(opts)
270 limit = get_limit(opts["limit"]) 286 limit = get_limit(opts["limit"])
271 start, stop = get_revs(repo, opts["rev"]) 287 start, stop = get_revs(repo, opts["rev"])
272 stop = max(stop, start - limit + 1) 288 stop = max(stop, start - limit + 1)
273 if start == nullrev: 289 if start == nullrev:
274 return 290 return
291 char = ctx.node() in repo_parents and '@' or 'o' 307 char = ctx.node() in repo_parents and '@' or 'o'
292 yield (ctx.rev(), parents, char, lines) 308 yield (ctx.rev(), parents, char, lines)
293 309
294 ascii(ui, grapher(graphabledag())) 310 ascii(ui, grapher(graphabledag()))
295 311
312 def outgoing_revs(ui, repo, dest, opts):
313 """cset DAG generator yielding (node, [parents]) tuples
314
315 This generator function walks through the revisions not found
316 in the destination
317 """
318 limit = cmdutil.loglimit(opts)
319 dest, revs, checkout = hg.parseurl(
320 ui.expandpath(dest or 'default-push', dest or 'default'),
321 opts.get('rev'))
322 cmdutil.setremoteconfig(ui, opts)
323 if revs:
324 revs = [repo.lookup(rev) for rev in revs]
325 other = hg.repository(ui, dest)
326 ui.status(_('comparing with %s\n') % url.hidepassword(dest))
327 o = repo.findoutgoing(other, force=opts.get('force'))
328 if not o:
329 ui.status(_("no changes found\n"))
330 return
331 o = repo.changelog.nodesbetween(o, revs)[0]
332 o.reverse()
333 revdict = {}
334 for n in o:
335 revdict[repo.changectx(n).rev()]=True
336 count = 0
337 for n in o:
338 if count >= limit:
339 break
340 ctx = repo.changectx(n)
341 parents = [p.rev() for p in ctx.parents() if p.rev() in revdict]
342 parents.sort()
343 yield (ctx, parents)
344 count += 1
345
346 def goutgoing(ui, repo, dest=None, **opts):
347 """show the outgoing changesets alongside an ASCII revision graph
348
349 Print the outgoing changesets alongside a revision graph drawn with
350 ASCII characters.
351
352 Nodes printed as an @ character are parents of the working
353 directory.
354 """
355 check_unsupported_flags(opts)
356 revdag = outgoing_revs(ui, repo, dest, opts)
357 repo_parents = repo.dirstate.parents()
358 displayer = show_changeset(ui, repo, opts, buffered=True)
359 def graphabledag():
360 for (ctx, parents) in revdag:
361 # log_strings is the list of all log strings to draw alongside
362 # the graph.
363 displayer.show(ctx)
364 lines = displayer.hunk.pop(ctx.rev()).split("\n")[:-1]
365 char = ctx.node() in repo_parents and '@' or 'o'
366 yield (ctx.rev(), parents, char, lines)
367
368 ascii(ui, grapher(graphabledag()))
369
370 def incoming_revs(other, chlist, opts):
371 """cset DAG generator yielding (node, [parents]) tuples
372
373 This generator function walks through the revisions of the destination
374 not found in repo
375 """
376 limit = cmdutil.loglimit(opts)
377 chlist.reverse()
378 revdict = {}
379 for n in chlist:
380 revdict[other.changectx(n).rev()]=True
381 count = 0
382 for n in chlist:
383 if count >= limit:
384 break
385 ctx = other.changectx(n)
386 parents = [p.rev() for p in ctx.parents() if p.rev() in revdict]
387 parents.sort()
388 yield (ctx, parents)
389 count += 1
390
391 def gincoming(ui, repo, source="default", **opts):
392 """show the incoming changesets alongside an ASCII revision graph
393
394 Print the incoming changesets alongside a revision graph drawn with
395 ASCII characters.
396
397 Nodes printed as an @ character are parents of the working
398 directory.
399 """
400
401 check_unsupported_flags(opts)
402 source, revs, checkout = hg.parseurl(ui.expandpath(source), opts.get('rev'))
403 cmdutil.setremoteconfig(ui, opts)
404
405 other = hg.repository(ui, source)
406 ui.status(_('comparing with %s\n') % url.hidepassword(source))
407 if revs:
408 revs = [other.lookup(rev) for rev in revs]
409 incoming = repo.findincoming(other, heads=revs, force=opts["force"])
410 if not incoming:
411 try:
412 os.unlink(opts["bundle"])
413 except:
414 pass
415 ui.status(_("no changes found\n"))
416 return
417
418 cleanup = None
419 try:
420 fname = opts["bundle"]
421 if fname or not other.local():
422 # create a bundle (uncompressed if other repo is not local)
423 if revs is None:
424 cg = other.changegroup(incoming, "incoming")
425 else:
426 cg = other.changegroupsubset(incoming, revs, 'incoming')
427 bundletype = other.local() and "HG10BZ" or "HG10UN"
428 fname = cleanup = changegroup.writebundle(cg, fname, bundletype)
429 # keep written bundle?
430 if opts["bundle"]:
431 cleanup = None
432 if not other.local():
433 # use the created uncompressed bundlerepo
434 other = bundlerepo.bundlerepository(ui, repo.root, fname)
435
436 chlist = other.changelog.nodesbetween(incoming, revs)[0]
437 revdag = incoming_revs(other, chlist, opts)
438 other_parents = other.dirstate.parents()
439 displayer = show_changeset(ui, other, opts, buffered=True)
440 def graphabledag():
441 for (ctx, parents) in revdag:
442 # log_strings is the list of all log strings to draw alongside
443 # the graph.
444 displayer.show(ctx)
445 lines = displayer.hunk.pop(ctx.rev()).split("\n")[:-1]
446 char = ctx.node() in other_parents and '@' or 'o'
447 yield (ctx.rev(), parents, char, lines)
448
449 ascii(ui, grapher(graphabledag()))
450 finally:
451 if hasattr(other, 'close'):
452 other.close()
453 if cleanup:
454 os.unlink(cleanup)
455
456 def uisetup(ui):
457 '''Initialize the extension.'''
458 _wrapcmd(ui, 'log', commands.table, graphlog)
459 _wrapcmd(ui, 'incoming', commands.table, gincoming)
460 _wrapcmd(ui, 'outgoing', commands.table, goutgoing)
461
462 def _wrapcmd(ui, cmd, table, wrapfn):
463 '''wrap the command'''
464 def graph(orig, *args, **kwargs):
465 if kwargs['graph']:
466 return wrapfn(*args, **kwargs)
467 return orig(*args, **kwargs)
468 entry = extensions.wrapcommand(table, cmd, graph)
469 entry[1].append(('g', 'graph', None, _("show the revision DAG")))
470
296 cmdtable = { 471 cmdtable = {
297 "glog": 472 "glog":
298 (graphlog, 473 (graphlog,
299 [('l', 'limit', '', _('limit number of changes displayed')), 474 [('l', 'limit', '', _('limit number of changes displayed')),
300 ('p', 'patch', False, _('show patch')), 475 ('p', 'patch', False, _('show patch')),