1# test_walk.py -- Tests for commit walking functionality.
2# Copyright (C) 2010 Google, Inc.
3#
4# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
5# General Public License as public by the Free Software Foundation; version 2.0
6# or (at your option) any later version. You can redistribute it and/or
7# modify it under the terms of either of these two licenses.
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14#
15# You should have received a copy of the licenses; if not, see
16# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
17# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
18# License, Version 2.0.
19#
20
21"""Tests for commit walking functionality."""
22
23from itertools import (
24    permutations,
25    )
26
27from dulwich.diff_tree import (
28    CHANGE_MODIFY,
29    CHANGE_RENAME,
30    TreeChange,
31    RenameDetector,
32    )
33from dulwich.errors import (
34    MissingCommitError,
35    )
36from dulwich.object_store import (
37    MemoryObjectStore,
38    )
39from dulwich.objects import (
40    Commit,
41    Blob,
42    )
43from dulwich.walk import (
44    ORDER_TOPO,
45    WalkEntry,
46    Walker,
47    _topo_reorder
48    )
49from dulwich.tests import TestCase
50from dulwich.tests.utils import (
51    F,
52    make_object,
53    make_tag,
54    build_commit_graph,
55    )
56
57
58class TestWalkEntry(object):
59
60    def __init__(self, commit, changes):
61        self.commit = commit
62        self.changes = changes
63
64    def __repr__(self):
65        return '<TestWalkEntry commit=%s, changes=%r>' % (
66          self.commit.id, self.changes)
67
68    def __eq__(self, other):
69        if not isinstance(other, WalkEntry) or self.commit != other.commit:
70            return False
71        if self.changes is None:
72            return True
73        return self.changes == other.changes()
74
75
76class WalkerTest(TestCase):
77
78    def setUp(self):
79        super(WalkerTest, self).setUp()
80        self.store = MemoryObjectStore()
81
82    def make_commits(self, commit_spec, **kwargs):
83        times = kwargs.pop('times', [])
84        attrs = kwargs.pop('attrs', {})
85        for i, t in enumerate(times):
86            attrs.setdefault(i + 1, {})['commit_time'] = t
87        return build_commit_graph(self.store, commit_spec, attrs=attrs,
88                                  **kwargs)
89
90    def make_linear_commits(self, num_commits, **kwargs):
91        commit_spec = []
92        for i in range(1, num_commits + 1):
93            c = [i]
94            if i > 1:
95                c.append(i - 1)
96            commit_spec.append(c)
97        return self.make_commits(commit_spec, **kwargs)
98
99    def assertWalkYields(self, expected, *args, **kwargs):
100        walker = Walker(self.store, *args, **kwargs)
101        expected = list(expected)
102        for i, entry in enumerate(expected):
103            if isinstance(entry, Commit):
104                expected[i] = TestWalkEntry(entry, None)
105        actual = list(walker)
106        self.assertEqual(expected, actual)
107
108    def test_tag(self):
109        c1, c2, c3 = self.make_linear_commits(3)
110        t2 = make_tag(target=c2)
111        self.store.add_object(t2)
112        self.assertWalkYields([c2, c1], [t2.id])
113
114    def test_linear(self):
115        c1, c2, c3 = self.make_linear_commits(3)
116        self.assertWalkYields([c1], [c1.id])
117        self.assertWalkYields([c2, c1], [c2.id])
118        self.assertWalkYields([c3, c2, c1], [c3.id])
119        self.assertWalkYields([c3, c2, c1], [c3.id, c1.id])
120        self.assertWalkYields([c3, c2], [c3.id], exclude=[c1.id])
121        self.assertWalkYields([c3, c2], [c3.id, c1.id], exclude=[c1.id])
122        self.assertWalkYields([c3], [c3.id, c1.id], exclude=[c2.id])
123
124    def test_missing(self):
125        cs = list(reversed(self.make_linear_commits(20)))
126        self.assertWalkYields(cs, [cs[0].id])
127
128        # Exactly how close we can get to a missing commit depends on our
129        # implementation (in particular the choice of _MAX_EXTRA_COMMITS), but
130        # we should at least be able to walk some history in a broken repo.
131        del self.store[cs[-1].id]
132        for i in range(1, 11):
133            self.assertWalkYields(cs[:i], [cs[0].id], max_entries=i)
134        self.assertRaises(MissingCommitError, Walker, self.store, [cs[-1].id])
135
136    def test_branch(self):
137        c1, x2, x3, y4 = self.make_commits([[1], [2, 1], [3, 2], [4, 1]])
138        self.assertWalkYields([x3, x2, c1], [x3.id])
139        self.assertWalkYields([y4, c1], [y4.id])
140        self.assertWalkYields([y4, x2, c1], [y4.id, x2.id])
141        self.assertWalkYields([y4, x2], [y4.id, x2.id], exclude=[c1.id])
142        self.assertWalkYields([y4, x3], [y4.id, x3.id], exclude=[x2.id])
143        self.assertWalkYields([y4], [y4.id], exclude=[x3.id])
144        self.assertWalkYields([x3, x2], [x3.id], exclude=[y4.id])
145
146    def test_merge(self):
147        c1, c2, c3, c4 = self.make_commits([[1], [2, 1], [3, 1], [4, 2, 3]])
148        self.assertWalkYields([c4, c3, c2, c1], [c4.id])
149        self.assertWalkYields([c3, c1], [c3.id])
150        self.assertWalkYields([c2, c1], [c2.id])
151        self.assertWalkYields([c4, c3], [c4.id], exclude=[c2.id])
152        self.assertWalkYields([c4, c2], [c4.id], exclude=[c3.id])
153
154    def test_reverse(self):
155        c1, c2, c3 = self.make_linear_commits(3)
156        self.assertWalkYields([c1, c2, c3], [c3.id], reverse=True)
157
158    def test_max_entries(self):
159        c1, c2, c3 = self.make_linear_commits(3)
160        self.assertWalkYields([c3, c2, c1], [c3.id], max_entries=3)
161        self.assertWalkYields([c3, c2], [c3.id], max_entries=2)
162        self.assertWalkYields([c3], [c3.id], max_entries=1)
163
164    def test_reverse_after_max_entries(self):
165        c1, c2, c3 = self.make_linear_commits(3)
166        self.assertWalkYields([c1, c2, c3], [c3.id], max_entries=3,
167                              reverse=True)
168        self.assertWalkYields([c2, c3], [c3.id], max_entries=2, reverse=True)
169        self.assertWalkYields([c3], [c3.id], max_entries=1, reverse=True)
170
171    def test_changes_one_parent(self):
172        blob_a1 = make_object(Blob, data=b'a1')
173        blob_a2 = make_object(Blob, data=b'a2')
174        blob_b2 = make_object(Blob, data=b'b2')
175        c1, c2 = self.make_linear_commits(
176            2, trees={1: [(b'a', blob_a1)],
177                      2: [(b'a', blob_a2), (b'b', blob_b2)]})
178        e1 = TestWalkEntry(c1, [TreeChange.add((b'a', F, blob_a1.id))])
179        e2 = TestWalkEntry(
180                c2,
181                [TreeChange(CHANGE_MODIFY, (b'a', F, blob_a1.id),
182                                           (b'a', F, blob_a2.id)),
183                 TreeChange.add((b'b', F, blob_b2.id))])
184        self.assertWalkYields([e2, e1], [c2.id])
185
186    def test_changes_multiple_parents(self):
187        blob_a1 = make_object(Blob, data=b'a1')
188        blob_b2 = make_object(Blob, data=b'b2')
189        blob_a3 = make_object(Blob, data=b'a3')
190        c1, c2, c3 = self.make_commits(
191            [[1], [2], [3, 1, 2]],
192            trees={1: [(b'a', blob_a1)], 2: [(b'b', blob_b2)],
193                   3: [(b'a', blob_a3), (b'b', blob_b2)]})
194        # a is a modify/add conflict and b is not conflicted.
195        changes = [[
196                TreeChange(CHANGE_MODIFY,
197                           (b'a', F, blob_a1.id), (b'a', F, blob_a3.id)),
198                TreeChange.add((b'a', F, blob_a3.id)),
199        ]]
200        self.assertWalkYields([TestWalkEntry(c3, changes)], [c3.id],
201                              exclude=[c1.id, c2.id])
202
203    def test_path_matches(self):
204        walker = Walker(None, [], paths=[b'foo', b'bar', b'baz/quux'])
205        self.assertTrue(walker._path_matches(b'foo'))
206        self.assertTrue(walker._path_matches(b'foo/a'))
207        self.assertTrue(walker._path_matches(b'foo/a/b'))
208        self.assertTrue(walker._path_matches(b'bar'))
209        self.assertTrue(walker._path_matches(b'baz/quux'))
210        self.assertTrue(walker._path_matches(b'baz/quux/a'))
211
212        self.assertFalse(walker._path_matches(None))
213        self.assertFalse(walker._path_matches(b'oops'))
214        self.assertFalse(walker._path_matches(b'fool'))
215        self.assertFalse(walker._path_matches(b'baz'))
216        self.assertFalse(walker._path_matches(b'baz/quu'))
217
218    def test_paths(self):
219        blob_a1 = make_object(Blob, data=b'a1')
220        blob_b2 = make_object(Blob, data=b'b2')
221        blob_a3 = make_object(Blob, data=b'a3')
222        blob_b3 = make_object(Blob, data=b'b3')
223        c1, c2, c3 = self.make_linear_commits(
224            3, trees={1: [(b'a', blob_a1)],
225                      2: [(b'a', blob_a1), (b'x/b', blob_b2)],
226                      3: [(b'a', blob_a3), (b'x/b', blob_b3)]})
227
228        self.assertWalkYields([c3, c2, c1], [c3.id])
229        self.assertWalkYields([c3, c1], [c3.id], paths=[b'a'])
230        self.assertWalkYields([c3, c2], [c3.id], paths=[b'x/b'])
231
232        # All changes are included, not just for requested paths.
233        changes = [
234            TreeChange(CHANGE_MODIFY, (b'a', F, blob_a1.id),
235                       (b'a', F, blob_a3.id)),
236            TreeChange(CHANGE_MODIFY, (b'x/b', F, blob_b2.id),
237                       (b'x/b', F, blob_b3.id)),
238        ]
239        self.assertWalkYields([TestWalkEntry(c3, changes)], [c3.id],
240                              max_entries=1, paths=[b'a'])
241
242    def test_paths_subtree(self):
243        blob_a = make_object(Blob, data=b'a')
244        blob_b = make_object(Blob, data=b'b')
245        c1, c2, c3 = self.make_linear_commits(
246            3, trees={1: [(b'x/a', blob_a)],
247                      2: [(b'b', blob_b), (b'x/a', blob_a)],
248                      3: [(b'b', blob_b), (b'x/a', blob_a), (b'x/b', blob_b)]})
249        self.assertWalkYields([c2], [c3.id], paths=[b'b'])
250        self.assertWalkYields([c3, c1], [c3.id], paths=[b'x'])
251
252    def test_paths_max_entries(self):
253        blob_a = make_object(Blob, data=b'a')
254        blob_b = make_object(Blob, data=b'b')
255        c1, c2 = self.make_linear_commits(
256            2, trees={1: [(b'a', blob_a)],
257                      2: [(b'a', blob_a), (b'b', blob_b)]})
258        self.assertWalkYields([c2], [c2.id], paths=[b'b'], max_entries=1)
259        self.assertWalkYields([c1], [c1.id], paths=[b'a'], max_entries=1)
260
261    def test_paths_merge(self):
262        blob_a1 = make_object(Blob, data=b'a1')
263        blob_a2 = make_object(Blob, data=b'a2')
264        blob_a3 = make_object(Blob, data=b'a3')
265        x1, y2, m3, m4 = self.make_commits(
266            [[1], [2], [3, 1, 2], [4, 1, 2]],
267            trees={1: [(b'a', blob_a1)],
268                   2: [(b'a', blob_a2)],
269                   3: [(b'a', blob_a3)],
270                   4: [(b'a', blob_a1)]})  # Non-conflicting
271        self.assertWalkYields([m3, y2, x1], [m3.id], paths=[b'a'])
272        self.assertWalkYields([y2, x1], [m4.id], paths=[b'a'])
273
274    def test_changes_with_renames(self):
275        blob = make_object(Blob, data=b'blob')
276        c1, c2 = self.make_linear_commits(
277            2, trees={1: [(b'a', blob)], 2: [(b'b', blob)]})
278        entry_a = (b'a', F, blob.id)
279        entry_b = (b'b', F, blob.id)
280        changes_without_renames = [TreeChange.delete(entry_a),
281                                   TreeChange.add(entry_b)]
282        changes_with_renames = [TreeChange(CHANGE_RENAME, entry_a, entry_b)]
283        self.assertWalkYields(
284          [TestWalkEntry(c2, changes_without_renames)], [c2.id], max_entries=1)
285        detector = RenameDetector(self.store)
286        self.assertWalkYields(
287          [TestWalkEntry(c2, changes_with_renames)], [c2.id], max_entries=1,
288          rename_detector=detector)
289
290    def test_follow_rename(self):
291        blob = make_object(Blob, data=b'blob')
292        names = [b'a', b'a', b'b', b'b', b'c', b'c']
293
294        trees = dict((i + 1, [(n, blob, F)]) for i, n in enumerate(names))
295        c1, c2, c3, c4, c5, c6 = self.make_linear_commits(6, trees=trees)
296        self.assertWalkYields([c5], [c6.id], paths=[b'c'])
297
298        def e(n):
299            return (n, F, blob.id)
300        self.assertWalkYields(
301            [TestWalkEntry(c5, [TreeChange(CHANGE_RENAME, e(b'b'), e(b'c'))]),
302             TestWalkEntry(c3, [TreeChange(CHANGE_RENAME, e(b'a'), e(b'b'))]),
303             TestWalkEntry(c1, [TreeChange.add(e(b'a'))])],
304            [c6.id], paths=[b'c'], follow=True)
305
306    def test_follow_rename_remove_path(self):
307        blob = make_object(Blob, data=b'blob')
308        _, _, _, c4, c5, c6 = self.make_linear_commits(
309            6, trees={1: [(b'a', blob), (b'c', blob)],
310                      2: [],
311                      3: [],
312                      4: [(b'b', blob)],
313                      5: [(b'a', blob)],
314                      6: [(b'c', blob)]})
315
316        def e(n):
317            return (n, F, blob.id)
318        # Once the path changes to b, we aren't interested in a or c anymore.
319        self.assertWalkYields(
320            [TestWalkEntry(c6, [TreeChange(CHANGE_RENAME, e(b'a'), e(b'c'))]),
321             TestWalkEntry(c5, [TreeChange(CHANGE_RENAME, e(b'b'), e(b'a'))]),
322             TestWalkEntry(c4, [TreeChange.add(e(b'b'))])],
323            [c6.id], paths=[b'c'], follow=True)
324
325    def test_since(self):
326        c1, c2, c3 = self.make_linear_commits(3)
327        self.assertWalkYields([c3, c2, c1], [c3.id], since=-1)
328        self.assertWalkYields([c3, c2, c1], [c3.id], since=0)
329        self.assertWalkYields([c3, c2], [c3.id], since=1)
330        self.assertWalkYields([c3, c2], [c3.id], since=99)
331        self.assertWalkYields([c3, c2], [c3.id], since=100)
332        self.assertWalkYields([c3], [c3.id], since=101)
333        self.assertWalkYields([c3], [c3.id], since=199)
334        self.assertWalkYields([c3], [c3.id], since=200)
335        self.assertWalkYields([], [c3.id], since=201)
336        self.assertWalkYields([], [c3.id], since=300)
337
338    def test_until(self):
339        c1, c2, c3 = self.make_linear_commits(3)
340        self.assertWalkYields([], [c3.id], until=-1)
341        self.assertWalkYields([c1], [c3.id], until=0)
342        self.assertWalkYields([c1], [c3.id], until=1)
343        self.assertWalkYields([c1], [c3.id], until=99)
344        self.assertWalkYields([c2, c1], [c3.id], until=100)
345        self.assertWalkYields([c2, c1], [c3.id], until=101)
346        self.assertWalkYields([c2, c1], [c3.id], until=199)
347        self.assertWalkYields([c3, c2, c1], [c3.id], until=200)
348        self.assertWalkYields([c3, c2, c1], [c3.id], until=201)
349        self.assertWalkYields([c3, c2, c1], [c3.id], until=300)
350
351    def test_since_until(self):
352        c1, c2, c3 = self.make_linear_commits(3)
353        self.assertWalkYields([], [c3.id], since=100, until=99)
354        self.assertWalkYields([c3, c2, c1], [c3.id], since=-1, until=201)
355        self.assertWalkYields([c2], [c3.id], since=100, until=100)
356        self.assertWalkYields([c2], [c3.id], since=50, until=150)
357
358    def test_since_over_scan(self):
359        commits = self.make_linear_commits(
360          11, times=[9, 0, 1, 2, 3, 4, 5, 8, 6, 7, 9])
361        c8, _, c10, c11 = commits[-4:]
362        del self.store[commits[0].id]
363        # c9 is older than we want to walk, but is out of order with its
364        # parent, so we need to walk past it to get to c8.
365        # c1 would also match, but we've deleted it, and it should get pruned
366        # even with over-scanning.
367        self.assertWalkYields([c11, c10, c8], [c11.id], since=7)
368
369    def assertTopoOrderEqual(self, expected_commits, commits):
370        entries = [TestWalkEntry(c, None) for c in commits]
371        actual_ids = [e.commit.id for e in list(_topo_reorder(entries))]
372        self.assertEqual([c.id for c in expected_commits], actual_ids)
373
374    def test_topo_reorder_linear(self):
375        commits = self.make_linear_commits(5)
376        commits.reverse()
377        for perm in permutations(commits):
378            self.assertTopoOrderEqual(commits, perm)
379
380    def test_topo_reorder_multiple_parents(self):
381        c1, c2, c3 = self.make_commits([[1], [2], [3, 1, 2]])
382        # Already sorted, so totally FIFO.
383        self.assertTopoOrderEqual([c3, c2, c1], [c3, c2, c1])
384        self.assertTopoOrderEqual([c3, c1, c2], [c3, c1, c2])
385
386        # c3 causes one parent to be yielded.
387        self.assertTopoOrderEqual([c3, c2, c1], [c2, c3, c1])
388        self.assertTopoOrderEqual([c3, c1, c2], [c1, c3, c2])
389
390        # c3 causes both parents to be yielded.
391        self.assertTopoOrderEqual([c3, c2, c1], [c1, c2, c3])
392        self.assertTopoOrderEqual([c3, c2, c1], [c2, c1, c3])
393
394    def test_topo_reorder_multiple_children(self):
395        c1, c2, c3 = self.make_commits([[1], [2, 1], [3, 1]])
396
397        # c2 and c3 are FIFO but c1 moves to the end.
398        self.assertTopoOrderEqual([c3, c2, c1], [c3, c2, c1])
399        self.assertTopoOrderEqual([c3, c2, c1], [c3, c1, c2])
400        self.assertTopoOrderEqual([c3, c2, c1], [c1, c3, c2])
401
402        self.assertTopoOrderEqual([c2, c3, c1], [c2, c3, c1])
403        self.assertTopoOrderEqual([c2, c3, c1], [c2, c1, c3])
404        self.assertTopoOrderEqual([c2, c3, c1], [c1, c2, c3])
405
406    def test_out_of_order_children(self):
407        c1, c2, c3, c4, c5 = self.make_commits(
408          [[1], [2, 1], [3, 2], [4, 1], [5, 3, 4]],
409          times=[2, 1, 3, 4, 5])
410        self.assertWalkYields([c5, c4, c3, c1, c2], [c5.id])
411        self.assertWalkYields([c5, c4, c3, c2, c1], [c5.id], order=ORDER_TOPO)
412
413    def test_out_of_order_with_exclude(self):
414        # Create the following graph:
415        # c1-------x2---m6
416        #   \          /
417        #    \-y3--y4-/--y5
418        # Due to skew, y5 is the oldest commit.
419        c1, x2, y3, y4, y5, m6 = self.make_commits(
420          [[1], [2, 1], [3, 1], [4, 3], [5, 4], [6, 2, 4]],
421          times=[2, 3, 4, 5, 1, 6])
422        self.assertWalkYields([m6, y4, y3, x2, c1], [m6.id])
423        # Ensure that c1..y4 get excluded even though they're popped from the
424        # priority queue long before y5.
425        self.assertWalkYields([m6, x2], [m6.id], exclude=[y5.id])
426
427    def test_empty_walk(self):
428        c1, c2, c3 = self.make_linear_commits(3)
429        self.assertWalkYields([], [c3.id], exclude=[c3.id])
430
431
432class WalkEntryTest(TestCase):
433
434    def setUp(self):
435        super(WalkEntryTest, self).setUp()
436        self.store = MemoryObjectStore()
437
438    def make_commits(self, commit_spec, **kwargs):
439        times = kwargs.pop('times', [])
440        attrs = kwargs.pop('attrs', {})
441        for i, t in enumerate(times):
442            attrs.setdefault(i + 1, {})['commit_time'] = t
443        return build_commit_graph(self.store, commit_spec, attrs=attrs,
444                                  **kwargs)
445
446    def make_linear_commits(self, num_commits, **kwargs):
447        commit_spec = []
448        for i in range(1, num_commits + 1):
449            c = [i]
450            if i > 1:
451                c.append(i - 1)
452            commit_spec.append(c)
453        return self.make_commits(commit_spec, **kwargs)
454
455    def test_all_changes(self):
456        # Construct a commit with 2 files in different subdirectories.
457        blob_a = make_object(Blob, data=b'a')
458        blob_b = make_object(Blob, data=b'b')
459        c1 = self.make_linear_commits(
460            1,
461            trees={1: [(b'x/a', blob_a), (b'y/b', blob_b)]},
462        )[0]
463
464        # Get the WalkEntry for the commit.
465        walker = Walker(self.store, c1.id)
466        walker_entry = list(walker)[0]
467        changes = walker_entry.changes()
468
469        # Compare the changes with the expected values.
470        entry_a = (b'x/a', F, blob_a.id)
471        entry_b = (b'y/b', F, blob_b.id)
472        self.assertEqual(
473            [TreeChange.add(entry_a),
474             TreeChange.add(entry_b)],
475            changes,
476        )
477
478    def test_all_with_merge(self):
479        blob_a = make_object(Blob, data=b'a')
480        blob_a2 = make_object(Blob, data=b'a2')
481        blob_b = make_object(Blob, data=b'b')
482        blob_b2 = make_object(Blob, data=b'b2')
483        x1, y2, m3 = self.make_commits(
484            [[1], [2], [3, 1, 2]],
485            trees={1: [(b'x/a', blob_a)],
486                   2: [(b'y/b', blob_b)],
487                   3: [(b'x/a', blob_a2), (b'y/b', blob_b2)]})
488
489        # Get the WalkEntry for the merge commit.
490        walker = Walker(self.store, m3.id)
491        entries = list(walker)
492        walker_entry = entries[0]
493        self.assertEqual(walker_entry.commit.id, m3.id)
494        changes = walker_entry.changes()
495        self.assertEqual(2, len(changes))
496
497        entry_a = (b'x/a', F, blob_a.id)
498        entry_a2 = (b'x/a', F, blob_a2.id)
499        entry_b = (b'y/b', F, blob_b.id)
500        entry_b2 = (b'y/b', F, blob_b2.id)
501        self.assertEqual(
502                [[TreeChange(CHANGE_MODIFY, entry_a, entry_a2),
503                  TreeChange.add(entry_a2)],
504                 [TreeChange.add(entry_b2),
505                  TreeChange(CHANGE_MODIFY, entry_b, entry_b2)]],
506                changes,
507        )
508
509    def test_filter_changes(self):
510        # Construct a commit with 2 files in different subdirectories.
511        blob_a = make_object(Blob, data=b'a')
512        blob_b = make_object(Blob, data=b'b')
513        c1 = self.make_linear_commits(
514            1,
515            trees={1: [(b'x/a', blob_a), (b'y/b', blob_b)]},
516        )[0]
517
518        # Get the WalkEntry for the commit.
519        walker = Walker(self.store, c1.id)
520        walker_entry = list(walker)[0]
521        changes = walker_entry.changes(path_prefix=b'x')
522
523        # Compare the changes with the expected values.
524        entry_a = (b'a', F, blob_a.id)
525        self.assertEqual(
526            [TreeChange.add(entry_a)],
527            changes,
528        )
529
530    def test_filter_with_merge(self):
531        blob_a = make_object(Blob, data=b'a')
532        blob_a2 = make_object(Blob, data=b'a2')
533        blob_b = make_object(Blob, data=b'b')
534        blob_b2 = make_object(Blob, data=b'b2')
535        x1, y2, m3 = self.make_commits(
536            [[1], [2], [3, 1, 2]],
537            trees={1: [(b'x/a', blob_a)],
538                   2: [(b'y/b', blob_b)],
539                   3: [(b'x/a', blob_a2), (b'y/b', blob_b2)]})
540
541        # Get the WalkEntry for the merge commit.
542        walker = Walker(self.store, m3.id)
543        entries = list(walker)
544        walker_entry = entries[0]
545        self.assertEqual(walker_entry.commit.id, m3.id)
546        changes = walker_entry.changes(b'x')
547        self.assertEqual(1, len(changes))
548
549        entry_a = (b'a', F, blob_a.id)
550        entry_a2 = (b'a', F, blob_a2.id)
551        self.assertEqual(
552            [[TreeChange(CHANGE_MODIFY, entry_a, entry_a2)]],
553            changes,
554        )
555