Merge lp:~garyvdm/bzr/loggraphviz into lp:bzr
- loggraphviz
- Merge into bzr.dev
Status: | Work in progress |
---|---|
Proposed branch: | lp:~garyvdm/bzr/loggraphviz |
Merge into: | lp:bzr |
Diff against target: |
2822 lines (+2802/-0) 3 files modified
bzrlib/loggraphviz.py (+1648/-0) bzrlib/tests/__init__.py (+1/-0) bzrlib/tests/test_loggraphviz.py (+1153/-0) |
To merge this branch: | bzr merge lp:~garyvdm/bzr/loggraphviz |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
bzr-core | Pending | ||
Review via email: mp+41146@code.launchpad.net |
Commit message
Description of the change
This branch includes the loggraphviz module from qbzr, and is the module that implements the most of the logic for qlog, which I have refactored in the hopes that it may be used other plugins.
I have implemented a proof of concept loggerhead viz view which use this. This is available here: lp:~garyvdm/loggerhead/loggraphviz
There is still lots of work that I would like to do before this actually lands. E.g.:
* The doc strings need to be completed, improved, and reviewed.
* I want to have the api between the State objects, and Filters, especially how one adds a filter to a state more clearly defined.
* I need to mark private/internal methods with _.
* I want to do a bzr-gtk viz poc. viz has a number of features that qlog does not, i.e. progress bar, a revision command line argument to specify a tip., and a limit argument. Implementing these my result in api changes.
* I would like to still investigate doing partial loading of merge sorted revisions using bzr-history-db for the use of loggerhead. This may also result in api changes.
So I know this is not 100% ready, but I feel that as I have now done the loggerhead poc, I now feel the time is right to get some comments on the code.
- 5539. By Gary van der Merwe
-
Restore the utf marker in test_loggraphvi
z.py.
John A Meinel (jameinel) wrote : | # |
John A Meinel (jameinel) wrote : | # |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
Sorry for sending this twice, hit reply to early.
On 11/18/2010 4:02 AM, Gary van der Merwe wrote:
> Gary van der Merwe has proposed merging lp:~garyvdm/bzr/loggraphviz into lp:bzr.
>
> Requested reviews:
> bzr-core (bzr-core)
>
>
> This branch includes the loggraphviz module from qbzr, and is the module that implements the most of the logic for qlog, which I have refactored in the hopes that it may be used other plugins.
>
> I have implemented a proof of concept loggerhead viz view which use this. This is available here: lp:~garyvdm/loggerhead/loggraphviz
>
> There is still lots of work that I would like to do before this actually lands. E.g.:
> * The doc strings need to be completed, improved, and reviewed.
> * I want to have the api between the State objects, and Filters, especially how one adds a filter to a state more clearly defined.
> * I need to mark private/internal methods with _.
> * I want to do a bzr-gtk viz poc. viz has a number of features that qlog does not, i.e. progress bar, a revision command line argument to specify a tip., and a limit argument. Implementing these my result in api changes.
> * I would like to still investigate doing partial loading of merge sorted revisions using bzr-history-db for the use of loggerhead. This may also result in api changes.
>
> So I know this is not 100% ready, but I feel that as I have now done the loggerhead poc, I now feel the time is right to get some comments on the code.
To start off, I think the work you've done on graph visualization is
great, and I agree that it would be good to pull the core of that into
bzrlib itself.
I'm being a bit picky here, but I'm hoping to provide some feedback to
make it easier to grow the code in the future. I certainly don't want to
turn you off about it. And some bits we could put comments of "this is
how we want it to be in the future", without having to do all of the
implementation up front.
I didn't go through everything thoroughly. I certainly didn't audit your
logic, etc. I have a feeling a lot of that is already well polished by
having this used by qbzr for so long.
'loggraphviz' is a bit clumsy for me. I don't have great ideas, but at
the very least it should be "log_graph_viz". I think calling it
"graph_
I don't think you need to call each class GraphVizFoo inside the
loggraphviz module, though we've had discussions about that (without
strict resolution, afaict).
For naming, I would think that something called "Loader()" would return
the state object as part of .load(), rather than internalizing it. Your
example is:
+.. python::
+ bi = loggraphviz.
+ graph_viz = loggraphviz.
+ graph_viz.load()
+ state = loggraphviz.
+ computed = graph_viz.
I would say,
1) Do we need the BranchInfo? Could it be done slightly differently?
loader = GraphVizLoader()
loader.
# loader.add_tree() ?
# loader.
state = loader.proce...
Gary van der Merwe (garyvdm) wrote : | # |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
On 18/11/2010 21:55, John A Meinel wrote:
> To start off, I think the work you've done on graph visualization is
> great, and I agree that it would be good to pull the core of that into
> bzrlib itself.
>
> I'm being a bit picky here, but I'm hoping to provide some feedback to
> make it easier to grow the code in the future. I certainly don't want to
> turn you off about it. And some bits we could put comments of "this is
> how we want it to be in the future", without having to do all of the
> implementation up front.
This is what I am looking for. You have raised some real issues, and
given some good suggestions. Thanks.
> 'loggraphviz' is a bit clumsy for me. I don't have great ideas, but at
> the very least it should be "log_graph_viz". I think calling it
> "graph_
I guess I called it *log*graphviz, because it is specific to then log
graph, an not generic to any other graph, but it is quite log, so
graph_viz sounds good.
> I don't think you need to call each class GraphVizFoo inside the
> loggraphviz module, though we've had discussions about that (without
> strict resolution, afaict).
Ok
> For naming, I would think that something called "Loader()" would return
> the state object as part of .load(), rather than internalizing it. Your
> example is:
>
> +.. python::
> + bi = loggraphviz.
> + graph_viz = loggraphviz.
> + graph_viz.load()
> + state = loggraphviz.
> + computed = graph_viz.
>
> I would say,
No, but maybe loader is a bad name. The loader keeps a cache of what it
loads. You may have more than state object per loader. In the case of
loggerhead, the loader is cached, valid until then branch tip changes.
For each http request, a new state object is created, with data parsed
from the http query. Maybe the load method of the loader should be
called by the loaders __init__ method.
> 1) Do we need the BranchInfo? Could it be done slightly differently?
> loader = GraphVizLoader()
> loader.
> # loader.add_tree() ?
> # loader.
> state = loader.process()
> for node in state.iter_nodes():
> ...
>
> Basically, BranchInfo can be an implementation detail, rather than
> something you create and-then-add.
Currently, having the qlog create the BranchInfo reduces it's plumbing,
because the BranchInfo are not created in then same place as then
GraphVizLoader. I need to look at changing this.
> + def __eq__(self, other):
> + if isinstance(other, BranchInfo):
> + return self.branch.
> + return False
>
> ^- it seems odd that you don't check whether the label, presence of a
> tree, or index are the same. I wonder if we really want to use "__eq__"
> for this comparison, versus ".same_
>
> Namely setting __hash__ and __eq__ in this way means that 2 BranchInfo
> structures that have some differences are going to end up in the same
> dictionary slot...
John A Meinel (jameinel) wrote : | # |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
...
> Ascii Unicode
> # * # ○
> # | # │
> # * | # ○ │
> # | | # │ │
> # * | | # ○ │ │
> # |--- # ├─╯─╯
> # * # ○
>
>
> That ok. But when the graphs get a bit more complicated, it starts to fail.
>
> # + # ⊕
> # |--- # ├┄╮─╮
> # | : * # │ ┆ ○
> # | -|- # │ ╰┄┼┄╮
> # | | * # │ │ ○
> # | | | # │ │ │
> # ~ | | # ⊖ │ │
> # |- | | # ├─╮ │ │
> # | * | | # │ ○ │ │
> # |----- # ├─╯─╯─╯
> # * # ○
>
> The unicode versions made it much easier for me to debug problems.
>
>> And I get some *really* strange results in whatever font Thunderbird
>> tries to set as default.
>
> Was that not because I left out the # -*- coding: utf-8 -*- line? Do
> the unicode characters show fine in this mail?
I'm attaching the screenshot of the default font that Thunderbird is
using. Even stranger, it looks better, but still poorly aligned when
replying to your email. But it is completely unreadable in Vim. I did
eventually find a font where it looks ok (Deja Vu Sans Mono). But most
other fonts (Courier New especially) just show garbage.
I do understand how it could help you, but I'm curious if it doesn't
just add confusion for a lot of other people....
>
>> And all those lines are >80 chars as well.
>
> I did start trying to fit them in to < 80 chars, but this was time
> consuming, and for me, makes it harder to read.
>
> Here is a quote from PEP 8, which I feel applies:
>
>> Two good reasons to break a particular rule:
>>
>> (1) When applying the rule would make the code less readable, even for
>> someone who is used to reading code that follows the rules.
>
Couldn't you just put the graph by itself in a comment ahead of time? I
do this in bzrlib/
If you did that, then you could even put both the ascii and unicode
versions. So you would have:
+ # branch lines should not cross over
+ # ascii unicode
+ # ~ ⊖
+ # |---- ├───╮
+ # | ~ │ ⊖
+ # | | │ │
+ # ~ | ⊖ │
+ # |-- | ├─╮ │
+ # | * | │ ○ │
+ # | |- │ ├─╯
+ # | * │ ○
+ # |- ├─╯
+ # * ○
+ self.assertComp
+ [('rev-f', 0, True, [(0, 0, 0, True), (0, 2, 3, True)]),
+ ('rev-e', 2, True, [(0, 0, 0, True), (2, 2, 2, True)]),
+ ('rev-d', 0, True, [(0, 0, 0, True), (0, 1, 2, True), (2,
2, 2, True)]),
+ ('rev-c', 1, None, [(0, 0, 0, True), (1, 1, 2, True), (2,
1, 2, True)]),
+ ('rev-b', 1, None, [(0, 0, 0, True), (1, 0, 0, True)]),
+ ('rev-a', 0, None, [])]
+ computed)
That allows:
a) Less that 80 chars
b) You don't have the artificial extra blank lines in the list output
c) Put the ascii first. At least when I look at it, Unicode is more
likely to screw up the spacing, causing the ascii art to get
incorrectly drawn.
I realize it doesn't give you a strict line-to-line correspondence of
which list entry corresponds to what graph entry. But as long as t...
Andrew Bennetts (spiv) wrote : | # |
Gary van der Merwe wrote:
[...]
>
> Was that not because I left out the # -*- coding: utf-8 -*- line? Do
> the unicode characters show fine in this mail?
FWIW, those characters all displayed fine for me (I read mail and edit
files using mutt and vim, both in gnome-terminal).
Andrew Bennetts (spiv) wrote : | # |
Andrew Bennetts wrote:
> Gary van der Merwe wrote:
> [...]
> >
> > Was that not because I left out the # -*- coding: utf-8 -*- line? Do
> > the unicode characters show fine in this mail?
>
> FWIW, those characters all displayed fine for me (I read mail and edit
> files using mutt and vim, both in gnome-terminal).
However, to clarify: the diff attached the original merge proposal mail
showed ? in place of the non-ascii characters. This appears to be
because Launchpad marked that attachment as encoded in ascii, rather
than utf-8.
It's not hard for me to workaround that with my mail client, but it may
be a bigger impediment for other people. And John's already reported
some trouble viewing those characters in his gvim configuration, so we
can assume a fair few other people may encounter issues too. It may be
possible to improve Launchpad to mark those attachments as utf-8, and
the issues people encounter may be easy to resolve... but unfortunately
it does mean we should tread carefully before accepting UTF-8 .py files
I think. We probably should raise the idea on the list first.
The not-ascii art is quite pretty though! So I'd definitely like us to
allow it, as it would make some comments much clearer. I'm just worried
about how disruptive it might be :/
One last quick comment: unfortunately the name “graph_viz” is a bad one
I think, because many people will assume it's associated with the very
well known graphviz software. :(
Martin Pool (mbp) wrote : | # |
On 22 November 2010 12:37, Andrew Bennetts
<email address hidden> wrote:
> One last quick comment: unfortunately the name “graph_viz” is a bad one
> I think, because many people will assume it's associated with the very
> well known graphviz software. :(
+1, let's not have any more silly confusable names.
--
Martin
Unmerged revisions
- 5539. By Gary van der Merwe
-
Restore the utf marker in test_loggraphvi
z.py. - 5538. By Gary van der Merwe
-
Make it possible to call GraphVizFilterS
tate.collapse_ expand_ rev without changing the state, but rather return a new branch_line_state dict. - 5537. By Gary van der Merwe
-
Assign copyright to Canonical Ltd.
- 5536. By Gary van der Merwe
-
Get tests to run.
- 5535. By Gary van der Merwe
-
Copy loggraphviz.py form qbzr rev 1336 revid:<email address hidden>
Preview Diff
1 | === added file 'bzrlib/loggraphviz.py' |
2 | --- bzrlib/loggraphviz.py 1970-01-01 00:00:00 +0000 |
3 | +++ bzrlib/loggraphviz.py 2010-11-18 10:15:44 +0000 |
4 | @@ -0,0 +1,1648 @@ |
5 | +# Copyright (C) 2009, 2010 Canonical Ltd |
6 | +# |
7 | +# This program is free software; you can redistribute it and/or |
8 | +# modify it under the terms of the GNU General Public License |
9 | +# as published by the Free Software Foundation; either version 2 |
10 | +# of the License, or (at your option) any later version. |
11 | +# |
12 | +# This program is distributed in the hope that it will be useful, |
13 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
14 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
15 | +# GNU General Public License for more details. |
16 | +# |
17 | +# You should have received a copy of the GNU General Public License |
18 | +# along with this program; if not, write to the Free Software |
19 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
20 | + |
21 | +""" |
22 | +Layout a revision graph for visual representation, allowing for filtering and |
23 | +collapsible branches. |
24 | + |
25 | +Usage example |
26 | +============= |
27 | + |
28 | +.. python:: |
29 | + bi = loggraphviz.BranchInfo('branch-label', tree, branch) |
30 | + graph_viz = loggraphviz.GraphVizLoader([bi], bi, False) |
31 | + graph_viz.load() |
32 | + state = loggraphviz.GraphVizFilterState(graph_viz) |
33 | + computed = graph_viz.compute_viz(state) |
34 | + |
35 | +""" |
36 | + |
37 | +import gc |
38 | +from itertools import izip |
39 | + |
40 | +from bzrlib import errors |
41 | +from bzrlib.bzrdir import BzrDir |
42 | +from bzrlib.transport.local import LocalTransport |
43 | +from bzrlib.revision import NULL_REVISION, CURRENT_REVISION |
44 | +from bzrlib.graph import ( |
45 | + Graph, |
46 | + StackedParentsProvider, |
47 | + KnownGraph, |
48 | + DictParentsProvider, |
49 | + ) |
50 | + |
51 | + |
52 | +class BranchInfo(object): |
53 | + """Wrapper for a branch, it's working tree, if available, and a label.""" |
54 | + |
55 | + def __init__(self, label, tree, branch, index=None): |
56 | + self.label = label |
57 | + self.tree = tree |
58 | + self.branch = branch |
59 | + self.index = index |
60 | + |
61 | + def __hash__(self): |
62 | + return self.branch.base.__hash__() |
63 | + |
64 | + def __eq__(self, other): |
65 | + if isinstance(other, BranchInfo): |
66 | + return self.branch.base.__eq__(other.branch.base) |
67 | + return False |
68 | + |
69 | + |
70 | +class RevisionData(object): |
71 | + """ |
72 | + Container for data for a revision in the graph that gets calculated |
73 | + when the graph is loaded. |
74 | + """ |
75 | + |
76 | + # Instance of this object are typically named "rev". |
77 | + |
78 | + __slots__ = ["index", "_merge_sort_node", "branch", "_revno_str", |
79 | + "merges", "merged_by", 'branch_id', 'color'] |
80 | + |
81 | + def __init__(self, index, merge_sort_node): |
82 | + """Create a new RevisionData instance.""" |
83 | + self.index = index |
84 | + self._merge_sort_node = merge_sort_node |
85 | + self.branch = None |
86 | + self._revno_str = None |
87 | + self.merges = [] |
88 | + """Revision indexes that this revision merges""" |
89 | + self.merged_by = None |
90 | + """Revision index that merges this revision.""" |
91 | + self.branch_id = self._merge_sort_node.revno[0:-1] |
92 | + self.color = reduce(lambda x, y: x + y, self.branch_id, 0) |
93 | + |
94 | + revid = property(lambda self: self._merge_sort_node.key) |
95 | + merge_depth = property(lambda self: self._merge_sort_node.merge_depth) |
96 | + revno_sequence = property(lambda self: self._merge_sort_node.revno) |
97 | + end_of_merge = property(lambda self: self._merge_sort_node.end_of_merge) |
98 | + |
99 | + def get_revno_str(self): |
100 | + if self._revno_str is None: |
101 | + self._revno_str = ".".join(["%d" % (revno) |
102 | + for revno in self.revno_sequence]) |
103 | + if self.revid.startswith(CURRENT_REVISION): |
104 | + self._revno_str += " ?" |
105 | + return self._revno_str |
106 | + revno_str = property(get_revno_str) |
107 | + |
108 | + def __repr__(self): |
109 | + return "%s <%s %s>" % (self.__class__.__name__, self.revno_str, |
110 | + self.revid) |
111 | + |
112 | +class BranchLine(object): |
113 | + """Container for data for a branch line, aka merge line.""" |
114 | + |
115 | + __slots__ = ["branch_id", "revs", "merges", "merged_by", |
116 | + "color", "merge_depth"] |
117 | + |
118 | + def __init__(self, branch_id): |
119 | + self.branch_id = branch_id |
120 | + self.revs = [] |
121 | + self.merges = [] |
122 | + self.merged_by = [] |
123 | + self.merge_depth = 0 |
124 | + |
125 | + def __repr__(self): |
126 | + return "%s <%s>" % (self.__class__.__name__, self.branch_id) |
127 | + |
128 | + |
129 | +class GhostRevisionError(errors.InternalBzrError): |
130 | + |
131 | + _fmt = "{%(revision_id)s} is a ghost." |
132 | + |
133 | + def __init__(self, revision_id): |
134 | + errors.BzrError.__init__(self) |
135 | + self.revision_id = revision_id |
136 | + |
137 | + |
138 | +class GraphVizLoader(object): |
139 | + """ |
140 | + Loads graph for branches and provides computed layout for visual |
141 | + representation. |
142 | + """ |
143 | + |
144 | + # Most list/dicts related to revisions are unfiltered. When we do a graph |
145 | + # layout, we filter these revisions. A revision may be filter out because: |
146 | + # * It's branch is hidden (or collapsed). |
147 | + # * We have a specified file_id(s), and the revision does not touch the |
148 | + # file_id(s). |
149 | + # * We have a search, and the revision does not match the search. |
150 | + # |
151 | + # The main list of unfiltered revisions is self.revisions. A revisions |
152 | + # index in revisions are normally called index. The main list of filtered |
153 | + # revisions is filtered_revs. Revision indexes in this list are called |
154 | + # f_index. |
155 | + |
156 | + def __init__(self, branches, primary_bi, no_graph): |
157 | + self.branches = branches |
158 | + """List of BranchInfo for each branch.""" |
159 | + self.primary_bi = primary_bi |
160 | + self.no_graph = no_graph |
161 | + |
162 | + self.repos = [] |
163 | + self.local_repo_copies = [] |
164 | + """A list of repositories that revisions will be attempted to be loaded |
165 | + from first.""" |
166 | + |
167 | + self.revid_head_info = {} |
168 | + """Dict with a keys of head revid and value of |
169 | + (list of (branch, label), |
170 | + list of revids that are unique to this head) |
171 | + |
172 | + We can't just store the BranchInfo, because the label may have |
173 | + have addition text in it, e.g. "Working Tree", "Pending Merges" |
174 | + """ |
175 | + |
176 | + self.revid_branch_info = {} |
177 | + |
178 | + self.ghosts = set() |
179 | + |
180 | + self.revisions = [] |
181 | + """List of RevisionInfo from merge_sort.""" |
182 | + |
183 | + self.revid_rev = {} |
184 | + self.graph_children = {} |
185 | + |
186 | + self.tags = {} # map revid -> tags set |
187 | + |
188 | + def load(self): |
189 | + # Get a unique list of repositories. If the url is the same, |
190 | + # we consider it the same repositories |
191 | + repo_urls = set() |
192 | + for bi in self.branches: |
193 | + repo = bi.branch.repository |
194 | + if repo.base not in repo_urls: |
195 | + repo_urls.add(repo.base) |
196 | + self.repos.append(repo) |
197 | + |
198 | + no_local_repos = True |
199 | + for repo in self.repos: |
200 | + if repo_is_local(repo): |
201 | + no_local_repos = False |
202 | + if no_local_repos: |
203 | + self.load_current_dir_repo() |
204 | + self.repos.sort(key=lambda repo: not repo_is_local(repo)) |
205 | + |
206 | + self.lock_read_branches() |
207 | + try: |
208 | + head_revids, graph_parents = self.load_graph_parents() |
209 | + self.process_graph_parents(head_revids, graph_parents) |
210 | + |
211 | + self.compute_head_info() |
212 | + del self.graph |
213 | + |
214 | + if not self.no_graph: |
215 | + self.compute_branch_lines() |
216 | + self.compute_merge_info() |
217 | + |
218 | + self.load_tags() |
219 | + finally: |
220 | + self.unlock_branches() |
221 | + |
222 | + |
223 | + def load_current_dir_repo(self): |
224 | + # There are no local repositories. Try open the repository |
225 | + # of the current directory, and try load revisions data from |
226 | + # this before trying from remote repositories. This makes |
227 | + # the common use case of viewing a remote branch that is |
228 | + # related to the current branch much faster, because most |
229 | + # of the revision can be loaded from the local repository. |
230 | + try: |
231 | + bzrdir, relpath = BzrDir.open_containing(u".") |
232 | + repo = bzrdir.find_repository() |
233 | + self.repos.add(repo) |
234 | + self.local_repo_copies.append(repo) |
235 | + except Exception: |
236 | + pass |
237 | + |
238 | + def update_ui(self): |
239 | + pass |
240 | + |
241 | + def throbber_show(self): |
242 | + pass |
243 | + |
244 | + def throbber_hide(self): |
245 | + pass |
246 | + |
247 | + def lock_read_branches(self): |
248 | + for bi in self.branches: |
249 | + bi.branch.lock_read() |
250 | + for repo in self.repos: |
251 | + repo.lock_read() |
252 | + |
253 | + def unlock_branches(self): |
254 | + for bi in self.branches: |
255 | + bi.branch.unlock() |
256 | + for repo in self.repos: |
257 | + repo.unlock() |
258 | + |
259 | + #def lock_read_repos(self): |
260 | + # for repo in self.repos.itervalues(): |
261 | + # repo.lock_read() |
262 | + # |
263 | + #def unlock_repos(self): |
264 | + # for repo in self.repos.itervalues(): |
265 | + # repo.unlock() |
266 | + |
267 | + def load_tags(self): |
268 | + self.tags = {} |
269 | + for bi in self.branches: |
270 | + # revid to tags map |
271 | + branch_tags = bi.branch.tags.get_reverse_tag_dict() |
272 | + for revid, tags in branch_tags.iteritems(): |
273 | + if revid in self.tags: |
274 | + self.tags[revid].update(set(tags)) |
275 | + else: |
276 | + self.tags[revid] = set(tags) |
277 | + |
278 | + def append_head_info(self, revid, branch_info, tag): |
279 | + if not revid == NULL_REVISION: |
280 | + if not revid in self.revid_head_info: |
281 | + self.revid_head_info[revid] = ([], []) |
282 | + self.revid_head_info[revid][0].append((branch_info, tag)) |
283 | + |
284 | + # So that early calls to get_revid_branch work |
285 | + self.revid_branch_info[revid] = branch_info |
286 | + |
287 | + def load_branch_heads(self, bi): |
288 | + branch_heads = [] |
289 | + |
290 | + def append_head_info(revid, branch_info, label): |
291 | + self.append_head_info(revid, branch_info, label) |
292 | + branch_heads.append(revid) |
293 | + |
294 | + if len(self.branches) > 0: |
295 | + label = bi.label |
296 | + else: |
297 | + label = None |
298 | + |
299 | + branch_last_revision = bi.branch.last_revision() |
300 | + append_head_info(branch_last_revision, bi, bi.label) |
301 | + self.update_ui() |
302 | + |
303 | + if bi.tree: |
304 | + parent_ids = bi.tree.get_parent_ids() |
305 | + if parent_ids: |
306 | + # first parent is last revision of the tree |
307 | + revid = parent_ids[0] |
308 | + if revid != branch_last_revision: |
309 | + # working tree is out of date |
310 | + if label: |
311 | + wt_label = "%s - Working Tree" % label |
312 | + else: |
313 | + wt_label = "Working Tree" |
314 | + append_head_info(revid, bi, wt_label) |
315 | + # other parents are pending merges |
316 | + for revid in parent_ids[1:]: |
317 | + if label: |
318 | + pm_label = "%s - Pending Merge" % label |
319 | + else: |
320 | + pm_label = "Pending Merge" |
321 | + append_head_info(revid, bi, pm_label) |
322 | + self.update_ui() |
323 | + return branch_heads, branch_heads, () |
324 | + |
325 | + def load_graph_parents(self): |
326 | + """Load the heads of the graph, and the graph parents""" |
327 | + |
328 | + extra_parents = {} |
329 | + branches_heads = [] |
330 | + |
331 | + def load_branch_heads(bi, insert_at_begin=False): |
332 | + load_heads, sort_heads, extra_parents_ = self.load_branch_heads(bi) |
333 | + for key, parents in extra_parents_: |
334 | + extra_parents[key] = parents |
335 | + if insert_at_begin: |
336 | + branches_heads.insert(0, (load_heads, sort_heads)) |
337 | + else: |
338 | + branches_heads.append((load_heads, sort_heads)) |
339 | + |
340 | + for bi in self.branches: |
341 | + # Don't do the primary branch, as that will be inserted later at |
342 | + # the first position. |
343 | + if bi != self.primary_bi: |
344 | + load_branch_heads(bi) |
345 | + |
346 | + if len(branches_heads) >= 2: |
347 | + head_revids = [revid for load_heads, sort_heads in branches_heads |
348 | + for revid in load_heads] |
349 | + head_revs = self.load_revisions(head_revids) |
350 | + |
351 | + get_max_timestamp = lambda branch_heads: max( |
352 | + [head_revs[revid].timestamp for revid in branch_heads[0]]) |
353 | + branches_heads.sort(key=get_max_timestamp, reverse=True) |
354 | + |
355 | + if self.primary_bi: |
356 | + load_branch_heads(self.primary_bi, True) |
357 | + |
358 | + load_heads = [revid for load_heads_, sort_heads_ in branches_heads |
359 | + for revid in load_heads_] |
360 | + sort_heads = [revid for load_heads_, sort_heads_ in branches_heads |
361 | + for revid in sort_heads_] |
362 | + |
363 | + parents_providers = [repo._make_parents_provider() \ |
364 | + for repo in self.repos] |
365 | + parents_providers.append(DictParentsProvider(extra_parents)) |
366 | + self.graph = Graph(StackedParentsProvider(parents_providers)) |
367 | + |
368 | + return sort_heads, self.graph.iter_ancestry(sort_heads) |
369 | + |
370 | + def process_graph_parents(self, head_revids, graph_parents_iter): |
371 | + graph_parents = {} |
372 | + self.ghosts = set() |
373 | + |
374 | + for (revid, parent_revids) in graph_parents_iter: |
375 | + if revid == NULL_REVISION: |
376 | + continue |
377 | + if parent_revids is None: |
378 | + #Ghost |
379 | + graph_parents[revid] = () |
380 | + self.ghosts.add(revid) |
381 | + elif parent_revids == (NULL_REVISION,): |
382 | + graph_parents[revid] = () |
383 | + else: |
384 | + graph_parents[revid] = parent_revids |
385 | + if len(graph_parents) % 100 == 0: |
386 | + self.update_ui() |
387 | + |
388 | + graph_parents["top:"] = head_revids |
389 | + |
390 | + if len(graph_parents) > 0: |
391 | + enabled = gc.isenabled() |
392 | + if enabled: |
393 | + gc.disable() |
394 | + |
395 | + def make_kg(): |
396 | + return KnownGraph(graph_parents) |
397 | + self.known_graph = make_kg() |
398 | + |
399 | + merge_sorted_revisions = self.known_graph.merge_sort('top:') |
400 | + # So far, we are a bit faster than the pure-python code. But the |
401 | + # last step hurts. Specifically, we take |
402 | + # 377ms KnownGraph(self.graph_parents) |
403 | + # 263ms kg.merge_sort() [640ms combined] |
404 | + # 1322ms self.revisions = [...] |
405 | + # vs |
406 | + # 1152ms tsort.merge_sort(self.graph_parents) |
407 | + # 691ms self.revisions = [...] |
408 | + # |
409 | + # It is a gc thing... :( |
410 | + # Adding gc.disable() / gc.enable() around this whole loop changes |
411 | + # things to be: |
412 | + # 100ms KnownGraph(self.graph_parents) |
413 | + # 77ms kg.merge_sort() [177ms combined] |
414 | + # 174ms self.revisions = [...] |
415 | + # vs |
416 | + # 639ms tsort.merge_sort(self.graph_parents) |
417 | + # 150ms self.revisions = [...] |
418 | + # Also known as "wow that's a lot faster". This is because KG() |
419 | + # creates a bunch of Python objects, then merge_sort() creates a |
420 | + # bunch more. And then self.revisions() creates another whole set. |
421 | + # And all of these are moderately long lived, so you have a *lot* |
422 | + # of allocations without removals (which triggers the gc checker |
423 | + # over and over again.) And they probably don't live in cycles |
424 | + # anyway, so you can skip it for now, and just run at the end. |
425 | + |
426 | + # self.revisions *is* a little bit slower. Probably because pyrex |
427 | + # MergeSortNodes use long integers rather than PyIntObject and thus |
428 | + # create them on-the-fly. |
429 | + |
430 | + # Get rid of the 'top:' revision |
431 | + merge_sorted_revisions.pop(0) |
432 | + self.revisions = [RevisionData(index, node) |
433 | + for index, node in enumerate(merge_sorted_revisions)] |
434 | + if enabled: |
435 | + gc.enable() |
436 | + else: |
437 | + self.revisions = () |
438 | + |
439 | + self.revid_rev = {} |
440 | + self.revno_rev = {} |
441 | + |
442 | + self.max_mainline_revno = 0 |
443 | + |
444 | + for rev in self.revisions: |
445 | + self.max_mainline_revno = max(self.max_mainline_revno, |
446 | + rev.revno_sequence[0]) |
447 | + self.revid_rev[rev.revid] = rev |
448 | + self.revno_rev[rev.revno_sequence] = rev |
449 | + |
450 | + def branch_id_sort_key(self, x): |
451 | + merge_depth = self.branch_lines[x].merge_depth |
452 | + |
453 | + # Note: This greatly affects the layout of the graph. |
454 | + # |
455 | + # Branch line that have a smaller merge depth should be to the left |
456 | + # of those with bigger merge depths. |
457 | + # |
458 | + # For branch lines that have the same parent in the mainline - |
459 | + # those with bigger branch numbers to be to the rights. E.g. for |
460 | + # the following dag, you want the graph to appear as on the left, |
461 | + # not as on the right: |
462 | + # |
463 | + # 3 F_ F |
464 | + # | \ |\ |
465 | + # 1.2.1 | E | E |
466 | + # | | | \ |
467 | + # 2 D | D_| |
468 | + # |\ | | +_ |
469 | + # 1.1.2 | C| | | C |
470 | + # | |/ | \| |
471 | + # 1.1.1 | B | B |
472 | + # |/ | / |
473 | + # 1 A A |
474 | + # |
475 | + # Otherwise, those with a greater mainline parent revno should |
476 | + # appear to the left. |
477 | + |
478 | + if len(x) == 0: |
479 | + return (merge_depth) |
480 | + else: |
481 | + return (merge_depth, -x[0], x[1]) |
482 | + |
483 | + def compute_branch_lines(self): |
484 | + self.branch_lines = {} |
485 | + |
486 | + """A list of branch lines (aka merge lines). |
487 | + |
488 | + For a revisions, the revision number less the least significant |
489 | + digit is the branch_id, and used as the key for the dict. Hence |
490 | + revision with the same revision number less the least significant |
491 | + digit are considered to be in the same branch line. e.g.: for |
492 | + revisions 290.12.1 and 290.12.2, the branch_id would be 290.12, |
493 | + and these two revisions will be in the same branch line. |
494 | + |
495 | + """ |
496 | + |
497 | + self.branch_ids = [] |
498 | + """List of branch ids, sorted in the order that the branches will |
499 | + be shown, from left to right on the graph.""" |
500 | + |
501 | + for rev in self.revisions: |
502 | + |
503 | + branch_line = None |
504 | + if rev.branch_id not in self.branch_lines: |
505 | + branch_line = BranchLine(rev.branch_id) |
506 | + self.branch_lines[rev.branch_id] = branch_line |
507 | + else: |
508 | + branch_line = self.branch_lines[rev.branch_id] |
509 | + |
510 | + branch_line.revs.append(rev) |
511 | + branch_line.merge_depth = max(rev.merge_depth, |
512 | + branch_line.merge_depth) |
513 | + rev.branch = branch_line |
514 | + |
515 | + self.branch_ids = self.branch_lines.keys() |
516 | + |
517 | + self.branch_ids.sort(key=self.branch_id_sort_key) |
518 | + |
519 | + def compute_merge_info(self): |
520 | + |
521 | + def set_merged_by(rev, merged_by, merged_by_rev, do_branches=False): |
522 | + if merged_by is None: |
523 | + return |
524 | + |
525 | + if merged_by_rev is None: |
526 | + merged_by_rev = self.revisions[merged_by] |
527 | + rev.merged_by = merged_by |
528 | + merged_by_rev.merges.append(rev.index) |
529 | + |
530 | + if do_branches: |
531 | + branch_id = rev.branch_id |
532 | + branch_merged_by = self.branch_lines[branch_id].merged_by |
533 | + merged_by_branch_id = self.revisions[merged_by].branch_id |
534 | + merged_by_branch_merges = \ |
535 | + self.branch_lines[merged_by_branch_id].merges |
536 | + |
537 | + if not branch_id in merged_by_branch_merges: |
538 | + merged_by_branch_merges.append(branch_id) |
539 | + if not merged_by_branch_id in branch_merged_by: |
540 | + branch_merged_by.append(merged_by_branch_id) |
541 | + |
542 | + for rev in self.revisions: |
543 | + |
544 | + parents = [self.revid_rev[parent] for parent in |
545 | + self.known_graph.get_parent_keys(rev.revid)] |
546 | + |
547 | + if len(parents) > 0: |
548 | + if rev.branch_id == parents[0].branch_id: |
549 | + set_merged_by(parents[0], rev.merged_by, None) |
550 | + |
551 | + for parent in parents[1:]: |
552 | + if rev.merge_depth <= parent.merge_depth: |
553 | + set_merged_by(parent, rev.index, rev, do_branches=True) |
554 | + |
555 | + def compute_head_info(self): |
556 | + def get_revid_head(heads): |
557 | + map = {} |
558 | + for i in xrange(len(heads)): |
559 | + prev_revids = [revid for revid, head in heads[:i]] |
560 | + unique_ancestors = self.graph.find_unique_ancestors( |
561 | + heads[i][0], prev_revids) |
562 | + for ancestor_revid in unique_ancestors: |
563 | + map[ancestor_revid] = heads[i][1] |
564 | + return map |
565 | + |
566 | + if len(self.branches) > 1: |
567 | + head_revid_branch_info = sorted( |
568 | + [(revid, branch_info) |
569 | + for revid, (head_info, ur) in self.revid_head_info.iteritems() |
570 | + for (branch_info, tag) in head_info], |
571 | + key=lambda x: not repo_is_local(x[1].branch.repository)) |
572 | + self.revid_branch_info = get_revid_head(head_revid_branch_info) |
573 | + else: |
574 | + self.revid_branch_info = {} |
575 | + |
576 | + head_count = 0 |
577 | + for head_info, ur in self.revid_head_info.itervalues(): |
578 | + head_count += len(head_info) |
579 | + |
580 | + if head_count > 1: |
581 | + # Populate unique revisions for heads |
582 | + for revid, (head_info, ur) in self.revid_head_info.iteritems(): |
583 | + rev = None |
584 | + if revid in self.revid_rev: |
585 | + rev = self.revid_rev[revid] |
586 | + if rev and rev.merged_by: |
587 | + # This head has been merged. |
588 | + # d |
589 | + # |\ |
590 | + # b c |
591 | + # |/ |
592 | + # a |
593 | + # if revid == c,then we want other_revids = [b] |
594 | + |
595 | + merged_by_revid = self.revisions[rev.merged_by].revid |
596 | + other_revids = [self.known_graph |
597 | + .get_parent_keys(merged_by_revid)[0]] |
598 | + else: |
599 | + other_revids = [other_revid for other_revid \ |
600 | + in self.revid_head_info.iterkeys() \ |
601 | + if not other_revid == revid] |
602 | + ur.append(revid) |
603 | + ur.extend([revid for revid \ |
604 | + in self.graph.find_unique_ancestors(revid, other_revids) \ |
605 | + if not revid == NULL_REVISION and revid in self.revid_rev]) |
606 | + ur.sort(key=lambda x: self.revid_rev[x].index) |
607 | + |
608 | + def compute_viz(self, state): |
609 | + |
610 | + # Overview: |
611 | + # Work out which revision need to be displayed. |
612 | + # Create ComputedGraphViz and ComputedRevisionData objects |
613 | + # Assign columns for branches, and lines that go between branches. |
614 | + # These are intermingled, because some of the lines need to come |
615 | + # before it's branch, and others need to come after. Other lines |
616 | + # (such a the line from the last rev in a branch) are treated a |
617 | + # special cases. |
618 | + # Return ComputedGraphViz object |
619 | + gc_enabled = gc.isenabled() |
620 | + gc.disable() |
621 | + try: |
622 | + computed = ComputedGraphViz(self) |
623 | + computed.filtered_revs = [ComputedRevisionData(rev) for rev in |
624 | + state.get_filtered_revisions()] |
625 | + |
626 | + c_revisions = computed.revisions |
627 | + for f_index, c_rev in enumerate(computed.filtered_revs): |
628 | + c_revisions[c_rev.rev.index] = c_rev |
629 | + c_rev.f_index = f_index |
630 | + |
631 | + for (revid, (head_info, |
632 | + unique_revids)) in self.revid_head_info.iteritems(): |
633 | + |
634 | + for unique_revid in unique_revids: |
635 | + rev = self.revid_rev[unique_revid] |
636 | + c_rev = c_revisions[rev.index] |
637 | + if c_rev is not None: |
638 | + c_rev.branch_labels.extend(head_info) |
639 | + break |
640 | + finally: |
641 | + if gc_enabled: |
642 | + gc.enable() |
643 | + |
644 | + if self.no_graph: |
645 | + for c_rev in computed.filtered_revs: |
646 | + c_rev.col_index = c_rev.rev.merge_depth * 0.5 |
647 | + return computed |
648 | + |
649 | + # This will hold a tuple of (child, parent, col_index, direct) for each |
650 | + # line that needs to be drawn. If col_index is not none, then the line |
651 | + # is drawn along that column, else the the line can be drawn directly |
652 | + # between the child and parent because either the child and parent are |
653 | + # in the same branch line, or the child and parent are 1 row apart. |
654 | + lines = [] |
655 | + lines_by_column = [] |
656 | + |
657 | + def branch_line_col_search_order(start_col_index): |
658 | + for col_index in range(start_col_index, len(lines_by_column)): |
659 | + yield col_index |
660 | + #for col_index in range(parent_col_index-1, -1, -1): |
661 | + # yield col_index |
662 | + |
663 | + def line_col_search_order(parent_col_index, child_col_index): |
664 | + if parent_col_index is not None and child_col_index is not None: |
665 | + max_index = max(parent_col_index, child_col_index) |
666 | + min_index = min(parent_col_index, child_col_index) |
667 | + # First yield the columns between the child and parent. |
668 | + for col_index in range(max_index, min_index - 1, -1): |
669 | + yield col_index |
670 | + elif child_col_index is not None: |
671 | + max_index = child_col_index |
672 | + min_index = child_col_index |
673 | + yield child_col_index |
674 | + elif parent_col_index is not None: |
675 | + max_index = parent_col_index |
676 | + min_index = parent_col_index |
677 | + yield parent_col_index |
678 | + else: |
679 | + max_index = 0 |
680 | + min_index = 0 |
681 | + yield 0 |
682 | + i = 1 |
683 | + # then yield the columns on either side. |
684 | + while max_index + i < len(lines_by_column) or \ |
685 | + min_index - i > -1: |
686 | + if max_index + i < len(lines_by_column): |
687 | + yield max_index + i |
688 | + #if min_index - i > -1: |
689 | + # yield min_index - i |
690 | + i += 1 |
691 | + |
692 | + def is_col_free_for_range(col_index, child_f_index, parent_f_index): |
693 | + return not any( |
694 | + range_overlaps(child_f_index, parent_f_index, |
695 | + line_child_f_index, line_parent_f_index) |
696 | + for line_child_f_index, line_parent_f_index |
697 | + in lines_by_column[col_index]) |
698 | + |
699 | + def find_free_column(col_search_order, child_f_index, parent_f_index): |
700 | + for col_index in col_search_order: |
701 | + if is_col_free_for_range(col_index, |
702 | + child_f_index, parent_f_index): |
703 | + break |
704 | + else: |
705 | + # No free columns found. Add an empty one on the end. |
706 | + col_index = len(lines_by_column) |
707 | + lines_by_column.append([]) |
708 | + return col_index |
709 | + |
710 | + def append_line(child, parent, direct, col_index=None): |
711 | + lines.append((child, parent, col_index, direct)) |
712 | + |
713 | + if col_index is not None: |
714 | + lines_by_column[int(round(col_index))].append( |
715 | + (child.f_index, parent.f_index)) |
716 | + |
717 | + def find_visible_parent(c_rev, parent, twisty_hidden_parents): |
718 | + if c_revisions[parent.index] is not None: |
719 | + return (c_rev, c_revisions[parent.index], True) |
720 | + else: |
721 | + if parent.index in twisty_hidden_parents: |
722 | + # no need to draw a line if there is a twisty, |
723 | + # except if this is the last in the branch. |
724 | + return None |
725 | + # The parent was not visible. Search for a ancestor |
726 | + # that is. Stop searching if we make a hop, i.e. we |
727 | + # go away from our branch, and we come back to it. |
728 | + has_seen_different_branch = False |
729 | + while c_revisions[parent.index] is None: |
730 | + if not parent.branch_id == c_rev.rev.branch_id: |
731 | + has_seen_different_branch = True |
732 | + # find grand parent. |
733 | + g_parent_ids = (self.known_graph |
734 | + .get_parent_keys(parent.revid)) |
735 | + |
736 | + if len(g_parent_ids) == 0: |
737 | + return None |
738 | + else: |
739 | + parent = self.revid_rev[g_parent_ids[0]] |
740 | + |
741 | + if (has_seen_different_branch and |
742 | + parent.branch_id == branch_id): |
743 | + # We have gone away and come back to our |
744 | + # branch. Stop. |
745 | + return None |
746 | + if parent: |
747 | + # Not Direct |
748 | + return (c_rev, c_revisions[parent.index], False) |
749 | + |
750 | + def append_branch_parent_lines(branch_rev_visible_parents): |
751 | + groups = group_overlapping(branch_rev_visible_parents) |
752 | + for parents, start, end, group_key in groups: |
753 | + # Since all parents go from the same branch line to the |
754 | + # same branch line, we can use the col indexes of the |
755 | + # parent. |
756 | + if end - start == 1: |
757 | + col_index = None |
758 | + else: |
759 | + col_search_order = line_col_search_order( |
760 | + parents[0][1].col_index, parents[0][0].col_index) |
761 | + col_index = find_free_column(col_search_order, |
762 | + start, end) |
763 | + |
764 | + col_offset_increment = 1.0 / len(parents) |
765 | + for i, (c_rev, parent_c_rev, direct) in enumerate(parents): |
766 | + if col_index is None: |
767 | + col_index_offset = None |
768 | + else: |
769 | + col_index_offset = (col_index - 0.5 + |
770 | + (i * col_offset_increment) + |
771 | + (col_offset_increment / 2)) |
772 | + append_line(c_rev, parent_c_rev, |
773 | + direct, col_index_offset) |
774 | + |
775 | + for branch_id in self.branch_ids: |
776 | + if not branch_id in state.branch_line_state: |
777 | + continue |
778 | + |
779 | + branch_line = self.branch_lines[branch_id] |
780 | + branch_revs = [c_revisions[rev.index] |
781 | + for rev in branch_line.revs |
782 | + if c_revisions[rev.index] is not None] |
783 | + |
784 | + if not branch_revs: |
785 | + continue |
786 | + |
787 | + branch_rev_visible_parents_post = [] |
788 | + branch_rev_visible_parents_pre = [] |
789 | + # Lists of ([(c_rev, parent_c_rev, is_direct)], |
790 | + # start, end, group_key] |
791 | + |
792 | + last_c_rev = branch_revs[-1] |
793 | + last_rev_left_parents = (self.known_graph |
794 | + .get_parent_keys(last_c_rev.rev.revid)) |
795 | + if last_rev_left_parents: |
796 | + last_parent = find_visible_parent( |
797 | + last_c_rev, self.revid_rev[last_rev_left_parents[0]], []) |
798 | + else: |
799 | + last_parent = None |
800 | + |
801 | + sprout_with_lines = {} |
802 | + |
803 | + merged_by_max_col_index = 0 |
804 | + |
805 | + # In this loop: |
806 | + # * Populate twisty_branch_ids and twisty_state |
807 | + # * Find visible parents. |
808 | + # * Append lines that go before the branch line. |
809 | + # * Append lines to children for sprouts. |
810 | + for c_rev in branch_revs: |
811 | + rev = c_rev.rev |
812 | + |
813 | + if rev.merged_by is not None: |
814 | + merged_by_c_rev = c_revisions[rev.merged_by] |
815 | + if merged_by_c_rev: |
816 | + merged_by_max_col_index = max( |
817 | + merged_by_max_col_index, merged_by_c_rev.col_index) |
818 | + |
819 | + parents = [self.revid_rev[parent_revid] for parent_revid in |
820 | + self.known_graph.get_parent_keys(rev.revid)] |
821 | + |
822 | + twisty_hidden_parents = [] |
823 | + # Find and add necessary twisties |
824 | + for parent in parents: |
825 | + if parent.branch_id == branch_id: |
826 | + continue |
827 | + if parent.branch_id == (): |
828 | + continue |
829 | + if parent.branch_id in branch_line.merged_by: |
830 | + continue |
831 | + parent_branch = self.branch_lines[parent.branch_id] |
832 | + # Does this branch have any visible revisions |
833 | + pb_visible = (parent_branch.branch_id in |
834 | + state.branch_line_state) |
835 | + for pb_rev in parent_branch.revs: |
836 | + if pb_visible: |
837 | + visible = c_revisions[pb_rev.index] is not None |
838 | + else: |
839 | + visible = state\ |
840 | + .get_revision_visible_if_branch_visible(pb_rev) |
841 | + if visible: |
842 | + (c_rev.twisty_expands_branch_ids |
843 | + .append(parent_branch.branch_id)) |
844 | + if not pb_visible: |
845 | + twisty_hidden_parents.append(parent.index) |
846 | + break |
847 | + |
848 | + # Work out if the twisty needs to show a + or -. If all |
849 | + # twisty_branch_ids are visible, show - else +. |
850 | + if len(c_rev.twisty_expands_branch_ids) > 0: |
851 | + c_rev.twisty_state = True |
852 | + for twisty_branch_id in c_rev.twisty_expands_branch_ids: |
853 | + if not twisty_branch_id in state.branch_line_state: |
854 | + c_rev.twisty_state = False |
855 | + break |
856 | + |
857 | + # Don't include left hand parents All of these parents in the |
858 | + # branch can be drawn with one line. |
859 | + parents = parents[1:] |
860 | + |
861 | + branch_id_sort_key = self.branch_id_sort_key(branch_id) |
862 | + for i, parent in enumerate(parents): |
863 | + parent_info = find_visible_parent(c_rev, parent, |
864 | + twisty_hidden_parents) |
865 | + if parent_info: |
866 | + c_rev, parent_c_rev, direct = parent_info |
867 | + if (last_parent and |
868 | + parent_c_rev.f_index <= last_parent[1].f_index and |
869 | + self.branch_id_sort_key(parent_c_rev.rev.branch_id) |
870 | + < branch_id_sort_key): |
871 | + # This line goes before the branch line |
872 | + dest = branch_rev_visible_parents_pre |
873 | + else: |
874 | + # This line goes after |
875 | + dest = branch_rev_visible_parents_post |
876 | + |
877 | + line_len = parent_c_rev.f_index - c_rev.f_index |
878 | + if line_len == 1: |
879 | + group_key = None |
880 | + else: |
881 | + group_key = parent_c_rev.rev.branch_id |
882 | + |
883 | + dest.append(([parent_info], |
884 | + c_rev.f_index, |
885 | + parent_c_rev.f_index, |
886 | + group_key)) |
887 | + |
888 | + # This may be a sprout. Add line to first visible child |
889 | + if c_rev.rev.merged_by is not None: |
890 | + merged_by = self.revisions[c_rev.rev.merged_by] |
891 | + if c_revisions[merged_by.index] is None and\ |
892 | + branch_revs[0].f_index == c_rev.f_index: |
893 | + # The revision that merges this revision is not |
894 | + # visible, and it is the first visible revision in |
895 | + # the branch line. This is a sprout. |
896 | + # |
897 | + # XXX What if multiple merges with --force, |
898 | + # aka octopus merge? |
899 | + # |
900 | + # Search until we find a descendant that is visible. |
901 | + |
902 | + while merged_by is not None and \ |
903 | + c_revisions[merged_by.index] is None: |
904 | + if merged_by.merged_by is not None: |
905 | + merged_by = self.revisions[merged_by.merged_by] |
906 | + else: |
907 | + merged_by = None |
908 | + |
909 | + if merged_by is not None: |
910 | + # Ensure only one line to a descendant. |
911 | + if (merged_by.index not in sprout_with_lines): |
912 | + sprout_with_lines[merged_by.index] = True |
913 | + parent = c_revisions[merged_by.index] |
914 | + if parent is not None: |
915 | + if c_rev.f_index - parent.f_index == 1: |
916 | + col_index = None |
917 | + else: |
918 | + col_search_order = line_col_search_order( |
919 | + parent.col_index, c_rev.col_index) |
920 | + col_index = find_free_column( |
921 | + col_search_order, |
922 | + parent.f_index, c_rev.f_index) |
923 | + append_line(parent, c_rev, False, |
924 | + col_index) |
925 | + |
926 | + # Find a column for this branch. |
927 | + # |
928 | + # Find the col_index for the direct parent branch. This will |
929 | + # be the starting point when looking for a free column. |
930 | + |
931 | + append_branch_parent_lines(branch_rev_visible_parents_pre) |
932 | + |
933 | + if branch_id == (): |
934 | + start_col_index = 0 |
935 | + else: |
936 | + start_col_index = 1 |
937 | + |
938 | + if last_parent and last_parent[0].col_index is not None: |
939 | + parent_col_index = last_parent[1].col_index |
940 | + start_col_index = max(start_col_index, parent_col_index) |
941 | + |
942 | + start_col_index = max(start_col_index, merged_by_max_col_index) |
943 | + |
944 | + col_search_order = branch_line_col_search_order(start_col_index) |
945 | + |
946 | + if last_parent: |
947 | + col_index = find_free_column(col_search_order, |
948 | + branch_revs[0].f_index, |
949 | + last_parent[1].f_index) |
950 | + else: |
951 | + col_index = find_free_column(col_search_order, |
952 | + branch_revs[0].f_index, |
953 | + branch_revs[-1].f_index) |
954 | + |
955 | + # Free column for this branch found. Set node for all |
956 | + # revision in this branch. |
957 | + for rev in branch_revs: |
958 | + rev.col_index = col_index |
959 | + |
960 | + append_line(branch_revs[0], branch_revs[-1], True, col_index) |
961 | + if last_parent: |
962 | + append_line(last_parent[0], last_parent[1], |
963 | + last_parent[2], col_index) |
964 | + |
965 | + branch_rev_visible_parents_post.reverse() |
966 | + append_branch_parent_lines(branch_rev_visible_parents_post) |
967 | + |
968 | + # It has now been calculated which column a line must go into. Now |
969 | + # copy the lines in to computed_revisions. |
970 | + for (child, parent, line_col_index, direct) in lines: |
971 | + |
972 | + parent_color = parent.rev.color |
973 | + |
974 | + line_length = parent.f_index - child.f_index |
975 | + if line_length == 0: |
976 | + # Nothing to do |
977 | + pass |
978 | + elif line_length == 1: |
979 | + child.lines.append( |
980 | + (child.col_index, |
981 | + parent.col_index, |
982 | + parent_color, |
983 | + direct)) |
984 | + else: |
985 | + # line from the child's column to the lines column |
986 | + child.lines.append( |
987 | + (child.col_index, |
988 | + line_col_index, |
989 | + parent_color, |
990 | + direct)) |
991 | + # lines down the line's column |
992 | + for line_part_f_index in range(child.f_index + 1, |
993 | + parent.f_index - 1): |
994 | + computed.filtered_revs[line_part_f_index].lines.append( |
995 | + (line_col_index, |
996 | + line_col_index, |
997 | + parent_color, |
998 | + direct)) |
999 | + # line from the line's column to the parent's column |
1000 | + computed.filtered_revs[parent.f_index - 1].lines.append( |
1001 | + (line_col_index, |
1002 | + parent.col_index, |
1003 | + parent_color, |
1004 | + direct)) |
1005 | + |
1006 | + return computed |
1007 | + |
1008 | + def get_revid_branch_info(self, revid): |
1009 | + """This returns a branch info whos branch contains the revision. |
1010 | + |
1011 | + If the revision exists more than one branch, it will only return the |
1012 | + first branch info. """ |
1013 | + |
1014 | + if revid in self.ghosts: |
1015 | + raise GhostRevisionError(revid) |
1016 | + |
1017 | + if len(self.branches) == 1 or revid not in self.revid_branch_info: |
1018 | + return self.branches[0] |
1019 | + return self.revid_branch_info[revid] |
1020 | + |
1021 | + def get_revid_branch(self, revid): |
1022 | + return self.get_revid_branch_info(revid).branch |
1023 | + |
1024 | + def get_revid_repo(self, revid): |
1025 | + return self.get_revid_branch_info(revid).branch.repository |
1026 | + |
1027 | + def get_repo_revids(self, revids): |
1028 | + """Returns list of tuple of (repo, revids)""" |
1029 | + repo_revids = {} |
1030 | + for repo in self.repos: |
1031 | + repo_revids[repo.base] = [] |
1032 | + |
1033 | + for local_repo_copy in self.local_repo_copies: |
1034 | + for revid in self.repos[local_repo_copy].has_revisions(revids): |
1035 | + revids.remove(revid) |
1036 | + repo_revids[local_repo_copy].append(revid) |
1037 | + |
1038 | + for revid in revids: |
1039 | + try: |
1040 | + repo = self.get_revid_repo(revid) |
1041 | + except GhostRevisionError: |
1042 | + pass |
1043 | + else: |
1044 | + repo_revids[repo.base].append(revid) |
1045 | + |
1046 | + return [(repo, repo_revids[repo.base]) |
1047 | + for repo in self.repos] |
1048 | + |
1049 | + def load_revisions(self, revids): |
1050 | + return_revisions = {} |
1051 | + for repo, revids in self.get_repo_revids(revids): |
1052 | + if revids: |
1053 | + repo.lock_read() |
1054 | + try: |
1055 | + self.update_ui() |
1056 | + for rev in repo.get_revisions(revids): |
1057 | + return_revisions[rev.revision_id] = rev |
1058 | + finally: |
1059 | + repo.unlock() |
1060 | + return return_revisions |
1061 | + |
1062 | + |
1063 | +def repo_is_local(repo): |
1064 | + return isinstance(repo.bzrdir.transport, LocalTransport) |
1065 | + |
1066 | +def group_overlapping(groups): |
1067 | + """ Groups items with overlapping ranges together. |
1068 | + |
1069 | + :param groups: List of uncollapsed groups. |
1070 | + :param group: (start of range, end of range, items in group) |
1071 | + :return: List of collapsed groups. |
1072 | + |
1073 | + """ |
1074 | + |
1075 | + has_change = True |
1076 | + while has_change: |
1077 | + has_change = False |
1078 | + a = 0 |
1079 | + while a < len(groups): |
1080 | + inner_has_change = False |
1081 | + items_a, start_a, end_a, group_key_a = groups[a] |
1082 | + if group_key_a is not None: |
1083 | + b = a + 1 |
1084 | + while b < len(groups): |
1085 | + items_b, start_b, end_b, group_key_b = groups[b] |
1086 | + if (group_key_a == group_key_b and |
1087 | + range_overlaps(start_a, end_a, start_b, end_b)): |
1088 | + # overlaps. Merge b into a |
1089 | + items_a.extend(items_b) |
1090 | + start_a = min(start_a, start_b) |
1091 | + end_a = max(end_a, end_b) |
1092 | + del groups[b] |
1093 | + has_change = True |
1094 | + inner_has_change = True |
1095 | + else: |
1096 | + b += 1 |
1097 | + if inner_has_change: |
1098 | + groups[a] = (items_a, start_a, end_a, group_key_a) |
1099 | + a += 1 |
1100 | + |
1101 | + return groups |
1102 | + |
1103 | +def range_overlaps (start_a, end_a, start_b, end_b): |
1104 | + """Tests if two ranges overlap.""" |
1105 | + return (start_b < start_a < end_b or |
1106 | + start_b < end_a < end_b or |
1107 | + (start_a <= start_b and end_a >= end_b)) |
1108 | + |
1109 | + |
1110 | +class PendingMergesGraphVizLoader(GraphVizLoader): |
1111 | + """GraphVizLoader that only loads pending merges. |
1112 | + |
1113 | + As only the pending merges are passed to merge_sort, the revno |
1114 | + are incorrect, and should be ignored. |
1115 | + |
1116 | + Only works on a single branch. |
1117 | + |
1118 | + """ |
1119 | + |
1120 | + def load_graph_parents(self): |
1121 | + if not len(self.branches) == 1 or not len(self.repos) == 1: |
1122 | + AssertionError("load_graph_pending_merges should only be called \ |
1123 | + when 1 branch and repo has been opened.") |
1124 | + |
1125 | + bi = self.branches[0] |
1126 | + if bi.tree is None: |
1127 | + AssertionError("PendingMergesGraphVizLoader must have a working " |
1128 | + "tree.") |
1129 | + |
1130 | + self.graph = bi.branch.repository.get_graph() |
1131 | + tree_heads = bi.tree.get_parent_ids() |
1132 | + other_revisions = [tree_heads[0], ] |
1133 | + self.update_ui() |
1134 | + |
1135 | + self.append_head_info('root:', bi, None) |
1136 | + pending_merges = [] |
1137 | + for head in tree_heads[1:]: |
1138 | + self.append_head_info(head, bi, None) |
1139 | + pending_merges.extend( |
1140 | + self.graph.find_unique_ancestors(head, other_revisions)) |
1141 | + other_revisions.append(head) |
1142 | + self.update_ui() |
1143 | + |
1144 | + graph_parents = self.graph.get_parent_map(pending_merges) |
1145 | + graph_parents["root:"] = () |
1146 | + self.update_ui() |
1147 | + |
1148 | + for (revid, parents) in graph_parents.items(): |
1149 | + new_parents = [] |
1150 | + for index, parent in enumerate(parents): |
1151 | + if parent in graph_parents: |
1152 | + new_parents.append(parent) |
1153 | + elif index == 0: |
1154 | + new_parents.append("root:") |
1155 | + graph_parents[revid] = tuple(new_parents) |
1156 | + |
1157 | + return ["root:", ] + tree_heads[1:], graph_parents.items() |
1158 | + |
1159 | + |
1160 | +class WithWorkingTreeGraphVizLoader(GraphVizLoader): |
1161 | + """ |
1162 | + GraphVizLoader that shows uncommitted working tree changes as a node |
1163 | + in the graph, as if it was already committed. |
1164 | + """ |
1165 | + |
1166 | + def tree_revid(self, tree): |
1167 | + return CURRENT_REVISION + tree.basedir.encode('unicode-escape') |
1168 | + |
1169 | + def load(self): |
1170 | + self.working_trees = {} |
1171 | + for bi in self.branches: |
1172 | + if not bi.tree is None: |
1173 | + self.working_trees[self.tree_revid(bi.tree)] = bi.tree |
1174 | + |
1175 | + super(WithWorkingTreeGraphVizLoader, self).load() |
1176 | + |
1177 | + def load_branch_heads(self, bi): |
1178 | + # returns load_heads, sort_heads and also calls append_head_info. |
1179 | + # |
1180 | + # == For branch with tree == |
1181 | + # Graph | load_heads | sort_heads | append_head_info |
1182 | + # wt | No | Yes | Yes |
1183 | + # | \ | | | |
1184 | + # | 1.1.2 pending merge | Yes | No | Yes |
1185 | + # 2 | basis rev | Yes | No | Yes |
1186 | + # |
1187 | + # == For branch with tree not up to date == |
1188 | + # Graph | load_heads | sort_heads | append_head_info |
1189 | + # wt | No | Yes | Yes |
1190 | + # | \ | | | |
1191 | + # | 1.1.2 pending merge | Yes | No | Yes |
1192 | + # 3/ | branch tip | Yes | Yes | Yes |
1193 | + # 2 | basis rev | Yes | No | No |
1194 | + # |
1195 | + # == For branch without tree == |
1196 | + # branch tip | Yes | head | yes |
1197 | + |
1198 | + load_heads = [] |
1199 | + sort_heads = [] |
1200 | + extra_parents = [] |
1201 | + |
1202 | + if len(self.branches) > 0: |
1203 | + label = bi.label |
1204 | + else: |
1205 | + label = None |
1206 | + |
1207 | + branch_last_revision = bi.branch.last_revision() |
1208 | + self.append_head_info(branch_last_revision, bi, bi.label) |
1209 | + load_heads.append(branch_last_revision) |
1210 | + self.update_ui() |
1211 | + |
1212 | + if bi.tree: |
1213 | + wt_revid = self.tree_revid(bi.tree) |
1214 | + if label: |
1215 | + wt_label = "%s - Working Tree" % label |
1216 | + else: |
1217 | + wt_label = "Working Tree" |
1218 | + self.append_head_info(wt_revid, bi, wt_label) |
1219 | + parent_ids = bi.tree.get_parent_ids() |
1220 | + |
1221 | + extra_parents.append((wt_revid, parent_ids)) |
1222 | + load_heads.extend(parent_ids) |
1223 | + |
1224 | + if parent_ids: |
1225 | + # first parent is last revision of the tree |
1226 | + if parent_ids[0] != branch_last_revision: |
1227 | + # tree is not up to date. |
1228 | + sort_heads.append(branch_last_revision) |
1229 | + |
1230 | + # other parents are pending merges |
1231 | + for revid in parent_ids[1:]: |
1232 | + if label: |
1233 | + pm_label = "%s - Pending Merge" % label |
1234 | + else: |
1235 | + pm_label = "Pending Merge" |
1236 | + self.append_head_info(revid, bi, pm_label) |
1237 | + |
1238 | + sort_heads.append(wt_revid) |
1239 | + self.update_ui() |
1240 | + else: |
1241 | + sort_heads.append(branch_last_revision) |
1242 | + |
1243 | + return load_heads, sort_heads, extra_parents |
1244 | + |
1245 | + |
1246 | +class GraphVizFilterState(object): |
1247 | + """ |
1248 | + Records the state of which branch lines are expanded, and what filters |
1249 | + are applied. |
1250 | + """ |
1251 | + |
1252 | + def __init__(self, graph_viz, filter_changed_callback=None): |
1253 | + self.graph_viz = graph_viz |
1254 | + self.filter_changed_callback = filter_changed_callback |
1255 | + |
1256 | + self.branch_line_state = {} |
1257 | + "If a branch_id is in this dict, it is visible. The value of the dict " |
1258 | + "indicates which branches expanded this branch." |
1259 | + |
1260 | + for revid in self.graph_viz.revid_head_info: |
1261 | + rev = self.graph_viz.revid_rev[revid] |
1262 | + self.branch_line_state[rev.branch_id] = None |
1263 | + |
1264 | + self.filters = [] |
1265 | + |
1266 | + # This keeps a cache of the filter state so that when one of the |
1267 | + # filters notifies us of a change, we can check if anything did change. |
1268 | + |
1269 | + self.filter_cache = [None for rev in self.graph_viz.revisions] |
1270 | + |
1271 | + def get_filtered_revisions(self): |
1272 | + if self.graph_viz.no_graph: |
1273 | + rev_whos_branch_is_visible = self.graph_viz.revisions |
1274 | + else: |
1275 | + rev_whos_branch_is_visible = [] |
1276 | + for branch_id in self.branch_line_state.iterkeys(): |
1277 | + try: |
1278 | + branch_line = self.graph_viz.branch_lines[branch_id] |
1279 | + except KeyError: |
1280 | + continue |
1281 | + rev_whos_branch_is_visible.extend(branch_line.revs) |
1282 | + rev_whos_branch_is_visible.sort(key=lambda rev: rev.index) |
1283 | + |
1284 | + visible = self.get_revision_visible_if_branch_visible |
1285 | + return (rev for rev in rev_whos_branch_is_visible if visible(rev)) |
1286 | + |
1287 | + def get_revision_visible_if_branch_visible(self, rev): |
1288 | + rev_filter_cache = self.filter_cache[rev.index] |
1289 | + if rev_filter_cache is None: |
1290 | + rev_filter_cache = \ |
1291 | + self._get_revision_visible_if_branch_visible(rev) |
1292 | + self.filter_cache[rev.index] = rev_filter_cache |
1293 | + return rev_filter_cache |
1294 | + |
1295 | + def _get_revision_visible_if_branch_visible(self, rev): |
1296 | + filters_value = True |
1297 | + for filter in self.filters: |
1298 | + if not filter.get_revision_visible(rev): |
1299 | + filters_value = False |
1300 | + break |
1301 | + if filters_value: |
1302 | + return True |
1303 | + |
1304 | + if not self.graph_viz.no_graph: |
1305 | + for merged_index in rev.merges: |
1306 | + merged_rev = self.graph_viz.revisions[merged_index] |
1307 | + if self.get_revision_visible_if_branch_visible(merged_rev): |
1308 | + return True |
1309 | + |
1310 | + return False |
1311 | + |
1312 | + def filter_changed(self, revs=None, last_call=True): |
1313 | + if revs is None: |
1314 | + self.filter_cache = [None for rev in self.graph_viz.revisions] |
1315 | + if self.filter_changed_callback: |
1316 | + self.filter_changed_callback() |
1317 | + else: |
1318 | + pending_revs = revs |
1319 | + processed_revs = set() |
1320 | + prev_cached_revs = [] |
1321 | + while pending_revs: |
1322 | + rev = pending_revs.pop(0) |
1323 | + if rev in processed_revs: |
1324 | + continue |
1325 | + processed_revs.add(rev) |
1326 | + |
1327 | + rev_filter_cache = self.filter_cache[rev.index] |
1328 | + |
1329 | + if rev_filter_cache is not None: |
1330 | + prev_cached_revs.append((rev, rev_filter_cache)) |
1331 | + self.filter_cache[rev.index] = None |
1332 | + |
1333 | + if not self.graph_viz.no_graph: |
1334 | + if rev.merged_by is not None: |
1335 | + pending_revs.append( |
1336 | + self.graph_viz.revisions[rev.merged_by]) |
1337 | + |
1338 | + # Check if any visibilities have changes. If they have, call |
1339 | + # filter_changed_callback |
1340 | + for rev, prev_visible in prev_cached_revs: |
1341 | + visible = self.get_revision_visible_if_branch_visible(rev) |
1342 | + if visible != prev_visible: |
1343 | + if self.filter_changed_callback: |
1344 | + self.filter_changed_callback() |
1345 | + break |
1346 | + |
1347 | + def ensure_rev_visible(self, rev): |
1348 | + if self.graph_viz.no_graph: |
1349 | + return False |
1350 | + |
1351 | + branch_id = rev.branch_id |
1352 | + if branch_id not in self.branch_line_state: |
1353 | + self.branch_line_state[branch_id] = None |
1354 | + if self.filter_changed_callback: |
1355 | + self.filter_changed_callback() |
1356 | + return True |
1357 | + return False |
1358 | + |
1359 | + def collapse_expand_rev(self, c_rev, in_place_mod=True): |
1360 | + if in_place_mod: |
1361 | + branch_line_state = self.branch_line_state |
1362 | + else: |
1363 | + # create a copy of the state dict |
1364 | + branch_line_state = dict(self.branch_line_state) |
1365 | + |
1366 | + if c_rev is None: |
1367 | + return False |
1368 | + visible = not c_rev.twisty_state |
1369 | + branch_ids = zip( |
1370 | + c_rev.twisty_expands_branch_ids, |
1371 | + [c_rev.rev.branch_id] * len(c_rev.twisty_expands_branch_ids)) |
1372 | + |
1373 | + seen_branch_ids = set(branch_id |
1374 | + for branch_id, expanded_by in branch_ids) |
1375 | + has_change = False |
1376 | + while branch_ids: |
1377 | + branch_id, expanded_by = branch_ids.pop() |
1378 | + if (branch_id in branch_line_state) != visible: |
1379 | + has_change = True |
1380 | + if not visible: |
1381 | + del branch_line_state[branch_id] |
1382 | + parents = self.graph_viz.branch_lines[branch_id].merges |
1383 | + for parent_branch_id in parents: |
1384 | + parent_visible = parent_branch_id in branch_line_state |
1385 | + if (not parent_visible or |
1386 | + parent_branch_id in seen_branch_ids): |
1387 | + continue |
1388 | + |
1389 | + if branch_line_state[parent_branch_id] == branch_id: |
1390 | + # This branch expanded the parent branch, so we must |
1391 | + # collapse it. |
1392 | + branch_ids.append((parent_branch_id, branch_id)) |
1393 | + seen_branch_ids.add(parent_branch_id) |
1394 | + else: |
1395 | + # Check if this parent has any other visible branches |
1396 | + # that merge it. |
1397 | + has_visible = False |
1398 | + parent = self.graph_viz.branch_lines[parent_branch_id] |
1399 | + for merged_by_branch_id in parent.merged_by: |
1400 | + if merged_by_branch_id in branch_line_state: |
1401 | + has_visible = True |
1402 | + break |
1403 | + if not has_visible: |
1404 | + branch_ids.append((parent_branch_id, branch_id)) |
1405 | + seen_branch_ids.add(parent_branch_id) |
1406 | + else: |
1407 | + branch_line_state[branch_id] = expanded_by |
1408 | + if has_change and self.filter_changed_callback and in_place_mod: |
1409 | + self.filter_changed_callback() |
1410 | + return branch_line_state |
1411 | + |
1412 | + def expand_all_branch_lines(self): |
1413 | + for branch_id in self.graph_viz.branch_lines.keys(): |
1414 | + if branch_id not in self.branch_line_state: |
1415 | + self.branch_line_state[branch_id] = None |
1416 | + |
1417 | + |
1418 | +class FileIdFilter (object): |
1419 | + """ |
1420 | + Filter that only shows revisions that modify one of the specified files. |
1421 | + """ |
1422 | + |
1423 | + def __init__(self, graph_viz, filter_changed_callback, file_ids): |
1424 | + self.graph_viz = graph_viz |
1425 | + self.filter_changed_callback = filter_changed_callback |
1426 | + self.file_ids = file_ids |
1427 | + self.has_dir = False |
1428 | + self.filter_file_id = [False for rev in self.graph_viz.revisions] |
1429 | + |
1430 | + # don't filter working tree nodes |
1431 | + if isinstance(self.graph_viz, WithWorkingTreeGraphVizLoader): |
1432 | + for wt_revid in self.graph_viz.working_trees.iterkeys(): |
1433 | + try: |
1434 | + rev_index = self.graph_viz.revid_rev[wt_revid].index |
1435 | + self.filter_file_id[rev_index] = True |
1436 | + except KeyError: |
1437 | + pass |
1438 | + |
1439 | + |
1440 | + def uses_inventory(self): |
1441 | + return self.has_dir |
1442 | + |
1443 | + def load(self, revids=None): |
1444 | + """Load which revisions affect the file_ids""" |
1445 | + if self.file_ids: |
1446 | + self.graph_viz.throbber_show() |
1447 | + |
1448 | + for bi in self.graph_viz.branches: |
1449 | + tree = bi.tree |
1450 | + if tree is None: |
1451 | + tree = bi.branch.basis_tree() |
1452 | + |
1453 | + tree.lock_read() |
1454 | + try: |
1455 | + for file_id in self.file_ids: |
1456 | + if tree.kind(file_id) in ('directory', |
1457 | + 'tree-reference'): |
1458 | + self.has_dir = True |
1459 | + break |
1460 | + if self.has_dir: |
1461 | + break |
1462 | + finally: |
1463 | + tree.unlock() |
1464 | + |
1465 | + if revids is None: |
1466 | + revids = [rev.revid for rev in self.graph_viz.revisions] |
1467 | + |
1468 | + for repo, revids in self.graph_viz.get_repo_revids(revids): |
1469 | + if self.uses_inventory(): |
1470 | + chunk_size = 200 |
1471 | + else: |
1472 | + chunk_size = 500 |
1473 | + |
1474 | + for start in xrange(0, len(revids), chunk_size): |
1475 | + self.load_filter_file_id_chunk(repo, |
1476 | + revids[start:start + chunk_size]) |
1477 | + |
1478 | + self.load_filter_file_id_chunk_finished() |
1479 | + |
1480 | + def load_filter_file_id_chunk(self, repo, revids): |
1481 | + def check_text_keys(text_keys): |
1482 | + changed_revs = [] |
1483 | + for file_id, revid in repo.texts.get_parent_map(text_keys): |
1484 | + rev = self.graph_viz.revid_rev[revid] |
1485 | + self.filter_file_id[rev.index] = True |
1486 | + changed_revs.append(rev) |
1487 | + |
1488 | + self.graph_viz.update_ui() |
1489 | + self.filter_changed_callback(changed_revs, False) |
1490 | + self.graph_viz.update_ui() |
1491 | + |
1492 | + repo.lock_read() |
1493 | + try: |
1494 | + if not self.uses_inventory(): |
1495 | + text_keys = [(file_id, revid) |
1496 | + for revid in revids |
1497 | + for file_id in self.file_ids] |
1498 | + check_text_keys(text_keys) |
1499 | + else: |
1500 | + text_keys = [] |
1501 | + # We have to load the inventory for each revisions, to find |
1502 | + # the children of any directories. |
1503 | + for inv, revid in izip(repo.iter_inventories(revids), revids): |
1504 | + entries = inv.iter_entries_by_dir( |
1505 | + specific_file_ids=self.file_ids) |
1506 | + for path, entry in entries: |
1507 | + text_keys.append((entry.file_id, revid)) |
1508 | + if entry.kind == "directory": |
1509 | + sub_entries = inv.iter_entries(from_dir=entry) |
1510 | + for rc_path, rc_entry in sub_entries: |
1511 | + text_keys.append((rc_entry.file_id, revid)) |
1512 | + |
1513 | + self.graph_viz.update_ui() |
1514 | + |
1515 | + check_text_keys(text_keys) |
1516 | + finally: |
1517 | + repo.unlock() |
1518 | + |
1519 | + def load_filter_file_id_chunk_finished(self): |
1520 | + self.filter_changed_callback([], True) |
1521 | + self.graph_viz.throbber_hide() |
1522 | + |
1523 | + def get_revision_visible(self, rev): |
1524 | + return self.filter_file_id[rev.index] |
1525 | + |
1526 | + |
1527 | +class WorkingTreeHasChangeFilter(object): |
1528 | + """ |
1529 | + Filter out working trees that don't have any changes. |
1530 | + """ |
1531 | + |
1532 | + def __init__(self, graph_viz, filter_changed_callback, file_ids): |
1533 | + self.graph_viz = graph_viz |
1534 | + self.file_ids = file_ids |
1535 | + if not isinstance(graph_viz, WithWorkingTreeGraphVizLoader): |
1536 | + raise TypeError('graph_viz expected to be a ' |
1537 | + 'WithWorkingTreeGraphVizLoader') |
1538 | + self.filter_changed_callback = filter_changed_callback |
1539 | + self.tree_revids_with_changes = set() |
1540 | + |
1541 | + def load(self): |
1542 | + """Load if the working trees have changes.""" |
1543 | + self.tree_revids_with_changes = set() |
1544 | + self.graph_viz.throbber_show() |
1545 | + try: |
1546 | + for wt_revid, tree in self.graph_viz.working_trees.iteritems(): |
1547 | + if self.has_changes(tree): |
1548 | + self.tree_revids_with_changes.add(wt_revid) |
1549 | + rev = self.graph_viz.revid_rev[wt_revid] |
1550 | + self.filter_changed_callback([rev], False) |
1551 | + self.filter_changed_callback([], True) |
1552 | + finally: |
1553 | + self.graph_viz.throbber_hide() |
1554 | + |
1555 | + def has_changes(self, tree): |
1556 | + """Quickly check that the tree contains at least one commitable change. |
1557 | + |
1558 | + :param _from_tree: tree to compare against to find changes (default to |
1559 | + the basis tree and is intended to be used by tests). |
1560 | + |
1561 | + :return: True if a change is found. False otherwise |
1562 | + """ |
1563 | + tree.lock_read() |
1564 | + try: |
1565 | + # Copied from mutabletree, cause we need file_ids too. |
1566 | + # Check pending merges |
1567 | + if len(tree.get_parent_ids()) > 1: |
1568 | + return True |
1569 | + from_tree = tree.basis_tree() |
1570 | + |
1571 | + specific_files = None |
1572 | + if self.file_ids: |
1573 | + specific_files = [tree.id2path(file_id) |
1574 | + for file_id in self.file_ids] |
1575 | + |
1576 | + changes = tree.iter_changes(from_tree, |
1577 | + specific_files=specific_files) |
1578 | + try: |
1579 | + change = changes.next() |
1580 | + # Exclude root (talk about black magic... --vila 20090629) |
1581 | + if change[4] == (None, None): |
1582 | + change = changes.next() |
1583 | + return True |
1584 | + except StopIteration: |
1585 | + # No changes |
1586 | + return False |
1587 | + finally: |
1588 | + tree.unlock() |
1589 | + |
1590 | + def get_revision_visible(self, rev): |
1591 | + if rev.revid.startswith(CURRENT_REVISION): |
1592 | + return rev.revid in self.tree_revids_with_changes |
1593 | + else: |
1594 | + return True |
1595 | + |
1596 | + |
1597 | +class ComputedRevisionData(object): |
1598 | + """Container for computed layout data for a revision. |
1599 | + |
1600 | + :ivar rev: Reference to RevisionData. Use to get revno, revid, color and |
1601 | + others. |
1602 | + :ivar f_index: Index in `ComputedGraphViz.filtered_revs`. |
1603 | + :ivar col_index: Column index to place node for revision in. |
1604 | + :ivar lines: Lines that need to be drawn from from this revision's line to |
1605 | + the next revision's line. Note that not all these lines relate to this |
1606 | + revision, but be a part of a longer line that is passing this revision. |
1607 | + |
1608 | + Each line is a tuple of `(end col_index, start col_index, color, |
1609 | + direct)`. |
1610 | + |
1611 | + If direct is False, it indicates that this line represents an |
1612 | + ancestry, with revisions that are filtered. This should be shown as |
1613 | + a dotted line. |
1614 | + |
1615 | + :ivar branch_labels: Labels for branch tips. |
1616 | + :ivar twisty_state: State of the revision: |
1617 | + |
1618 | + * None: No twisty. |
1619 | + * True: There are branch lines that this revision merges that can |
1620 | + expanded. Show a '+'. |
1621 | + * False: All branches that this revision merges are already expanded. |
1622 | + Show a '-'. |
1623 | + :ivar twisty_expands_branch_ids: Branch lines that will be expanded if the |
1624 | + twisty is clicked. |
1625 | + """ |
1626 | + |
1627 | + # Instance of this object are typically named "c_rev". |
1628 | + __slots__ = ['rev', 'f_index', 'lines', 'col_index', 'branch_labels', |
1629 | + 'twisty_state', 'twisty_expands_branch_ids'] |
1630 | + |
1631 | + def __init__(self, rev): |
1632 | + self.rev = rev |
1633 | + self.lines = [] |
1634 | + self.col_index = None |
1635 | + self.twisty_state = None |
1636 | + self.twisty_expands_branch_ids = [] |
1637 | + self.branch_labels = [] |
1638 | + |
1639 | + |
1640 | +class ComputedGraphViz(object): |
1641 | + """Computed layout data for a graph. |
1642 | + |
1643 | + :ivar graph_viz: Reference to parent `GraphVizLoader`. |
1644 | + :ivar filtered_revs: List `ComputedRevisionData`. Only visible revisions |
1645 | + are included. |
1646 | + :ivar revisions: List `ComputedRevisionData`. Revision that are not |
1647 | + visible are None. |
1648 | + """ |
1649 | + def __init__(self, graph_viz): |
1650 | + self.graph_viz = graph_viz |
1651 | + self.filtered_revs = [] |
1652 | + self.revisions = [None for i in xrange(len(graph_viz.revisions))] |
1653 | |
1654 | === modified file 'bzrlib/tests/__init__.py' |
1655 | --- bzrlib/tests/__init__.py 2010-10-18 17:06:37 +0000 |
1656 | +++ bzrlib/tests/__init__.py 2010-11-18 10:15:44 +0000 |
1657 | @@ -3739,6 +3739,7 @@ |
1658 | 'bzrlib.tests.test_lockable_files', |
1659 | 'bzrlib.tests.test_lockdir', |
1660 | 'bzrlib.tests.test_log', |
1661 | + 'bzrlib.tests.test_loggraphviz', |
1662 | 'bzrlib.tests.test_lru_cache', |
1663 | 'bzrlib.tests.test_lsprof', |
1664 | 'bzrlib.tests.test_mail_client', |
1665 | |
1666 | === added file 'bzrlib/tests/test_loggraphviz.py' |
1667 | --- bzrlib/tests/test_loggraphviz.py 1970-01-01 00:00:00 +0000 |
1668 | +++ bzrlib/tests/test_loggraphviz.py 2010-11-18 10:15:44 +0000 |
1669 | @@ -0,0 +1,1153 @@ |
1670 | +# -*- coding: utf-8 -*- |
1671 | +# Copyright (C) 2010 Canonical Ltd |
1672 | +# |
1673 | +# This program is free software; you can redistribute it and/or |
1674 | +# modify it under the terms of the GNU General Public License |
1675 | +# as published by the Free Software Foundation; either version 2 |
1676 | +# of the License, or (at your option) any later version. |
1677 | +# |
1678 | +# This program is distributed in the hope that it will be useful, |
1679 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1680 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1681 | +# GNU General Public License for more details. |
1682 | +# |
1683 | +# You should have received a copy of the GNU General Public License |
1684 | +# along with this program; if not, write to the Free Software |
1685 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
1686 | + |
1687 | +from bzrlib.tests import TestCase, TestCaseWithTransport |
1688 | +from StringIO import StringIO |
1689 | + |
1690 | +from bzrlib import loggraphviz |
1691 | +from bzrlib.revision import NULL_REVISION |
1692 | + |
1693 | +# TODO: |
1694 | +# Tag loading |
1695 | +# Branch labels + Filtering |
1696 | +# Ghosts |
1697 | +# file_ids filtering |
1698 | + |
1699 | +class TestLogGraphVizMixin(object): |
1700 | + def computed_to_list(self, computed, branch_labels=False): |
1701 | + if not branch_labels: |
1702 | + item = lambda c_rev: (c_rev.rev.revid, |
1703 | + c_rev.col_index, |
1704 | + c_rev.twisty_state, |
1705 | + sorted(c_rev.lines),) |
1706 | + else: |
1707 | + item = lambda c_rev: (c_rev.rev.revid, |
1708 | + c_rev.col_index, |
1709 | + c_rev.twisty_state, |
1710 | + sorted(c_rev.lines), |
1711 | + [label for bi, label in c_rev.branch_labels]) |
1712 | + |
1713 | + return [item(c_rev) for c_rev in computed.filtered_revs] |
1714 | + |
1715 | + def assertComputed(self, expected_list, computed, branch_labels=False): |
1716 | + computed_list = self.computed_to_list(computed, branch_labels) |
1717 | + if not expected_list == computed_list: |
1718 | + raise AssertionError( |
1719 | + "not equal: \nexpected_list = \n%scomputed_list = \n%s" |
1720 | + % (format_graph_lines(expected_list, use_unicode=True), |
1721 | + format_graph_lines(computed_list, use_unicode=True),)) |
1722 | + |
1723 | + |
1724 | +class TestLogGraphVizWithBranches(TestCaseWithTransport, TestLogGraphVizMixin): |
1725 | + |
1726 | + def test_no_commits(self): |
1727 | + br = self.make_branch('.') |
1728 | + |
1729 | + bi = loggraphviz.BranchInfo('', None, br) |
1730 | + gv = loggraphviz.GraphVizLoader([bi], bi, False) |
1731 | + gv.load() |
1732 | + |
1733 | + self.assertEqual(len(gv.revisions), 0) |
1734 | + |
1735 | + state = loggraphviz.GraphVizFilterState(gv) |
1736 | + computed = gv.compute_viz(state) |
1737 | + self.assertEqual(len(computed.revisions), 0) |
1738 | + |
1739 | + def make_banches_for_tips_date_sorted(self): |
1740 | + builder = self.make_branch_builder('trunk') |
1741 | + builder.start_series() |
1742 | + builder.build_snapshot('rev-a', None, [ |
1743 | + ('add', ('', 'TREE_ROOT', 'directory', '')),]) |
1744 | + builder.build_snapshot('rev-old', ['rev-a'], []) |
1745 | + builder.build_snapshot('rev-new', ['rev-a'], []) |
1746 | + builder.build_snapshot('rev-trunk', ['rev-a'], []) |
1747 | + builder.finish_series() |
1748 | + |
1749 | + trunk = builder.get_branch() |
1750 | + #trunk.set_last_revision('rev-trunk') |
1751 | + |
1752 | + old = trunk.bzrdir.sprout('../old', revision_id='rev-old').open_branch() |
1753 | + |
1754 | + new = trunk.bzrdir.sprout('../new', revision_id='rev-new').open_branch() |
1755 | + |
1756 | + return trunk, old, new |
1757 | + |
1758 | + def test_branch_tips_date_sorted(self): |
1759 | + trunk, old, new = self.make_banches_for_tips_date_sorted() |
1760 | + |
1761 | + trunk_bi = loggraphviz.BranchInfo('trunk', None, trunk) |
1762 | + gv = loggraphviz.GraphVizLoader( |
1763 | + [trunk_bi, |
1764 | + loggraphviz.BranchInfo('old', None, old), |
1765 | + loggraphviz.BranchInfo('new', None, new),], |
1766 | + trunk_bi, False) |
1767 | + gv.load() |
1768 | + |
1769 | + state = loggraphviz.GraphVizFilterState(gv) |
1770 | + computed = gv.compute_viz(state) |
1771 | + |
1772 | + self.assertComputed( |
1773 | + [('rev-new', 2, None, [(2, 2, 0, True)]) , # ○ |
1774 | + # │ |
1775 | + ('rev-old', 1, None, [(1, 1, 0, True), (2, 2, 0, True)]), # ○ │ |
1776 | + # │ │ |
1777 | + ('rev-trunk', 0, None, [(0, 0, 0, True), (1, 0, 0, True), # ○ │ │ |
1778 | + (2, 0, 0, True)]), # ├─╯─╯ |
1779 | + ('rev-a', 0, None, []) ],# ○ |
1780 | + computed) |
1781 | + |
1782 | + def test_branch_tips_date_sorted_with_working_tree_provider(self): |
1783 | + trunk, old, new = self.make_banches_for_tips_date_sorted() |
1784 | + trunk_tree = trunk.bzrdir.create_workingtree() |
1785 | + old_tree = old.bzrdir.open_workingtree() |
1786 | + new_tree = new.bzrdir.open_workingtree() |
1787 | + |
1788 | + trunk_bi = loggraphviz.BranchInfo('trunk', trunk_tree, trunk) |
1789 | + gv = loggraphviz.WithWorkingTreeGraphVizLoader( |
1790 | + [trunk_bi, |
1791 | + loggraphviz.BranchInfo('old', old_tree, old), |
1792 | + loggraphviz.BranchInfo('new', new_tree, new),], |
1793 | + trunk_bi, False) |
1794 | + gv.load() |
1795 | + |
1796 | + state = loggraphviz.GraphVizFilterState(gv) |
1797 | + computed = gv.compute_viz(state) |
1798 | + |
1799 | + self.assertComputed( |
1800 | + [(gv.tree_revid(new_tree), 2, None, [(2, 2, 3, True)]), # ○ |
1801 | + # │ |
1802 | + ('rev-new', 2, None, [(2, 2, 0, True)]), # ○ |
1803 | + # │ |
1804 | + (gv.tree_revid(old_tree), 1, None, [(1, 1, 2, True), # ○ │ |
1805 | + (2, 2, 0, True)]), # │ │ |
1806 | + ('rev-old', 1, None, [(1, 1, 0, True), (2, 2, 0, True)]), # ○ │ |
1807 | + # │ │ |
1808 | + (gv.tree_revid(trunk_tree), 0, None, [ # ○ │ │ |
1809 | + (0, 0, 0, True), (1, 1, 0, True), (2, 2, 0, True)]), # │ │ │ |
1810 | + ('rev-trunk', 0, None, [(0, 0, 0, True), (1, 0, 0, True), # ○ │ │ |
1811 | + (2, 0, 0, True)]), # ├─╯─╯ |
1812 | + ('rev-a', 0, None, [])], # ○ |
1813 | + computed) |
1814 | + |
1815 | + |
1816 | + def make_tree_with_pending_merge(self, path): |
1817 | + builder = self.make_branch_builder('branch') |
1818 | + builder.start_series() |
1819 | + builder.build_snapshot('rev-a', None, [ |
1820 | + ('add', ('', 'TREE_ROOT', 'directory', '')),]) |
1821 | + builder.build_snapshot('rev-b', ['rev-a'], []) |
1822 | + builder.finish_series() |
1823 | + |
1824 | + branch = builder.get_branch() |
1825 | + branch.set_last_revision_info(1, 'rev-a') # go back to rev-a |
1826 | + tree = branch.bzrdir.create_workingtree() |
1827 | + tree.merge_from_branch(branch, to_revision='rev-b') |
1828 | + |
1829 | + return tree |
1830 | + |
1831 | + def make_tree_not_up_to_date(self, path): |
1832 | + builder = self.make_branch_builder('branch') |
1833 | + builder.start_series() |
1834 | + builder.build_snapshot('rev-a', None, [ |
1835 | + ('add', ('', 'TREE_ROOT', 'directory', '')),]) |
1836 | + builder.build_snapshot('rev-b', ['rev-a'], []) |
1837 | + builder.finish_series() |
1838 | + |
1839 | + branch = builder.get_branch() |
1840 | + tree = branch.bzrdir.create_workingtree() |
1841 | + tree.update(revision='rev-a') |
1842 | + return tree |
1843 | + |
1844 | + def test_pending_merge(self): |
1845 | + tree = self.make_tree_with_pending_merge('branch') |
1846 | + |
1847 | + bi = loggraphviz.BranchInfo(None, tree, tree.branch) |
1848 | + gv = loggraphviz.GraphVizLoader([bi], bi, False) |
1849 | + gv.load() |
1850 | + |
1851 | + state = loggraphviz.GraphVizFilterState(gv) |
1852 | + computed = gv.compute_viz(state) |
1853 | + |
1854 | + self.assertComputed( |
1855 | + [('rev-b', 1, None, [(1, 0, 0, True)], ['Pending Merge']), # ○ |
1856 | + # ╭─╯ |
1857 | + ('rev-a', 0, None, [], [None]) ],# ○ |
1858 | + computed, branch_labels=True) |
1859 | + |
1860 | + def test_out_of_date_wt(self): |
1861 | + tree = self.make_tree_not_up_to_date('branch') |
1862 | + bi = loggraphviz.BranchInfo(None, tree, tree.branch) |
1863 | + gv = loggraphviz.GraphVizLoader([bi], bi, False) |
1864 | + gv.load() |
1865 | + |
1866 | + state = loggraphviz.GraphVizFilterState(gv) |
1867 | + computed = gv.compute_viz(state) |
1868 | + |
1869 | + self.assertComputed( |
1870 | + [('rev-b', 0, None, [(0, 0, 0, True)], [None]), # ○ |
1871 | + # │ |
1872 | + ('rev-a', 0, None, [], ['Working Tree']) ],# ○ |
1873 | + computed, branch_labels=True) |
1874 | + |
1875 | + def test_with_working_tree_provider(self): |
1876 | + tree = self.make_tree_with_pending_merge('branch') |
1877 | + |
1878 | + bi = loggraphviz.BranchInfo('branch', tree, tree.branch) |
1879 | + gv = loggraphviz.WithWorkingTreeGraphVizLoader([bi], bi, False) |
1880 | + gv.load() |
1881 | + |
1882 | + state = loggraphviz.GraphVizFilterState(gv) |
1883 | + computed = gv.compute_viz(state) |
1884 | + |
1885 | + self.assertComputed( |
1886 | + [(u'current:%s' % tree.basedir, 0, True, # ⊖ |
1887 | + [(0, 0, 0, True), (0, 1, 2, True)], # ├─╮ |
1888 | + ['branch - Working Tree']), # │ │ |
1889 | + ('rev-b', 1, None, [(0, 0, 0, True), (1, 0, 0, True)], # │ ○ |
1890 | + ['branch - Pending Merge']), # ├─╯ |
1891 | + ('rev-a', 0, None, [], ['branch'])], # ○ |
1892 | + computed, branch_labels=True) |
1893 | + |
1894 | + def test_with_working_tree_provider_out_of_date_wt(self): |
1895 | + tree = self.make_tree_not_up_to_date('branch') |
1896 | + bi = loggraphviz.BranchInfo('branch', tree, tree.branch) |
1897 | + gv = loggraphviz.WithWorkingTreeGraphVizLoader([bi], bi, False) |
1898 | + gv.load() |
1899 | + |
1900 | + state = loggraphviz.GraphVizFilterState(gv) |
1901 | + computed = gv.compute_viz(state) |
1902 | + |
1903 | + self.assertComputed( |
1904 | + [(u'current:%s' % tree.basedir, 1, None, [(1, 1, 0, True)], # ○ |
1905 | + ['branch - Working Tree']), # │ |
1906 | + ('rev-b', 0, None, [(0, 0, 0, True), (1, 0, 0, True)], # ○ │ |
1907 | + ['branch']), # ├─╯ |
1908 | + ('rev-a', 0, None, [], []) ], # ○ |
1909 | + computed, branch_labels=True) |
1910 | + |
1911 | + def test_with_working_tree_provider_filtered(self): |
1912 | + # This test makes sure that lable for a Working Tree shows for on it's |
1913 | + # nearest visble unique ansestor when the working tree node is |
1914 | + # filtered. |
1915 | + builder = self.make_branch_builder('branch') |
1916 | + builder.start_series() |
1917 | + builder.build_snapshot('rev-a', None, [ |
1918 | + ('add', ('', 'TREE_ROOT', 'directory', '')),]) |
1919 | + builder.build_snapshot('rev-b', ['rev-a'], []) |
1920 | + builder.build_snapshot('rev-c', ['rev-a'], []) |
1921 | + builder.finish_series() |
1922 | + |
1923 | + branch = builder.get_branch() |
1924 | + tree = branch.bzrdir.create_workingtree() |
1925 | + tree.update(revision='rev-b') |
1926 | + |
1927 | + bi = loggraphviz.BranchInfo('branch', tree, tree.branch) |
1928 | + gv = loggraphviz.WithWorkingTreeGraphVizLoader([bi], bi, False) |
1929 | + gv.load() |
1930 | + |
1931 | + state = loggraphviz.GraphVizFilterState(gv) |
1932 | + state.filters.append(BasicFilterer(set([ |
1933 | + 'current:%s' % tree.basedir.encode('unicode-escape')]))) |
1934 | + computed = gv.compute_viz(state) |
1935 | + self.assertComputed( |
1936 | + [('rev-b', 1, None, [(1, 1, 0, True)], ['branch - Working Tree']) , # ○ |
1937 | + # │ |
1938 | + ('rev-c', 0, None, [(0, 0, 0, True), (1, 0, 0, True)], ['branch']), # ○ │ |
1939 | + # ├─╯ |
1940 | + ('rev-a', 0, None, [], []) ],# ○ |
1941 | + computed, branch_labels=True) |
1942 | + |
1943 | + def test_pending_merges_provider(self): |
1944 | + tree = self.make_tree_with_pending_merge('branch') |
1945 | + |
1946 | + bi = loggraphviz.BranchInfo(None, tree, tree.branch) |
1947 | + gv = loggraphviz.PendingMergesGraphVizLoader([bi], bi, False) |
1948 | + gv.load() |
1949 | + |
1950 | + state = loggraphviz.GraphVizFilterState(gv) |
1951 | + computed = gv.compute_viz(state) |
1952 | + |
1953 | + self.assertComputed( |
1954 | + [('rev-b', 1, None, [(1, 0, 0, True)]), # ○ |
1955 | + # ╭─╯ |
1956 | + ('root:', 0, None, []) ],# ○ |
1957 | + computed) |
1958 | + |
1959 | + def test_with_ghost(self): |
1960 | + tree = self.make_branch_and_tree('tree') |
1961 | + tree.commit('a', rev_id='rev-a') |
1962 | + tree.add_parent_tree_id('rev-b') |
1963 | + tree.commit('c', rev_id='rev-c') |
1964 | + # rev-b is a ghost. We think he is there, but he dose not exist. Boo! |
1965 | + |
1966 | + bi = loggraphviz.BranchInfo(None, tree, tree.branch) |
1967 | + gv = loggraphviz.GraphVizLoader([bi], bi, False) |
1968 | + gv.load() |
1969 | + |
1970 | + state = loggraphviz.GraphVizFilterState(gv) |
1971 | + state.expand_all_branch_lines() |
1972 | + computed = gv.compute_viz(state) |
1973 | + |
1974 | + self.assertComputed( |
1975 | + [('rev-c', 0, True, [(0, 0, 0, True), (0, 1, 1, True)]), # ⊖ |
1976 | + # ├─╮ |
1977 | + ('rev-b', 1, None, [(0, 0, 0, True)]) , # │ ○ |
1978 | + # │ |
1979 | + ('rev-a', 0, None, []) ],# ○ |
1980 | + computed) |
1981 | + |
1982 | + def test_with_ghost_mainline(self): |
1983 | + tree = self.make_branch_and_tree('tree') |
1984 | + tree.add_parent_tree_id('rev-a', allow_leftmost_as_ghost=True) |
1985 | + tree.commit('b', rev_id='rev-b') |
1986 | + |
1987 | + bi = loggraphviz.BranchInfo(None, tree, tree.branch) |
1988 | + gv = loggraphviz.GraphVizLoader([bi], bi, False) |
1989 | + gv.load() |
1990 | + |
1991 | + state = loggraphviz.GraphVizFilterState(gv) |
1992 | + computed = gv.compute_viz(state) |
1993 | + |
1994 | + self.assertComputed( |
1995 | + [('rev-b', 0, None, [(0, 0, 0, True)]), # ○ |
1996 | + # │ |
1997 | + ('rev-a', 0, None, []) ],# ○ |
1998 | + computed) |
1999 | + |
2000 | + def test_get_revid_branch_info(self): |
2001 | + builder = self.make_branch_builder('trunk') |
2002 | + builder.start_series() |
2003 | + builder.build_snapshot('rev-a', None, [ |
2004 | + ('add', ('', 'TREE_ROOT', 'directory', '')),]) |
2005 | + builder.build_snapshot('rev-branch', ['rev-a'], []) |
2006 | + builder.build_snapshot('rev-trunk', ['rev-a'], []) |
2007 | + builder.finish_series() |
2008 | + # ○ branch |
2009 | + # │ |
2010 | + # ○ │ trunk |
2011 | + # ├─╯ |
2012 | + # ○ rev-a |
2013 | + |
2014 | + trunk = builder.get_branch() |
2015 | + #trunk.set_last_revision('rev-trunk') |
2016 | + |
2017 | + branch = trunk.bzrdir.sprout('../branch', |
2018 | + revision_id='rev-branch').open_branch() |
2019 | + |
2020 | + trunk_bi = loggraphviz.BranchInfo('trunk', None, trunk) |
2021 | + branch_bi = loggraphviz.BranchInfo('branch', None, branch) |
2022 | + gv = loggraphviz.GraphVizLoader( |
2023 | + [trunk_bi, branch_bi], |
2024 | + trunk_bi, False) |
2025 | + gv.load() |
2026 | + |
2027 | + self.assertEqual(trunk_bi, gv.get_revid_branch_info('rev-trunk')) |
2028 | + self.assertEqual(branch_bi, gv.get_revid_branch_info('rev-branch')) |
2029 | + |
2030 | + # may return either |
2031 | + self.assertIn(gv.get_revid_branch_info('rev-a'), |
2032 | + (branch_bi, trunk_bi)) |
2033 | + |
2034 | + def test_get_revid_branch_info_with_ghost(self): |
2035 | + tree = self.make_branch_and_tree('tree') |
2036 | + tree.commit('a', rev_id='rev-a') |
2037 | + tree.add_parent_tree_id('rev-b') |
2038 | + tree.commit('c', rev_id='rev-c') |
2039 | + # rev-b is a ghost. We think he is there, but he dose not exist. Boo! |
2040 | + # c |
2041 | + # ├─╮ |
2042 | + # │ b |
2043 | + # │ |
2044 | + # a |
2045 | + |
2046 | + bi = loggraphviz.BranchInfo(None, tree, tree.branch) |
2047 | + gv = loggraphviz.GraphVizLoader([bi], bi, False) |
2048 | + gv.load() |
2049 | + |
2050 | + self.assertRaises(loggraphviz.GhostRevisionError, |
2051 | + gv.get_revid_branch_info, 'rev-b') |
2052 | + |
2053 | +class TestLogGraphVizLayouts(TestCase, TestLogGraphVizMixin): |
2054 | + |
2055 | + def test_basic_branch_line(self): |
2056 | + gv = BasicGraphVizLoader(('rev-d',), { |
2057 | + 'rev-a': (NULL_REVISION, ), |
2058 | + 'rev-b': ('rev-a', ), |
2059 | + 'rev-c': ('rev-a', ), |
2060 | + 'rev-d': ('rev-b', 'rev-c'), |
2061 | + }) |
2062 | + gv.load() |
2063 | + |
2064 | + state = loggraphviz.GraphVizFilterState(gv) |
2065 | + computed = gv.compute_viz(state) |
2066 | + |
2067 | + # only mainline. |
2068 | + self.assertComputed( |
2069 | + [('rev-d', 0, False, [(0, 0, 0, True)]), # ⊕ |
2070 | + # │ |
2071 | + ('rev-b', 0, None, [(0, 0, 0, True)]) , # ○ |
2072 | + # │ |
2073 | + ('rev-a', 0, None, []) ],# ○ |
2074 | + computed) |
2075 | + |
2076 | + state.collapse_expand_rev(computed.filtered_revs[0]) |
2077 | + computed = gv.compute_viz(state) |
2078 | + |
2079 | + # expanded branch line. |
2080 | + self.assertComputed( |
2081 | + [('rev-d', 0, True, [(0, 0, 0, True), (0, 1, 2, True)]), # ⊖ |
2082 | + # ├─╮ |
2083 | + ('rev-c', 1, None, [(0, 0, 0, True), (1, 1, 0, True)]), # │ ○ |
2084 | + # │ │ |
2085 | + ('rev-b', 0, None, [(0, 0, 0, True), (1, 0, 0, True)]), # ○ │ |
2086 | + # ├─╯ |
2087 | + ('rev-a', 0, None, []) ],# ○ |
2088 | + computed) |
2089 | + |
2090 | + def test_branch_line_order(self): |
2091 | + gv = BasicGraphVizLoader(('rev-f',), { |
2092 | + 'rev-a': (NULL_REVISION, ), |
2093 | + 'rev-b': ('rev-a', ), |
2094 | + 'rev-c': ('rev-b', ), |
2095 | + 'rev-d': ('rev-a', 'rev-c'), |
2096 | + 'rev-e': ('rev-b', ), |
2097 | + 'rev-f': ('rev-d', 'rev-e' ), |
2098 | + }) |
2099 | + gv.load() |
2100 | + |
2101 | + state = loggraphviz.GraphVizFilterState(gv) |
2102 | + state.expand_all_branch_lines() |
2103 | + computed = gv.compute_viz(state) |
2104 | + |
2105 | + # branch lines should not cross over |
2106 | + self.assertComputed( |
2107 | + [('rev-f', 0, True, [(0, 0, 0, True), (0, 2, 3, True)]) , # ⊖ |
2108 | + # ├───╮ |
2109 | + ('rev-e', 2, True, [(0, 0, 0, True), (2, 2, 2, True)]) , # │ ⊖ |
2110 | + # │ │ |
2111 | + ('rev-d', 0, True, [(0, 0, 0, True), (0, 1, 2, True), (2, 2, 2, True)]), # ⊖ │ |
2112 | + # ├─╮ │ |
2113 | + ('rev-c', 1, None, [(0, 0, 0, True), (1, 1, 2, True), (2, 1, 2, True)]), # │ ○ │ |
2114 | + # │ ├─╯ |
2115 | + ('rev-b', 1, None, [(0, 0, 0, True), (1, 0, 0, True)]) , # │ ○ |
2116 | + # ├─╯ |
2117 | + ('rev-a', 0, None, []) ],# ○ |
2118 | + computed) |
2119 | + |
2120 | + def test_branch_line_order2(self): |
2121 | + gv = BasicGraphVizLoader(('rev-h',), { |
2122 | + 'rev-a': (NULL_REVISION, ), |
2123 | + 'rev-b': ('rev-a', ), |
2124 | + 'rev-c': ('rev-b', ), |
2125 | + 'rev-d': ('rev-a', ), |
2126 | + 'rev-e': ('rev-d', 'rev-b' ), |
2127 | + 'rev-f': ('rev-e', ), |
2128 | + 'rev-g': ('rev-e', 'rev-f' ), |
2129 | + 'rev-h': ('rev-c', 'rev-g' ), |
2130 | + }) |
2131 | + gv.load() |
2132 | + |
2133 | + state = loggraphviz.GraphVizFilterState(gv) |
2134 | + state.expand_all_branch_lines() |
2135 | + computed = gv.compute_viz(state) |
2136 | + |
2137 | + # branch lines should not cross over |
2138 | + self.assertComputed( |
2139 | + [('rev-h', 0, True, [(0, 0, 0, True), (0, 2, 2, True)]) , # ⊖ |
2140 | + # ├───╮ |
2141 | + ('rev-g', 2, True, [(0, 0, 0, True), (2, 2, 2, True), (2, 3, 3, True)]), # │ ⊖ |
2142 | + # │ ├─╮ |
2143 | + ('rev-f', 3, None, [(0, 0, 0, True), (2, 2, 2, True), (3, 2, 2, True)]), # │ │ ○ |
2144 | + # │ ├─╯ |
2145 | + ('rev-e', 2, None, [(0, 0, 0, True), (2, 1, 0, True), (2, 2, 2, True)]), # │ ○ |
2146 | + # │ ╭─┤ |
2147 | + ('rev-d', 2, None, [(0, 0, 0, True), (1, 1, 0, True), (2, 2, 0, True)]), # │ │ ○ |
2148 | + # │ │ │ |
2149 | + ('rev-c', 0, None, [(0, 0, 0, True), (1, 0, 0, True), (2, 2, 0, True)]), # ○ │ │ |
2150 | + # ├─╯ │ |
2151 | + ('rev-b', 0, None, [(0, 0, 0, True), (2, 0, 0, True)]) , # ○ │ |
2152 | + # ├───╯ |
2153 | + ('rev-a', 0, None, []) ],# ○ |
2154 | + computed) |
2155 | + |
2156 | + def test_octopus_merge(self): |
2157 | + gv = BasicGraphVizLoader(('rev-e',), { |
2158 | + 'rev-a': (NULL_REVISION, ), |
2159 | + 'rev-b': ('rev-a', ), |
2160 | + 'rev-c': ('rev-a', ), |
2161 | + 'rev-d': ('rev-a', ), |
2162 | + 'rev-e': ('rev-a', 'rev-b', 'rev-c', 'rev-d'), |
2163 | + }) |
2164 | + gv.load() |
2165 | + |
2166 | + state = loggraphviz.GraphVizFilterState(gv) |
2167 | + state.expand_all_branch_lines() |
2168 | + computed = gv.compute_viz(state) |
2169 | + |
2170 | + self.assertComputed( |
2171 | + [('rev-e', 0, True, [(0, 0, 0, True), (0, 1, 2, True), (0, 2, 3, True), (0, 3, 4, True)]), # ⊖ |
2172 | + # ├─╮─╮─╮ |
2173 | + ('rev-b', 3, None, [(0, 0, 0, True), (1, 1, 2, True), (2, 2, 3, True), (3, 3, 0, True)]), # │ │ │ ○ |
2174 | + # │ │ │ │ |
2175 | + ('rev-c', 2, None, [(0, 0, 0, True), (1, 1, 2, True), (2, 2, 0, True), (3, 3, 0, True)]), # │ │ ○ │ |
2176 | + # │ │ │ │ |
2177 | + ('rev-d', 1, None, [(0, 0, 0, True), (1, 0, 0, True), (2, 0, 0, True), (3, 0, 0, True)]), # │ ○ │ │ |
2178 | + # ├─╯─╯─╯ |
2179 | + ('rev-a', 0, None, []) ],# ○ |
2180 | + computed) |
2181 | + |
2182 | + def test_lots_of_merges_between_branch_lines(self): |
2183 | + gv = BasicGraphVizLoader(('rev-g',), { |
2184 | + 'rev-a': (NULL_REVISION, ), |
2185 | + 'rev-b': ('rev-a', ), |
2186 | + 'rev-c': ('rev-b', ), |
2187 | + 'rev-d': ('rev-a', ), |
2188 | + 'rev-e': ('rev-d', 'rev-b',), |
2189 | + 'rev-f': ('rev-e', 'rev-c',), |
2190 | + 'rev-g': ('rev-c', 'rev-f',), |
2191 | + }) |
2192 | + gv.load() |
2193 | + |
2194 | + state = loggraphviz.GraphVizFilterState(gv) |
2195 | + state.expand_all_branch_lines() |
2196 | + computed = gv.compute_viz(state) |
2197 | + |
2198 | + self.assertComputed( |
2199 | + [('rev-g', 0, True, [(0, 0, 0, True), (0, 2, 2, True)]) , # ⊖ |
2200 | + # ├───╮ |
2201 | + ('rev-f', 2, None, [(0, 0, 0, True), (2, 0.75, 0, True), (2, 2, 2, True)]) , # │ ○ |
2202 | + # │ ╭─┤ |
2203 | + ('rev-e', 2, None, [(0, 0, 0, True), (0.75, 0.75, 0, True), (2, 1.25, 0, True), (2, 2, 2, True)]), # │ │ ○ |
2204 | + # │ ├─┤ |
2205 | + ('rev-d', 2, None, [(0, 0, 0, True), (0.75, 0, 0, True), (1.25, 1.25, 0, True), (2, 2, 0, True)]), # │ │ ○ |
2206 | + # ├─┤ │ |
2207 | + ('rev-c', 0, None, [(0, 0, 0, True), (1.25, 0, 0, True), (2, 2, 0, True)]) , # ○ │ │ |
2208 | + # ├─╯ │ |
2209 | + ('rev-b', 0, None, [(0, 0, 0, True), (2, 0, 0, True)]) , # ○ │ |
2210 | + # ├───╯ |
2211 | + ('rev-a', 0, None, []) ],# ○ |
2212 | + computed) |
2213 | + |
2214 | + def test_hidden_branch_line_hides_child_line(self): |
2215 | + gv = BasicGraphVizLoader(('rev-g',), { |
2216 | + 'rev-a': (NULL_REVISION, ), |
2217 | + 'rev-b': ('rev-a', ), |
2218 | + 'rev-c': ('rev-a', ), |
2219 | + 'rev-d': ('rev-b', 'rev-c', ), |
2220 | + 'rev-e': ('rev-b', 'rev-d', ), |
2221 | + 'rev-f': ('rev-c', ), |
2222 | + 'rev-g': ('rev-e', 'rev-f', ), |
2223 | + }) |
2224 | + gv.load() |
2225 | + |
2226 | + state = loggraphviz.GraphVizFilterState(gv) |
2227 | + state.branch_line_state[(2, 1)] = None |
2228 | + computed = gv.compute_viz(state) |
2229 | + |
2230 | + self.assertComputed( |
2231 | + [('rev-g', 0, False, [(0, 0, 0, True)]) , # ⊕ |
2232 | + # │ |
2233 | + ('rev-e', 0, True, [(0, 0, 0, True), (0, 1, 3, True)]) , # ⊖ |
2234 | + # ├─╮ |
2235 | + ('rev-d', 1, False, [(0, 0, 0, True), (1, 0, 0, True)]), # │ ⊕ |
2236 | + # ├─╯ |
2237 | + ('rev-b', 0, None, [(0, 0, 0, True)]) , # ○ |
2238 | + # │ |
2239 | + ('rev-a', 0, None, []) ],# ○ |
2240 | + computed) |
2241 | + |
2242 | + def test_merge_line_hidden(self): |
2243 | + gv = BasicGraphVizLoader(('rev-d',), { |
2244 | + 'rev-a': (NULL_REVISION, ), |
2245 | + 'rev-b': ('rev-a', ), |
2246 | + 'rev-c': ('rev-a', 'rev-b'), |
2247 | + 'rev-d': ('rev-a', 'rev-c'), |
2248 | + }) |
2249 | + # d |
2250 | + # ├─╮ |
2251 | + # │ c |
2252 | + # │ ├─╮ |
2253 | + # │ │ b |
2254 | + # ├─╯─╯ |
2255 | + # a |
2256 | + gv.load() |
2257 | + |
2258 | + state = loggraphviz.GraphVizFilterState(gv) |
2259 | + state.branch_line_state[(1, 1)] = None |
2260 | + |
2261 | + computed = gv.compute_viz(state) |
2262 | + # when the merge by branch line, we should show a non direct line |
2263 | + self.assertComputed( |
2264 | + [('rev-d', 0, False, [(0, 0, 0, True), (0, 1, 2, False)]), # ⊕ |
2265 | + # ├┄╮ |
2266 | + ('rev-b', 1, None, [(0, 0, 0, True), (1, 0, 0, True)]) , # │ ○ |
2267 | + # ├─╯ |
2268 | + ('rev-a', 0, None, []) ],# ○ |
2269 | + computed) |
2270 | + |
2271 | + def test_merge_line_hidden2(self): |
2272 | + gv = BasicGraphVizLoader(('rev-e',), { |
2273 | + 'rev-a': (NULL_REVISION, ), |
2274 | + 'rev-z': ('rev-a', ), |
2275 | + 'rev-y': ('rev-a', 'rev-z', ), |
2276 | + 'rev-b': ('rev-a', ), |
2277 | + 'rev-c': ('rev-a', ), |
2278 | + 'rev-d': ('rev-a', 'rev-c'), |
2279 | + 'rev-e': ('rev-y', 'rev-b', 'rev-d'), |
2280 | + }) |
2281 | + # f |
2282 | + # ├─╮─╮ |
2283 | + # │ b │ |
2284 | + # │ │ │ |
2285 | + # │ │ e |
2286 | + # │ │ ├─╮ |
2287 | + # │ │ │ d |
2288 | + # ├─╯─╯─╯ |
2289 | + # a |
2290 | + gv.load() |
2291 | + |
2292 | + state = loggraphviz.GraphVizFilterState(gv) |
2293 | + #state.expand_all_branch_lines() |
2294 | + state.branch_line_state[(1, 1)] = None |
2295 | + state.branch_line_state[(1, 2)] = None |
2296 | + #state.branch_line_state[(1, 3)] = None |
2297 | + state.branch_line_state[(1, 4)] = None |
2298 | + |
2299 | + computed = gv.compute_viz(state) |
2300 | + # when the merge by branch line, we should show a non direct line |
2301 | + # this could layout better, but that's another story... |
2302 | + self.assertComputed( |
2303 | + [('rev-e', 0, False, [(0, 0, 0, True), (0, 1, 3, False), (0, 2, 5, True)]) , # ⊕ |
2304 | + # ├┄╮─╮ |
2305 | + ('rev-b', 2, None, [(0, 0, 0, True), (1, 3, 3, False), (2, 2, 0, True)]) , # │ ┆ ○ |
2306 | + # │ ╰┄┼┄╮ |
2307 | + ('rev-c', 3, None, [(0, 0, 0, True), (2, 2, 0, True), (3, 3, 0, True)]) , # │ │ ○ |
2308 | + # │ │ │ |
2309 | + ('rev-y', 0, True, [(0, 0, 0, True), (0, 1, 2, True), (2, 2, 0, True), (3, 3, 0, True)]), # ⊖ │ │ |
2310 | + # ├─╮ │ │ |
2311 | + ('rev-z', 1, None, [(0, 0, 0, True), (1, 0, 0, True), (2, 0, 0, True), (3, 0, 0, True)]), # │ ○ │ │ |
2312 | + # ├─╯─╯─╯ |
2313 | + ('rev-a', 0, None, []) ],# ○ |
2314 | + computed) |
2315 | + |
2316 | + def test_merge_line_hidden_merge_rev_filtered(self): |
2317 | + gv = BasicGraphVizLoader(('rev-e',), { |
2318 | + 'rev-a': (NULL_REVISION, ), |
2319 | + 'rev-b': ('rev-a', ), |
2320 | + 'rev-c': ('rev-b', ), |
2321 | + 'rev-d': ('rev-a', 'rev-c'), |
2322 | + 'rev-e': ('rev-a', 'rev-d'), |
2323 | + }) |
2324 | + gv.load() |
2325 | + |
2326 | + state = loggraphviz.GraphVizFilterState(gv) |
2327 | + state.filters.append(BasicFilterer(set(['rev-c']))) |
2328 | + state.branch_line_state[(1, 1)] = None |
2329 | + |
2330 | + computed = gv.compute_viz(state) |
2331 | + |
2332 | + # when the merge by branch line, we should show a non direct line |
2333 | + self.assertComputed( |
2334 | + [('rev-e', 0, False, [(0, 0, 0, True), (0, 1, 2, False)]), # ⊕ |
2335 | + # ├┄╮ |
2336 | + ('rev-b', 1, None, [(0, 0, 0, True), (1, 0, 0, True)]) , # │ ○ |
2337 | + # ├─╯ |
2338 | + ('rev-a', 0, None, []) ],# ○ |
2339 | + computed) |
2340 | + |
2341 | + def test_non_direct_hidden_branch(self): |
2342 | + gv = BasicGraphVizLoader(('rev-f',), { |
2343 | + 'rev-a': (NULL_REVISION, ), |
2344 | + 'rev-b': ('rev-a', ), |
2345 | + 'rev-c': ('rev-b', ), |
2346 | + 'rev-d': ('rev-a', 'rev-c', ), |
2347 | + 'rev-e': ('rev-b', ), |
2348 | + 'rev-f': ('rev-d', 'rev-e', ), |
2349 | + }) |
2350 | + gv.load() |
2351 | + |
2352 | + state = loggraphviz.GraphVizFilterState(gv) |
2353 | + state.branch_line_state[(1, 2)] = None |
2354 | + |
2355 | + computed = gv.compute_viz(state) |
2356 | + |
2357 | + self.assertComputed( |
2358 | + [('rev-f', 0, True, [(0, 0, 0, True), (0, 1, 3, True)]) , # ⊖ |
2359 | + # ├─╮ |
2360 | + ('rev-e', 1, False, [(0, 0, 0, True), (1, 1, 0, False)]), # │ ⊕ |
2361 | + # │ ┆ |
2362 | + ('rev-d', 0, False, [(0, 0, 0, True), (1, 0, 0, False)]), # ⊕ ┆ |
2363 | + # ├┄╯ |
2364 | + ('rev-a', 0, None, []) ],# ○ |
2365 | + computed) |
2366 | + |
2367 | + def test_non_direct_hidden_parent(self): |
2368 | + gv = BasicGraphVizLoader(('rev-e',), { |
2369 | + 'rev-a': (NULL_REVISION, ), |
2370 | + 'rev-b': ('rev-a', ), |
2371 | + 'rev-c': ('rev-b', ), |
2372 | + 'rev-d': ('rev-a', 'rev-c'), |
2373 | + 'rev-e': ('rev-a', 'rev-d'), |
2374 | + }) |
2375 | + gv.load() |
2376 | + |
2377 | + state = loggraphviz.GraphVizFilterState(gv) |
2378 | + state.filters.append(BasicFilterer(set(['rev-c']))) |
2379 | + state.expand_all_branch_lines() |
2380 | + |
2381 | + computed = gv.compute_viz(state) |
2382 | + |
2383 | + self.assertComputed( |
2384 | + [('rev-e', 0, True, [(0, 0, 0, True), (0, 1, 3, True)]) , # ⊖ |
2385 | + # ├─╮ |
2386 | + ('rev-d', 1, True, [(0, 0, 0, True), (1, 1, 0, True), (1, 2, 2, False)]), # │ ⊖ |
2387 | + # │ ├┄╮ |
2388 | + ('rev-b', 2, None, [(0, 0, 0, True), (1, 0, 0, True), (2, 0, 0, True)]) , # │ │ ○ |
2389 | + # ├─╯─╯ |
2390 | + ('rev-a', 0, None, []) ],# ○ |
2391 | + computed) |
2392 | + |
2393 | + def test_no_graph(self): |
2394 | + gv = BasicGraphVizLoader(('rev-d',), { |
2395 | + 'rev-a': (NULL_REVISION, ), |
2396 | + 'rev-b': ('rev-a', ), |
2397 | + 'rev-c': ('rev-a', ), |
2398 | + 'rev-d': ('rev-b', 'rev-c'), |
2399 | + }, no_graph=True) |
2400 | + gv.load() |
2401 | + |
2402 | + state = loggraphviz.GraphVizFilterState(gv) |
2403 | + computed = gv.compute_viz(state) |
2404 | + self.assertComputed( |
2405 | + [('rev-d', 0.0, None, []), # ○ |
2406 | + # |
2407 | + ('rev-c', 0.5, None, []), # ○ |
2408 | + # |
2409 | + ('rev-b', 0.0, None, []), # ○ |
2410 | + # |
2411 | + ('rev-a', 0.0, None, [])],# ○ |
2412 | + computed) |
2413 | + |
2414 | + def test_no_graph_filtered(self): |
2415 | + gv = BasicGraphVizLoader(('rev-d',), { |
2416 | + 'rev-a': (NULL_REVISION, ), |
2417 | + 'rev-b': ('rev-a', ), |
2418 | + 'rev-c': ('rev-a', ), |
2419 | + 'rev-d': ('rev-b', 'rev-c'), |
2420 | + }, no_graph=True) |
2421 | + gv.load() |
2422 | + |
2423 | + state = loggraphviz.GraphVizFilterState(gv) |
2424 | + state.filters.append(BasicFilterer(set(['rev-b']))) |
2425 | + computed = gv.compute_viz(state) |
2426 | + self.assertComputed( |
2427 | + [('rev-d', 0.0, None, []), # ○ |
2428 | + # |
2429 | + ('rev-c', 0.5, None, []), # ○ |
2430 | + # |
2431 | + ('rev-a', 0.0, None, [])],# ○ |
2432 | + computed) |
2433 | + |
2434 | +class TestLogGraphProviderState(TestCase): |
2435 | + |
2436 | + def assertFilteredRevisions(self, expected_revids, state): |
2437 | + revids = [rev.revid for rev in state.get_filtered_revisions()] |
2438 | + self.assertEqual(list(expected_revids), revids) |
2439 | + |
2440 | + def test_collapse_expand_rev_basic(self): |
2441 | + gv = BasicGraphVizLoader(('c',), { |
2442 | + 'a': (NULL_REVISION, ), |
2443 | + 'b': ('a', ), |
2444 | + 'c': ('a', 'b'), |
2445 | + }) |
2446 | + gv.load() |
2447 | + # c |
2448 | + # ├─╮ |
2449 | + # │ b |
2450 | + # ├─╯ |
2451 | + # a |
2452 | + |
2453 | + state = loggraphviz.GraphVizFilterState(gv) |
2454 | + |
2455 | + # just mainline showing |
2456 | + self.assertFilteredRevisions('ca', state) |
2457 | + |
2458 | + # bla - we need a computed to call collapse_expand_rev |
2459 | + # expand 'c' |
2460 | + state.collapse_expand_rev(gv.compute_viz(state).filtered_revs[0]) |
2461 | + |
2462 | + # all should be showing |
2463 | + self.assertFilteredRevisions('cba', state) |
2464 | + |
2465 | + # colapse 'c' |
2466 | + state.collapse_expand_rev(gv.compute_viz(state).filtered_revs[0]) |
2467 | + |
2468 | + # just mainline showing |
2469 | + self.assertFilteredRevisions('ca', state) |
2470 | + |
2471 | + def get_expanded_by_graph_provider(self): |
2472 | + gv = BasicGraphVizLoader(('f',), { |
2473 | + 'a': (NULL_REVISION, ), |
2474 | + 'b': ('a', ), |
2475 | + 'c': ('a', 'b'), |
2476 | + 'd': ('a', 'c', ), |
2477 | + 'e': ('b', ), |
2478 | + 'f': ('d', 'e') |
2479 | + }) |
2480 | + gv.load() |
2481 | + # f |
2482 | + # ├───╮ |
2483 | + # │ e |
2484 | + # │ │ |
2485 | + # d │ |
2486 | + # ├─╮ │ |
2487 | + # │ c │ |
2488 | + # │ │\│ |
2489 | + # │ │ b |
2490 | + # ├─╯─╯ |
2491 | + # a |
2492 | + return gv |
2493 | + |
2494 | + def test_collapse_colapses_sub_expand(self): |
2495 | + gv = self.get_expanded_by_graph_provider() |
2496 | + |
2497 | + state = loggraphviz.GraphVizFilterState(gv) |
2498 | + # just mainline showing |
2499 | + self.assertFilteredRevisions('fda', state) |
2500 | + |
2501 | + # expand 'd' |
2502 | + state.collapse_expand_rev(gv.compute_viz(state).revisions[2]) |
2503 | + # branchline c now showing |
2504 | + self.assertFilteredRevisions('fdca', state) |
2505 | + |
2506 | + # expand 'c' |
2507 | + state.collapse_expand_rev(gv.compute_viz(state).revisions[3]) |
2508 | + # all showing |
2509 | + self.assertFilteredRevisions('fedcba', state) |
2510 | + |
2511 | + # colapse 'd' |
2512 | + state.collapse_expand_rev(gv.compute_viz(state).filtered_revs[2]) |
2513 | + # cause c expanded branchline eb, and d expanded c, d colapses |
2514 | + # just mainline showing |
2515 | + self.assertFilteredRevisions('fda', state) |
2516 | + |
2517 | + def test_collapse_dosent_colapses_prev_expand(self): |
2518 | + gv = self.get_expanded_by_graph_provider() |
2519 | + |
2520 | + state = loggraphviz.GraphVizFilterState(gv) |
2521 | + # just mainline showing |
2522 | + self.assertFilteredRevisions('fda', state) |
2523 | + |
2524 | + # expand 'f' |
2525 | + state.collapse_expand_rev(gv.compute_viz(state).revisions[0]) |
2526 | + # branchline eb now showing |
2527 | + self.assertFilteredRevisions('fedba', state) |
2528 | + |
2529 | + # expand 'd' |
2530 | + state.collapse_expand_rev(gv.compute_viz(state).revisions[2]) |
2531 | + # all showing |
2532 | + self.assertFilteredRevisions('fedcba', state) |
2533 | + |
2534 | + # colapse 'd' |
2535 | + state.collapse_expand_rev(gv.compute_viz(state).filtered_revs[2]) |
2536 | + # cause branchline eb was expanded by f, and not d, collapsing d dose |
2537 | + # not collapse branchline eb, even though it expanded it |
2538 | + # branchline eb and mainline left showing |
2539 | + self.assertFilteredRevisions('fedba', state) |
2540 | + |
2541 | + def test_collapse_deep_expanded_by(self): |
2542 | + # This use to error at one point |
2543 | + gv = BasicGraphVizLoader(('g',), { |
2544 | + 'a': (NULL_REVISION, ), |
2545 | + 'b': ('a', ), |
2546 | + 'c': ('a', 'b'), |
2547 | + 'd': ('a', 'c', ), |
2548 | + 'e': ('b', ), |
2549 | + 'f': ('d', 'e'), |
2550 | + 'g': ('a', 'f'), |
2551 | + }) |
2552 | + # g v-----1.3 |
2553 | + # ├─╮ |
2554 | + # │ f v-1.1 |
2555 | + # │ ├───╮ |
2556 | + # │ │ e |
2557 | + # │ │ │ |
2558 | + # │ d v-│-1.2 |
2559 | + # │ ├─╮ │ |
2560 | + # │ │ c │ |
2561 | + # │ │ │\│ |
2562 | + # │ │ │ b |
2563 | + # ├─╯─╯─╯ |
2564 | + # a |
2565 | + |
2566 | + gv.load() |
2567 | + |
2568 | + state = loggraphviz.GraphVizFilterState(gv) |
2569 | + # just mainline showing |
2570 | + self.assertFilteredRevisions('ga', state) |
2571 | + |
2572 | + # expand 'g' |
2573 | + state.collapse_expand_rev(gv.compute_viz(state).revisions[0]) |
2574 | + # branchline fd now showing |
2575 | + self.assertFilteredRevisions('gfda', state) |
2576 | + |
2577 | + # expand 'f' |
2578 | + state.collapse_expand_rev(gv.compute_viz(state).revisions[1]) |
2579 | + # branchline eb now showing |
2580 | + self.assertFilteredRevisions('gfedba', state) |
2581 | + |
2582 | + # expand 'd' |
2583 | + state.collapse_expand_rev(gv.compute_viz(state).revisions[3]) |
2584 | + # branchline c now showing (all showing) |
2585 | + self.assertFilteredRevisions('gfedcba', state) |
2586 | + |
2587 | + # colapse 'g' |
2588 | + state.collapse_expand_rev(gv.compute_viz(state).filtered_revs[0]) |
2589 | + # back to just mainline showing |
2590 | + self.assertFilteredRevisions('ga', state) |
2591 | + |
2592 | + def test_filter(self): |
2593 | + gv = BasicGraphVizLoader(('e',), { |
2594 | + 'a': (NULL_REVISION, ), |
2595 | + 'b': ('a', ), |
2596 | + 'c': ('a', 'b'), |
2597 | + 'd': ('c', ), |
2598 | + 'e': ('d', ), |
2599 | + }) |
2600 | + gv.load() |
2601 | + # e |
2602 | + # │ |
2603 | + # d |
2604 | + # │ |
2605 | + # c |
2606 | + # ├─╮ |
2607 | + # │ b |
2608 | + # ├─╯ |
2609 | + # a |
2610 | + |
2611 | + state = loggraphviz.GraphVizFilterState(gv) |
2612 | + |
2613 | + # expand 'c' |
2614 | + state.collapse_expand_rev(gv.compute_viz(state).filtered_revs[2]) |
2615 | + |
2616 | + # all should be showing |
2617 | + self.assertFilteredRevisions('edcba', state) |
2618 | + |
2619 | + state.filters.append(BasicFilterer(('d', 'c', 'a'))) |
2620 | + state.filter_changed() |
2621 | + # d and a not showing bucause of filter |
2622 | + # c shows even though it is filtered, because it merges a revision |
2623 | + # that is not filtered. |
2624 | + self.assertFilteredRevisions('ecb', state) |
2625 | + |
2626 | + |
2627 | + |
2628 | +class BasicGraphVizLoader(loggraphviz.GraphVizLoader): |
2629 | + |
2630 | + def __init__(self, heads, graph_dict, no_graph=False): |
2631 | + self.heads = heads |
2632 | + self.graph_dict = graph_dict |
2633 | + bi = loggraphviz.BranchInfo(None, None, None) |
2634 | + loggraphviz.GraphVizLoader.__init__(self, [bi], bi, no_graph) |
2635 | + |
2636 | + def load(self): |
2637 | + for head in self.heads: |
2638 | + self.append_head_info(head, self.branches[0], head) |
2639 | + |
2640 | + self.process_graph_parents(self.heads, self.graph_dict.items()) |
2641 | + |
2642 | + self.compute_head_info() |
2643 | + |
2644 | + if not self.no_graph: |
2645 | + self.compute_branch_lines() |
2646 | + self.compute_merge_info() |
2647 | + |
2648 | + |
2649 | +class BasicFilterer(object): |
2650 | + def __init__(self, filtered_revids): |
2651 | + self.filtered_revids = filtered_revids |
2652 | + |
2653 | + def get_revision_visible(self, rev): |
2654 | + return rev.revid not in self.filtered_revids |
2655 | + |
2656 | +def print_computed(computed): |
2657 | + print_lines([(c_rev.rev.revid, |
2658 | + c_rev.col_index, |
2659 | + c_rev.twisty_state, |
2660 | + c_rev.lines,) |
2661 | + for c_rev in computed.filtered_revs]) |
2662 | + |
2663 | +def print_lines(list): |
2664 | + print '' |
2665 | + print format_graph_lines(list) |
2666 | + |
2667 | +def format_graph_lines(list, use_unicode=True): |
2668 | + if not list: |
2669 | + return list.__repr__() + '\n' |
2670 | + s = StringIO() |
2671 | + item_repr = [item.__repr__() for item in list] |
2672 | + repr_width = max([len(repr) for repr in item_repr]) |
2673 | + if use_unicode: |
2674 | + twisty_char = {None: u'○', |
2675 | + True: u'⊖', |
2676 | + False: u'⊕'} |
2677 | + ver_char = {True: u'│', |
2678 | + False: u'┆'} |
2679 | + |
2680 | + hor_char = {True: {u' ': u'─', |
2681 | + u'│': u'┼', |
2682 | + u'┆': u'┼'}, |
2683 | + False: {u' ': u'┄', |
2684 | + u'│': u'┼', |
2685 | + u'┆': u'┼'}} |
2686 | + |
2687 | + tl_char = {u' ': u'╭', |
2688 | + u'│': u'├', |
2689 | + u'┆': u'├', |
2690 | + u'─': u'┬', |
2691 | + u'┄': u'┬', |
2692 | + u'┴': u'┼', |
2693 | + u'┤': u'┼',} |
2694 | + |
2695 | + tr_char = {u' ': u'╮', |
2696 | + u'│': u'┤', |
2697 | + u'┆': u'┤', |
2698 | + u'─': u'┬', |
2699 | + u'┄': u'┬', |
2700 | + u'┴': u'┼', |
2701 | + u'├': u'┼',} |
2702 | + |
2703 | + bl_char = {u' ': u'╰', |
2704 | + u'│': u'├', |
2705 | + u'┆': u'├', |
2706 | + u'─': u'┴', |
2707 | + u'┄': u'┴', |
2708 | + u'┬': u'┼', |
2709 | + u'┤': u'┼',} |
2710 | + |
2711 | + br_char = {u' ': u'╯', |
2712 | + u'│': u'┤', |
2713 | + u'┆': u'┤', |
2714 | + u'─': u'┴', |
2715 | + u'┄': u'┴', |
2716 | + u'┬': u'┼', |
2717 | + u'├': u'┼',} |
2718 | + else: |
2719 | + twisty_char = {None: '*', |
2720 | + True: '~', |
2721 | + False: '+'} |
2722 | + ver_char = {True: '|', |
2723 | + False: ':'} |
2724 | + |
2725 | + hor_char = {True: {' ': '-',}, |
2726 | + False: {' ': '-'}} |
2727 | + |
2728 | + tl_char = {} |
2729 | + |
2730 | + tr_char = {} |
2731 | + |
2732 | + bl_char = {} |
2733 | + |
2734 | + br_char = {} |
2735 | + |
2736 | + for row, (item, repr) in enumerate(zip(list, item_repr)): |
2737 | + if row == 0: |
2738 | + s.write('[') |
2739 | + else: |
2740 | + s.write(' ') |
2741 | + s.write(repr.ljust(repr_width)) |
2742 | + if row == len(list)-1: |
2743 | + s.write('] # ') |
2744 | + else: |
2745 | + s.write(', # ') |
2746 | + |
2747 | + if len(item) == 4: |
2748 | + revid, col_index, twisty_state, lines = item |
2749 | + if len(item) == 5: |
2750 | + revid, col_index, twisty_state, lines, labels = item |
2751 | + |
2752 | + all_cols = [col_index] |
2753 | + all_cols += [start for start, end, color, direct in lines] |
2754 | + all_cols += [end for start, end, color, direct in lines] |
2755 | + num_cols = (max(all_cols) + 1) * 2 |
2756 | + this_line = [' ' for i in range(num_cols)] |
2757 | + next_line = [' ' for i in range(num_cols)] |
2758 | + |
2759 | + for start, end, color, direct in lines: |
2760 | + if start is None or end is None: |
2761 | + continue |
2762 | + start = int(round(start)) |
2763 | + end = int(round(end)) |
2764 | + if start == end: |
2765 | + this_line[start * 2] = ver_char[direct] |
2766 | + next_line[start * 2] = ver_char[direct] |
2767 | + else: |
2768 | + this_line[start * 2] = ver_char[direct] |
2769 | + |
2770 | + def replace_char(line, i, char_dict): |
2771 | + old_char = line[i] |
2772 | + if old_char in char_dict: |
2773 | + line[i] = char_dict[old_char] |
2774 | + |
2775 | + for start, end, color, direct in lines: |
2776 | + if start is None or end is None: |
2777 | + continue |
2778 | + start = int(round(start)) |
2779 | + end = int(round(end)) |
2780 | + if start < end: |
2781 | + for i in range(start * 2 + 1, end * 2): |
2782 | + replace_char(next_line, i, hor_char[direct]) |
2783 | + replace_char(next_line, start * 2, bl_char) |
2784 | + replace_char(next_line, end * 2, tr_char) |
2785 | + elif start > end: |
2786 | + for i in range(end * 2 + 1, start * 2): |
2787 | + replace_char(next_line, i, hor_char[direct]) |
2788 | + replace_char(next_line, start * 2, br_char) |
2789 | + replace_char(next_line, end * 2, tl_char) |
2790 | + |
2791 | + this_line[int(col_index * 2)] = twisty_char[twisty_state] |
2792 | + |
2793 | + s.write(''.join(this_line)) |
2794 | + s.write('\n') |
2795 | + |
2796 | + if not row == len(list)-1: |
2797 | + s.write('# '.rjust(repr_width + 5)) |
2798 | + s.write(''.join(next_line)) |
2799 | + s.write('\n') |
2800 | + return s.getvalue() |
2801 | + |
2802 | + |
2803 | +class TestGroupOverlapping(TestCase): |
2804 | + def test_group_overlapping(self): |
2805 | + lines = [ |
2806 | + (['a1'], 1, 3, 'a'), |
2807 | + (['a2'], 2, 5, 'a'), |
2808 | + (['a3'], 4, 6, 'a'), |
2809 | + (['a4'], 6, 8, 'a'), |
2810 | + (['b1'], 1, 3, 'b'), |
2811 | + (['b2'], 2, 5, 'b'), |
2812 | + (['n1'], 1, 8, None), |
2813 | + (['n2'], 1, 8, None), |
2814 | + ] |
2815 | + groups = loggraphviz.group_overlapping(lines) |
2816 | + self.assertEqual( |
2817 | + [(['a1', 'a2', 'a3'], 1, 6, 'a'), |
2818 | + (['a4'], 6, 8, 'a'), |
2819 | + (['b1', 'b2'], 1, 5, 'b'), |
2820 | + (['n1'], 1, 8, None), |
2821 | + (['n2'], 1, 8, None)], |
2822 | + groups) |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
On 11/18/2010 4:02 AM, Gary van der Merwe wrote:
> Gary van der Merwe has proposed merging lp:~garyvdm/bzr/loggraphviz into lp:bzr.
>
> Requested reviews:
> bzr-core (bzr-core)
>
>
> This branch includes the loggraphviz module from qbzr, and is the module that implements the most of the logic for qlog, which I have refactored in the hopes that it may be used other plugins.
>
> I have implemented a proof of concept loggerhead viz view which use this. This is available here: lp:~garyvdm/loggerhead/loggraphviz
>
> There is still lots of work that I would like to do before this actually lands. E.g.:
> * The doc strings need to be completed, improved, and reviewed.
> * I want to have the api between the State objects, and Filters, especially how one adds a filter to a state more clearly defined.
> * I need to mark private/internal methods with _.
> * I want to do a bzr-gtk viz poc. viz has a number of features that qlog does not, i.e. progress bar, a revision command line argument to specify a tip., and a limit argument. Implementing these my result in api changes.
> * I would like to still investigate doing partial loading of merge sorted revisions using bzr-history-db for the use of loggerhead. This may also result in api changes.
>
> So I know this is not 100% ready, but I feel that as I have now done the loggerhead poc, I now feel the time is right to get some comments on the code.
To start off, I think the work you've done on graph visualization is
great, and I agree that it would be good to pull the core of that into
bzrlib itself.
'loggraphviz' is a bit clumsy for me. I don't have great ideas, but at visualization" , or even just "graph_viz" would be reasonable.
the very least it should be "log_graph_viz". I think calling it
"graph_
I don't think you need to call each class GraphVizFoo inside the
loggraphviz module, though we've had discussions about that (without
strict resolution, afaict).
For naming, I would think that something called "Loader()" would return
the state object as part of .load(), rather than internalizing it. Your
example is:
+.. python:: BranchInfo( 'branch- label', tree, branch) GraphVizLoader( [bi], bi, False) GraphVizFilterS tate(graph_ viz) compute_ viz(state)
+ bi = loggraphviz.
+ graph_viz = loggraphviz.
+ graph_viz.load()
+ state = loggraphviz.
+ computed = graph_viz.
I would say,
1) Do we need the BranchInfo? Could it be done slightly differently? add_branch( 'branch- label', branch) add_branch( 'branch- label', branch, tree=<optional>)
loader = GraphVizLoader()
loader.
# loader.add_tree() ?
# loader.
state = loader.process()
for node in state.iter_nodes():
...
Basically, BranchInfo can be an implementation detail, rather than
something you create and-then-add.
+ def __eq__(self, other): base.__ eq__(other. branch. base)
+ if isinstance(other, BranchInfo):
+ return self.branch.
+ return False
^- it seems odd that you don't check whether the label, presence of a branch( other)"
tree, or index are the same. I wonder if we really want to use "__eq__"
for this comparison, versus ".same_
Namely setting __hash__ and __eq__ in this w...