test_walk.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. # test_walk.py -- Tests for commit walking functionality.
  2. # Copyright (C) 2010 Google, Inc.
  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; version 2
  7. # or (at your option) any later version of the License.
  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,
  17. # MA 02110-1301, USA.
  18. """Tests for commit walking functionality."""
  19. from itertools import (
  20. permutations,
  21. )
  22. from dulwich.diff_tree import (
  23. CHANGE_MODIFY,
  24. CHANGE_RENAME,
  25. TreeChange,
  26. RenameDetector,
  27. )
  28. from dulwich.errors import (
  29. MissingCommitError,
  30. )
  31. from dulwich.object_store import (
  32. MemoryObjectStore,
  33. )
  34. from dulwich.objects import (
  35. Commit,
  36. Blob,
  37. )
  38. from dulwich.walk import (
  39. ORDER_TOPO,
  40. WalkEntry,
  41. Walker,
  42. _topo_reorder
  43. )
  44. from dulwich.tests import TestCase
  45. from dulwich.tests.utils import (
  46. F,
  47. make_object,
  48. build_commit_graph,
  49. )
  50. class TestWalkEntry(object):
  51. def __init__(self, commit, changes):
  52. self.commit = commit
  53. self.changes = changes
  54. def __repr__(self):
  55. return '<TestWalkEntry commit=%s, changes=%r>' % (
  56. self.commit.id, self.changes)
  57. def __eq__(self, other):
  58. if not isinstance(other, WalkEntry) or self.commit != other.commit:
  59. return False
  60. if self.changes is None:
  61. return True
  62. return self.changes == other.changes()
  63. class WalkerTest(TestCase):
  64. def setUp(self):
  65. super(WalkerTest, self).setUp()
  66. self.store = MemoryObjectStore()
  67. def make_commits(self, commit_spec, **kwargs):
  68. times = kwargs.pop('times', [])
  69. attrs = kwargs.pop('attrs', {})
  70. for i, t in enumerate(times):
  71. attrs.setdefault(i + 1, {})['commit_time'] = t
  72. return build_commit_graph(self.store, commit_spec, attrs=attrs,
  73. **kwargs)
  74. def make_linear_commits(self, num_commits, **kwargs):
  75. commit_spec = []
  76. for i in range(1, num_commits + 1):
  77. c = [i]
  78. if i > 1:
  79. c.append(i - 1)
  80. commit_spec.append(c)
  81. return self.make_commits(commit_spec, **kwargs)
  82. def assertWalkYields(self, expected, *args, **kwargs):
  83. walker = Walker(self.store, *args, **kwargs)
  84. expected = list(expected)
  85. for i, entry in enumerate(expected):
  86. if isinstance(entry, Commit):
  87. expected[i] = TestWalkEntry(entry, None)
  88. actual = list(walker)
  89. self.assertEqual(expected, actual)
  90. def test_linear(self):
  91. c1, c2, c3 = self.make_linear_commits(3)
  92. self.assertWalkYields([c1], [c1.id])
  93. self.assertWalkYields([c2, c1], [c2.id])
  94. self.assertWalkYields([c3, c2, c1], [c3.id])
  95. self.assertWalkYields([c3, c2, c1], [c3.id, c1.id])
  96. self.assertWalkYields([c3, c2], [c3.id], exclude=[c1.id])
  97. self.assertWalkYields([c3, c2], [c3.id, c1.id], exclude=[c1.id])
  98. self.assertWalkYields([c3], [c3.id, c1.id], exclude=[c2.id])
  99. def test_missing(self):
  100. cs = list(reversed(self.make_linear_commits(20)))
  101. self.assertWalkYields(cs, [cs[0].id])
  102. # Exactly how close we can get to a missing commit depends on our
  103. # implementation (in particular the choice of _MAX_EXTRA_COMMITS), but
  104. # we should at least be able to walk some history in a broken repo.
  105. del self.store[cs[-1].id]
  106. for i in range(1, 11):
  107. self.assertWalkYields(cs[:i], [cs[0].id], max_entries=i)
  108. self.assertRaises(MissingCommitError, Walker, self.store, [cs[-1].id])
  109. def test_branch(self):
  110. c1, x2, x3, y4 = self.make_commits([[1], [2, 1], [3, 2], [4, 1]])
  111. self.assertWalkYields([x3, x2, c1], [x3.id])
  112. self.assertWalkYields([y4, c1], [y4.id])
  113. self.assertWalkYields([y4, x2, c1], [y4.id, x2.id])
  114. self.assertWalkYields([y4, x2], [y4.id, x2.id], exclude=[c1.id])
  115. self.assertWalkYields([y4, x3], [y4.id, x3.id], exclude=[x2.id])
  116. self.assertWalkYields([y4], [y4.id], exclude=[x3.id])
  117. self.assertWalkYields([x3, x2], [x3.id], exclude=[y4.id])
  118. def test_merge(self):
  119. c1, c2, c3, c4 = self.make_commits([[1], [2, 1], [3, 1], [4, 2, 3]])
  120. self.assertWalkYields([c4, c3, c2, c1], [c4.id])
  121. self.assertWalkYields([c3, c1], [c3.id])
  122. self.assertWalkYields([c2, c1], [c2.id])
  123. self.assertWalkYields([c4, c3], [c4.id], exclude=[c2.id])
  124. self.assertWalkYields([c4, c2], [c4.id], exclude=[c3.id])
  125. def test_reverse(self):
  126. c1, c2, c3 = self.make_linear_commits(3)
  127. self.assertWalkYields([c1, c2, c3], [c3.id], reverse=True)
  128. def test_max_entries(self):
  129. c1, c2, c3 = self.make_linear_commits(3)
  130. self.assertWalkYields([c3, c2, c1], [c3.id], max_entries=3)
  131. self.assertWalkYields([c3, c2], [c3.id], max_entries=2)
  132. self.assertWalkYields([c3], [c3.id], max_entries=1)
  133. def test_reverse_after_max_entries(self):
  134. c1, c2, c3 = self.make_linear_commits(3)
  135. self.assertWalkYields([c1, c2, c3], [c3.id], max_entries=3,
  136. reverse=True)
  137. self.assertWalkYields([c2, c3], [c3.id], max_entries=2, reverse=True)
  138. self.assertWalkYields([c3], [c3.id], max_entries=1, reverse=True)
  139. def test_changes_one_parent(self):
  140. blob_a1 = make_object(Blob, data=b'a1')
  141. blob_a2 = make_object(Blob, data=b'a2')
  142. blob_b2 = make_object(Blob, data=b'b2')
  143. c1, c2 = self.make_linear_commits(
  144. 2, trees={1: [(b'a', blob_a1)],
  145. 2: [(b'a', blob_a2), (b'b', blob_b2)]})
  146. e1 = TestWalkEntry(c1, [TreeChange.add((b'a', F, blob_a1.id))])
  147. e2 = TestWalkEntry(c2, [TreeChange(CHANGE_MODIFY, (b'a', F, blob_a1.id),
  148. (b'a', F, blob_a2.id)),
  149. TreeChange.add((b'b', F, blob_b2.id))])
  150. self.assertWalkYields([e2, e1], [c2.id])
  151. def test_changes_multiple_parents(self):
  152. blob_a1 = make_object(Blob, data=b'a1')
  153. blob_b2 = make_object(Blob, data=b'b2')
  154. blob_a3 = make_object(Blob, data=b'a3')
  155. c1, c2, c3 = self.make_commits(
  156. [[1], [2], [3, 1, 2]],
  157. trees={1: [(b'a', blob_a1)], 2: [(b'b', blob_b2)],
  158. 3: [(b'a', blob_a3), (b'b', blob_b2)]})
  159. # a is a modify/add conflict and b is not conflicted.
  160. changes = [[
  161. TreeChange(CHANGE_MODIFY, (b'a', F, blob_a1.id), (b'a', F, blob_a3.id)),
  162. TreeChange.add((b'a', F, blob_a3.id)),
  163. ]]
  164. self.assertWalkYields([TestWalkEntry(c3, changes)], [c3.id],
  165. exclude=[c1.id, c2.id])
  166. def test_path_matches(self):
  167. walker = Walker(None, [], paths=[b'foo', b'bar', b'baz/quux'])
  168. self.assertTrue(walker._path_matches(b'foo'))
  169. self.assertTrue(walker._path_matches(b'foo/a'))
  170. self.assertTrue(walker._path_matches(b'foo/a/b'))
  171. self.assertTrue(walker._path_matches(b'bar'))
  172. self.assertTrue(walker._path_matches(b'baz/quux'))
  173. self.assertTrue(walker._path_matches(b'baz/quux/a'))
  174. self.assertFalse(walker._path_matches(None))
  175. self.assertFalse(walker._path_matches(b'oops'))
  176. self.assertFalse(walker._path_matches(b'fool'))
  177. self.assertFalse(walker._path_matches(b'baz'))
  178. self.assertFalse(walker._path_matches(b'baz/quu'))
  179. def test_paths(self):
  180. blob_a1 = make_object(Blob, data=b'a1')
  181. blob_b2 = make_object(Blob, data=b'b2')
  182. blob_a3 = make_object(Blob, data=b'a3')
  183. blob_b3 = make_object(Blob, data=b'b3')
  184. c1, c2, c3 = self.make_linear_commits(
  185. 3, trees={1: [(b'a', blob_a1)],
  186. 2: [(b'a', blob_a1), (b'x/b', blob_b2)],
  187. 3: [(b'a', blob_a3), (b'x/b', blob_b3)]})
  188. self.assertWalkYields([c3, c2, c1], [c3.id])
  189. self.assertWalkYields([c3, c1], [c3.id], paths=[b'a'])
  190. self.assertWalkYields([c3, c2], [c3.id], paths=[b'x/b'])
  191. # All changes are included, not just for requested paths.
  192. changes = [
  193. TreeChange(CHANGE_MODIFY, (b'a', F, blob_a1.id),
  194. (b'a', F, blob_a3.id)),
  195. TreeChange(CHANGE_MODIFY, (b'x/b', F, blob_b2.id),
  196. (b'x/b', F, blob_b3.id)),
  197. ]
  198. self.assertWalkYields([TestWalkEntry(c3, changes)], [c3.id],
  199. max_entries=1, paths=[b'a'])
  200. def test_paths_subtree(self):
  201. blob_a = make_object(Blob, data=b'a')
  202. blob_b = make_object(Blob, data=b'b')
  203. c1, c2, c3 = self.make_linear_commits(
  204. 3, trees={1: [(b'x/a', blob_a)],
  205. 2: [(b'b', blob_b), (b'x/a', blob_a)],
  206. 3: [(b'b', blob_b), (b'x/a', blob_a), (b'x/b', blob_b)]})
  207. self.assertWalkYields([c2], [c3.id], paths=[b'b'])
  208. self.assertWalkYields([c3, c1], [c3.id], paths=[b'x'])
  209. def test_paths_max_entries(self):
  210. blob_a = make_object(Blob, data=b'a')
  211. blob_b = make_object(Blob, data=b'b')
  212. c1, c2 = self.make_linear_commits(
  213. 2, trees={1: [(b'a', blob_a)],
  214. 2: [(b'a', blob_a), (b'b', blob_b)]})
  215. self.assertWalkYields([c2], [c2.id], paths=[b'b'], max_entries=1)
  216. self.assertWalkYields([c1], [c1.id], paths=[b'a'], max_entries=1)
  217. def test_paths_merge(self):
  218. blob_a1 = make_object(Blob, data=b'a1')
  219. blob_a2 = make_object(Blob, data=b'a2')
  220. blob_a3 = make_object(Blob, data=b'a3')
  221. x1, y2, m3, m4 = self.make_commits(
  222. [[1], [2], [3, 1, 2], [4, 1, 2]],
  223. trees={1: [(b'a', blob_a1)],
  224. 2: [(b'a', blob_a2)],
  225. 3: [(b'a', blob_a3)],
  226. 4: [(b'a', blob_a1)]}) # Non-conflicting
  227. self.assertWalkYields([m3, y2, x1], [m3.id], paths=[b'a'])
  228. self.assertWalkYields([y2, x1], [m4.id], paths=[b'a'])
  229. def test_changes_with_renames(self):
  230. blob = make_object(Blob, data=b'blob')
  231. c1, c2 = self.make_linear_commits(
  232. 2, trees={1: [(b'a', blob)], 2: [(b'b', blob)]})
  233. entry_a = (b'a', F, blob.id)
  234. entry_b = (b'b', F, blob.id)
  235. changes_without_renames = [TreeChange.delete(entry_a),
  236. TreeChange.add(entry_b)]
  237. changes_with_renames = [TreeChange(CHANGE_RENAME, entry_a, entry_b)]
  238. self.assertWalkYields(
  239. [TestWalkEntry(c2, changes_without_renames)], [c2.id], max_entries=1)
  240. detector = RenameDetector(self.store)
  241. self.assertWalkYields(
  242. [TestWalkEntry(c2, changes_with_renames)], [c2.id], max_entries=1,
  243. rename_detector=detector)
  244. def test_follow_rename(self):
  245. blob = make_object(Blob, data=b'blob')
  246. names = [b'a', b'a', b'b', b'b', b'c', b'c']
  247. trees = dict((i + 1, [(n, blob, F)]) for i, n in enumerate(names))
  248. c1, c2, c3, c4, c5, c6 = self.make_linear_commits(6, trees=trees)
  249. self.assertWalkYields([c5], [c6.id], paths=[b'c'])
  250. e = lambda n: (n, F, blob.id)
  251. self.assertWalkYields(
  252. [TestWalkEntry(c5, [TreeChange(CHANGE_RENAME, e(b'b'), e(b'c'))]),
  253. TestWalkEntry(c3, [TreeChange(CHANGE_RENAME, e(b'a'), e(b'b'))]),
  254. TestWalkEntry(c1, [TreeChange.add(e(b'a'))])],
  255. [c6.id], paths=[b'c'], follow=True)
  256. def test_follow_rename_remove_path(self):
  257. blob = make_object(Blob, data=b'blob')
  258. _, _, _, c4, c5, c6 = self.make_linear_commits(
  259. 6, trees={1: [(b'a', blob), (b'c', blob)],
  260. 2: [],
  261. 3: [],
  262. 4: [(b'b', blob)],
  263. 5: [(b'a', blob)],
  264. 6: [(b'c', blob)]})
  265. e = lambda n: (n, F, blob.id)
  266. # Once the path changes to b, we aren't interested in a or c anymore.
  267. self.assertWalkYields(
  268. [TestWalkEntry(c6, [TreeChange(CHANGE_RENAME, e(b'a'), e(b'c'))]),
  269. TestWalkEntry(c5, [TreeChange(CHANGE_RENAME, e(b'b'), e(b'a'))]),
  270. TestWalkEntry(c4, [TreeChange.add(e(b'b'))])],
  271. [c6.id], paths=[b'c'], follow=True)
  272. def test_since(self):
  273. c1, c2, c3 = self.make_linear_commits(3)
  274. self.assertWalkYields([c3, c2, c1], [c3.id], since=-1)
  275. self.assertWalkYields([c3, c2, c1], [c3.id], since=0)
  276. self.assertWalkYields([c3, c2], [c3.id], since=1)
  277. self.assertWalkYields([c3, c2], [c3.id], since=99)
  278. self.assertWalkYields([c3, c2], [c3.id], since=100)
  279. self.assertWalkYields([c3], [c3.id], since=101)
  280. self.assertWalkYields([c3], [c3.id], since=199)
  281. self.assertWalkYields([c3], [c3.id], since=200)
  282. self.assertWalkYields([], [c3.id], since=201)
  283. self.assertWalkYields([], [c3.id], since=300)
  284. def test_until(self):
  285. c1, c2, c3 = self.make_linear_commits(3)
  286. self.assertWalkYields([], [c3.id], until=-1)
  287. self.assertWalkYields([c1], [c3.id], until=0)
  288. self.assertWalkYields([c1], [c3.id], until=1)
  289. self.assertWalkYields([c1], [c3.id], until=99)
  290. self.assertWalkYields([c2, c1], [c3.id], until=100)
  291. self.assertWalkYields([c2, c1], [c3.id], until=101)
  292. self.assertWalkYields([c2, c1], [c3.id], until=199)
  293. self.assertWalkYields([c3, c2, c1], [c3.id], until=200)
  294. self.assertWalkYields([c3, c2, c1], [c3.id], until=201)
  295. self.assertWalkYields([c3, c2, c1], [c3.id], until=300)
  296. def test_since_until(self):
  297. c1, c2, c3 = self.make_linear_commits(3)
  298. self.assertWalkYields([], [c3.id], since=100, until=99)
  299. self.assertWalkYields([c3, c2, c1], [c3.id], since=-1, until=201)
  300. self.assertWalkYields([c2], [c3.id], since=100, until=100)
  301. self.assertWalkYields([c2], [c3.id], since=50, until=150)
  302. def test_since_over_scan(self):
  303. commits = self.make_linear_commits(
  304. 11, times=[9, 0, 1, 2, 3, 4, 5, 8, 6, 7, 9])
  305. c8, _, c10, c11 = commits[-4:]
  306. del self.store[commits[0].id]
  307. # c9 is older than we want to walk, but is out of order with its parent,
  308. # so we need to walk past it to get to c8.
  309. # c1 would also match, but we've deleted it, and it should get pruned
  310. # even with over-scanning.
  311. self.assertWalkYields([c11, c10, c8], [c11.id], since=7)
  312. def assertTopoOrderEqual(self, expected_commits, commits):
  313. entries = [TestWalkEntry(c, None) for c in commits]
  314. actual_ids = [e.commit.id for e in list(_topo_reorder(entries))]
  315. self.assertEqual([c.id for c in expected_commits], actual_ids)
  316. def test_topo_reorder_linear(self):
  317. commits = self.make_linear_commits(5)
  318. commits.reverse()
  319. for perm in permutations(commits):
  320. self.assertTopoOrderEqual(commits, perm)
  321. def test_topo_reorder_multiple_parents(self):
  322. c1, c2, c3 = self.make_commits([[1], [2], [3, 1, 2]])
  323. # Already sorted, so totally FIFO.
  324. self.assertTopoOrderEqual([c3, c2, c1], [c3, c2, c1])
  325. self.assertTopoOrderEqual([c3, c1, c2], [c3, c1, c2])
  326. # c3 causes one parent to be yielded.
  327. self.assertTopoOrderEqual([c3, c2, c1], [c2, c3, c1])
  328. self.assertTopoOrderEqual([c3, c1, c2], [c1, c3, c2])
  329. # c3 causes both parents to be yielded.
  330. self.assertTopoOrderEqual([c3, c2, c1], [c1, c2, c3])
  331. self.assertTopoOrderEqual([c3, c2, c1], [c2, c1, c3])
  332. def test_topo_reorder_multiple_children(self):
  333. c1, c2, c3 = self.make_commits([[1], [2, 1], [3, 1]])
  334. # c2 and c3 are FIFO but c1 moves to the end.
  335. self.assertTopoOrderEqual([c3, c2, c1], [c3, c2, c1])
  336. self.assertTopoOrderEqual([c3, c2, c1], [c3, c1, c2])
  337. self.assertTopoOrderEqual([c3, c2, c1], [c1, c3, c2])
  338. self.assertTopoOrderEqual([c2, c3, c1], [c2, c3, c1])
  339. self.assertTopoOrderEqual([c2, c3, c1], [c2, c1, c3])
  340. self.assertTopoOrderEqual([c2, c3, c1], [c1, c2, c3])
  341. def test_out_of_order_children(self):
  342. c1, c2, c3, c4, c5 = self.make_commits(
  343. [[1], [2, 1], [3, 2], [4, 1], [5, 3, 4]],
  344. times=[2, 1, 3, 4, 5])
  345. self.assertWalkYields([c5, c4, c3, c1, c2], [c5.id])
  346. self.assertWalkYields([c5, c4, c3, c2, c1], [c5.id], order=ORDER_TOPO)
  347. def test_out_of_order_with_exclude(self):
  348. # Create the following graph:
  349. # c1-------x2---m6
  350. # \ /
  351. # \-y3--y4-/--y5
  352. # Due to skew, y5 is the oldest commit.
  353. c1, x2, y3, y4, y5, m6 = self.make_commits(
  354. [[1], [2, 1], [3, 1], [4, 3], [5, 4], [6, 2, 4]],
  355. times=[2, 3, 4, 5, 1, 6])
  356. self.assertWalkYields([m6, y4, y3, x2, c1], [m6.id])
  357. # Ensure that c1..y4 get excluded even though they're popped from the
  358. # priority queue long before y5.
  359. self.assertWalkYields([m6, x2], [m6.id], exclude=[y5.id])
  360. def test_empty_walk(self):
  361. c1, c2, c3 = self.make_linear_commits(3)
  362. self.assertWalkYields([], [c3.id], exclude=[c3.id])