Merge lp:~garyvdm/bzr/loggraphviz into lp:bzr

Proposed by Gary van der Merwe
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
Reviewer Review Type Date Requested Status
bzr-core Pending
Review via email: mp+41146@code.launchpad.net

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.

To post a comment you must log in.
lp:~garyvdm/bzr/loggraphviz updated
5539. By Gary van der Merwe

Restore the utf marker in test_loggraphviz.py.

Revision history for this message
John A Meinel (jameinel) wrote :
Download full text (6.7 KiB)

-----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
the very least it should be "log_graph_viz". I think calling it
"graph_visualization", or even just "graph_viz" would be reasonable.

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.BranchInfo('branch-label', tree, branch)
+ graph_viz = loggraphviz.GraphVizLoader([bi], bi, False)
+ graph_viz.load()
+ state = loggraphviz.GraphVizFilterState(graph_viz)
+ computed = graph_viz.compute_viz(state)

I would say,

1) Do we need the BranchInfo? Could it be done slightly differently?
 loader = GraphVizLoader()
 loader.add_branch('branch-label', branch)
 # loader.add_tree() ?
 # loader.add_branch('branch-label', branch, tree=<optional>)
 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):
+ if isinstance(other, BranchInfo):
+ return self.branch.base.__eq__(other.branch.base)
+ 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_branch(other)"

Namely setting __hash__ and __eq__ in this w...

Read more...

Revision history for this message
John A Meinel (jameinel) wrote :
Download full text (8.4 KiB)

-----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_visualization", or even just "graph_viz" would be reasonable.

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.BranchInfo('branch-label', tree, branch)
+ graph_viz = loggraphviz.GraphVizLoader([bi], bi, False)
+ graph_viz.load()
+ state = loggraphviz.GraphVizFilterState(graph_viz)
+ computed = graph_viz.compute_viz(state)

I would say,

1) Do we need the BranchInfo? Could it be done slightly differently?
 loader = GraphVizLoader()
 loader.add_branch('branch-label', branch)
 # loader.add_tree() ?
 # loader.add_branch('branch-label', branch, tree=<optional>)
 state = loader.proce...

Read more...

Revision history for this message
Gary van der Merwe (garyvdm) wrote :
Download full text (10.2 KiB)

-----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_visualization", or even just "graph_viz" would be reasonable.

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.BranchInfo('branch-label', tree, branch)
> + graph_viz = loggraphviz.GraphVizLoader([bi], bi, False)
> + graph_viz.load()
> + state = loggraphviz.GraphVizFilterState(graph_viz)
> + computed = graph_viz.compute_viz(state)
>
> 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.add_branch('branch-label', branch)
> # loader.add_tree() ?
> # loader.add_branch('branch-label', branch, tree=<optional>)
> 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.base.__eq__(other.branch.base)
> + 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_branch(other)"
>
> 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...

Revision history for this message
John A Meinel (jameinel) wrote :
Download full text (3.6 KiB)

-----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/tests/test_graph.py and it worked well for me.

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.assertComputed(
+ [('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...

Read more...

Revision history for this message
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).

Revision history for this message
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. :(

Revision history for this message
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_loggraphviz.py.

5538. By Gary van der Merwe

Make it possible to call GraphVizFilterState.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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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)