Merge lp:~vila/bzr/320119-exclude-ancestry into lp:bzr

Proposed by Vincent Ladeuil
Status: Merged
Approved by: Vincent Ladeuil
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~vila/bzr/320119-exclude-ancestry
Merge into: lp:bzr
Prerequisite: lp:~vila/bzr/cleanup-log-direction
Diff against target: 428 lines (+189/-17)
8 files modified
NEWS (+7/-1)
bzrlib/branch.py (+16/-2)
bzrlib/builtins.py (+14/-2)
bzrlib/log.py (+35/-11)
bzrlib/tests/blackbox/test_log.py (+12/-0)
bzrlib/tests/per_branch/test_iter_merge_sorted_revisions.py (+44/-0)
bzrlib/tests/per_repository_reference/__init__.py (+1/-1)
bzrlib/tests/test_log.py (+60/-0)
To merge this branch: bzr merge lp:~vila/bzr/320119-exclude-ancestry
Reviewer Review Type Date Requested Status
Andrew Bennetts Needs Fixing
Review via email: mp+23394@code.launchpad.net

Commit message

Add --exclude-common-ancestry log option

Description of the change

This patch adds an --exclude-common-anccestry to bzr log so that -rX..Y becomes
a real graph difference (killing two birds with one stone, it also fixes bug #320119).

From now on, I consider that the log has reached the point where it's really
hard to add new features. The major culprit is the optimization to avoid
loading the whole graph, so no need to spend much time on the log code
unless this problem is correctly addressed.

The performance impact may be significant but I'd like real-life feedback on its
usage before trying to replace the is_ancestor() call by any caching of the X ancestry
(which may be quite significant anyway).

Again, having a better lazy-loaded graph ancestry is needed there.

To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote :

This seems like something other commands than log will want; perhaps we
should tackle it using an algreba, like git does?

Revision history for this message
Andrew Bennetts (spiv) wrote :

You have a few conflicts because this branch waited so long for a review. Sorry :(

You define a new stop_rule for iter_merge_sorted_revisions, but you haven't added it to the docstring. Please fix that.

It's a shame that make_branch_with_alternate_ancestries is duplicated in test_log and per_branch.test_iter_merge_sorted_revisions. Perhaps make it a function and have the latter import it from the former? Please at least add comments in both places noting the duplication.

(And further, it's a shame that the description of the ancestry is duplicated within that code... if only there were some way to do "make_branch_from_ascii_art_ancestry".)

Other than those, this seems ok to me. Once you fix those issues you can consider my vote upgraded to Approve.

I get the feeling that the next time we are tempted to another parameter to log we should think about refactoring the way we pass log options through the various layers (maybe with a LogOptions object? Or maybe Robert's idea about an algebra...), but this is ok for now.

review: Needs Fixing
Revision history for this message
Vincent Ladeuil (vila) wrote :

>>>>> Andrew Bennetts <email address hidden> writes:

    > Review: Needs Fixing

    > You have a few conflicts because this branch waited so long for a review. Sorry :(

    > You define a new stop_rule for iter_merge_sorted_revisions, but
    > you haven't added it to the docstring. Please fix that.

Done.

    > It's a shame that make_branch_with_alternate_ancestries is
    > duplicated in test_log and
    > per_branch.test_iter_merge_sorted_revisions. Perhaps make it a
    > function and have the latter import it from the former? Please at
    > least add comments in both places noting the duplication.

Since there is a valuable slight variation, I went with adding comments.

    > (And further, it's a shame that the description of the ancestry is
    > duplicated within that code... if only there were some way to do
    > "make_branch_from_ascii_art_ancestry".)

Hehe, ideally I'd prefer some GUI to define the graph and get both the
ascii art and the branchbuilder stuff from that. ascii art is *not* fun
to write ;)

    > Other than those, this seems ok to me. Once you fix those issues
    > you can consider my vote upgraded to Approve.

Thanks.

    > I get the feeling that the next time we are tempted to another
    > parameter to log we should think about refactoring the way we pass
    > log options through the various layers (maybe with a LogOptions
    > object?

Definitely, but we also need to wire that up to the command line as many
log formatters will want their own options, I've punted on that one so
far for lack of time to research how to catch the options not recognized
by the parser and allow plugins or any external code to give it a try.

    > Or maybe Robert's idea about an algebra...), but this is ok for
    > now.

Revision history for this message
bzr PQM (bzr-pqm) wrote :

Successful steps
Failure output:
All lines of log output:
Executing star-merge lp:~vila/bzr/320119-exclude-ancestry at Wed Apr 28 10:41:57 2010
['Nothing to merge.']

Revision history for this message
Vincent Ladeuil (vila) wrote :

Meh, where is that pqm failure coming from ?
I sent a single submission with feed-pqm and it was merged.... (I didn't get a success email for it though).

Revision history for this message
Robert Collins (lifeless) wrote :

This is probably due to lp taking too long to mark the branch as
merged: if it takes longer than pqm takes to try again, then pqm will
see it as still pending. Possibly we should:
mark things we succeed at as approved
adding a comment that it landed ok

or mark it as merged.

I'm a little worried about triggering launchpadlib errors though,
because lp is going to be updating the status at the same time - we
can collide.

Revision history for this message
John A Meinel (jameinel) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Robert Collins wrote:
> This is probably due to lp taking too long to mark the branch as
> merged: if it takes longer than pqm takes to try again, then pqm will
> see it as still pending. Possibly we should:
> mark things we succeed at as approved
> adding a comment that it landed ok
>
> or mark it as merged.
>
> I'm a little worried about triggering launchpadlib errors though,
> because lp is going to be updating the status at the same time - we
> can collide.

I think it would be reasonable to have your script notice that "Nothing
to be Merged" obviously means that the branch is already merged...

John
=:->

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAkvY+TsACgkQJdeBCYSNAAPIMQCgqjpPMmQSCREDGp9/5OdQKQVN
V8YAn34mPZMKIzBYQqYSNCR5lVb6+31+
=hVtp
-----END PGP SIGNATURE-----

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'NEWS'
2--- NEWS 2010-04-27 18:09:12 +0000
3+++ NEWS 2010-04-28 07:18:51 +0000
4@@ -34,6 +34,11 @@
5 better with sudo.
6 (Martin <gzlist@googlemail.com>, Parth Malwankar, #376388)
7
8+* ``bzr log --exclude-common-ancestry -r X..Y`` displays the revisions that
9+ are part of Y ancestry but not part of X ancestry (aka the graph
10+ difference).
11+ (Vincent Ladeuil, #320119)
12+
13 * ``bzr selftest --parallel=fork`` wait for its children avoiding zombies.
14 (Vincent Ladeuil, #566670)
15
16@@ -145,7 +150,8 @@
17 (Andrew Bennetts)
18
19 * When invoked with a range revision, ``bzr log`` doesn't show revisions
20- that are not part of the ancestry anymore.
21+ that are not part of the Y revisions ancestry anymore when invoked with
22+ -rX..Y.
23 (Vincent Ladeuil, #474807)
24
25 Improvements
26
27=== modified file 'bzrlib/branch.py'
28--- bzrlib/branch.py 2010-04-23 07:15:23 +0000
29+++ bzrlib/branch.py 2010-04-28 07:18:51 +0000
30@@ -420,6 +420,8 @@
31 * 'include' - the stop revision is the last item in the result
32 * 'with-merges' - include the stop revision and all of its
33 merged revisions in the result
34+ * 'with-merges-without-common-ancestry' - filter out revisions
35+ that are in both ancestries
36 :param direction: either 'reverse' or 'forward':
37 * reverse means return the start_revision_id first, i.e.
38 start at the most recent revision and go backwards in history
39@@ -456,7 +458,7 @@
40 stop_revision_id, stop_rule)
41 # Make sure we don't return revisions that are not part of the
42 # start_revision_id ancestry.
43- filtered = self._filter_non_ancestors(filtered)
44+ filtered = self._filter_start_non_ancestors(filtered)
45 if direction == 'reverse':
46 return filtered
47 if direction == 'forward':
48@@ -499,6 +501,18 @@
49 node.end_of_merge)
50 if rev_id == stop_revision_id:
51 return
52+ elif stop_rule == 'with-merges-without-common-ancestry':
53+ # We want to exclude all revisions that are already part of the
54+ # stop_revision_id ancestry.
55+ graph = self.repository.get_graph()
56+ ancestors = graph.find_unique_ancestors(start_revision_id,
57+ [stop_revision_id])
58+ for node in rev_iter:
59+ rev_id = node.key[-1]
60+ if rev_id not in ancestors:
61+ continue
62+ yield (rev_id, node.merge_depth, node.revno,
63+ node.end_of_merge)
64 elif stop_rule == 'with-merges':
65 stop_rev = self.repository.get_revision(stop_revision_id)
66 if stop_rev.parent_ids:
67@@ -527,7 +541,7 @@
68 else:
69 raise ValueError('invalid stop_rule %r' % stop_rule)
70
71- def _filter_non_ancestors(self, rev_iter):
72+ def _filter_start_non_ancestors(self, rev_iter):
73 # If we started from a dotted revno, we want to consider it as a tip
74 # and don't want to yield revisions that are not part of its
75 # ancestry. Given the order guaranteed by the merge sort, we will see
76
77=== modified file 'bzrlib/builtins.py'
78--- bzrlib/builtins.py 2010-04-23 11:11:22 +0000
79+++ bzrlib/builtins.py 2010-04-28 07:18:51 +0000
80@@ -2299,6 +2299,10 @@
81 help='Show changes made in each revision as a patch.'),
82 Option('include-merges',
83 help='Show merged revisions like --levels 0 does.'),
84+ Option('exclude-common-ancestry',
85+ help='Display only the revisions that are not part'
86+ ' of both ancestries (require -rX..Y)'
87+ )
88 ]
89 encoding_type = 'replace'
90
91@@ -2314,13 +2318,19 @@
92 message=None,
93 limit=None,
94 show_diff=False,
95- include_merges=False):
96+ include_merges=False,
97+ exclude_common_ancestry=False,
98+ ):
99 from bzrlib.log import (
100 Logger,
101 make_log_request_dict,
102 _get_info_for_log_files,
103 )
104 direction = (forward and 'forward') or 'reverse'
105+ if (exclude_common_ancestry
106+ and (revision is None or len(revision) != 2)):
107+ raise errors.BzrCommandError(
108+ '--exclude-common-ancestry requires -r with two revisions')
109 if include_merges:
110 if levels is None:
111 levels = 0
112@@ -2419,7 +2429,9 @@
113 direction=direction, specific_fileids=file_ids,
114 start_revision=rev1, end_revision=rev2, limit=limit,
115 message_search=message, delta_type=delta_type,
116- diff_type=diff_type, _match_using_deltas=match_using_deltas)
117+ diff_type=diff_type, _match_using_deltas=match_using_deltas,
118+ exclude_common_ancestry=exclude_common_ancestry,
119+ )
120 Logger(b, rqst).show(lf)
121
122
123
124=== modified file 'bzrlib/log.py'
125--- bzrlib/log.py 2010-04-14 10:38:57 +0000
126+++ bzrlib/log.py 2010-04-28 07:18:51 +0000
127@@ -220,14 +220,18 @@
128 'direction': 'reverse',
129 'levels': 1,
130 'generate_tags': True,
131+ 'exclude_common_ancestry': False,
132 '_match_using_deltas': True,
133 }
134
135
136 def make_log_request_dict(direction='reverse', specific_fileids=None,
137- start_revision=None, end_revision=None, limit=None,
138- message_search=None, levels=1, generate_tags=True, delta_type=None,
139- diff_type=None, _match_using_deltas=True):
140+ start_revision=None, end_revision=None, limit=None,
141+ message_search=None, levels=1, generate_tags=True,
142+ delta_type=None,
143+ diff_type=None, _match_using_deltas=True,
144+ exclude_common_ancestry=False,
145+ ):
146 """Convenience function for making a logging request dictionary.
147
148 Using this function may make code slightly safer by ensuring
149@@ -271,6 +275,9 @@
150 algorithm used for matching specific_fileids. This parameter
151 may be removed in the future so bzrlib client code should NOT
152 use it.
153+
154+ :param exclude_common_ancestry: Whether -rX..Y should be interpreted as a
155+ range operator or as a graph difference.
156 """
157 return {
158 'direction': direction,
159@@ -283,6 +290,7 @@
160 'generate_tags': generate_tags,
161 'delta_type': delta_type,
162 'diff_type': diff_type,
163+ 'exclude_common_ancestry': exclude_common_ancestry,
164 # Add 'private' attributes for features that may be deprecated
165 '_match_using_deltas': _match_using_deltas,
166 }
167@@ -459,7 +467,8 @@
168 self.branch, self.start_rev_id, self.end_rev_id,
169 rqst.get('direction'),
170 generate_merge_revisions=generate_merge_revisions,
171- delayed_graph_generation=delayed_graph_generation)
172+ delayed_graph_generation=delayed_graph_generation,
173+ exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
174
175 # Apply the other filters
176 return make_log_rev_iterator(self.branch, view_revisions,
177@@ -474,7 +483,8 @@
178 rqst = self.rqst
179 view_revisions = _calc_view_revisions(
180 self.branch, self.start_rev_id, self.end_rev_id,
181- rqst.get('direction'), generate_merge_revisions=True)
182+ rqst.get('direction'), generate_merge_revisions=True,
183+ exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
184 if not isinstance(view_revisions, list):
185 view_revisions = list(view_revisions)
186 view_revisions = _filter_revisions_touching_file_id(self.branch,
187@@ -485,12 +495,18 @@
188
189
190 def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
191- generate_merge_revisions, delayed_graph_generation=False):
192+ generate_merge_revisions,
193+ delayed_graph_generation=False,
194+ exclude_common_ancestry=False,
195+ ):
196 """Calculate the revisions to view.
197
198 :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
199 a list of the same tuples.
200 """
201+ if (exclude_common_ancestry and start_rev_id == end_rev_id):
202+ raise errors.BzrCommandError(
203+ '--exclude-common-ancestry requires two different revisions')
204 if direction not in ('reverse', 'forward'):
205 raise ValueError('invalid direction %r' % direction)
206 br_revno, br_rev_id = branch.last_revision_info()
207@@ -511,7 +527,8 @@
208 iter_revs = reversed(iter_revs)
209 else:
210 iter_revs = _generate_all_revisions(branch, start_rev_id, end_rev_id,
211- direction, delayed_graph_generation)
212+ direction, delayed_graph_generation,
213+ exclude_common_ancestry)
214 if direction == 'forward':
215 iter_revs = _rebase_merge_depth(reverse_by_depth(list(iter_revs)))
216 return iter_revs
217@@ -542,7 +559,8 @@
218
219
220 def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
221- delayed_graph_generation):
222+ delayed_graph_generation,
223+ exclude_common_ancestry=False):
224 # On large trees, generating the merge graph can take 30-60 seconds
225 # so we delay doing it until a merge is detected, incrementally
226 # returning initial (non-merge) revisions while we can.
227@@ -594,7 +612,8 @@
228 # indented at the end seems slightly nicer in that case.
229 view_revisions = chain(iter(initial_revisions),
230 _graph_view_revisions(branch, start_rev_id, end_rev_id,
231- rebase_initial_depths=(direction == 'reverse')))
232+ rebase_initial_depths=(direction == 'reverse'),
233+ exclude_common_ancestry=exclude_common_ancestry))
234 return view_revisions
235
236
237@@ -659,7 +678,8 @@
238
239
240 def _graph_view_revisions(branch, start_rev_id, end_rev_id,
241- rebase_initial_depths=True):
242+ rebase_initial_depths=True,
243+ exclude_common_ancestry=False):
244 """Calculate revisions to view including merges, newest to oldest.
245
246 :param branch: the branch
247@@ -669,9 +689,13 @@
248 revision is found?
249 :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
250 """
251+ if exclude_common_ancestry:
252+ stop_rule = 'with-merges-without-common-ancestry'
253+ else:
254+ stop_rule = 'with-merges'
255 view_revisions = branch.iter_merge_sorted_revisions(
256 start_revision_id=end_rev_id, stop_revision_id=start_rev_id,
257- stop_rule="with-merges")
258+ stop_rule=stop_rule)
259 if not rebase_initial_depths:
260 for (rev_id, merge_depth, revno, end_of_merge
261 ) in view_revisions:
262
263=== modified file 'bzrlib/tests/blackbox/test_log.py'
264--- bzrlib/tests/blackbox/test_log.py 2010-03-24 14:15:01 +0000
265+++ bzrlib/tests/blackbox/test_log.py 2010-04-28 07:18:51 +0000
266@@ -365,6 +365,18 @@
267 'options are "utc", "original", "local".'],
268 ['log', '--timezone', 'foo'])
269
270+ def test_log_exclude_ancestry_no_range(self):
271+ self.make_linear_branch()
272+ self.run_bzr_error(['bzr: ERROR: --exclude-common-ancestry'
273+ ' requires -r with two revisions'],
274+ ['log', '--exclude-common-ancestry'])
275+
276+ def test_log_exclude_ancestry_single_revision(self):
277+ self.make_merged_branch()
278+ self.run_bzr_error(['bzr: ERROR: --exclude-common-ancestry'
279+ ' requires two different revisions'],
280+ ['log', '--exclude-common-ancestry',
281+ '-r1.1.1..1.1.1'])
282
283 class TestLogTags(TestLog):
284
285
286=== modified file 'bzrlib/tests/per_branch/test_iter_merge_sorted_revisions.py'
287--- bzrlib/tests/per_branch/test_iter_merge_sorted_revisions.py 2010-04-02 15:05:24 +0000
288+++ bzrlib/tests/per_branch/test_iter_merge_sorted_revisions.py 2010-04-28 07:18:51 +0000
289@@ -229,6 +229,37 @@
290 self.addCleanup(br.unlock)
291 return br
292
293+ def make_branch_with_alternate_ancestries(self, relpath='.'):
294+ # See test_merge_sorted_exclude_ancestry below for the difference with
295+ # bt.test_log.TestLogExcludeAncestry.
296+ # make_branch_with_alternate_ancestries and
297+ # test_merge_sorted_exclude_ancestry
298+ # See the FIXME in assertLogRevnos there too.
299+ builder = self.make_branch_builder(relpath)
300+ # 1
301+ # |\
302+ # | 1.1.1
303+ # | /| \
304+ # 2 | |
305+ # | | 1.2.1
306+ # | | /
307+ # | 1.1.2
308+ # | /
309+ # 3
310+ builder.start_series()
311+ builder.build_snapshot('1', None, [
312+ ('add', ('', 'TREE_ROOT', 'directory', '')),])
313+ builder.build_snapshot('1.1.1', ['1'], [])
314+ builder.build_snapshot('2', ['1', '1.1.1'], [])
315+ builder.build_snapshot('1.2.1', ['1.1.1'], [])
316+ builder.build_snapshot('1.1.2', ['1.1.1', '1.2.1'], [])
317+ builder.build_snapshot('3', ['2', '1.1.2'], [])
318+ builder.finish_series()
319+ br = builder.get_branch()
320+ br.lock_read()
321+ self.addCleanup(br.unlock)
322+ return br
323+
324 def assertIterRevids(self, expected, branch, *args, **kwargs):
325 # We don't care about depths and revnos here, only about returning the
326 # right revids.
327@@ -259,3 +290,16 @@
328 self.assertIterRevids(['2.2.1', '2.1.1', '2', '1'],
329 branch, start_revision_id='2.2.1',
330 stop_rule='with-merges')
331+
332+ def test_merge_sorted_exclude_ancestry(self):
333+ branch = self.make_branch_with_alternate_ancestries()
334+ self.assertIterRevids(['3', '1.1.2', '1.2.1', '2', '1.1.1', '1'],
335+ branch)
336+ # '2' is not part of the ancestry even if merge_sort order will make it
337+ # appear before 1.1.1
338+ self.assertIterRevids(['1.1.2', '1.2.1'],
339+ branch,
340+ stop_rule='with-merges-without-common-ancestry',
341+ start_revision_id='1.1.2',
342+ stop_revision_id='1.1.1')
343+
344
345=== modified file 'bzrlib/tests/per_repository_reference/__init__.py'
346--- bzrlib/tests/per_repository_reference/__init__.py 2010-04-16 06:51:59 +0000
347+++ bzrlib/tests/per_repository_reference/__init__.py 2010-04-28 07:18:51 +0000
348@@ -1,4 +1,4 @@
349-# Copyright (C) 2008, 2009 Canonical Ltd
350+# Copyright (C) 2008, 2009, 2010 Canonical Ltd
351 #
352 # This program is free software; you can redistribute it and/or modify
353 # it under the terms of the GNU General Public License as published by
354
355=== modified file 'bzrlib/tests/test_log.py'
356--- bzrlib/tests/test_log.py 2010-03-25 08:14:04 +0000
357+++ bzrlib/tests/test_log.py 2010-04-28 07:18:51 +0000
358@@ -18,6 +18,7 @@
359 from cStringIO import StringIO
360
361 from bzrlib import (
362+ branchbuilder,
363 errors,
364 log,
365 registry,
366@@ -1541,3 +1542,62 @@
367
368 def test_bugs_handler_present(self):
369 self.properties_handler_registry.get('bugs_properties_handler')
370+
371+class TestLogExcludeAncestry(tests.TestCaseWithTransport):
372+
373+ def make_branch_with_alternate_ancestries(self, relpath='.'):
374+ # See test_merge_sorted_exclude_ancestry below for the difference with
375+ # bt.per_branch.test_iter_merge_sorted_revision.
376+ # TestIterMergeSortedRevisionsBushyGraph.
377+ # make_branch_with_alternate_ancestries
378+ # and test_merge_sorted_exclude_ancestry
379+ # See the FIXME in assertLogRevnos too.
380+ builder = branchbuilder.BranchBuilder(self.get_transport(relpath))
381+ # 1
382+ # |\
383+ # 2 \
384+ # | |
385+ # | 1.1.1
386+ # | | \
387+ # | | 1.2.1
388+ # | | /
389+ # | 1.1.2
390+ # | /
391+ # 3
392+ builder.start_series()
393+ builder.build_snapshot('1', None, [
394+ ('add', ('', 'TREE_ROOT', 'directory', '')),])
395+ builder.build_snapshot('1.1.1', ['1'], [])
396+ builder.build_snapshot('2', ['1'], [])
397+ builder.build_snapshot('1.2.1', ['1.1.1'], [])
398+ builder.build_snapshot('1.1.2', ['1.1.1', '1.2.1'], [])
399+ builder.build_snapshot('3', ['2', '1.1.2'], [])
400+ builder.finish_series()
401+ br = builder.get_branch()
402+ br.lock_read()
403+ self.addCleanup(br.unlock)
404+ return br
405+
406+ def assertLogRevnos(self, expected_revnos, b, start, end,
407+ exclude_common_ancestry):
408+ # FIXME: the layering in log makes it hard to test intermediate levels,
409+ # I wish adding filters with their parameters were easier...
410+ # -- vila 20100413
411+ iter_revs = log._calc_view_revisions(
412+ b, start, end, direction='reverse',
413+ generate_merge_revisions=True,
414+ exclude_common_ancestry=exclude_common_ancestry)
415+ self.assertEqual(expected_revnos,
416+ [revid for revid, revno, depth in iter_revs])
417+
418+ def test_merge_sorted_exclude_ancestry(self):
419+ b = self.make_branch_with_alternate_ancestries()
420+ self.assertLogRevnos(['3', '1.1.2', '1.2.1', '1.1.1', '2', '1'],
421+ b, '1', '3', False)
422+ # '2' is part of the '3' ancestry but not part of '1.1.1' ancestry so
423+ # it should be mentioned even if merge_sort order will make it appear
424+ # after 1.1.1
425+ self.assertLogRevnos(['3', '1.1.2', '1.2.1', '2'],
426+ b, '1.1.1', '3', True)
427+
428+