hgext/graphlog.py
changeset 17180 ae0629161090
parent 17179 0849d725e2f9
child 17181 6f71167292f2
equal deleted inserted replaced
17179:0849d725e2f9 17180:ae0629161090
    13 '''
    13 '''
    14 
    14 
    15 from mercurial.cmdutil import show_changeset
    15 from mercurial.cmdutil import show_changeset
    16 from mercurial.i18n import _
    16 from mercurial.i18n import _
    17 from mercurial import cmdutil, commands, extensions, scmutil
    17 from mercurial import cmdutil, commands, extensions, scmutil
    18 from mercurial import hg, util, graphmod, templatekw, revset
    18 from mercurial import hg, util, graphmod, templatekw
    19 
    19 
    20 cmdtable = {}
    20 cmdtable = {}
    21 command = cmdutil.command(cmdtable)
    21 command = cmdutil.command(cmdtable)
    22 testedwith = 'internal'
    22 testedwith = 'internal'
    23 
    23 
    24 def _checkunsupportedflags(pats, opts):
    24 def _checkunsupportedflags(pats, opts):
    25     for op in ["newest_first"]:
    25     for op in ["newest_first"]:
    26         if op in opts and opts[op]:
    26         if op in opts and opts[op]:
    27             raise util.Abort(_("-G/--graph option is incompatible with --%s")
    27             raise util.Abort(_("-G/--graph option is incompatible with --%s")
    28                              % op.replace("_", "-"))
    28                              % op.replace("_", "-"))
    29 
       
    30 def _makefilematcher(repo, pats, followfirst):
       
    31     # When displaying a revision with --patch --follow FILE, we have
       
    32     # to know which file of the revision must be diffed. With
       
    33     # --follow, we want the names of the ancestors of FILE in the
       
    34     # revision, stored in "fcache". "fcache" is populated by
       
    35     # reproducing the graph traversal already done by --follow revset
       
    36     # and relating linkrevs to file names (which is not "correct" but
       
    37     # good enough).
       
    38     fcache = {}
       
    39     fcacheready = [False]
       
    40     pctx = repo['.']
       
    41     wctx = repo[None]
       
    42 
       
    43     def populate():
       
    44         for fn in pats:
       
    45             for i in ((pctx[fn],), pctx[fn].ancestors(followfirst=followfirst)):
       
    46                 for c in i:
       
    47                     fcache.setdefault(c.linkrev(), set()).add(c.path())
       
    48 
       
    49     def filematcher(rev):
       
    50         if not fcacheready[0]:
       
    51             # Lazy initialization
       
    52             fcacheready[0] = True
       
    53             populate()
       
    54         return scmutil.match(wctx, fcache.get(rev, []), default='path')
       
    55 
       
    56     return filematcher
       
    57 
       
    58 def _makelogrevset(repo, pats, opts, revs):
       
    59     """Return (expr, filematcher) where expr is a revset string built
       
    60     from log options and file patterns or None. If --stat or --patch
       
    61     are not passed filematcher is None. Otherwise it is a callable
       
    62     taking a revision number and returning a match objects filtering
       
    63     the files to be detailed when displaying the revision.
       
    64     """
       
    65     opt2revset = {
       
    66         'no_merges':        ('not merge()', None),
       
    67         'only_merges':      ('merge()', None),
       
    68         '_ancestors':       ('ancestors(%(val)s)', None),
       
    69         '_fancestors':      ('_firstancestors(%(val)s)', None),
       
    70         '_descendants':     ('descendants(%(val)s)', None),
       
    71         '_fdescendants':    ('_firstdescendants(%(val)s)', None),
       
    72         '_matchfiles':      ('_matchfiles(%(val)s)', None),
       
    73         'date':             ('date(%(val)r)', None),
       
    74         'branch':           ('branch(%(val)r)', ' or '),
       
    75         '_patslog':         ('filelog(%(val)r)', ' or '),
       
    76         '_patsfollow':      ('follow(%(val)r)', ' or '),
       
    77         '_patsfollowfirst': ('_followfirst(%(val)r)', ' or '),
       
    78         'keyword':          ('keyword(%(val)r)', ' or '),
       
    79         'prune':            ('not (%(val)r or ancestors(%(val)r))', ' and '),
       
    80         'user':             ('user(%(val)r)', ' or '),
       
    81         }
       
    82 
       
    83     opts = dict(opts)
       
    84     # follow or not follow?
       
    85     follow = opts.get('follow') or opts.get('follow_first')
       
    86     followfirst = opts.get('follow_first') and 1 or 0
       
    87     # --follow with FILE behaviour depends on revs...
       
    88     startrev = revs[0]
       
    89     followdescendants = (len(revs) > 1 and revs[0] < revs[1]) and 1 or 0
       
    90 
       
    91     # branch and only_branch are really aliases and must be handled at
       
    92     # the same time
       
    93     opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
       
    94     opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
       
    95     # pats/include/exclude are passed to match.match() directly in
       
    96     # _matchfile() revset but walkchangerevs() builds its matcher with
       
    97     # scmutil.match(). The difference is input pats are globbed on
       
    98     # platforms without shell expansion (windows).
       
    99     pctx = repo[None]
       
   100     match, pats = scmutil.matchandpats(pctx, pats, opts)
       
   101     slowpath = match.anypats() or (match.files() and opts.get('removed'))
       
   102     if not slowpath:
       
   103         for f in match.files():
       
   104             if follow and f not in pctx:
       
   105                 raise util.Abort(_('cannot follow file not in parent '
       
   106                                    'revision: "%s"') % f)
       
   107             filelog = repo.file(f)
       
   108             if not len(filelog):
       
   109                 # A zero count may be a directory or deleted file, so
       
   110                 # try to find matching entries on the slow path.
       
   111                 if follow:
       
   112                     raise util.Abort(
       
   113                         _('cannot follow nonexistent file: "%s"') % f)
       
   114                 slowpath = True
       
   115     if slowpath:
       
   116         # See cmdutil.walkchangerevs() slow path.
       
   117         #
       
   118         if follow:
       
   119             raise util.Abort(_('can only follow copies/renames for explicit '
       
   120                                'filenames'))
       
   121         # pats/include/exclude cannot be represented as separate
       
   122         # revset expressions as their filtering logic applies at file
       
   123         # level. For instance "-I a -X a" matches a revision touching
       
   124         # "a" and "b" while "file(a) and not file(b)" does
       
   125         # not. Besides, filesets are evaluated against the working
       
   126         # directory.
       
   127         matchargs = ['r:', 'd:relpath']
       
   128         for p in pats:
       
   129             matchargs.append('p:' + p)
       
   130         for p in opts.get('include', []):
       
   131             matchargs.append('i:' + p)
       
   132         for p in opts.get('exclude', []):
       
   133             matchargs.append('x:' + p)
       
   134         matchargs = ','.join(('%r' % p) for p in matchargs)
       
   135         opts['_matchfiles'] = matchargs
       
   136     else:
       
   137         if follow:
       
   138             fpats = ('_patsfollow', '_patsfollowfirst')
       
   139             fnopats = (('_ancestors', '_fancestors'),
       
   140                        ('_descendants', '_fdescendants'))
       
   141             if pats:
       
   142                 # follow() revset inteprets its file argument as a
       
   143                 # manifest entry, so use match.files(), not pats.
       
   144                 opts[fpats[followfirst]] = list(match.files())
       
   145             else:
       
   146                 opts[fnopats[followdescendants][followfirst]] = str(startrev)
       
   147         else:
       
   148             opts['_patslog'] = list(pats)
       
   149 
       
   150     filematcher = None
       
   151     if opts.get('patch') or opts.get('stat'):
       
   152         if follow:
       
   153             filematcher = _makefilematcher(repo, pats, followfirst)
       
   154         else:
       
   155             filematcher = lambda rev: match
       
   156 
       
   157     expr = []
       
   158     for op, val in opts.iteritems():
       
   159         if not val:
       
   160             continue
       
   161         if op not in opt2revset:
       
   162             continue
       
   163         revop, andor = opt2revset[op]
       
   164         if '%(val)' not in revop:
       
   165             expr.append(revop)
       
   166         else:
       
   167             if not isinstance(val, list):
       
   168                 e = revop % {'val': val}
       
   169             else:
       
   170                 e = '(' + andor.join((revop % {'val': v}) for v in val) + ')'
       
   171             expr.append(e)
       
   172 
       
   173     if expr:
       
   174         expr = '(' + ' and '.join(expr) + ')'
       
   175     else:
       
   176         expr = None
       
   177     return expr, filematcher
       
   178 
       
   179 def getlogrevs(repo, pats, opts):
       
   180     """Return (revs, expr, filematcher) where revs is an iterable of
       
   181     revision numbers, expr is a revset string built from log options
       
   182     and file patterns or None, and used to filter 'revs'. If --stat or
       
   183     --patch are not passed filematcher is None. Otherwise it is a
       
   184     callable taking a revision number and returning a match objects
       
   185     filtering the files to be detailed when displaying the revision.
       
   186     """
       
   187     def increasingrevs(repo, revs, matcher):
       
   188         # The sorted input rev sequence is chopped in sub-sequences
       
   189         # which are sorted in ascending order and passed to the
       
   190         # matcher. The filtered revs are sorted again as they were in
       
   191         # the original sub-sequence. This achieve several things:
       
   192         #
       
   193         # - getlogrevs() now returns a generator which behaviour is
       
   194         #   adapted to log need. First results come fast, last ones
       
   195         #   are batched for performances.
       
   196         #
       
   197         # - revset matchers often operate faster on revision in
       
   198         #   changelog order, because most filters deal with the
       
   199         #   changelog.
       
   200         #
       
   201         # - revset matchers can reorder revisions. "A or B" typically
       
   202         #   returns returns the revision matching A then the revision
       
   203         #   matching B. We want to hide this internal implementation
       
   204         #   detail from the caller, and sorting the filtered revision
       
   205         #   again achieves this.
       
   206         for i, window in cmdutil.increasingwindows(0, len(revs), windowsize=1):
       
   207             orevs = revs[i:i + window]
       
   208             nrevs = set(matcher(repo, sorted(orevs)))
       
   209             for rev in orevs:
       
   210                 if rev in nrevs:
       
   211                     yield rev
       
   212 
       
   213     if not len(repo):
       
   214         return iter([]), None, None
       
   215     # Default --rev value depends on --follow but --follow behaviour
       
   216     # depends on revisions resolved from --rev...
       
   217     follow = opts.get('follow') or opts.get('follow_first')
       
   218     if opts.get('rev'):
       
   219         revs = scmutil.revrange(repo, opts['rev'])
       
   220     else:
       
   221         if follow and len(repo) > 0:
       
   222             revs = scmutil.revrange(repo, ['.:0'])
       
   223         else:
       
   224             revs = range(len(repo) - 1, -1, -1)
       
   225     if not revs:
       
   226         return iter([]), None, None
       
   227     expr, filematcher = _makelogrevset(repo, pats, opts, revs)
       
   228     if expr:
       
   229         matcher = revset.match(repo.ui, expr)
       
   230         revs = increasingrevs(repo, revs, matcher)
       
   231     if not opts.get('hidden'):
       
   232         # --hidden is still experimental and not worth a dedicated revset
       
   233         # yet. Fortunately, filtering revision number is fast.
       
   234         revs = (r for r in revs if r not in repo.changelog.hiddenrevs)
       
   235     else:
       
   236         revs = iter(revs)
       
   237     return revs, expr, filematcher
       
   238 
       
   239 def generate(ui, dag, displayer, showparents, edgefn, getrenamed=None,
       
   240              filematcher=None):
       
   241     seen, state = [], graphmod.asciistate()
       
   242     for rev, type, ctx, parents in dag:
       
   243         char = 'o'
       
   244         if ctx.node() in showparents:
       
   245             char = '@'
       
   246         elif ctx.obsolete():
       
   247             char = 'x'
       
   248         copies = None
       
   249         if getrenamed and ctx.rev():
       
   250             copies = []
       
   251             for fn in ctx.files():
       
   252                 rename = getrenamed(fn, ctx.rev())
       
   253                 if rename:
       
   254                     copies.append((fn, rename[0]))
       
   255         revmatchfn = None
       
   256         if filematcher is not None:
       
   257             revmatchfn = filematcher(ctx.rev())
       
   258         displayer.show(ctx, copies=copies, matchfn=revmatchfn)
       
   259         lines = displayer.hunk.pop(rev).split('\n')
       
   260         if not lines[-1]:
       
   261             del lines[-1]
       
   262         displayer.flush(rev)
       
   263         edges = edgefn(type, char, lines, seen, rev, parents)
       
   264         for type, char, lines, coldata in edges:
       
   265             graphmod.ascii(ui, state, type, char, lines, coldata)
       
   266     displayer.close()
       
   267 
    29 
   268 @command('glog',
    30 @command('glog',
   269     [('f', 'follow', None,
    31     [('f', 'follow', None,
   270      _('follow changeset history, or file history across copies and renames')),
    32      _('follow changeset history, or file history across copies and renames')),
   271     ('', 'follow-first', None,
    33     ('', 'follow-first', None,
   296 
    58 
   297     Nodes printed as an @ character are parents of the working
    59     Nodes printed as an @ character are parents of the working
   298     directory.
    60     directory.
   299     """
    61     """
   300 
    62 
   301     revs, expr, filematcher = getlogrevs(repo, pats, opts)
    63     revs, expr, filematcher = cmdutil.getgraphlogrevs(repo, pats, opts)
   302     revs = sorted(revs, reverse=1)
    64     revs = sorted(revs, reverse=1)
   303     limit = cmdutil.loglimit(opts)
    65     limit = cmdutil.loglimit(opts)
   304     if limit is not None:
    66     if limit is not None:
   305         revs = revs[:limit]
    67         revs = revs[:limit]
   306     revdag = graphmod.dagwalker(repo, revs)
    68     revdag = graphmod.dagwalker(repo, revs)
   311         if opts.get('rev'):
    73         if opts.get('rev'):
   312             endrev = max(scmutil.revrange(repo, opts.get('rev'))) + 1
    74             endrev = max(scmutil.revrange(repo, opts.get('rev'))) + 1
   313         getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
    75         getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
   314     displayer = show_changeset(ui, repo, opts, buffered=True)
    76     displayer = show_changeset(ui, repo, opts, buffered=True)
   315     showparents = [ctx.node() for ctx in repo[None].parents()]
    77     showparents = [ctx.node() for ctx in repo[None].parents()]
   316     generate(ui, revdag, displayer, showparents, graphmod.asciiedges,
    78     cmdutil.displaygraph(ui, revdag, displayer, showparents,
   317              getrenamed, filematcher)
    79                          graphmod.asciiedges, getrenamed, filematcher)
   318 
    80 
   319 def graphrevs(repo, nodes, opts):
    81 def graphrevs(repo, nodes, opts):
   320     limit = cmdutil.loglimit(opts)
    82     limit = cmdutil.loglimit(opts)
   321     nodes.reverse()
    83     nodes.reverse()
   322     if limit is not None:
    84     if limit is not None:
   339         return
   101         return
   340 
   102 
   341     revdag = graphrevs(repo, o, opts)
   103     revdag = graphrevs(repo, o, opts)
   342     displayer = show_changeset(ui, repo, opts, buffered=True)
   104     displayer = show_changeset(ui, repo, opts, buffered=True)
   343     showparents = [ctx.node() for ctx in repo[None].parents()]
   105     showparents = [ctx.node() for ctx in repo[None].parents()]
   344     generate(ui, revdag, displayer, showparents, graphmod.asciiedges)
   106     cmdutil.displaygraph(ui, revdag, displayer, showparents,
       
   107                          graphmod.asciiedges)
   345 
   108 
   346 def gincoming(ui, repo, source="default", **opts):
   109 def gincoming(ui, repo, source="default", **opts):
   347     """show the incoming changesets alongside an ASCII revision graph
   110     """show the incoming changesets alongside an ASCII revision graph
   348 
   111 
   349     Print the incoming changesets alongside a revision graph drawn with
   112     Print the incoming changesets alongside a revision graph drawn with
   357 
   120 
   358     _checkunsupportedflags([], opts)
   121     _checkunsupportedflags([], opts)
   359     def display(other, chlist, displayer):
   122     def display(other, chlist, displayer):
   360         revdag = graphrevs(other, chlist, opts)
   123         revdag = graphrevs(other, chlist, opts)
   361         showparents = [ctx.node() for ctx in repo[None].parents()]
   124         showparents = [ctx.node() for ctx in repo[None].parents()]
   362         generate(ui, revdag, displayer, showparents, graphmod.asciiedges)
   125         cmdutil.displaygraph(ui, revdag, displayer, showparents,
       
   126                              graphmod.asciiedges)
   363 
   127 
   364     hg._incoming(display, subreporecurse, ui, repo, source, opts, buffered=True)
   128     hg._incoming(display, subreporecurse, ui, repo, source, opts, buffered=True)
   365 
   129 
   366 def uisetup(ui):
   130 def uisetup(ui):
   367     '''Initialize the extension.'''
   131     '''Initialize the extension.'''