comparison mercurial/graphmod.py @ 14042:9966c95b8c4f

graphmod: use revsets internally Thanks for the idea and most of the implementation to Klaus Koch Backs revisions() and filerevs() with DAG walker which can iterate through arbitrary list of revisions instead of strict one by one iteration from start to stop. When a gap occurs in a revisions (i.e. in file log), the next topological parent within the revset is searched and the connection to it is printed in the ascii graph. File graph can draw sometimes more connections than previous version, because graph is produced according to the revset, not according to a file's filelog. In case the graph contains several branches where the left parent is null, the graphs for each are printed sequentially, not in parallel as it was a case earlier (see for example the graph for README in hg-dev).
author Alexander Solovyov <alexander@solovyov.net>
date Sun, 13 Mar 2011 15:53:38 +0100
parents 101366ad816c
children 1c1e1232abdc
comparison
equal deleted inserted replaced
14041:bcc6ed0f6c3b 14042:9966c95b8c4f
16 context of the graph returned. Type is a constant specifying the node type. 16 context of the graph returned. Type is a constant specifying the node type.
17 Data depends on type. 17 Data depends on type.
18 """ 18 """
19 19
20 from mercurial.node import nullrev 20 from mercurial.node import nullrev
21 from mercurial.cmdutil import revrange
21 22
22 CHANGESET = 'C' 23 CHANGESET = 'C'
23 24
24 def revisions(repo, start, stop): 25 def revisions(repo, start, end):
26 """DAG generator for revisions between start and end
27 """
28 revset = '%s:%s' % (start, end)
29 return dagwalker(repo, revrange(repo, [revset]))
30
31 def filerevs(repo, path, start, stop, limit=None):
32 """DAG generator, which is limited by file passed
33 """
34 revset = '%s:%s and file("%s")' % (start, stop, path)
35 if limit:
36 revset = 'limit(%s, %s)' % (revset, limit)
37 return dagwalker(repo, revrange(repo, [revset]))
38
39 def dagwalker(repo, revs):
25 """cset DAG generator yielding (id, CHANGESET, ctx, [parentids]) tuples 40 """cset DAG generator yielding (id, CHANGESET, ctx, [parentids]) tuples
26 41
27 This generator function walks through the revision history from revision 42 This generator function walks through revisions (which should be ordered
28 start to revision stop (which must be less than or equal to start). It 43 from bigger to lower). It returns a tuple for each node. The node and parent
29 returns a tuple for each node. The node and parent ids are arbitrary 44 ids are arbitrary integers which identify a node in the context of the graph
30 integers which identify a node in the context of the graph returned. 45 returned.
31 """ 46 """
32 cur = start 47 if not revs:
33 while cur >= stop: 48 return []
34 ctx = repo[cur] 49
35 parents = set([p.rev() for p in ctx.parents() if p.rev() != nullrev]) 50 ns = [repo[r].node() for r in revs]
36 yield (cur, CHANGESET, ctx, sorted(parents)) 51 revdag = list(nodes(repo, ns))
37 cur -= 1 52
38 53 cl = repo.changelog
39 def filerevs(repo, path, start, stop, limit=None): 54 lowestrev = min(revs)
40 """file cset DAG generator yielding (id, CHANGESET, ctx, [parentids]) tuples 55 gpcache = {}
41 56 leafs = {}
42 This generator function walks through the revision history of a single 57
43 file from revision start down to revision stop. 58 for i, (id, c, ctx, parents) in enumerate(revdag):
44 """ 59 mpars = [p.rev() for p in ctx.parents() if
45 filerev = len(repo.file(path)) - 1 60 p.rev() != nullrev and p.rev() not in parents]
46 rev = stop + 1 61 grandparents = []
47 count = 0 62
48 while filerev >= 0 and rev > stop: 63 for mpar in mpars:
49 fctx = repo.filectx(path, fileid=filerev) 64 gp = gpcache.get(mpar) or grandparent(cl, lowestrev, revs, mpar)
50 parents = set([f.linkrev() for f in fctx.parents() if f.path() == path]) 65 gpcache[mpar] = gp
51 rev = fctx.rev() 66 if gp is None:
52 if rev <= start: 67 leafs.setdefault(mpar, []).append((i, ctx))
53 yield (rev, CHANGESET, fctx.changectx(), sorted(parents)) 68 else:
54 count += 1 69 grandparents.append(gp)
55 if count == limit: 70
56 break 71 if grandparents:
57 filerev -= 1 72 for gp in grandparents:
73 if gp not in revdag[i][3]:
74 revdag[i][3].append(gp)
75
76 for parent, leafs in leafs.iteritems():
77 for i, ctx in leafs:
78 revdag[i][3].append(parent)
79
80 return revdag
58 81
59 def nodes(repo, nodes): 82 def nodes(repo, nodes):
60 """cset DAG generator yielding (id, CHANGESET, ctx, [parentids]) tuples 83 """cset DAG generator yielding (id, CHANGESET, ctx, [parentids]) tuples
61 84
62 This generator function walks the given nodes. It only returns parents 85 This generator function walks the given nodes. It only returns parents
118 edges.append((ecol, next.index(p), color)) 141 edges.append((ecol, next.index(p), color))
119 142
120 # Yield and move on 143 # Yield and move on
121 yield (cur, type, data, (col, color), edges) 144 yield (cur, type, data, (col, color), edges)
122 seen = next 145 seen = next
146
147
148 def grandparent(cl, lowestrev, roots, head):
149 """Return closest 'root' rev in topological path from 'roots' to 'head'.
150
151 Derived from revlog.revlog.nodesbetween, but only returns next rev
152 of topologically sorted list of all nodes N that satisfy of these
153 constraints:
154
155 1. N is a descendant of some node in 'roots'
156 2. N is an ancestor of 'head'
157 3. N is some node in 'roots' or nullrev
158
159 Every node is considered to be both a descendant and an ancestor
160 of itself, so every reachable node in 'roots' and 'head' will be
161 included in 'nodes'.
162 """
163 ancestors = set()
164 # Start at the top and keep marking parents until we're done.
165 revstotag = set([head])
166 revstotag.discard(nullrev)
167 llowestrev = max(nullrev, lowestrev)
168
169 while revstotag:
170 r = revstotag.pop()
171 # A node's revision number represents its place in a
172 # topologically sorted list of nodes.
173 if r >= llowestrev:
174 if r not in ancestors:
175 # If we are possibly a descendent of one of the roots
176 # and we haven't already been marked as an ancestor
177 ancestors.add(r) # Mark as ancestor
178 # Add non-nullrev parents to list of nodes to tag.
179 revstotag.update([p for p in cl.parentrevs(r)])
180
181 if not ancestors:
182 return
183 # Now that we have our set of ancestors, we want to remove any
184 # roots that are not ancestors.
185
186 # If one of the roots was nullrev, everything is included anyway.
187 if lowestrev > nullrev:
188 # But, since we weren't, let's recompute the lowest rev to not
189 # include roots that aren't ancestors.
190
191 # Filter out roots that aren't ancestors of heads
192 _roots = ancestors.intersection(roots)
193 if not _roots:
194 return
195 # Recompute the lowest revision
196 lowestrev = min(roots)
197 else:
198 # We are descending from nullrev, and don't need to care about
199 # any other roots.
200 lowestrev = nullrev
201 _roots = [nullrev]
202
203 # The roots are just the descendants.
204 # Don't start at nullrev since we don't want nullrev in our output list,
205 # and if nullrev shows up in descedents, empty parents will look like
206 # they're descendents.
207 lowestrevisnullrev = (lowestrev == nullrev)
208 for r in xrange(head - 1, max(lowestrev, -1) - 1, -1):
209 if lowestrevisnullrev or r in _roots:
210 pass
211 elif _roots.issuperset(cl.parentrevs(r)):
212 # A node is a descendent if either of its parents are
213 # descendents. (We seeded the dependents list with the roots
214 # up there, remember?)
215 _roots.add(r)
216 else:
217 continue
218 if r in ancestors:
219 # Only include nodes that are both descendents and ancestors.
220 return r