test_reflog.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. # test_reflog.py -- tests for reflog.py
  2. # Copyright (C) 2015 Jelmer Vernooij <jelmer@jelmer.uk>
  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 published 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 dulwich.reflog."""
  22. import tempfile
  23. from io import BytesIO
  24. from dulwich.objects import ZERO_SHA, Blob, Commit, Tree
  25. from dulwich.reflog import (
  26. drop_reflog_entry,
  27. expire_reflog,
  28. format_reflog_line,
  29. iter_reflogs,
  30. parse_reflog_line,
  31. parse_reflog_spec,
  32. read_reflog,
  33. )
  34. from dulwich.repo import Repo
  35. from . import TestCase
  36. class ReflogSpecTests(TestCase):
  37. def test_parse_reflog_spec_basic(self) -> None:
  38. # Test basic reflog spec
  39. ref, index = parse_reflog_spec("HEAD@{1}")
  40. self.assertEqual(b"HEAD", ref)
  41. self.assertEqual(1, index)
  42. def test_parse_reflog_spec_with_full_ref(self) -> None:
  43. # Test with full ref name
  44. ref, index = parse_reflog_spec("refs/heads/master@{5}")
  45. self.assertEqual(b"refs/heads/master", ref)
  46. self.assertEqual(5, index)
  47. def test_parse_reflog_spec_bytes(self) -> None:
  48. # Test with bytes input
  49. ref, index = parse_reflog_spec(b"develop@{0}")
  50. self.assertEqual(b"develop", ref)
  51. self.assertEqual(0, index)
  52. def test_parse_reflog_spec_no_ref(self) -> None:
  53. # Test with no ref (defaults to HEAD)
  54. ref, index = parse_reflog_spec("@{2}")
  55. self.assertEqual(b"HEAD", ref)
  56. self.assertEqual(2, index)
  57. def test_parse_reflog_spec_invalid_no_brace(self) -> None:
  58. # Test invalid spec without @{
  59. with self.assertRaises(ValueError) as cm:
  60. parse_reflog_spec("HEAD")
  61. self.assertIn("Expected format: ref@{n}", str(cm.exception))
  62. def test_parse_reflog_spec_invalid_no_closing_brace(self) -> None:
  63. # Test invalid spec without closing brace
  64. with self.assertRaises(ValueError) as cm:
  65. parse_reflog_spec("HEAD@{1")
  66. self.assertIn("Expected format: ref@{n}", str(cm.exception))
  67. def test_parse_reflog_spec_invalid_non_numeric(self) -> None:
  68. # Test invalid spec with non-numeric index
  69. with self.assertRaises(ValueError) as cm:
  70. parse_reflog_spec("HEAD@{foo}")
  71. self.assertIn("Expected integer", str(cm.exception))
  72. class ReflogLineTests(TestCase):
  73. def test_format(self) -> None:
  74. self.assertEqual(
  75. b"0000000000000000000000000000000000000000 "
  76. b"49030649db3dfec5a9bc03e5dde4255a14499f16 Jelmer Vernooij "
  77. b"<jelmer@jelmer.uk> 1446552482 +0000 "
  78. b"clone: from git://jelmer.uk/samba",
  79. format_reflog_line(
  80. b"0000000000000000000000000000000000000000",
  81. b"49030649db3dfec5a9bc03e5dde4255a14499f16",
  82. b"Jelmer Vernooij <jelmer@jelmer.uk>",
  83. 1446552482,
  84. 0,
  85. b"clone: from git://jelmer.uk/samba",
  86. ),
  87. )
  88. self.assertEqual(
  89. b"0000000000000000000000000000000000000000 "
  90. b"49030649db3dfec5a9bc03e5dde4255a14499f16 Jelmer Vernooij "
  91. b"<jelmer@jelmer.uk> 1446552482 +0000 "
  92. b"clone: from git://jelmer.uk/samba",
  93. format_reflog_line(
  94. None,
  95. b"49030649db3dfec5a9bc03e5dde4255a14499f16",
  96. b"Jelmer Vernooij <jelmer@jelmer.uk>",
  97. 1446552482,
  98. 0,
  99. b"clone: from git://jelmer.uk/samba",
  100. ),
  101. )
  102. def test_parse(self) -> None:
  103. reflog_line = (
  104. b"0000000000000000000000000000000000000000 "
  105. b"49030649db3dfec5a9bc03e5dde4255a14499f16 Jelmer Vernooij "
  106. b"<jelmer@jelmer.uk> 1446552482 +0000 "
  107. b"clone: from git://jelmer.uk/samba"
  108. )
  109. self.assertEqual(
  110. (
  111. b"0000000000000000000000000000000000000000",
  112. b"49030649db3dfec5a9bc03e5dde4255a14499f16",
  113. b"Jelmer Vernooij <jelmer@jelmer.uk>",
  114. 1446552482,
  115. 0,
  116. b"clone: from git://jelmer.uk/samba",
  117. ),
  118. parse_reflog_line(reflog_line),
  119. )
  120. _TEST_REFLOG = (
  121. b"0000000000000000000000000000000000000000 "
  122. b"49030649db3dfec5a9bc03e5dde4255a14499f16 Jelmer Vernooij "
  123. b"<jelmer@jelmer.uk> 1446552482 +0000 "
  124. b"clone: from git://jelmer.uk/samba\n"
  125. b"49030649db3dfec5a9bc03e5dde4255a14499f16 "
  126. b"42d06bd4b77fed026b154d16493e5deab78f02ec Jelmer Vernooij "
  127. b"<jelmer@jelmer.uk> 1446552483 +0000 "
  128. b"clone: from git://jelmer.uk/samba\n"
  129. b"42d06bd4b77fed026b154d16493e5deab78f02ec "
  130. b"df6800012397fb85c56e7418dd4eb9405dee075c Jelmer Vernooij "
  131. b"<jelmer@jelmer.uk> 1446552484 +0000 "
  132. b"clone: from git://jelmer.uk/samba\n"
  133. )
  134. class ReflogDropTests(TestCase):
  135. def setUp(self) -> None:
  136. TestCase.setUp(self)
  137. self.f = BytesIO(_TEST_REFLOG)
  138. self.original_log = list(read_reflog(self.f))
  139. self.f.seek(0)
  140. def _read_log(self):
  141. self.f.seek(0)
  142. return list(read_reflog(self.f))
  143. def test_invalid(self) -> None:
  144. self.assertRaises(ValueError, drop_reflog_entry, self.f, -1)
  145. def test_drop_entry(self) -> None:
  146. drop_reflog_entry(self.f, 0)
  147. log = self._read_log()
  148. self.assertEqual(len(log), 2)
  149. self.assertEqual(self.original_log[0:2], log)
  150. self.f.seek(0)
  151. drop_reflog_entry(self.f, 1)
  152. log = self._read_log()
  153. self.assertEqual(len(log), 1)
  154. self.assertEqual(self.original_log[1], log[0])
  155. def test_drop_entry_with_rewrite(self) -> None:
  156. drop_reflog_entry(self.f, 1, True)
  157. log = self._read_log()
  158. self.assertEqual(len(log), 2)
  159. self.assertEqual(self.original_log[0], log[0])
  160. self.assertEqual(self.original_log[0].new_sha, log[1].old_sha)
  161. self.assertEqual(self.original_log[2].new_sha, log[1].new_sha)
  162. self.f.seek(0)
  163. drop_reflog_entry(self.f, 1, True)
  164. log = self._read_log()
  165. self.assertEqual(len(log), 1)
  166. self.assertEqual(ZERO_SHA, log[0].old_sha)
  167. self.assertEqual(self.original_log[2].new_sha, log[0].new_sha)
  168. class RepoReflogTests(TestCase):
  169. def setUp(self) -> None:
  170. TestCase.setUp(self)
  171. self.test_dir = tempfile.mkdtemp()
  172. self.repo = Repo.init(self.test_dir)
  173. def tearDown(self) -> None:
  174. TestCase.tearDown(self)
  175. import shutil
  176. shutil.rmtree(self.test_dir)
  177. def test_read_reflog_nonexistent(self) -> None:
  178. # Reading a reflog that doesn't exist should return empty
  179. entries = list(self.repo.read_reflog(b"refs/heads/nonexistent"))
  180. self.assertEqual([], entries)
  181. def test_read_reflog_head(self) -> None:
  182. # Create a commit to generate a reflog entry
  183. blob = Blob.from_string(b"test content")
  184. self.repo.object_store.add_object(blob)
  185. tree = Tree()
  186. tree.add(b"test", 0o100644, blob.id)
  187. self.repo.object_store.add_object(tree)
  188. commit = Commit()
  189. commit.tree = tree.id
  190. commit.author = b"Test Author <test@example.com>"
  191. commit.committer = b"Test Author <test@example.com>"
  192. commit.commit_time = 1234567890
  193. commit.commit_timezone = 0
  194. commit.author_time = 1234567890
  195. commit.author_timezone = 0
  196. commit.message = b"Initial commit"
  197. self.repo.object_store.add_object(commit)
  198. # Manually write a reflog entry
  199. self.repo._write_reflog(
  200. b"HEAD",
  201. ZERO_SHA,
  202. commit.id,
  203. b"Test Author <test@example.com>",
  204. 1234567890,
  205. 0,
  206. b"commit (initial): Initial commit",
  207. )
  208. # Read the reflog
  209. entries = list(self.repo.read_reflog(b"HEAD"))
  210. self.assertEqual(1, len(entries))
  211. self.assertEqual(ZERO_SHA, entries[0].old_sha)
  212. self.assertEqual(commit.id, entries[0].new_sha)
  213. self.assertEqual(b"Test Author <test@example.com>", entries[0].committer)
  214. self.assertEqual(1234567890, entries[0].timestamp)
  215. self.assertEqual(0, entries[0].timezone)
  216. self.assertEqual(b"commit (initial): Initial commit", entries[0].message)
  217. def test_iter_reflogs(self) -> None:
  218. # Create commits and reflog entries
  219. blob = Blob.from_string(b"test content")
  220. self.repo.object_store.add_object(blob)
  221. tree = Tree()
  222. tree.add(b"test", 0o100644, blob.id)
  223. self.repo.object_store.add_object(tree)
  224. commit = Commit()
  225. commit.tree = tree.id
  226. commit.author = b"Test Author <test@example.com>"
  227. commit.committer = b"Test Author <test@example.com>"
  228. commit.commit_time = 1234567890
  229. commit.commit_timezone = 0
  230. commit.author_time = 1234567890
  231. commit.author_timezone = 0
  232. commit.message = b"Initial commit"
  233. self.repo.object_store.add_object(commit)
  234. # Write reflog entries for multiple refs
  235. self.repo._write_reflog(
  236. b"HEAD",
  237. ZERO_SHA,
  238. commit.id,
  239. b"Test Author <test@example.com>",
  240. 1234567890,
  241. 0,
  242. b"commit (initial): Initial commit",
  243. )
  244. self.repo._write_reflog(
  245. b"refs/heads/master",
  246. ZERO_SHA,
  247. commit.id,
  248. b"Test Author <test@example.com>",
  249. 1234567891,
  250. 0,
  251. b"branch: Created from HEAD",
  252. )
  253. self.repo._write_reflog(
  254. b"refs/heads/develop",
  255. ZERO_SHA,
  256. commit.id,
  257. b"Test Author <test@example.com>",
  258. 1234567892,
  259. 0,
  260. b"branch: Created from HEAD",
  261. )
  262. # Use iter_reflogs to get all reflogs
  263. import os
  264. logs_dir = os.path.join(self.repo.controldir(), "logs")
  265. reflogs = list(iter_reflogs(logs_dir))
  266. # Should have at least HEAD, refs/heads/master, and refs/heads/develop
  267. self.assertIn(b"HEAD", reflogs)
  268. self.assertIn(b"refs/heads/master", reflogs)
  269. self.assertIn(b"refs/heads/develop", reflogs)
  270. class ReflogExpireTests(TestCase):
  271. def setUp(self) -> None:
  272. TestCase.setUp(self)
  273. # Create a reflog with entries at different timestamps
  274. self.f = BytesIO()
  275. # Old entry (timestamp: 1000000000)
  276. self.f.write(
  277. b"0000000000000000000000000000000000000000 "
  278. b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa "
  279. b"Test <test@example.com> 1000000000 +0000\told entry\n"
  280. )
  281. # Medium entry (timestamp: 1500000000)
  282. self.f.write(
  283. b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa "
  284. b"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb "
  285. b"Test <test@example.com> 1500000000 +0000\tmedium entry\n"
  286. )
  287. # Recent entry (timestamp: 2000000000)
  288. self.f.write(
  289. b"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb "
  290. b"cccccccccccccccccccccccccccccccccccccccc "
  291. b"Test <test@example.com> 2000000000 +0000\trecent entry\n"
  292. )
  293. self.f.seek(0)
  294. def _read_log(self):
  295. self.f.seek(0)
  296. return list(read_reflog(self.f))
  297. def test_expire_no_criteria(self) -> None:
  298. # If no expiration criteria, nothing should be expired
  299. count = expire_reflog(self.f)
  300. self.assertEqual(0, count)
  301. log = self._read_log()
  302. self.assertEqual(3, len(log))
  303. def test_expire_by_time(self) -> None:
  304. # Expire entries older than timestamp 1600000000
  305. # Should remove the first two entries
  306. count = expire_reflog(self.f, expire_time=1600000000)
  307. self.assertEqual(2, count)
  308. log = self._read_log()
  309. self.assertEqual(1, len(log))
  310. self.assertEqual(b"recent entry", log[0].message)
  311. def test_expire_unreachable(self) -> None:
  312. # Test expiring unreachable entries
  313. # Mark the middle entry as unreachable
  314. def reachable_checker(sha):
  315. return sha != b"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
  316. count = expire_reflog(
  317. self.f,
  318. expire_unreachable_time=1600000000,
  319. reachable_checker=reachable_checker,
  320. )
  321. self.assertEqual(1, count)
  322. log = self._read_log()
  323. self.assertEqual(2, len(log))
  324. # First and third entries should remain
  325. self.assertEqual(b"old entry", log[0].message)
  326. self.assertEqual(b"recent entry", log[1].message)
  327. def test_expire_mixed(self) -> None:
  328. # Test with both expire_time and expire_unreachable_time
  329. def reachable_checker(sha):
  330. # Only the most recent entry is reachable
  331. return sha == b"cccccccccccccccccccccccccccccccccccccccc"
  332. count = expire_reflog(
  333. self.f,
  334. expire_time=1800000000, # Would expire first two if reachable
  335. expire_unreachable_time=1200000000, # Would expire first if unreachable
  336. reachable_checker=reachable_checker,
  337. )
  338. # First entry is unreachable and old enough -> expired
  339. # Second entry is unreachable but not old enough -> kept
  340. # Third entry is reachable and recent -> kept
  341. self.assertEqual(1, count)
  342. log = self._read_log()
  343. self.assertEqual(2, len(log))
  344. self.assertEqual(b"medium entry", log[0].message)
  345. self.assertEqual(b"recent entry", log[1].message)
  346. def test_expire_all_entries(self) -> None:
  347. # Expire all entries
  348. count = expire_reflog(self.f, expire_time=3000000000)
  349. self.assertEqual(3, count)
  350. log = self._read_log()
  351. self.assertEqual(0, len(log))