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