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
=== added file 'bzrlib/loggraphviz.py'
--- bzrlib/loggraphviz.py 1970-01-01 00:00:00 +0000
+++ bzrlib/loggraphviz.py 2010-11-18 10:15:44 +0000
@@ -0,0 +1,1648 @@
1# Copyright (C) 2009, 2010 Canonical Ltd
2#
3# This program is free software; you can redistribute it and/or
4# modify it under the terms of the GNU General Public License
5# as published by the Free Software Foundation; either version 2
6# of the License, or (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16
17"""
18Layout a revision graph for visual representation, allowing for filtering and
19collapsible branches.
20
21Usage example
22=============
23
24.. python::
25 bi = loggraphviz.BranchInfo('branch-label', tree, branch)
26 graph_viz = loggraphviz.GraphVizLoader([bi], bi, False)
27 graph_viz.load()
28 state = loggraphviz.GraphVizFilterState(graph_viz)
29 computed = graph_viz.compute_viz(state)
30
31"""
32
33import gc
34from itertools import izip
35
36from bzrlib import errors
37from bzrlib.bzrdir import BzrDir
38from bzrlib.transport.local import LocalTransport
39from bzrlib.revision import NULL_REVISION, CURRENT_REVISION
40from bzrlib.graph import (
41 Graph,
42 StackedParentsProvider,
43 KnownGraph,
44 DictParentsProvider,
45 )
46
47
48class BranchInfo(object):
49 """Wrapper for a branch, it's working tree, if available, and a label."""
50
51 def __init__(self, label, tree, branch, index=None):
52 self.label = label
53 self.tree = tree
54 self.branch = branch
55 self.index = index
56
57 def __hash__(self):
58 return self.branch.base.__hash__()
59
60 def __eq__(self, other):
61 if isinstance(other, BranchInfo):
62 return self.branch.base.__eq__(other.branch.base)
63 return False
64
65
66class RevisionData(object):
67 """
68 Container for data for a revision in the graph that gets calculated
69 when the graph is loaded.
70 """
71
72 # Instance of this object are typically named "rev".
73
74 __slots__ = ["index", "_merge_sort_node", "branch", "_revno_str",
75 "merges", "merged_by", 'branch_id', 'color']
76
77 def __init__(self, index, merge_sort_node):
78 """Create a new RevisionData instance."""
79 self.index = index
80 self._merge_sort_node = merge_sort_node
81 self.branch = None
82 self._revno_str = None
83 self.merges = []
84 """Revision indexes that this revision merges"""
85 self.merged_by = None
86 """Revision index that merges this revision."""
87 self.branch_id = self._merge_sort_node.revno[0:-1]
88 self.color = reduce(lambda x, y: x + y, self.branch_id, 0)
89
90 revid = property(lambda self: self._merge_sort_node.key)
91 merge_depth = property(lambda self: self._merge_sort_node.merge_depth)
92 revno_sequence = property(lambda self: self._merge_sort_node.revno)
93 end_of_merge = property(lambda self: self._merge_sort_node.end_of_merge)
94
95 def get_revno_str(self):
96 if self._revno_str is None:
97 self._revno_str = ".".join(["%d" % (revno)
98 for revno in self.revno_sequence])
99 if self.revid.startswith(CURRENT_REVISION):
100 self._revno_str += " ?"
101 return self._revno_str
102 revno_str = property(get_revno_str)
103
104 def __repr__(self):
105 return "%s <%s %s>" % (self.__class__.__name__, self.revno_str,
106 self.revid)
107
108class BranchLine(object):
109 """Container for data for a branch line, aka merge line."""
110
111 __slots__ = ["branch_id", "revs", "merges", "merged_by",
112 "color", "merge_depth"]
113
114 def __init__(self, branch_id):
115 self.branch_id = branch_id
116 self.revs = []
117 self.merges = []
118 self.merged_by = []
119 self.merge_depth = 0
120
121 def __repr__(self):
122 return "%s <%s>" % (self.__class__.__name__, self.branch_id)
123
124
125class GhostRevisionError(errors.InternalBzrError):
126
127 _fmt = "{%(revision_id)s} is a ghost."
128
129 def __init__(self, revision_id):
130 errors.BzrError.__init__(self)
131 self.revision_id = revision_id
132
133
134class GraphVizLoader(object):
135 """
136 Loads graph for branches and provides computed layout for visual
137 representation.
138 """
139
140 # Most list/dicts related to revisions are unfiltered. When we do a graph
141 # layout, we filter these revisions. A revision may be filter out because:
142 # * It's branch is hidden (or collapsed).
143 # * We have a specified file_id(s), and the revision does not touch the
144 # file_id(s).
145 # * We have a search, and the revision does not match the search.
146 #
147 # The main list of unfiltered revisions is self.revisions. A revisions
148 # index in revisions are normally called index. The main list of filtered
149 # revisions is filtered_revs. Revision indexes in this list are called
150 # f_index.
151
152 def __init__(self, branches, primary_bi, no_graph):
153 self.branches = branches
154 """List of BranchInfo for each branch."""
155 self.primary_bi = primary_bi
156 self.no_graph = no_graph
157
158 self.repos = []
159 self.local_repo_copies = []
160 """A list of repositories that revisions will be attempted to be loaded
161 from first."""
162
163 self.revid_head_info = {}
164 """Dict with a keys of head revid and value of
165 (list of (branch, label),
166 list of revids that are unique to this head)
167
168 We can't just store the BranchInfo, because the label may have
169 have addition text in it, e.g. "Working Tree", "Pending Merges"
170 """
171
172 self.revid_branch_info = {}
173
174 self.ghosts = set()
175
176 self.revisions = []
177 """List of RevisionInfo from merge_sort."""
178
179 self.revid_rev = {}
180 self.graph_children = {}
181
182 self.tags = {} # map revid -> tags set
183
184 def load(self):
185 # Get a unique list of repositories. If the url is the same,
186 # we consider it the same repositories
187 repo_urls = set()
188 for bi in self.branches:
189 repo = bi.branch.repository
190 if repo.base not in repo_urls:
191 repo_urls.add(repo.base)
192 self.repos.append(repo)
193
194 no_local_repos = True
195 for repo in self.repos:
196 if repo_is_local(repo):
197 no_local_repos = False
198 if no_local_repos:
199 self.load_current_dir_repo()
200 self.repos.sort(key=lambda repo: not repo_is_local(repo))
201
202 self.lock_read_branches()
203 try:
204 head_revids, graph_parents = self.load_graph_parents()
205 self.process_graph_parents(head_revids, graph_parents)
206
207 self.compute_head_info()
208 del self.graph
209
210 if not self.no_graph:
211 self.compute_branch_lines()
212 self.compute_merge_info()
213
214 self.load_tags()
215 finally:
216 self.unlock_branches()
217
218
219 def load_current_dir_repo(self):
220 # There are no local repositories. Try open the repository
221 # of the current directory, and try load revisions data from
222 # this before trying from remote repositories. This makes
223 # the common use case of viewing a remote branch that is
224 # related to the current branch much faster, because most
225 # of the revision can be loaded from the local repository.
226 try:
227 bzrdir, relpath = BzrDir.open_containing(u".")
228 repo = bzrdir.find_repository()
229 self.repos.add(repo)
230 self.local_repo_copies.append(repo)
231 except Exception:
232 pass
233
234 def update_ui(self):
235 pass
236
237 def throbber_show(self):
238 pass
239
240 def throbber_hide(self):
241 pass
242
243 def lock_read_branches(self):
244 for bi in self.branches:
245 bi.branch.lock_read()
246 for repo in self.repos:
247 repo.lock_read()
248
249 def unlock_branches(self):
250 for bi in self.branches:
251 bi.branch.unlock()
252 for repo in self.repos:
253 repo.unlock()
254
255 #def lock_read_repos(self):
256 # for repo in self.repos.itervalues():
257 # repo.lock_read()
258 #
259 #def unlock_repos(self):
260 # for repo in self.repos.itervalues():
261 # repo.unlock()
262
263 def load_tags(self):
264 self.tags = {}
265 for bi in self.branches:
266 # revid to tags map
267 branch_tags = bi.branch.tags.get_reverse_tag_dict()
268 for revid, tags in branch_tags.iteritems():
269 if revid in self.tags:
270 self.tags[revid].update(set(tags))
271 else:
272 self.tags[revid] = set(tags)
273
274 def append_head_info(self, revid, branch_info, tag):
275 if not revid == NULL_REVISION:
276 if not revid in self.revid_head_info:
277 self.revid_head_info[revid] = ([], [])
278 self.revid_head_info[revid][0].append((branch_info, tag))
279
280 # So that early calls to get_revid_branch work
281 self.revid_branch_info[revid] = branch_info
282
283 def load_branch_heads(self, bi):
284 branch_heads = []
285
286 def append_head_info(revid, branch_info, label):
287 self.append_head_info(revid, branch_info, label)
288 branch_heads.append(revid)
289
290 if len(self.branches) > 0:
291 label = bi.label
292 else:
293 label = None
294
295 branch_last_revision = bi.branch.last_revision()
296 append_head_info(branch_last_revision, bi, bi.label)
297 self.update_ui()
298
299 if bi.tree:
300 parent_ids = bi.tree.get_parent_ids()
301 if parent_ids:
302 # first parent is last revision of the tree
303 revid = parent_ids[0]
304 if revid != branch_last_revision:
305 # working tree is out of date
306 if label:
307 wt_label = "%s - Working Tree" % label
308 else:
309 wt_label = "Working Tree"
310 append_head_info(revid, bi, wt_label)
311 # other parents are pending merges
312 for revid in parent_ids[1:]:
313 if label:
314 pm_label = "%s - Pending Merge" % label
315 else:
316 pm_label = "Pending Merge"
317 append_head_info(revid, bi, pm_label)
318 self.update_ui()
319 return branch_heads, branch_heads, ()
320
321 def load_graph_parents(self):
322 """Load the heads of the graph, and the graph parents"""
323
324 extra_parents = {}
325 branches_heads = []
326
327 def load_branch_heads(bi, insert_at_begin=False):
328 load_heads, sort_heads, extra_parents_ = self.load_branch_heads(bi)
329 for key, parents in extra_parents_:
330 extra_parents[key] = parents
331 if insert_at_begin:
332 branches_heads.insert(0, (load_heads, sort_heads))
333 else:
334 branches_heads.append((load_heads, sort_heads))
335
336 for bi in self.branches:
337 # Don't do the primary branch, as that will be inserted later at
338 # the first position.
339 if bi != self.primary_bi:
340 load_branch_heads(bi)
341
342 if len(branches_heads) >= 2:
343 head_revids = [revid for load_heads, sort_heads in branches_heads
344 for revid in load_heads]
345 head_revs = self.load_revisions(head_revids)
346
347 get_max_timestamp = lambda branch_heads: max(
348 [head_revs[revid].timestamp for revid in branch_heads[0]])
349 branches_heads.sort(key=get_max_timestamp, reverse=True)
350
351 if self.primary_bi:
352 load_branch_heads(self.primary_bi, True)
353
354 load_heads = [revid for load_heads_, sort_heads_ in branches_heads
355 for revid in load_heads_]
356 sort_heads = [revid for load_heads_, sort_heads_ in branches_heads
357 for revid in sort_heads_]
358
359 parents_providers = [repo._make_parents_provider() \
360 for repo in self.repos]
361 parents_providers.append(DictParentsProvider(extra_parents))
362 self.graph = Graph(StackedParentsProvider(parents_providers))
363
364 return sort_heads, self.graph.iter_ancestry(sort_heads)
365
366 def process_graph_parents(self, head_revids, graph_parents_iter):
367 graph_parents = {}
368 self.ghosts = set()
369
370 for (revid, parent_revids) in graph_parents_iter:
371 if revid == NULL_REVISION:
372 continue
373 if parent_revids is None:
374 #Ghost
375 graph_parents[revid] = ()
376 self.ghosts.add(revid)
377 elif parent_revids == (NULL_REVISION,):
378 graph_parents[revid] = ()
379 else:
380 graph_parents[revid] = parent_revids
381 if len(graph_parents) % 100 == 0:
382 self.update_ui()
383
384 graph_parents["top:"] = head_revids
385
386 if len(graph_parents) > 0:
387 enabled = gc.isenabled()
388 if enabled:
389 gc.disable()
390
391 def make_kg():
392 return KnownGraph(graph_parents)
393 self.known_graph = make_kg()
394
395 merge_sorted_revisions = self.known_graph.merge_sort('top:')
396 # So far, we are a bit faster than the pure-python code. But the
397 # last step hurts. Specifically, we take
398 # 377ms KnownGraph(self.graph_parents)
399 # 263ms kg.merge_sort() [640ms combined]
400 # 1322ms self.revisions = [...]
401 # vs
402 # 1152ms tsort.merge_sort(self.graph_parents)
403 # 691ms self.revisions = [...]
404 #
405 # It is a gc thing... :(
406 # Adding gc.disable() / gc.enable() around this whole loop changes
407 # things to be:
408 # 100ms KnownGraph(self.graph_parents)
409 # 77ms kg.merge_sort() [177ms combined]
410 # 174ms self.revisions = [...]
411 # vs
412 # 639ms tsort.merge_sort(self.graph_parents)
413 # 150ms self.revisions = [...]
414 # Also known as "wow that's a lot faster". This is because KG()
415 # creates a bunch of Python objects, then merge_sort() creates a
416 # bunch more. And then self.revisions() creates another whole set.
417 # And all of these are moderately long lived, so you have a *lot*
418 # of allocations without removals (which triggers the gc checker
419 # over and over again.) And they probably don't live in cycles
420 # anyway, so you can skip it for now, and just run at the end.
421
422 # self.revisions *is* a little bit slower. Probably because pyrex
423 # MergeSortNodes use long integers rather than PyIntObject and thus
424 # create them on-the-fly.
425
426 # Get rid of the 'top:' revision
427 merge_sorted_revisions.pop(0)
428 self.revisions = [RevisionData(index, node)
429 for index, node in enumerate(merge_sorted_revisions)]
430 if enabled:
431 gc.enable()
432 else:
433 self.revisions = ()
434
435 self.revid_rev = {}
436 self.revno_rev = {}
437
438 self.max_mainline_revno = 0
439
440 for rev in self.revisions:
441 self.max_mainline_revno = max(self.max_mainline_revno,
442 rev.revno_sequence[0])
443 self.revid_rev[rev.revid] = rev
444 self.revno_rev[rev.revno_sequence] = rev
445
446 def branch_id_sort_key(self, x):
447 merge_depth = self.branch_lines[x].merge_depth
448
449 # Note: This greatly affects the layout of the graph.
450 #
451 # Branch line that have a smaller merge depth should be to the left
452 # of those with bigger merge depths.
453 #
454 # For branch lines that have the same parent in the mainline -
455 # those with bigger branch numbers to be to the rights. E.g. for
456 # the following dag, you want the graph to appear as on the left,
457 # not as on the right:
458 #
459 # 3 F_ F
460 # | \ |\
461 # 1.2.1 | E | E
462 # | | | \
463 # 2 D | D_|
464 # |\ | | +_
465 # 1.1.2 | C| | | C
466 # | |/ | \|
467 # 1.1.1 | B | B
468 # |/ | /
469 # 1 A A
470 #
471 # Otherwise, those with a greater mainline parent revno should
472 # appear to the left.
473
474 if len(x) == 0:
475 return (merge_depth)
476 else:
477 return (merge_depth, -x[0], x[1])
478
479 def compute_branch_lines(self):
480 self.branch_lines = {}
481
482 """A list of branch lines (aka merge lines).
483
484 For a revisions, the revision number less the least significant
485 digit is the branch_id, and used as the key for the dict. Hence
486 revision with the same revision number less the least significant
487 digit are considered to be in the same branch line. e.g.: for
488 revisions 290.12.1 and 290.12.2, the branch_id would be 290.12,
489 and these two revisions will be in the same branch line.
490
491 """
492
493 self.branch_ids = []
494 """List of branch ids, sorted in the order that the branches will
495 be shown, from left to right on the graph."""
496
497 for rev in self.revisions:
498
499 branch_line = None
500 if rev.branch_id not in self.branch_lines:
501 branch_line = BranchLine(rev.branch_id)
502 self.branch_lines[rev.branch_id] = branch_line
503 else:
504 branch_line = self.branch_lines[rev.branch_id]
505
506 branch_line.revs.append(rev)
507 branch_line.merge_depth = max(rev.merge_depth,
508 branch_line.merge_depth)
509 rev.branch = branch_line
510
511 self.branch_ids = self.branch_lines.keys()
512
513 self.branch_ids.sort(key=self.branch_id_sort_key)
514
515 def compute_merge_info(self):
516
517 def set_merged_by(rev, merged_by, merged_by_rev, do_branches=False):
518 if merged_by is None:
519 return
520
521 if merged_by_rev is None:
522 merged_by_rev = self.revisions[merged_by]
523 rev.merged_by = merged_by
524 merged_by_rev.merges.append(rev.index)
525
526 if do_branches:
527 branch_id = rev.branch_id
528 branch_merged_by = self.branch_lines[branch_id].merged_by
529 merged_by_branch_id = self.revisions[merged_by].branch_id
530 merged_by_branch_merges = \
531 self.branch_lines[merged_by_branch_id].merges
532
533 if not branch_id in merged_by_branch_merges:
534 merged_by_branch_merges.append(branch_id)
535 if not merged_by_branch_id in branch_merged_by:
536 branch_merged_by.append(merged_by_branch_id)
537
538 for rev in self.revisions:
539
540 parents = [self.revid_rev[parent] for parent in
541 self.known_graph.get_parent_keys(rev.revid)]
542
543 if len(parents) > 0:
544 if rev.branch_id == parents[0].branch_id:
545 set_merged_by(parents[0], rev.merged_by, None)
546
547 for parent in parents[1:]:
548 if rev.merge_depth <= parent.merge_depth:
549 set_merged_by(parent, rev.index, rev, do_branches=True)
550
551 def compute_head_info(self):
552 def get_revid_head(heads):
553 map = {}
554 for i in xrange(len(heads)):
555 prev_revids = [revid for revid, head in heads[:i]]
556 unique_ancestors = self.graph.find_unique_ancestors(
557 heads[i][0], prev_revids)
558 for ancestor_revid in unique_ancestors:
559 map[ancestor_revid] = heads[i][1]
560 return map
561
562 if len(self.branches) > 1:
563 head_revid_branch_info = sorted(
564 [(revid, branch_info)
565 for revid, (head_info, ur) in self.revid_head_info.iteritems()
566 for (branch_info, tag) in head_info],
567 key=lambda x: not repo_is_local(x[1].branch.repository))
568 self.revid_branch_info = get_revid_head(head_revid_branch_info)
569 else:
570 self.revid_branch_info = {}
571
572 head_count = 0
573 for head_info, ur in self.revid_head_info.itervalues():
574 head_count += len(head_info)
575
576 if head_count > 1:
577 # Populate unique revisions for heads
578 for revid, (head_info, ur) in self.revid_head_info.iteritems():
579 rev = None
580 if revid in self.revid_rev:
581 rev = self.revid_rev[revid]
582 if rev and rev.merged_by:
583 # This head has been merged.
584 # d
585 # |\
586 # b c
587 # |/
588 # a
589 # if revid == c,then we want other_revids = [b]
590
591 merged_by_revid = self.revisions[rev.merged_by].revid
592 other_revids = [self.known_graph
593 .get_parent_keys(merged_by_revid)[0]]
594 else:
595 other_revids = [other_revid for other_revid \
596 in self.revid_head_info.iterkeys() \
597 if not other_revid == revid]
598 ur.append(revid)
599 ur.extend([revid for revid \
600 in self.graph.find_unique_ancestors(revid, other_revids) \
601 if not revid == NULL_REVISION and revid in self.revid_rev])
602 ur.sort(key=lambda x: self.revid_rev[x].index)
603
604 def compute_viz(self, state):
605
606 # Overview:
607 # Work out which revision need to be displayed.
608 # Create ComputedGraphViz and ComputedRevisionData objects
609 # Assign columns for branches, and lines that go between branches.
610 # These are intermingled, because some of the lines need to come
611 # before it's branch, and others need to come after. Other lines
612 # (such a the line from the last rev in a branch) are treated a
613 # special cases.
614 # Return ComputedGraphViz object
615 gc_enabled = gc.isenabled()
616 gc.disable()
617 try:
618 computed = ComputedGraphViz(self)
619 computed.filtered_revs = [ComputedRevisionData(rev) for rev in
620 state.get_filtered_revisions()]
621
622 c_revisions = computed.revisions
623 for f_index, c_rev in enumerate(computed.filtered_revs):
624 c_revisions[c_rev.rev.index] = c_rev
625 c_rev.f_index = f_index
626
627 for (revid, (head_info,
628 unique_revids)) in self.revid_head_info.iteritems():
629
630 for unique_revid in unique_revids:
631 rev = self.revid_rev[unique_revid]
632 c_rev = c_revisions[rev.index]
633 if c_rev is not None:
634 c_rev.branch_labels.extend(head_info)
635 break
636 finally:
637 if gc_enabled:
638 gc.enable()
639
640 if self.no_graph:
641 for c_rev in computed.filtered_revs:
642 c_rev.col_index = c_rev.rev.merge_depth * 0.5
643 return computed
644
645 # This will hold a tuple of (child, parent, col_index, direct) for each
646 # line that needs to be drawn. If col_index is not none, then the line
647 # is drawn along that column, else the the line can be drawn directly
648 # between the child and parent because either the child and parent are
649 # in the same branch line, or the child and parent are 1 row apart.
650 lines = []
651 lines_by_column = []
652
653 def branch_line_col_search_order(start_col_index):
654 for col_index in range(start_col_index, len(lines_by_column)):
655 yield col_index
656 #for col_index in range(parent_col_index-1, -1, -1):
657 # yield col_index
658
659 def line_col_search_order(parent_col_index, child_col_index):
660 if parent_col_index is not None and child_col_index is not None:
661 max_index = max(parent_col_index, child_col_index)
662 min_index = min(parent_col_index, child_col_index)
663 # First yield the columns between the child and parent.
664 for col_index in range(max_index, min_index - 1, -1):
665 yield col_index
666 elif child_col_index is not None:
667 max_index = child_col_index
668 min_index = child_col_index
669 yield child_col_index
670 elif parent_col_index is not None:
671 max_index = parent_col_index
672 min_index = parent_col_index
673 yield parent_col_index
674 else:
675 max_index = 0
676 min_index = 0
677 yield 0
678 i = 1
679 # then yield the columns on either side.
680 while max_index + i < len(lines_by_column) or \
681 min_index - i > -1:
682 if max_index + i < len(lines_by_column):
683 yield max_index + i
684 #if min_index - i > -1:
685 # yield min_index - i
686 i += 1
687
688 def is_col_free_for_range(col_index, child_f_index, parent_f_index):
689 return not any(
690 range_overlaps(child_f_index, parent_f_index,
691 line_child_f_index, line_parent_f_index)
692 for line_child_f_index, line_parent_f_index
693 in lines_by_column[col_index])
694
695 def find_free_column(col_search_order, child_f_index, parent_f_index):
696 for col_index in col_search_order:
697 if is_col_free_for_range(col_index,
698 child_f_index, parent_f_index):
699 break
700 else:
701 # No free columns found. Add an empty one on the end.
702 col_index = len(lines_by_column)
703 lines_by_column.append([])
704 return col_index
705
706 def append_line(child, parent, direct, col_index=None):
707 lines.append((child, parent, col_index, direct))
708
709 if col_index is not None:
710 lines_by_column[int(round(col_index))].append(
711 (child.f_index, parent.f_index))
712
713 def find_visible_parent(c_rev, parent, twisty_hidden_parents):
714 if c_revisions[parent.index] is not None:
715 return (c_rev, c_revisions[parent.index], True)
716 else:
717 if parent.index in twisty_hidden_parents:
718 # no need to draw a line if there is a twisty,
719 # except if this is the last in the branch.
720 return None
721 # The parent was not visible. Search for a ancestor
722 # that is. Stop searching if we make a hop, i.e. we
723 # go away from our branch, and we come back to it.
724 has_seen_different_branch = False
725 while c_revisions[parent.index] is None:
726 if not parent.branch_id == c_rev.rev.branch_id:
727 has_seen_different_branch = True
728 # find grand parent.
729 g_parent_ids = (self.known_graph
730 .get_parent_keys(parent.revid))
731
732 if len(g_parent_ids) == 0:
733 return None
734 else:
735 parent = self.revid_rev[g_parent_ids[0]]
736
737 if (has_seen_different_branch and
738 parent.branch_id == branch_id):
739 # We have gone away and come back to our
740 # branch. Stop.
741 return None
742 if parent:
743 # Not Direct
744 return (c_rev, c_revisions[parent.index], False)
745
746 def append_branch_parent_lines(branch_rev_visible_parents):
747 groups = group_overlapping(branch_rev_visible_parents)
748 for parents, start, end, group_key in groups:
749 # Since all parents go from the same branch line to the
750 # same branch line, we can use the col indexes of the
751 # parent.
752 if end - start == 1:
753 col_index = None
754 else:
755 col_search_order = line_col_search_order(
756 parents[0][1].col_index, parents[0][0].col_index)
757 col_index = find_free_column(col_search_order,
758 start, end)
759
760 col_offset_increment = 1.0 / len(parents)
761 for i, (c_rev, parent_c_rev, direct) in enumerate(parents):
762 if col_index is None:
763 col_index_offset = None
764 else:
765 col_index_offset = (col_index - 0.5 +
766 (i * col_offset_increment) +
767 (col_offset_increment / 2))
768 append_line(c_rev, parent_c_rev,
769 direct, col_index_offset)
770
771 for branch_id in self.branch_ids:
772 if not branch_id in state.branch_line_state:
773 continue
774
775 branch_line = self.branch_lines[branch_id]
776 branch_revs = [c_revisions[rev.index]
777 for rev in branch_line.revs
778 if c_revisions[rev.index] is not None]
779
780 if not branch_revs:
781 continue
782
783 branch_rev_visible_parents_post = []
784 branch_rev_visible_parents_pre = []
785 # Lists of ([(c_rev, parent_c_rev, is_direct)],
786 # start, end, group_key]
787
788 last_c_rev = branch_revs[-1]
789 last_rev_left_parents = (self.known_graph
790 .get_parent_keys(last_c_rev.rev.revid))
791 if last_rev_left_parents:
792 last_parent = find_visible_parent(
793 last_c_rev, self.revid_rev[last_rev_left_parents[0]], [])
794 else:
795 last_parent = None
796
797 sprout_with_lines = {}
798
799 merged_by_max_col_index = 0
800
801 # In this loop:
802 # * Populate twisty_branch_ids and twisty_state
803 # * Find visible parents.
804 # * Append lines that go before the branch line.
805 # * Append lines to children for sprouts.
806 for c_rev in branch_revs:
807 rev = c_rev.rev
808
809 if rev.merged_by is not None:
810 merged_by_c_rev = c_revisions[rev.merged_by]
811 if merged_by_c_rev:
812 merged_by_max_col_index = max(
813 merged_by_max_col_index, merged_by_c_rev.col_index)
814
815 parents = [self.revid_rev[parent_revid] for parent_revid in
816 self.known_graph.get_parent_keys(rev.revid)]
817
818 twisty_hidden_parents = []
819 # Find and add necessary twisties
820 for parent in parents:
821 if parent.branch_id == branch_id:
822 continue
823 if parent.branch_id == ():
824 continue
825 if parent.branch_id in branch_line.merged_by:
826 continue
827 parent_branch = self.branch_lines[parent.branch_id]
828 # Does this branch have any visible revisions
829 pb_visible = (parent_branch.branch_id in
830 state.branch_line_state)
831 for pb_rev in parent_branch.revs:
832 if pb_visible:
833 visible = c_revisions[pb_rev.index] is not None
834 else:
835 visible = state\
836 .get_revision_visible_if_branch_visible(pb_rev)
837 if visible:
838 (c_rev.twisty_expands_branch_ids
839 .append(parent_branch.branch_id))
840 if not pb_visible:
841 twisty_hidden_parents.append(parent.index)
842 break
843
844 # Work out if the twisty needs to show a + or -. If all
845 # twisty_branch_ids are visible, show - else +.
846 if len(c_rev.twisty_expands_branch_ids) > 0:
847 c_rev.twisty_state = True
848 for twisty_branch_id in c_rev.twisty_expands_branch_ids:
849 if not twisty_branch_id in state.branch_line_state:
850 c_rev.twisty_state = False
851 break
852
853 # Don't include left hand parents All of these parents in the
854 # branch can be drawn with one line.
855 parents = parents[1:]
856
857 branch_id_sort_key = self.branch_id_sort_key(branch_id)
858 for i, parent in enumerate(parents):
859 parent_info = find_visible_parent(c_rev, parent,
860 twisty_hidden_parents)
861 if parent_info:
862 c_rev, parent_c_rev, direct = parent_info
863 if (last_parent and
864 parent_c_rev.f_index <= last_parent[1].f_index and
865 self.branch_id_sort_key(parent_c_rev.rev.branch_id)
866 < branch_id_sort_key):
867 # This line goes before the branch line
868 dest = branch_rev_visible_parents_pre
869 else:
870 # This line goes after
871 dest = branch_rev_visible_parents_post
872
873 line_len = parent_c_rev.f_index - c_rev.f_index
874 if line_len == 1:
875 group_key = None
876 else:
877 group_key = parent_c_rev.rev.branch_id
878
879 dest.append(([parent_info],
880 c_rev.f_index,
881 parent_c_rev.f_index,
882 group_key))
883
884 # This may be a sprout. Add line to first visible child
885 if c_rev.rev.merged_by is not None:
886 merged_by = self.revisions[c_rev.rev.merged_by]
887 if c_revisions[merged_by.index] is None and\
888 branch_revs[0].f_index == c_rev.f_index:
889 # The revision that merges this revision is not
890 # visible, and it is the first visible revision in
891 # the branch line. This is a sprout.
892 #
893 # XXX What if multiple merges with --force,
894 # aka octopus merge?
895 #
896 # Search until we find a descendant that is visible.
897
898 while merged_by is not None and \
899 c_revisions[merged_by.index] is None:
900 if merged_by.merged_by is not None:
901 merged_by = self.revisions[merged_by.merged_by]
902 else:
903 merged_by = None
904
905 if merged_by is not None:
906 # Ensure only one line to a descendant.
907 if (merged_by.index not in sprout_with_lines):
908 sprout_with_lines[merged_by.index] = True
909 parent = c_revisions[merged_by.index]
910 if parent is not None:
911 if c_rev.f_index - parent.f_index == 1:
912 col_index = None
913 else:
914 col_search_order = line_col_search_order(
915 parent.col_index, c_rev.col_index)
916 col_index = find_free_column(
917 col_search_order,
918 parent.f_index, c_rev.f_index)
919 append_line(parent, c_rev, False,
920 col_index)
921
922 # Find a column for this branch.
923 #
924 # Find the col_index for the direct parent branch. This will
925 # be the starting point when looking for a free column.
926
927 append_branch_parent_lines(branch_rev_visible_parents_pre)
928
929 if branch_id == ():
930 start_col_index = 0
931 else:
932 start_col_index = 1
933
934 if last_parent and last_parent[0].col_index is not None:
935 parent_col_index = last_parent[1].col_index
936 start_col_index = max(start_col_index, parent_col_index)
937
938 start_col_index = max(start_col_index, merged_by_max_col_index)
939
940 col_search_order = branch_line_col_search_order(start_col_index)
941
942 if last_parent:
943 col_index = find_free_column(col_search_order,
944 branch_revs[0].f_index,
945 last_parent[1].f_index)
946 else:
947 col_index = find_free_column(col_search_order,
948 branch_revs[0].f_index,
949 branch_revs[-1].f_index)
950
951 # Free column for this branch found. Set node for all
952 # revision in this branch.
953 for rev in branch_revs:
954 rev.col_index = col_index
955
956 append_line(branch_revs[0], branch_revs[-1], True, col_index)
957 if last_parent:
958 append_line(last_parent[0], last_parent[1],
959 last_parent[2], col_index)
960
961 branch_rev_visible_parents_post.reverse()
962 append_branch_parent_lines(branch_rev_visible_parents_post)
963
964 # It has now been calculated which column a line must go into. Now
965 # copy the lines in to computed_revisions.
966 for (child, parent, line_col_index, direct) in lines:
967
968 parent_color = parent.rev.color
969
970 line_length = parent.f_index - child.f_index
971 if line_length == 0:
972 # Nothing to do
973 pass
974 elif line_length == 1:
975 child.lines.append(
976 (child.col_index,
977 parent.col_index,
978 parent_color,
979 direct))
980 else:
981 # line from the child's column to the lines column
982 child.lines.append(
983 (child.col_index,
984 line_col_index,
985 parent_color,
986 direct))
987 # lines down the line's column
988 for line_part_f_index in range(child.f_index + 1,
989 parent.f_index - 1):
990 computed.filtered_revs[line_part_f_index].lines.append(
991 (line_col_index,
992 line_col_index,
993 parent_color,
994 direct))
995 # line from the line's column to the parent's column
996 computed.filtered_revs[parent.f_index - 1].lines.append(
997 (line_col_index,
998 parent.col_index,
999 parent_color,
1000 direct))
1001
1002 return computed
1003
1004 def get_revid_branch_info(self, revid):
1005 """This returns a branch info whos branch contains the revision.
1006
1007 If the revision exists more than one branch, it will only return the
1008 first branch info. """
1009
1010 if revid in self.ghosts:
1011 raise GhostRevisionError(revid)
1012
1013 if len(self.branches) == 1 or revid not in self.revid_branch_info:
1014 return self.branches[0]
1015 return self.revid_branch_info[revid]
1016
1017 def get_revid_branch(self, revid):
1018 return self.get_revid_branch_info(revid).branch
1019
1020 def get_revid_repo(self, revid):
1021 return self.get_revid_branch_info(revid).branch.repository
1022
1023 def get_repo_revids(self, revids):
1024 """Returns list of tuple of (repo, revids)"""
1025 repo_revids = {}
1026 for repo in self.repos:
1027 repo_revids[repo.base] = []
1028
1029 for local_repo_copy in self.local_repo_copies:
1030 for revid in self.repos[local_repo_copy].has_revisions(revids):
1031 revids.remove(revid)
1032 repo_revids[local_repo_copy].append(revid)
1033
1034 for revid in revids:
1035 try:
1036 repo = self.get_revid_repo(revid)
1037 except GhostRevisionError:
1038 pass
1039 else:
1040 repo_revids[repo.base].append(revid)
1041
1042 return [(repo, repo_revids[repo.base])
1043 for repo in self.repos]
1044
1045 def load_revisions(self, revids):
1046 return_revisions = {}
1047 for repo, revids in self.get_repo_revids(revids):
1048 if revids:
1049 repo.lock_read()
1050 try:
1051 self.update_ui()
1052 for rev in repo.get_revisions(revids):
1053 return_revisions[rev.revision_id] = rev
1054 finally:
1055 repo.unlock()
1056 return return_revisions
1057
1058
1059def repo_is_local(repo):
1060 return isinstance(repo.bzrdir.transport, LocalTransport)
1061
1062def group_overlapping(groups):
1063 """ Groups items with overlapping ranges together.
1064
1065 :param groups: List of uncollapsed groups.
1066 :param group: (start of range, end of range, items in group)
1067 :return: List of collapsed groups.
1068
1069 """
1070
1071 has_change = True
1072 while has_change:
1073 has_change = False
1074 a = 0
1075 while a < len(groups):
1076 inner_has_change = False
1077 items_a, start_a, end_a, group_key_a = groups[a]
1078 if group_key_a is not None:
1079 b = a + 1
1080 while b < len(groups):
1081 items_b, start_b, end_b, group_key_b = groups[b]
1082 if (group_key_a == group_key_b and
1083 range_overlaps(start_a, end_a, start_b, end_b)):
1084 # overlaps. Merge b into a
1085 items_a.extend(items_b)
1086 start_a = min(start_a, start_b)
1087 end_a = max(end_a, end_b)
1088 del groups[b]
1089 has_change = True
1090 inner_has_change = True
1091 else:
1092 b += 1
1093 if inner_has_change:
1094 groups[a] = (items_a, start_a, end_a, group_key_a)
1095 a += 1
1096
1097 return groups
1098
1099def range_overlaps (start_a, end_a, start_b, end_b):
1100 """Tests if two ranges overlap."""
1101 return (start_b < start_a < end_b or
1102 start_b < end_a < end_b or
1103 (start_a <= start_b and end_a >= end_b))
1104
1105
1106class PendingMergesGraphVizLoader(GraphVizLoader):
1107 """GraphVizLoader that only loads pending merges.
1108
1109 As only the pending merges are passed to merge_sort, the revno
1110 are incorrect, and should be ignored.
1111
1112 Only works on a single branch.
1113
1114 """
1115
1116 def load_graph_parents(self):
1117 if not len(self.branches) == 1 or not len(self.repos) == 1:
1118 AssertionError("load_graph_pending_merges should only be called \
1119 when 1 branch and repo has been opened.")
1120
1121 bi = self.branches[0]
1122 if bi.tree is None:
1123 AssertionError("PendingMergesGraphVizLoader must have a working "
1124 "tree.")
1125
1126 self.graph = bi.branch.repository.get_graph()
1127 tree_heads = bi.tree.get_parent_ids()
1128 other_revisions = [tree_heads[0], ]
1129 self.update_ui()
1130
1131 self.append_head_info('root:', bi, None)
1132 pending_merges = []
1133 for head in tree_heads[1:]:
1134 self.append_head_info(head, bi, None)
1135 pending_merges.extend(
1136 self.graph.find_unique_ancestors(head, other_revisions))
1137 other_revisions.append(head)
1138 self.update_ui()
1139
1140 graph_parents = self.graph.get_parent_map(pending_merges)
1141 graph_parents["root:"] = ()
1142 self.update_ui()
1143
1144 for (revid, parents) in graph_parents.items():
1145 new_parents = []
1146 for index, parent in enumerate(parents):
1147 if parent in graph_parents:
1148 new_parents.append(parent)
1149 elif index == 0:
1150 new_parents.append("root:")
1151 graph_parents[revid] = tuple(new_parents)
1152
1153 return ["root:", ] + tree_heads[1:], graph_parents.items()
1154
1155
1156class WithWorkingTreeGraphVizLoader(GraphVizLoader):
1157 """
1158 GraphVizLoader that shows uncommitted working tree changes as a node
1159 in the graph, as if it was already committed.
1160 """
1161
1162 def tree_revid(self, tree):
1163 return CURRENT_REVISION + tree.basedir.encode('unicode-escape')
1164
1165 def load(self):
1166 self.working_trees = {}
1167 for bi in self.branches:
1168 if not bi.tree is None:
1169 self.working_trees[self.tree_revid(bi.tree)] = bi.tree
1170
1171 super(WithWorkingTreeGraphVizLoader, self).load()
1172
1173 def load_branch_heads(self, bi):
1174 # returns load_heads, sort_heads and also calls append_head_info.
1175 #
1176 # == For branch with tree ==
1177 # Graph | load_heads | sort_heads | append_head_info
1178 # wt | No | Yes | Yes
1179 # | \ | | |
1180 # | 1.1.2 pending merge | Yes | No | Yes
1181 # 2 | basis rev | Yes | No | Yes
1182 #
1183 # == For branch with tree not up to date ==
1184 # Graph | load_heads | sort_heads | append_head_info
1185 # wt | No | Yes | Yes
1186 # | \ | | |
1187 # | 1.1.2 pending merge | Yes | No | Yes
1188 # 3/ | branch tip | Yes | Yes | Yes
1189 # 2 | basis rev | Yes | No | No
1190 #
1191 # == For branch without tree ==
1192 # branch tip | Yes | head | yes
1193
1194 load_heads = []
1195 sort_heads = []
1196 extra_parents = []
1197
1198 if len(self.branches) > 0:
1199 label = bi.label
1200 else:
1201 label = None
1202
1203 branch_last_revision = bi.branch.last_revision()
1204 self.append_head_info(branch_last_revision, bi, bi.label)
1205 load_heads.append(branch_last_revision)
1206 self.update_ui()
1207
1208 if bi.tree:
1209 wt_revid = self.tree_revid(bi.tree)
1210 if label:
1211 wt_label = "%s - Working Tree" % label
1212 else:
1213 wt_label = "Working Tree"
1214 self.append_head_info(wt_revid, bi, wt_label)
1215 parent_ids = bi.tree.get_parent_ids()
1216
1217 extra_parents.append((wt_revid, parent_ids))
1218 load_heads.extend(parent_ids)
1219
1220 if parent_ids:
1221 # first parent is last revision of the tree
1222 if parent_ids[0] != branch_last_revision:
1223 # tree is not up to date.
1224 sort_heads.append(branch_last_revision)
1225
1226 # other parents are pending merges
1227 for revid in parent_ids[1:]:
1228 if label:
1229 pm_label = "%s - Pending Merge" % label
1230 else:
1231 pm_label = "Pending Merge"
1232 self.append_head_info(revid, bi, pm_label)
1233
1234 sort_heads.append(wt_revid)
1235 self.update_ui()
1236 else:
1237 sort_heads.append(branch_last_revision)
1238
1239 return load_heads, sort_heads, extra_parents
1240
1241
1242class GraphVizFilterState(object):
1243 """
1244 Records the state of which branch lines are expanded, and what filters
1245 are applied.
1246 """
1247
1248 def __init__(self, graph_viz, filter_changed_callback=None):
1249 self.graph_viz = graph_viz
1250 self.filter_changed_callback = filter_changed_callback
1251
1252 self.branch_line_state = {}
1253 "If a branch_id is in this dict, it is visible. The value of the dict "
1254 "indicates which branches expanded this branch."
1255
1256 for revid in self.graph_viz.revid_head_info:
1257 rev = self.graph_viz.revid_rev[revid]
1258 self.branch_line_state[rev.branch_id] = None
1259
1260 self.filters = []
1261
1262 # This keeps a cache of the filter state so that when one of the
1263 # filters notifies us of a change, we can check if anything did change.
1264
1265 self.filter_cache = [None for rev in self.graph_viz.revisions]
1266
1267 def get_filtered_revisions(self):
1268 if self.graph_viz.no_graph:
1269 rev_whos_branch_is_visible = self.graph_viz.revisions
1270 else:
1271 rev_whos_branch_is_visible = []
1272 for branch_id in self.branch_line_state.iterkeys():
1273 try:
1274 branch_line = self.graph_viz.branch_lines[branch_id]
1275 except KeyError:
1276 continue
1277 rev_whos_branch_is_visible.extend(branch_line.revs)
1278 rev_whos_branch_is_visible.sort(key=lambda rev: rev.index)
1279
1280 visible = self.get_revision_visible_if_branch_visible
1281 return (rev for rev in rev_whos_branch_is_visible if visible(rev))
1282
1283 def get_revision_visible_if_branch_visible(self, rev):
1284 rev_filter_cache = self.filter_cache[rev.index]
1285 if rev_filter_cache is None:
1286 rev_filter_cache = \
1287 self._get_revision_visible_if_branch_visible(rev)
1288 self.filter_cache[rev.index] = rev_filter_cache
1289 return rev_filter_cache
1290
1291 def _get_revision_visible_if_branch_visible(self, rev):
1292 filters_value = True
1293 for filter in self.filters:
1294 if not filter.get_revision_visible(rev):
1295 filters_value = False
1296 break
1297 if filters_value:
1298 return True
1299
1300 if not self.graph_viz.no_graph:
1301 for merged_index in rev.merges:
1302 merged_rev = self.graph_viz.revisions[merged_index]
1303 if self.get_revision_visible_if_branch_visible(merged_rev):
1304 return True
1305
1306 return False
1307
1308 def filter_changed(self, revs=None, last_call=True):
1309 if revs is None:
1310 self.filter_cache = [None for rev in self.graph_viz.revisions]
1311 if self.filter_changed_callback:
1312 self.filter_changed_callback()
1313 else:
1314 pending_revs = revs
1315 processed_revs = set()
1316 prev_cached_revs = []
1317 while pending_revs:
1318 rev = pending_revs.pop(0)
1319 if rev in processed_revs:
1320 continue
1321 processed_revs.add(rev)
1322
1323 rev_filter_cache = self.filter_cache[rev.index]
1324
1325 if rev_filter_cache is not None:
1326 prev_cached_revs.append((rev, rev_filter_cache))
1327 self.filter_cache[rev.index] = None
1328
1329 if not self.graph_viz.no_graph:
1330 if rev.merged_by is not None:
1331 pending_revs.append(
1332 self.graph_viz.revisions[rev.merged_by])
1333
1334 # Check if any visibilities have changes. If they have, call
1335 # filter_changed_callback
1336 for rev, prev_visible in prev_cached_revs:
1337 visible = self.get_revision_visible_if_branch_visible(rev)
1338 if visible != prev_visible:
1339 if self.filter_changed_callback:
1340 self.filter_changed_callback()
1341 break
1342
1343 def ensure_rev_visible(self, rev):
1344 if self.graph_viz.no_graph:
1345 return False
1346
1347 branch_id = rev.branch_id
1348 if branch_id not in self.branch_line_state:
1349 self.branch_line_state[branch_id] = None
1350 if self.filter_changed_callback:
1351 self.filter_changed_callback()
1352 return True
1353 return False
1354
1355 def collapse_expand_rev(self, c_rev, in_place_mod=True):
1356 if in_place_mod:
1357 branch_line_state = self.branch_line_state
1358 else:
1359 # create a copy of the state dict
1360 branch_line_state = dict(self.branch_line_state)
1361
1362 if c_rev is None:
1363 return False
1364 visible = not c_rev.twisty_state
1365 branch_ids = zip(
1366 c_rev.twisty_expands_branch_ids,
1367 [c_rev.rev.branch_id] * len(c_rev.twisty_expands_branch_ids))
1368
1369 seen_branch_ids = set(branch_id
1370 for branch_id, expanded_by in branch_ids)
1371 has_change = False
1372 while branch_ids:
1373 branch_id, expanded_by = branch_ids.pop()
1374 if (branch_id in branch_line_state) != visible:
1375 has_change = True
1376 if not visible:
1377 del branch_line_state[branch_id]
1378 parents = self.graph_viz.branch_lines[branch_id].merges
1379 for parent_branch_id in parents:
1380 parent_visible = parent_branch_id in branch_line_state
1381 if (not parent_visible or
1382 parent_branch_id in seen_branch_ids):
1383 continue
1384
1385 if branch_line_state[parent_branch_id] == branch_id:
1386 # This branch expanded the parent branch, so we must
1387 # collapse it.
1388 branch_ids.append((parent_branch_id, branch_id))
1389 seen_branch_ids.add(parent_branch_id)
1390 else:
1391 # Check if this parent has any other visible branches
1392 # that merge it.
1393 has_visible = False
1394 parent = self.graph_viz.branch_lines[parent_branch_id]
1395 for merged_by_branch_id in parent.merged_by:
1396 if merged_by_branch_id in branch_line_state:
1397 has_visible = True
1398 break
1399 if not has_visible:
1400 branch_ids.append((parent_branch_id, branch_id))
1401 seen_branch_ids.add(parent_branch_id)
1402 else:
1403 branch_line_state[branch_id] = expanded_by
1404 if has_change and self.filter_changed_callback and in_place_mod:
1405 self.filter_changed_callback()
1406 return branch_line_state
1407
1408 def expand_all_branch_lines(self):
1409 for branch_id in self.graph_viz.branch_lines.keys():
1410 if branch_id not in self.branch_line_state:
1411 self.branch_line_state[branch_id] = None
1412
1413
1414class FileIdFilter (object):
1415 """
1416 Filter that only shows revisions that modify one of the specified files.
1417 """
1418
1419 def __init__(self, graph_viz, filter_changed_callback, file_ids):
1420 self.graph_viz = graph_viz
1421 self.filter_changed_callback = filter_changed_callback
1422 self.file_ids = file_ids
1423 self.has_dir = False
1424 self.filter_file_id = [False for rev in self.graph_viz.revisions]
1425
1426 # don't filter working tree nodes
1427 if isinstance(self.graph_viz, WithWorkingTreeGraphVizLoader):
1428 for wt_revid in self.graph_viz.working_trees.iterkeys():
1429 try:
1430 rev_index = self.graph_viz.revid_rev[wt_revid].index
1431 self.filter_file_id[rev_index] = True
1432 except KeyError:
1433 pass
1434
1435
1436 def uses_inventory(self):
1437 return self.has_dir
1438
1439 def load(self, revids=None):
1440 """Load which revisions affect the file_ids"""
1441 if self.file_ids:
1442 self.graph_viz.throbber_show()
1443
1444 for bi in self.graph_viz.branches:
1445 tree = bi.tree
1446 if tree is None:
1447 tree = bi.branch.basis_tree()
1448
1449 tree.lock_read()
1450 try:
1451 for file_id in self.file_ids:
1452 if tree.kind(file_id) in ('directory',
1453 'tree-reference'):
1454 self.has_dir = True
1455 break
1456 if self.has_dir:
1457 break
1458 finally:
1459 tree.unlock()
1460
1461 if revids is None:
1462 revids = [rev.revid for rev in self.graph_viz.revisions]
1463
1464 for repo, revids in self.graph_viz.get_repo_revids(revids):
1465 if self.uses_inventory():
1466 chunk_size = 200
1467 else:
1468 chunk_size = 500
1469
1470 for start in xrange(0, len(revids), chunk_size):
1471 self.load_filter_file_id_chunk(repo,
1472 revids[start:start + chunk_size])
1473
1474 self.load_filter_file_id_chunk_finished()
1475
1476 def load_filter_file_id_chunk(self, repo, revids):
1477 def check_text_keys(text_keys):
1478 changed_revs = []
1479 for file_id, revid in repo.texts.get_parent_map(text_keys):
1480 rev = self.graph_viz.revid_rev[revid]
1481 self.filter_file_id[rev.index] = True
1482 changed_revs.append(rev)
1483
1484 self.graph_viz.update_ui()
1485 self.filter_changed_callback(changed_revs, False)
1486 self.graph_viz.update_ui()
1487
1488 repo.lock_read()
1489 try:
1490 if not self.uses_inventory():
1491 text_keys = [(file_id, revid)
1492 for revid in revids
1493 for file_id in self.file_ids]
1494 check_text_keys(text_keys)
1495 else:
1496 text_keys = []
1497 # We have to load the inventory for each revisions, to find
1498 # the children of any directories.
1499 for inv, revid in izip(repo.iter_inventories(revids), revids):
1500 entries = inv.iter_entries_by_dir(
1501 specific_file_ids=self.file_ids)
1502 for path, entry in entries:
1503 text_keys.append((entry.file_id, revid))
1504 if entry.kind == "directory":
1505 sub_entries = inv.iter_entries(from_dir=entry)
1506 for rc_path, rc_entry in sub_entries:
1507 text_keys.append((rc_entry.file_id, revid))
1508
1509 self.graph_viz.update_ui()
1510
1511 check_text_keys(text_keys)
1512 finally:
1513 repo.unlock()
1514
1515 def load_filter_file_id_chunk_finished(self):
1516 self.filter_changed_callback([], True)
1517 self.graph_viz.throbber_hide()
1518
1519 def get_revision_visible(self, rev):
1520 return self.filter_file_id[rev.index]
1521
1522
1523class WorkingTreeHasChangeFilter(object):
1524 """
1525 Filter out working trees that don't have any changes.
1526 """
1527
1528 def __init__(self, graph_viz, filter_changed_callback, file_ids):
1529 self.graph_viz = graph_viz
1530 self.file_ids = file_ids
1531 if not isinstance(graph_viz, WithWorkingTreeGraphVizLoader):
1532 raise TypeError('graph_viz expected to be a '
1533 'WithWorkingTreeGraphVizLoader')
1534 self.filter_changed_callback = filter_changed_callback
1535 self.tree_revids_with_changes = set()
1536
1537 def load(self):
1538 """Load if the working trees have changes."""
1539 self.tree_revids_with_changes = set()
1540 self.graph_viz.throbber_show()
1541 try:
1542 for wt_revid, tree in self.graph_viz.working_trees.iteritems():
1543 if self.has_changes(tree):
1544 self.tree_revids_with_changes.add(wt_revid)
1545 rev = self.graph_viz.revid_rev[wt_revid]
1546 self.filter_changed_callback([rev], False)
1547 self.filter_changed_callback([], True)
1548 finally:
1549 self.graph_viz.throbber_hide()
1550
1551 def has_changes(self, tree):
1552 """Quickly check that the tree contains at least one commitable change.
1553
1554 :param _from_tree: tree to compare against to find changes (default to
1555 the basis tree and is intended to be used by tests).
1556
1557 :return: True if a change is found. False otherwise
1558 """
1559 tree.lock_read()
1560 try:
1561 # Copied from mutabletree, cause we need file_ids too.
1562 # Check pending merges
1563 if len(tree.get_parent_ids()) > 1:
1564 return True
1565 from_tree = tree.basis_tree()
1566
1567 specific_files = None
1568 if self.file_ids:
1569 specific_files = [tree.id2path(file_id)
1570 for file_id in self.file_ids]
1571
1572 changes = tree.iter_changes(from_tree,
1573 specific_files=specific_files)
1574 try:
1575 change = changes.next()
1576 # Exclude root (talk about black magic... --vila 20090629)
1577 if change[4] == (None, None):
1578 change = changes.next()
1579 return True
1580 except StopIteration:
1581 # No changes
1582 return False
1583 finally:
1584 tree.unlock()
1585
1586 def get_revision_visible(self, rev):
1587 if rev.revid.startswith(CURRENT_REVISION):
1588 return rev.revid in self.tree_revids_with_changes
1589 else:
1590 return True
1591
1592
1593class ComputedRevisionData(object):
1594 """Container for computed layout data for a revision.
1595
1596 :ivar rev: Reference to RevisionData. Use to get revno, revid, color and
1597 others.
1598 :ivar f_index: Index in `ComputedGraphViz.filtered_revs`.
1599 :ivar col_index: Column index to place node for revision in.
1600 :ivar lines: Lines that need to be drawn from from this revision's line to
1601 the next revision's line. Note that not all these lines relate to this
1602 revision, but be a part of a longer line that is passing this revision.
1603
1604 Each line is a tuple of `(end col_index, start col_index, color,
1605 direct)`.
1606
1607 If direct is False, it indicates that this line represents an
1608 ancestry, with revisions that are filtered. This should be shown as
1609 a dotted line.
1610
1611 :ivar branch_labels: Labels for branch tips.
1612 :ivar twisty_state: State of the revision:
1613
1614 * None: No twisty.
1615 * True: There are branch lines that this revision merges that can
1616 expanded. Show a '+'.
1617 * False: All branches that this revision merges are already expanded.
1618 Show a '-'.
1619 :ivar twisty_expands_branch_ids: Branch lines that will be expanded if the
1620 twisty is clicked.
1621 """
1622
1623 # Instance of this object are typically named "c_rev".
1624 __slots__ = ['rev', 'f_index', 'lines', 'col_index', 'branch_labels',
1625 'twisty_state', 'twisty_expands_branch_ids']
1626
1627 def __init__(self, rev):
1628 self.rev = rev
1629 self.lines = []
1630 self.col_index = None
1631 self.twisty_state = None
1632 self.twisty_expands_branch_ids = []
1633 self.branch_labels = []
1634
1635
1636class ComputedGraphViz(object):
1637 """Computed layout data for a graph.
1638
1639 :ivar graph_viz: Reference to parent `GraphVizLoader`.
1640 :ivar filtered_revs: List `ComputedRevisionData`. Only visible revisions
1641 are included.
1642 :ivar revisions: List `ComputedRevisionData`. Revision that are not
1643 visible are None.
1644 """
1645 def __init__(self, graph_viz):
1646 self.graph_viz = graph_viz
1647 self.filtered_revs = []
1648 self.revisions = [None for i in xrange(len(graph_viz.revisions))]
01649
=== modified file 'bzrlib/tests/__init__.py'
--- bzrlib/tests/__init__.py 2010-10-18 17:06:37 +0000
+++ bzrlib/tests/__init__.py 2010-11-18 10:15:44 +0000
@@ -3739,6 +3739,7 @@
3739 'bzrlib.tests.test_lockable_files',3739 'bzrlib.tests.test_lockable_files',
3740 'bzrlib.tests.test_lockdir',3740 'bzrlib.tests.test_lockdir',
3741 'bzrlib.tests.test_log',3741 'bzrlib.tests.test_log',
3742 'bzrlib.tests.test_loggraphviz',
3742 'bzrlib.tests.test_lru_cache',3743 'bzrlib.tests.test_lru_cache',
3743 'bzrlib.tests.test_lsprof',3744 'bzrlib.tests.test_lsprof',
3744 'bzrlib.tests.test_mail_client',3745 'bzrlib.tests.test_mail_client',
37453746
=== added file 'bzrlib/tests/test_loggraphviz.py'
--- bzrlib/tests/test_loggraphviz.py 1970-01-01 00:00:00 +0000
+++ bzrlib/tests/test_loggraphviz.py 2010-11-18 10:15:44 +0000
@@ -0,0 +1,1153 @@
1# -*- coding: utf-8 -*-
2# Copyright (C) 2010 Canonical Ltd
3#
4# This program is free software; you can redistribute it and/or
5# modify it under the terms of the GNU General Public License
6# as published by the Free Software Foundation; either version 2
7# of the License, or (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program; if not, write to the Free Software
16# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18from bzrlib.tests import TestCase, TestCaseWithTransport
19from StringIO import StringIO
20
21from bzrlib import loggraphviz
22from bzrlib.revision import NULL_REVISION
23
24# TODO:
25# Tag loading
26# Branch labels + Filtering
27# Ghosts
28# file_ids filtering
29
30class TestLogGraphVizMixin(object):
31 def computed_to_list(self, computed, branch_labels=False):
32 if not branch_labels:
33 item = lambda c_rev: (c_rev.rev.revid,
34 c_rev.col_index,
35 c_rev.twisty_state,
36 sorted(c_rev.lines),)
37 else:
38 item = lambda c_rev: (c_rev.rev.revid,
39 c_rev.col_index,
40 c_rev.twisty_state,
41 sorted(c_rev.lines),
42 [label for bi, label in c_rev.branch_labels])
43
44 return [item(c_rev) for c_rev in computed.filtered_revs]
45
46 def assertComputed(self, expected_list, computed, branch_labels=False):
47 computed_list = self.computed_to_list(computed, branch_labels)
48 if not expected_list == computed_list:
49 raise AssertionError(
50 "not equal: \nexpected_list = \n%scomputed_list = \n%s"
51 % (format_graph_lines(expected_list, use_unicode=True),
52 format_graph_lines(computed_list, use_unicode=True),))
53
54
55class TestLogGraphVizWithBranches(TestCaseWithTransport, TestLogGraphVizMixin):
56
57 def test_no_commits(self):
58 br = self.make_branch('.')
59
60 bi = loggraphviz.BranchInfo('', None, br)
61 gv = loggraphviz.GraphVizLoader([bi], bi, False)
62 gv.load()
63
64 self.assertEqual(len(gv.revisions), 0)
65
66 state = loggraphviz.GraphVizFilterState(gv)
67 computed = gv.compute_viz(state)
68 self.assertEqual(len(computed.revisions), 0)
69
70 def make_banches_for_tips_date_sorted(self):
71 builder = self.make_branch_builder('trunk')
72 builder.start_series()
73 builder.build_snapshot('rev-a', None, [
74 ('add', ('', 'TREE_ROOT', 'directory', '')),])
75 builder.build_snapshot('rev-old', ['rev-a'], [])
76 builder.build_snapshot('rev-new', ['rev-a'], [])
77 builder.build_snapshot('rev-trunk', ['rev-a'], [])
78 builder.finish_series()
79
80 trunk = builder.get_branch()
81 #trunk.set_last_revision('rev-trunk')
82
83 old = trunk.bzrdir.sprout('../old', revision_id='rev-old').open_branch()
84
85 new = trunk.bzrdir.sprout('../new', revision_id='rev-new').open_branch()
86
87 return trunk, old, new
88
89 def test_branch_tips_date_sorted(self):
90 trunk, old, new = self.make_banches_for_tips_date_sorted()
91
92 trunk_bi = loggraphviz.BranchInfo('trunk', None, trunk)
93 gv = loggraphviz.GraphVizLoader(
94 [trunk_bi,
95 loggraphviz.BranchInfo('old', None, old),
96 loggraphviz.BranchInfo('new', None, new),],
97 trunk_bi, False)
98 gv.load()
99
100 state = loggraphviz.GraphVizFilterState(gv)
101 computed = gv.compute_viz(state)
102
103 self.assertComputed(
104 [('rev-new', 2, None, [(2, 2, 0, True)]) , # ○
105 # │
106 ('rev-old', 1, None, [(1, 1, 0, True), (2, 2, 0, True)]), # ○ │
107 # │ │
108 ('rev-trunk', 0, None, [(0, 0, 0, True), (1, 0, 0, True), # ○ │ │
109 (2, 0, 0, True)]), # ├─╯─╯
110 ('rev-a', 0, None, []) ],# ○
111 computed)
112
113 def test_branch_tips_date_sorted_with_working_tree_provider(self):
114 trunk, old, new = self.make_banches_for_tips_date_sorted()
115 trunk_tree = trunk.bzrdir.create_workingtree()
116 old_tree = old.bzrdir.open_workingtree()
117 new_tree = new.bzrdir.open_workingtree()
118
119 trunk_bi = loggraphviz.BranchInfo('trunk', trunk_tree, trunk)
120 gv = loggraphviz.WithWorkingTreeGraphVizLoader(
121 [trunk_bi,
122 loggraphviz.BranchInfo('old', old_tree, old),
123 loggraphviz.BranchInfo('new', new_tree, new),],
124 trunk_bi, False)
125 gv.load()
126
127 state = loggraphviz.GraphVizFilterState(gv)
128 computed = gv.compute_viz(state)
129
130 self.assertComputed(
131 [(gv.tree_revid(new_tree), 2, None, [(2, 2, 3, True)]), # ○
132 # │
133 ('rev-new', 2, None, [(2, 2, 0, True)]), # ○
134 # │
135 (gv.tree_revid(old_tree), 1, None, [(1, 1, 2, True), # ○ │
136 (2, 2, 0, True)]), # │ │
137 ('rev-old', 1, None, [(1, 1, 0, True), (2, 2, 0, True)]), # ○ │
138 # │ │
139 (gv.tree_revid(trunk_tree), 0, None, [ # ○ │ │
140 (0, 0, 0, True), (1, 1, 0, True), (2, 2, 0, True)]), # │ │ │
141 ('rev-trunk', 0, None, [(0, 0, 0, True), (1, 0, 0, True), # ○ │ │
142 (2, 0, 0, True)]), # ├─╯─╯
143 ('rev-a', 0, None, [])], # ○
144 computed)
145
146
147 def make_tree_with_pending_merge(self, path):
148 builder = self.make_branch_builder('branch')
149 builder.start_series()
150 builder.build_snapshot('rev-a', None, [
151 ('add', ('', 'TREE_ROOT', 'directory', '')),])
152 builder.build_snapshot('rev-b', ['rev-a'], [])
153 builder.finish_series()
154
155 branch = builder.get_branch()
156 branch.set_last_revision_info(1, 'rev-a') # go back to rev-a
157 tree = branch.bzrdir.create_workingtree()
158 tree.merge_from_branch(branch, to_revision='rev-b')
159
160 return tree
161
162 def make_tree_not_up_to_date(self, path):
163 builder = self.make_branch_builder('branch')
164 builder.start_series()
165 builder.build_snapshot('rev-a', None, [
166 ('add', ('', 'TREE_ROOT', 'directory', '')),])
167 builder.build_snapshot('rev-b', ['rev-a'], [])
168 builder.finish_series()
169
170 branch = builder.get_branch()
171 tree = branch.bzrdir.create_workingtree()
172 tree.update(revision='rev-a')
173 return tree
174
175 def test_pending_merge(self):
176 tree = self.make_tree_with_pending_merge('branch')
177
178 bi = loggraphviz.BranchInfo(None, tree, tree.branch)
179 gv = loggraphviz.GraphVizLoader([bi], bi, False)
180 gv.load()
181
182 state = loggraphviz.GraphVizFilterState(gv)
183 computed = gv.compute_viz(state)
184
185 self.assertComputed(
186 [('rev-b', 1, None, [(1, 0, 0, True)], ['Pending Merge']), # ○
187 # ╭─╯
188 ('rev-a', 0, None, [], [None]) ],# ○
189 computed, branch_labels=True)
190
191 def test_out_of_date_wt(self):
192 tree = self.make_tree_not_up_to_date('branch')
193 bi = loggraphviz.BranchInfo(None, tree, tree.branch)
194 gv = loggraphviz.GraphVizLoader([bi], bi, False)
195 gv.load()
196
197 state = loggraphviz.GraphVizFilterState(gv)
198 computed = gv.compute_viz(state)
199
200 self.assertComputed(
201 [('rev-b', 0, None, [(0, 0, 0, True)], [None]), # ○
202 # │
203 ('rev-a', 0, None, [], ['Working Tree']) ],# ○
204 computed, branch_labels=True)
205
206 def test_with_working_tree_provider(self):
207 tree = self.make_tree_with_pending_merge('branch')
208
209 bi = loggraphviz.BranchInfo('branch', tree, tree.branch)
210 gv = loggraphviz.WithWorkingTreeGraphVizLoader([bi], bi, False)
211 gv.load()
212
213 state = loggraphviz.GraphVizFilterState(gv)
214 computed = gv.compute_viz(state)
215
216 self.assertComputed(
217 [(u'current:%s' % tree.basedir, 0, True, # ⊖
218 [(0, 0, 0, True), (0, 1, 2, True)], # ├─╮
219 ['branch - Working Tree']), # │ │
220 ('rev-b', 1, None, [(0, 0, 0, True), (1, 0, 0, True)], # │ ○
221 ['branch - Pending Merge']), # ├─╯
222 ('rev-a', 0, None, [], ['branch'])], # ○
223 computed, branch_labels=True)
224
225 def test_with_working_tree_provider_out_of_date_wt(self):
226 tree = self.make_tree_not_up_to_date('branch')
227 bi = loggraphviz.BranchInfo('branch', tree, tree.branch)
228 gv = loggraphviz.WithWorkingTreeGraphVizLoader([bi], bi, False)
229 gv.load()
230
231 state = loggraphviz.GraphVizFilterState(gv)
232 computed = gv.compute_viz(state)
233
234 self.assertComputed(
235 [(u'current:%s' % tree.basedir, 1, None, [(1, 1, 0, True)], # ○
236 ['branch - Working Tree']), # │
237 ('rev-b', 0, None, [(0, 0, 0, True), (1, 0, 0, True)], # ○ │
238 ['branch']), # ├─╯
239 ('rev-a', 0, None, [], []) ], # ○
240 computed, branch_labels=True)
241
242 def test_with_working_tree_provider_filtered(self):
243 # This test makes sure that lable for a Working Tree shows for on it's
244 # nearest visble unique ansestor when the working tree node is
245 # filtered.
246 builder = self.make_branch_builder('branch')
247 builder.start_series()
248 builder.build_snapshot('rev-a', None, [
249 ('add', ('', 'TREE_ROOT', 'directory', '')),])
250 builder.build_snapshot('rev-b', ['rev-a'], [])
251 builder.build_snapshot('rev-c', ['rev-a'], [])
252 builder.finish_series()
253
254 branch = builder.get_branch()
255 tree = branch.bzrdir.create_workingtree()
256 tree.update(revision='rev-b')
257
258 bi = loggraphviz.BranchInfo('branch', tree, tree.branch)
259 gv = loggraphviz.WithWorkingTreeGraphVizLoader([bi], bi, False)
260 gv.load()
261
262 state = loggraphviz.GraphVizFilterState(gv)
263 state.filters.append(BasicFilterer(set([
264 'current:%s' % tree.basedir.encode('unicode-escape')])))
265 computed = gv.compute_viz(state)
266 self.assertComputed(
267 [('rev-b', 1, None, [(1, 1, 0, True)], ['branch - Working Tree']) , # ○
268 # │
269 ('rev-c', 0, None, [(0, 0, 0, True), (1, 0, 0, True)], ['branch']), # ○ │
270 # ├─╯
271 ('rev-a', 0, None, [], []) ],# ○
272 computed, branch_labels=True)
273
274 def test_pending_merges_provider(self):
275 tree = self.make_tree_with_pending_merge('branch')
276
277 bi = loggraphviz.BranchInfo(None, tree, tree.branch)
278 gv = loggraphviz.PendingMergesGraphVizLoader([bi], bi, False)
279 gv.load()
280
281 state = loggraphviz.GraphVizFilterState(gv)
282 computed = gv.compute_viz(state)
283
284 self.assertComputed(
285 [('rev-b', 1, None, [(1, 0, 0, True)]), # ○
286 # ╭─╯
287 ('root:', 0, None, []) ],# ○
288 computed)
289
290 def test_with_ghost(self):
291 tree = self.make_branch_and_tree('tree')
292 tree.commit('a', rev_id='rev-a')
293 tree.add_parent_tree_id('rev-b')
294 tree.commit('c', rev_id='rev-c')
295 # rev-b is a ghost. We think he is there, but he dose not exist. Boo!
296
297 bi = loggraphviz.BranchInfo(None, tree, tree.branch)
298 gv = loggraphviz.GraphVizLoader([bi], bi, False)
299 gv.load()
300
301 state = loggraphviz.GraphVizFilterState(gv)
302 state.expand_all_branch_lines()
303 computed = gv.compute_viz(state)
304
305 self.assertComputed(
306 [('rev-c', 0, True, [(0, 0, 0, True), (0, 1, 1, True)]), # ⊖
307 # ├─╮
308 ('rev-b', 1, None, [(0, 0, 0, True)]) , # │ ○
309 # │
310 ('rev-a', 0, None, []) ],# ○
311 computed)
312
313 def test_with_ghost_mainline(self):
314 tree = self.make_branch_and_tree('tree')
315 tree.add_parent_tree_id('rev-a', allow_leftmost_as_ghost=True)
316 tree.commit('b', rev_id='rev-b')
317
318 bi = loggraphviz.BranchInfo(None, tree, tree.branch)
319 gv = loggraphviz.GraphVizLoader([bi], bi, False)
320 gv.load()
321
322 state = loggraphviz.GraphVizFilterState(gv)
323 computed = gv.compute_viz(state)
324
325 self.assertComputed(
326 [('rev-b', 0, None, [(0, 0, 0, True)]), # ○
327 # │
328 ('rev-a', 0, None, []) ],# ○
329 computed)
330
331 def test_get_revid_branch_info(self):
332 builder = self.make_branch_builder('trunk')
333 builder.start_series()
334 builder.build_snapshot('rev-a', None, [
335 ('add', ('', 'TREE_ROOT', 'directory', '')),])
336 builder.build_snapshot('rev-branch', ['rev-a'], [])
337 builder.build_snapshot('rev-trunk', ['rev-a'], [])
338 builder.finish_series()
339 # ○ branch
340 # │
341 # ○ │ trunk
342 # ├─╯
343 # ○ rev-a
344
345 trunk = builder.get_branch()
346 #trunk.set_last_revision('rev-trunk')
347
348 branch = trunk.bzrdir.sprout('../branch',
349 revision_id='rev-branch').open_branch()
350
351 trunk_bi = loggraphviz.BranchInfo('trunk', None, trunk)
352 branch_bi = loggraphviz.BranchInfo('branch', None, branch)
353 gv = loggraphviz.GraphVizLoader(
354 [trunk_bi, branch_bi],
355 trunk_bi, False)
356 gv.load()
357
358 self.assertEqual(trunk_bi, gv.get_revid_branch_info('rev-trunk'))
359 self.assertEqual(branch_bi, gv.get_revid_branch_info('rev-branch'))
360
361 # may return either
362 self.assertIn(gv.get_revid_branch_info('rev-a'),
363 (branch_bi, trunk_bi))
364
365 def test_get_revid_branch_info_with_ghost(self):
366 tree = self.make_branch_and_tree('tree')
367 tree.commit('a', rev_id='rev-a')
368 tree.add_parent_tree_id('rev-b')
369 tree.commit('c', rev_id='rev-c')
370 # rev-b is a ghost. We think he is there, but he dose not exist. Boo!
371 # c
372 # ├─╮
373 # │ b
374 # │
375 # a
376
377 bi = loggraphviz.BranchInfo(None, tree, tree.branch)
378 gv = loggraphviz.GraphVizLoader([bi], bi, False)
379 gv.load()
380
381 self.assertRaises(loggraphviz.GhostRevisionError,
382 gv.get_revid_branch_info, 'rev-b')
383
384class TestLogGraphVizLayouts(TestCase, TestLogGraphVizMixin):
385
386 def test_basic_branch_line(self):
387 gv = BasicGraphVizLoader(('rev-d',), {
388 'rev-a': (NULL_REVISION, ),
389 'rev-b': ('rev-a', ),
390 'rev-c': ('rev-a', ),
391 'rev-d': ('rev-b', 'rev-c'),
392 })
393 gv.load()
394
395 state = loggraphviz.GraphVizFilterState(gv)
396 computed = gv.compute_viz(state)
397
398 # only mainline.
399 self.assertComputed(
400 [('rev-d', 0, False, [(0, 0, 0, True)]), # ⊕
401 # │
402 ('rev-b', 0, None, [(0, 0, 0, True)]) , # ○
403 # │
404 ('rev-a', 0, None, []) ],# ○
405 computed)
406
407 state.collapse_expand_rev(computed.filtered_revs[0])
408 computed = gv.compute_viz(state)
409
410 # expanded branch line.
411 self.assertComputed(
412 [('rev-d', 0, True, [(0, 0, 0, True), (0, 1, 2, True)]), # ⊖
413 # ├─╮
414 ('rev-c', 1, None, [(0, 0, 0, True), (1, 1, 0, True)]), # │ ○
415 # │ │
416 ('rev-b', 0, None, [(0, 0, 0, True), (1, 0, 0, True)]), # ○ │
417 # ├─╯
418 ('rev-a', 0, None, []) ],# ○
419 computed)
420
421 def test_branch_line_order(self):
422 gv = BasicGraphVizLoader(('rev-f',), {
423 'rev-a': (NULL_REVISION, ),
424 'rev-b': ('rev-a', ),
425 'rev-c': ('rev-b', ),
426 'rev-d': ('rev-a', 'rev-c'),
427 'rev-e': ('rev-b', ),
428 'rev-f': ('rev-d', 'rev-e' ),
429 })
430 gv.load()
431
432 state = loggraphviz.GraphVizFilterState(gv)
433 state.expand_all_branch_lines()
434 computed = gv.compute_viz(state)
435
436 # branch lines should not cross over
437 self.assertComputed(
438 [('rev-f', 0, True, [(0, 0, 0, True), (0, 2, 3, True)]) , # ⊖
439 # ├───╮
440 ('rev-e', 2, True, [(0, 0, 0, True), (2, 2, 2, True)]) , # │ ⊖
441 # │ │
442 ('rev-d', 0, True, [(0, 0, 0, True), (0, 1, 2, True), (2, 2, 2, True)]), # ⊖ │
443 # ├─╮ │
444 ('rev-c', 1, None, [(0, 0, 0, True), (1, 1, 2, True), (2, 1, 2, True)]), # │ ○ │
445 # │ ├─╯
446 ('rev-b', 1, None, [(0, 0, 0, True), (1, 0, 0, True)]) , # │ ○
447 # ├─╯
448 ('rev-a', 0, None, []) ],# ○
449 computed)
450
451 def test_branch_line_order2(self):
452 gv = BasicGraphVizLoader(('rev-h',), {
453 'rev-a': (NULL_REVISION, ),
454 'rev-b': ('rev-a', ),
455 'rev-c': ('rev-b', ),
456 'rev-d': ('rev-a', ),
457 'rev-e': ('rev-d', 'rev-b' ),
458 'rev-f': ('rev-e', ),
459 'rev-g': ('rev-e', 'rev-f' ),
460 'rev-h': ('rev-c', 'rev-g' ),
461 })
462 gv.load()
463
464 state = loggraphviz.GraphVizFilterState(gv)
465 state.expand_all_branch_lines()
466 computed = gv.compute_viz(state)
467
468 # branch lines should not cross over
469 self.assertComputed(
470 [('rev-h', 0, True, [(0, 0, 0, True), (0, 2, 2, True)]) , # ⊖
471 # ├───╮
472 ('rev-g', 2, True, [(0, 0, 0, True), (2, 2, 2, True), (2, 3, 3, True)]), # │ ⊖
473 # │ ├─╮
474 ('rev-f', 3, None, [(0, 0, 0, True), (2, 2, 2, True), (3, 2, 2, True)]), # │ │ ○
475 # │ ├─╯
476 ('rev-e', 2, None, [(0, 0, 0, True), (2, 1, 0, True), (2, 2, 2, True)]), # │ ○
477 # │ ╭─┤
478 ('rev-d', 2, None, [(0, 0, 0, True), (1, 1, 0, True), (2, 2, 0, True)]), # │ │ ○
479 # │ │ │
480 ('rev-c', 0, None, [(0, 0, 0, True), (1, 0, 0, True), (2, 2, 0, True)]), # ○ │ │
481 # ├─╯ │
482 ('rev-b', 0, None, [(0, 0, 0, True), (2, 0, 0, True)]) , # ○ │
483 # ├───╯
484 ('rev-a', 0, None, []) ],# ○
485 computed)
486
487 def test_octopus_merge(self):
488 gv = BasicGraphVizLoader(('rev-e',), {
489 'rev-a': (NULL_REVISION, ),
490 'rev-b': ('rev-a', ),
491 'rev-c': ('rev-a', ),
492 'rev-d': ('rev-a', ),
493 'rev-e': ('rev-a', 'rev-b', 'rev-c', 'rev-d'),
494 })
495 gv.load()
496
497 state = loggraphviz.GraphVizFilterState(gv)
498 state.expand_all_branch_lines()
499 computed = gv.compute_viz(state)
500
501 self.assertComputed(
502 [('rev-e', 0, True, [(0, 0, 0, True), (0, 1, 2, True), (0, 2, 3, True), (0, 3, 4, True)]), # ⊖
503 # ├─╮─╮─╮
504 ('rev-b', 3, None, [(0, 0, 0, True), (1, 1, 2, True), (2, 2, 3, True), (3, 3, 0, True)]), # │ │ │ ○
505 # │ │ │ │
506 ('rev-c', 2, None, [(0, 0, 0, True), (1, 1, 2, True), (2, 2, 0, True), (3, 3, 0, True)]), # │ │ ○ │
507 # │ │ │ │
508 ('rev-d', 1, None, [(0, 0, 0, True), (1, 0, 0, True), (2, 0, 0, True), (3, 0, 0, True)]), # │ ○ │ │
509 # ├─╯─╯─╯
510 ('rev-a', 0, None, []) ],# ○
511 computed)
512
513 def test_lots_of_merges_between_branch_lines(self):
514 gv = BasicGraphVizLoader(('rev-g',), {
515 'rev-a': (NULL_REVISION, ),
516 'rev-b': ('rev-a', ),
517 'rev-c': ('rev-b', ),
518 'rev-d': ('rev-a', ),
519 'rev-e': ('rev-d', 'rev-b',),
520 'rev-f': ('rev-e', 'rev-c',),
521 'rev-g': ('rev-c', 'rev-f',),
522 })
523 gv.load()
524
525 state = loggraphviz.GraphVizFilterState(gv)
526 state.expand_all_branch_lines()
527 computed = gv.compute_viz(state)
528
529 self.assertComputed(
530 [('rev-g', 0, True, [(0, 0, 0, True), (0, 2, 2, True)]) , # ⊖
531 # ├───╮
532 ('rev-f', 2, None, [(0, 0, 0, True), (2, 0.75, 0, True), (2, 2, 2, True)]) , # │ ○
533 # │ ╭─┤
534 ('rev-e', 2, None, [(0, 0, 0, True), (0.75, 0.75, 0, True), (2, 1.25, 0, True), (2, 2, 2, True)]), # │ │ ○
535 # │ ├─┤
536 ('rev-d', 2, None, [(0, 0, 0, True), (0.75, 0, 0, True), (1.25, 1.25, 0, True), (2, 2, 0, True)]), # │ │ ○
537 # ├─┤ │
538 ('rev-c', 0, None, [(0, 0, 0, True), (1.25, 0, 0, True), (2, 2, 0, True)]) , # ○ │ │
539 # ├─╯ │
540 ('rev-b', 0, None, [(0, 0, 0, True), (2, 0, 0, True)]) , # ○ │
541 # ├───╯
542 ('rev-a', 0, None, []) ],# ○
543 computed)
544
545 def test_hidden_branch_line_hides_child_line(self):
546 gv = BasicGraphVizLoader(('rev-g',), {
547 'rev-a': (NULL_REVISION, ),
548 'rev-b': ('rev-a', ),
549 'rev-c': ('rev-a', ),
550 'rev-d': ('rev-b', 'rev-c', ),
551 'rev-e': ('rev-b', 'rev-d', ),
552 'rev-f': ('rev-c', ),
553 'rev-g': ('rev-e', 'rev-f', ),
554 })
555 gv.load()
556
557 state = loggraphviz.GraphVizFilterState(gv)
558 state.branch_line_state[(2, 1)] = None
559 computed = gv.compute_viz(state)
560
561 self.assertComputed(
562 [('rev-g', 0, False, [(0, 0, 0, True)]) , # ⊕
563 # │
564 ('rev-e', 0, True, [(0, 0, 0, True), (0, 1, 3, True)]) , # ⊖
565 # ├─╮
566 ('rev-d', 1, False, [(0, 0, 0, True), (1, 0, 0, True)]), # │ ⊕
567 # ├─╯
568 ('rev-b', 0, None, [(0, 0, 0, True)]) , # ○
569 # │
570 ('rev-a', 0, None, []) ],# ○
571 computed)
572
573 def test_merge_line_hidden(self):
574 gv = BasicGraphVizLoader(('rev-d',), {
575 'rev-a': (NULL_REVISION, ),
576 'rev-b': ('rev-a', ),
577 'rev-c': ('rev-a', 'rev-b'),
578 'rev-d': ('rev-a', 'rev-c'),
579 })
580 # d
581 # ├─╮
582 # │ c
583 # │ ├─╮
584 # │ │ b
585 # ├─╯─╯
586 # a
587 gv.load()
588
589 state = loggraphviz.GraphVizFilterState(gv)
590 state.branch_line_state[(1, 1)] = None
591
592 computed = gv.compute_viz(state)
593 # when the merge by branch line, we should show a non direct line
594 self.assertComputed(
595 [('rev-d', 0, False, [(0, 0, 0, True), (0, 1, 2, False)]), # ⊕
596 # ├┄╮
597 ('rev-b', 1, None, [(0, 0, 0, True), (1, 0, 0, True)]) , # │ ○
598 # ├─╯
599 ('rev-a', 0, None, []) ],# ○
600 computed)
601
602 def test_merge_line_hidden2(self):
603 gv = BasicGraphVizLoader(('rev-e',), {
604 'rev-a': (NULL_REVISION, ),
605 'rev-z': ('rev-a', ),
606 'rev-y': ('rev-a', 'rev-z', ),
607 'rev-b': ('rev-a', ),
608 'rev-c': ('rev-a', ),
609 'rev-d': ('rev-a', 'rev-c'),
610 'rev-e': ('rev-y', 'rev-b', 'rev-d'),
611 })
612 # f
613 # ├─╮─╮
614 # │ b │
615 # │ │ │
616 # │ │ e
617 # │ │ ├─╮
618 # │ │ │ d
619 # ├─╯─╯─╯
620 # a
621 gv.load()
622
623 state = loggraphviz.GraphVizFilterState(gv)
624 #state.expand_all_branch_lines()
625 state.branch_line_state[(1, 1)] = None
626 state.branch_line_state[(1, 2)] = None
627 #state.branch_line_state[(1, 3)] = None
628 state.branch_line_state[(1, 4)] = None
629
630 computed = gv.compute_viz(state)
631 # when the merge by branch line, we should show a non direct line
632 # this could layout better, but that's another story...
633 self.assertComputed(
634 [('rev-e', 0, False, [(0, 0, 0, True), (0, 1, 3, False), (0, 2, 5, True)]) , # ⊕
635 # ├┄╮─╮
636 ('rev-b', 2, None, [(0, 0, 0, True), (1, 3, 3, False), (2, 2, 0, True)]) , # │ ┆ ○
637 # │ ╰┄┼┄╮
638 ('rev-c', 3, None, [(0, 0, 0, True), (2, 2, 0, True), (3, 3, 0, True)]) , # │ │ ○
639 # │ │ │
640 ('rev-y', 0, True, [(0, 0, 0, True), (0, 1, 2, True), (2, 2, 0, True), (3, 3, 0, True)]), # ⊖ │ │
641 # ├─╮ │ │
642 ('rev-z', 1, None, [(0, 0, 0, True), (1, 0, 0, True), (2, 0, 0, True), (3, 0, 0, True)]), # │ ○ │ │
643 # ├─╯─╯─╯
644 ('rev-a', 0, None, []) ],# ○
645 computed)
646
647 def test_merge_line_hidden_merge_rev_filtered(self):
648 gv = BasicGraphVizLoader(('rev-e',), {
649 'rev-a': (NULL_REVISION, ),
650 'rev-b': ('rev-a', ),
651 'rev-c': ('rev-b', ),
652 'rev-d': ('rev-a', 'rev-c'),
653 'rev-e': ('rev-a', 'rev-d'),
654 })
655 gv.load()
656
657 state = loggraphviz.GraphVizFilterState(gv)
658 state.filters.append(BasicFilterer(set(['rev-c'])))
659 state.branch_line_state[(1, 1)] = None
660
661 computed = gv.compute_viz(state)
662
663 # when the merge by branch line, we should show a non direct line
664 self.assertComputed(
665 [('rev-e', 0, False, [(0, 0, 0, True), (0, 1, 2, False)]), # ⊕
666 # ├┄╮
667 ('rev-b', 1, None, [(0, 0, 0, True), (1, 0, 0, True)]) , # │ ○
668 # ├─╯
669 ('rev-a', 0, None, []) ],# ○
670 computed)
671
672 def test_non_direct_hidden_branch(self):
673 gv = BasicGraphVizLoader(('rev-f',), {
674 'rev-a': (NULL_REVISION, ),
675 'rev-b': ('rev-a', ),
676 'rev-c': ('rev-b', ),
677 'rev-d': ('rev-a', 'rev-c', ),
678 'rev-e': ('rev-b', ),
679 'rev-f': ('rev-d', 'rev-e', ),
680 })
681 gv.load()
682
683 state = loggraphviz.GraphVizFilterState(gv)
684 state.branch_line_state[(1, 2)] = None
685
686 computed = gv.compute_viz(state)
687
688 self.assertComputed(
689 [('rev-f', 0, True, [(0, 0, 0, True), (0, 1, 3, True)]) , # ⊖
690 # ├─╮
691 ('rev-e', 1, False, [(0, 0, 0, True), (1, 1, 0, False)]), # │ ⊕
692 # │ ┆
693 ('rev-d', 0, False, [(0, 0, 0, True), (1, 0, 0, False)]), # ⊕ ┆
694 # ├┄╯
695 ('rev-a', 0, None, []) ],# ○
696 computed)
697
698 def test_non_direct_hidden_parent(self):
699 gv = BasicGraphVizLoader(('rev-e',), {
700 'rev-a': (NULL_REVISION, ),
701 'rev-b': ('rev-a', ),
702 'rev-c': ('rev-b', ),
703 'rev-d': ('rev-a', 'rev-c'),
704 'rev-e': ('rev-a', 'rev-d'),
705 })
706 gv.load()
707
708 state = loggraphviz.GraphVizFilterState(gv)
709 state.filters.append(BasicFilterer(set(['rev-c'])))
710 state.expand_all_branch_lines()
711
712 computed = gv.compute_viz(state)
713
714 self.assertComputed(
715 [('rev-e', 0, True, [(0, 0, 0, True), (0, 1, 3, True)]) , # ⊖
716 # ├─╮
717 ('rev-d', 1, True, [(0, 0, 0, True), (1, 1, 0, True), (1, 2, 2, False)]), # │ ⊖
718 # │ ├┄╮
719 ('rev-b', 2, None, [(0, 0, 0, True), (1, 0, 0, True), (2, 0, 0, True)]) , # │ │ ○
720 # ├─╯─╯
721 ('rev-a', 0, None, []) ],# ○
722 computed)
723
724 def test_no_graph(self):
725 gv = BasicGraphVizLoader(('rev-d',), {
726 'rev-a': (NULL_REVISION, ),
727 'rev-b': ('rev-a', ),
728 'rev-c': ('rev-a', ),
729 'rev-d': ('rev-b', 'rev-c'),
730 }, no_graph=True)
731 gv.load()
732
733 state = loggraphviz.GraphVizFilterState(gv)
734 computed = gv.compute_viz(state)
735 self.assertComputed(
736 [('rev-d', 0.0, None, []), # ○
737 #
738 ('rev-c', 0.5, None, []), # ○
739 #
740 ('rev-b', 0.0, None, []), # ○
741 #
742 ('rev-a', 0.0, None, [])],# ○
743 computed)
744
745 def test_no_graph_filtered(self):
746 gv = BasicGraphVizLoader(('rev-d',), {
747 'rev-a': (NULL_REVISION, ),
748 'rev-b': ('rev-a', ),
749 'rev-c': ('rev-a', ),
750 'rev-d': ('rev-b', 'rev-c'),
751 }, no_graph=True)
752 gv.load()
753
754 state = loggraphviz.GraphVizFilterState(gv)
755 state.filters.append(BasicFilterer(set(['rev-b'])))
756 computed = gv.compute_viz(state)
757 self.assertComputed(
758 [('rev-d', 0.0, None, []), # ○
759 #
760 ('rev-c', 0.5, None, []), # ○
761 #
762 ('rev-a', 0.0, None, [])],# ○
763 computed)
764
765class TestLogGraphProviderState(TestCase):
766
767 def assertFilteredRevisions(self, expected_revids, state):
768 revids = [rev.revid for rev in state.get_filtered_revisions()]
769 self.assertEqual(list(expected_revids), revids)
770
771 def test_collapse_expand_rev_basic(self):
772 gv = BasicGraphVizLoader(('c',), {
773 'a': (NULL_REVISION, ),
774 'b': ('a', ),
775 'c': ('a', 'b'),
776 })
777 gv.load()
778 # c
779 # ├─╮
780 # │ b
781 # ├─╯
782 # a
783
784 state = loggraphviz.GraphVizFilterState(gv)
785
786 # just mainline showing
787 self.assertFilteredRevisions('ca', state)
788
789 # bla - we need a computed to call collapse_expand_rev
790 # expand 'c'
791 state.collapse_expand_rev(gv.compute_viz(state).filtered_revs[0])
792
793 # all should be showing
794 self.assertFilteredRevisions('cba', state)
795
796 # colapse 'c'
797 state.collapse_expand_rev(gv.compute_viz(state).filtered_revs[0])
798
799 # just mainline showing
800 self.assertFilteredRevisions('ca', state)
801
802 def get_expanded_by_graph_provider(self):
803 gv = BasicGraphVizLoader(('f',), {
804 'a': (NULL_REVISION, ),
805 'b': ('a', ),
806 'c': ('a', 'b'),
807 'd': ('a', 'c', ),
808 'e': ('b', ),
809 'f': ('d', 'e')
810 })
811 gv.load()
812 # f
813 # ├───╮
814 # │ e
815 # │ │
816 # d │
817 # ├─╮ │
818 # │ c │
819 # │ │\│
820 # │ │ b
821 # ├─╯─╯
822 # a
823 return gv
824
825 def test_collapse_colapses_sub_expand(self):
826 gv = self.get_expanded_by_graph_provider()
827
828 state = loggraphviz.GraphVizFilterState(gv)
829 # just mainline showing
830 self.assertFilteredRevisions('fda', state)
831
832 # expand 'd'
833 state.collapse_expand_rev(gv.compute_viz(state).revisions[2])
834 # branchline c now showing
835 self.assertFilteredRevisions('fdca', state)
836
837 # expand 'c'
838 state.collapse_expand_rev(gv.compute_viz(state).revisions[3])
839 # all showing
840 self.assertFilteredRevisions('fedcba', state)
841
842 # colapse 'd'
843 state.collapse_expand_rev(gv.compute_viz(state).filtered_revs[2])
844 # cause c expanded branchline eb, and d expanded c, d colapses
845 # just mainline showing
846 self.assertFilteredRevisions('fda', state)
847
848 def test_collapse_dosent_colapses_prev_expand(self):
849 gv = self.get_expanded_by_graph_provider()
850
851 state = loggraphviz.GraphVizFilterState(gv)
852 # just mainline showing
853 self.assertFilteredRevisions('fda', state)
854
855 # expand 'f'
856 state.collapse_expand_rev(gv.compute_viz(state).revisions[0])
857 # branchline eb now showing
858 self.assertFilteredRevisions('fedba', state)
859
860 # expand 'd'
861 state.collapse_expand_rev(gv.compute_viz(state).revisions[2])
862 # all showing
863 self.assertFilteredRevisions('fedcba', state)
864
865 # colapse 'd'
866 state.collapse_expand_rev(gv.compute_viz(state).filtered_revs[2])
867 # cause branchline eb was expanded by f, and not d, collapsing d dose
868 # not collapse branchline eb, even though it expanded it
869 # branchline eb and mainline left showing
870 self.assertFilteredRevisions('fedba', state)
871
872 def test_collapse_deep_expanded_by(self):
873 # This use to error at one point
874 gv = BasicGraphVizLoader(('g',), {
875 'a': (NULL_REVISION, ),
876 'b': ('a', ),
877 'c': ('a', 'b'),
878 'd': ('a', 'c', ),
879 'e': ('b', ),
880 'f': ('d', 'e'),
881 'g': ('a', 'f'),
882 })
883 # g v-----1.3
884 # ├─╮
885 # │ f v-1.1
886 # │ ├───╮
887 # │ │ e
888 # │ │ │
889 # │ d v-│-1.2
890 # │ ├─╮ │
891 # │ │ c │
892 # │ │ │\│
893 # │ │ │ b
894 # ├─╯─╯─╯
895 # a
896
897 gv.load()
898
899 state = loggraphviz.GraphVizFilterState(gv)
900 # just mainline showing
901 self.assertFilteredRevisions('ga', state)
902
903 # expand 'g'
904 state.collapse_expand_rev(gv.compute_viz(state).revisions[0])
905 # branchline fd now showing
906 self.assertFilteredRevisions('gfda', state)
907
908 # expand 'f'
909 state.collapse_expand_rev(gv.compute_viz(state).revisions[1])
910 # branchline eb now showing
911 self.assertFilteredRevisions('gfedba', state)
912
913 # expand 'd'
914 state.collapse_expand_rev(gv.compute_viz(state).revisions[3])
915 # branchline c now showing (all showing)
916 self.assertFilteredRevisions('gfedcba', state)
917
918 # colapse 'g'
919 state.collapse_expand_rev(gv.compute_viz(state).filtered_revs[0])
920 # back to just mainline showing
921 self.assertFilteredRevisions('ga', state)
922
923 def test_filter(self):
924 gv = BasicGraphVizLoader(('e',), {
925 'a': (NULL_REVISION, ),
926 'b': ('a', ),
927 'c': ('a', 'b'),
928 'd': ('c', ),
929 'e': ('d', ),
930 })
931 gv.load()
932 # e
933 # │
934 # d
935 # │
936 # c
937 # ├─╮
938 # │ b
939 # ├─╯
940 # a
941
942 state = loggraphviz.GraphVizFilterState(gv)
943
944 # expand 'c'
945 state.collapse_expand_rev(gv.compute_viz(state).filtered_revs[2])
946
947 # all should be showing
948 self.assertFilteredRevisions('edcba', state)
949
950 state.filters.append(BasicFilterer(('d', 'c', 'a')))
951 state.filter_changed()
952 # d and a not showing bucause of filter
953 # c shows even though it is filtered, because it merges a revision
954 # that is not filtered.
955 self.assertFilteredRevisions('ecb', state)
956
957
958
959class BasicGraphVizLoader(loggraphviz.GraphVizLoader):
960
961 def __init__(self, heads, graph_dict, no_graph=False):
962 self.heads = heads
963 self.graph_dict = graph_dict
964 bi = loggraphviz.BranchInfo(None, None, None)
965 loggraphviz.GraphVizLoader.__init__(self, [bi], bi, no_graph)
966
967 def load(self):
968 for head in self.heads:
969 self.append_head_info(head, self.branches[0], head)
970
971 self.process_graph_parents(self.heads, self.graph_dict.items())
972
973 self.compute_head_info()
974
975 if not self.no_graph:
976 self.compute_branch_lines()
977 self.compute_merge_info()
978
979
980class BasicFilterer(object):
981 def __init__(self, filtered_revids):
982 self.filtered_revids = filtered_revids
983
984 def get_revision_visible(self, rev):
985 return rev.revid not in self.filtered_revids
986
987def print_computed(computed):
988 print_lines([(c_rev.rev.revid,
989 c_rev.col_index,
990 c_rev.twisty_state,
991 c_rev.lines,)
992 for c_rev in computed.filtered_revs])
993
994def print_lines(list):
995 print ''
996 print format_graph_lines(list)
997
998def format_graph_lines(list, use_unicode=True):
999 if not list:
1000 return list.__repr__() + '\n'
1001 s = StringIO()
1002 item_repr = [item.__repr__() for item in list]
1003 repr_width = max([len(repr) for repr in item_repr])
1004 if use_unicode:
1005 twisty_char = {None: u'○',
1006 True: u'⊖',
1007 False: u'⊕'}
1008 ver_char = {True: u'│',
1009 False: u'┆'}
1010
1011 hor_char = {True: {u' ': u'─',
1012 u'│': u'┼',
1013 u'┆': u'┼'},
1014 False: {u' ': u'┄',
1015 u'│': u'┼',
1016 u'┆': u'┼'}}
1017
1018 tl_char = {u' ': u'╭',
1019 u'│': u'├',
1020 u'┆': u'├',
1021 u'─': u'┬',
1022 u'┄': u'┬',
1023 u'┴': u'┼',
1024 u'┤': u'┼',}
1025
1026 tr_char = {u' ': u'╮',
1027 u'│': u'┤',
1028 u'┆': u'┤',
1029 u'─': u'┬',
1030 u'┄': u'┬',
1031 u'┴': u'┼',
1032 u'├': u'┼',}
1033
1034 bl_char = {u' ': u'╰',
1035 u'│': u'├',
1036 u'┆': u'├',
1037 u'─': u'┴',
1038 u'┄': u'┴',
1039 u'┬': u'┼',
1040 u'┤': u'┼',}
1041
1042 br_char = {u' ': u'╯',
1043 u'│': u'┤',
1044 u'┆': u'┤',
1045 u'─': u'┴',
1046 u'┄': u'┴',
1047 u'┬': u'┼',
1048 u'├': u'┼',}
1049 else:
1050 twisty_char = {None: '*',
1051 True: '~',
1052 False: '+'}
1053 ver_char = {True: '|',
1054 False: ':'}
1055
1056 hor_char = {True: {' ': '-',},
1057 False: {' ': '-'}}
1058
1059 tl_char = {}
1060
1061 tr_char = {}
1062
1063 bl_char = {}
1064
1065 br_char = {}
1066
1067 for row, (item, repr) in enumerate(zip(list, item_repr)):
1068 if row == 0:
1069 s.write('[')
1070 else:
1071 s.write(' ')
1072 s.write(repr.ljust(repr_width))
1073 if row == len(list)-1:
1074 s.write('] # ')
1075 else:
1076 s.write(', # ')
1077
1078 if len(item) == 4:
1079 revid, col_index, twisty_state, lines = item
1080 if len(item) == 5:
1081 revid, col_index, twisty_state, lines, labels = item
1082
1083 all_cols = [col_index]
1084 all_cols += [start for start, end, color, direct in lines]
1085 all_cols += [end for start, end, color, direct in lines]
1086 num_cols = (max(all_cols) + 1) * 2
1087 this_line = [' ' for i in range(num_cols)]
1088 next_line = [' ' for i in range(num_cols)]
1089
1090 for start, end, color, direct in lines:
1091 if start is None or end is None:
1092 continue
1093 start = int(round(start))
1094 end = int(round(end))
1095 if start == end:
1096 this_line[start * 2] = ver_char[direct]
1097 next_line[start * 2] = ver_char[direct]
1098 else:
1099 this_line[start * 2] = ver_char[direct]
1100
1101 def replace_char(line, i, char_dict):
1102 old_char = line[i]
1103 if old_char in char_dict:
1104 line[i] = char_dict[old_char]
1105
1106 for start, end, color, direct in lines:
1107 if start is None or end is None:
1108 continue
1109 start = int(round(start))
1110 end = int(round(end))
1111 if start < end:
1112 for i in range(start * 2 + 1, end * 2):
1113 replace_char(next_line, i, hor_char[direct])
1114 replace_char(next_line, start * 2, bl_char)
1115 replace_char(next_line, end * 2, tr_char)
1116 elif start > end:
1117 for i in range(end * 2 + 1, start * 2):
1118 replace_char(next_line, i, hor_char[direct])
1119 replace_char(next_line, start * 2, br_char)
1120 replace_char(next_line, end * 2, tl_char)
1121
1122 this_line[int(col_index * 2)] = twisty_char[twisty_state]
1123
1124 s.write(''.join(this_line))
1125 s.write('\n')
1126
1127 if not row == len(list)-1:
1128 s.write('# '.rjust(repr_width + 5))
1129 s.write(''.join(next_line))
1130 s.write('\n')
1131 return s.getvalue()
1132
1133
1134class TestGroupOverlapping(TestCase):
1135 def test_group_overlapping(self):
1136 lines = [
1137 (['a1'], 1, 3, 'a'),
1138 (['a2'], 2, 5, 'a'),
1139 (['a3'], 4, 6, 'a'),
1140 (['a4'], 6, 8, 'a'),
1141 (['b1'], 1, 3, 'b'),
1142 (['b2'], 2, 5, 'b'),
1143 (['n1'], 1, 8, None),
1144 (['n2'], 1, 8, None),
1145 ]
1146 groups = loggraphviz.group_overlapping(lines)
1147 self.assertEqual(
1148 [(['a1', 'a2', 'a3'], 1, 6, 'a'),
1149 (['a4'], 6, 8, 'a'),
1150 (['b1', 'b2'], 1, 5, 'b'),
1151 (['n1'], 1, 8, None),
1152 (['n2'], 1, 8, None)],
1153 groups)