test_walk.py 24 KB

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