2
0

test_cli.py 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401
  1. #!/usr/bin/env python
  2. # test_cli.py -- tests for dulwich.cli
  3. # vim: expandtab
  4. #
  5. # Copyright (C) 2024 Jelmer Vernooij <jelmer@jelmer.uk>
  6. #
  7. # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
  8. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  9. # General Public License as public by the Free Software Foundation; version 2.0
  10. # or (at your option) any later version. You can redistribute it and/or
  11. # modify it under the terms of either of these two licenses.
  12. #
  13. # Unless required by applicable law or agreed to in writing, software
  14. # distributed under the License is distributed on an "AS IS" BASIS,
  15. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16. # See the License for the specific language governing permissions and
  17. # limitations under the License.
  18. #
  19. # You should have received a copy of the licenses; if not, see
  20. # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
  21. # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
  22. # License, Version 2.0.
  23. """Tests for dulwich.cli."""
  24. import io
  25. import os
  26. import shutil
  27. import sys
  28. import tempfile
  29. import unittest
  30. from unittest import skipIf
  31. from unittest.mock import MagicMock, patch
  32. from dulwich import cli
  33. from dulwich.cli import format_bytes, parse_relative_time
  34. from dulwich.repo import Repo
  35. from dulwich.tests.utils import (
  36. build_commit_graph,
  37. )
  38. from . import TestCase
  39. class DulwichCliTestCase(TestCase):
  40. """Base class for CLI tests."""
  41. def setUp(self) -> None:
  42. super().setUp()
  43. self.test_dir = tempfile.mkdtemp()
  44. self.addCleanup(shutil.rmtree, self.test_dir)
  45. self.repo_path = os.path.join(self.test_dir, "repo")
  46. os.mkdir(self.repo_path)
  47. self.repo = Repo.init(self.repo_path)
  48. self.addCleanup(self.repo.close)
  49. def _run_cli(self, *args, stdout_stream=None):
  50. """Run CLI command and capture output."""
  51. class MockStream:
  52. def __init__(self):
  53. self._buffer = io.BytesIO()
  54. self.buffer = self._buffer
  55. def write(self, data):
  56. if isinstance(data, bytes):
  57. self._buffer.write(data)
  58. else:
  59. self._buffer.write(data.encode("utf-8"))
  60. def getvalue(self):
  61. value = self._buffer.getvalue()
  62. try:
  63. return value.decode("utf-8")
  64. except UnicodeDecodeError:
  65. return value
  66. def __getattr__(self, name):
  67. return getattr(self._buffer, name)
  68. old_stdout = sys.stdout
  69. old_stderr = sys.stderr
  70. old_cwd = os.getcwd()
  71. try:
  72. # Use custom stdout_stream if provided, otherwise use MockStream
  73. if stdout_stream:
  74. sys.stdout = stdout_stream
  75. if not hasattr(sys.stdout, "buffer"):
  76. sys.stdout.buffer = sys.stdout
  77. else:
  78. sys.stdout = MockStream()
  79. sys.stderr = MockStream()
  80. os.chdir(self.repo_path)
  81. result = cli.main(list(args))
  82. return result, sys.stdout.getvalue(), sys.stderr.getvalue()
  83. finally:
  84. sys.stdout = old_stdout
  85. sys.stderr = old_stderr
  86. os.chdir(old_cwd)
  87. class InitCommandTest(DulwichCliTestCase):
  88. """Tests for init command."""
  89. def test_init_basic(self):
  90. # Create a new directory for init
  91. new_repo_path = os.path.join(self.test_dir, "new_repo")
  92. result, stdout, stderr = self._run_cli("init", new_repo_path)
  93. self.assertTrue(os.path.exists(os.path.join(new_repo_path, ".git")))
  94. def test_init_bare(self):
  95. # Create a new directory for bare repo
  96. bare_repo_path = os.path.join(self.test_dir, "bare_repo")
  97. result, stdout, stderr = self._run_cli("init", "--bare", bare_repo_path)
  98. self.assertTrue(os.path.exists(os.path.join(bare_repo_path, "HEAD")))
  99. self.assertFalse(os.path.exists(os.path.join(bare_repo_path, ".git")))
  100. class AddCommandTest(DulwichCliTestCase):
  101. """Tests for add command."""
  102. def test_add_single_file(self):
  103. # Create a file to add
  104. test_file = os.path.join(self.repo_path, "test.txt")
  105. with open(test_file, "w") as f:
  106. f.write("test content")
  107. result, stdout, stderr = self._run_cli("add", "test.txt")
  108. # Check that file is in index
  109. self.assertIn(b"test.txt", self.repo.open_index())
  110. def test_add_multiple_files(self):
  111. # Create multiple files
  112. for i in range(3):
  113. test_file = os.path.join(self.repo_path, f"test{i}.txt")
  114. with open(test_file, "w") as f:
  115. f.write(f"content {i}")
  116. result, stdout, stderr = self._run_cli(
  117. "add", "test0.txt", "test1.txt", "test2.txt"
  118. )
  119. index = self.repo.open_index()
  120. self.assertIn(b"test0.txt", index)
  121. self.assertIn(b"test1.txt", index)
  122. self.assertIn(b"test2.txt", index)
  123. class RmCommandTest(DulwichCliTestCase):
  124. """Tests for rm command."""
  125. def test_rm_file(self):
  126. # Create, add and commit a file first
  127. test_file = os.path.join(self.repo_path, "test.txt")
  128. with open(test_file, "w") as f:
  129. f.write("test content")
  130. self._run_cli("add", "test.txt")
  131. self._run_cli("commit", "--message=Add test file")
  132. # Now remove it from index and working directory
  133. result, stdout, stderr = self._run_cli("rm", "test.txt")
  134. # Check that file is not in index
  135. self.assertNotIn(b"test.txt", self.repo.open_index())
  136. class CommitCommandTest(DulwichCliTestCase):
  137. """Tests for commit command."""
  138. def test_commit_basic(self):
  139. # Create and add a file
  140. test_file = os.path.join(self.repo_path, "test.txt")
  141. with open(test_file, "w") as f:
  142. f.write("test content")
  143. self._run_cli("add", "test.txt")
  144. # Commit
  145. result, stdout, stderr = self._run_cli("commit", "--message=Initial commit")
  146. # Check that HEAD points to a commit
  147. self.assertIsNotNone(self.repo.head())
  148. class LogCommandTest(DulwichCliTestCase):
  149. """Tests for log command."""
  150. def test_log_empty_repo(self):
  151. result, stdout, stderr = self._run_cli("log")
  152. # Empty repo should not crash
  153. def test_log_with_commits(self):
  154. # Create some commits
  155. c1, c2, c3 = build_commit_graph(
  156. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  157. )
  158. self.repo.refs[b"HEAD"] = c3.id
  159. result, stdout, stderr = self._run_cli("log")
  160. self.assertIn("Commit 3", stdout)
  161. self.assertIn("Commit 2", stdout)
  162. self.assertIn("Commit 1", stdout)
  163. def test_log_reverse(self):
  164. # Create some commits
  165. c1, c2, c3 = build_commit_graph(
  166. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  167. )
  168. self.repo.refs[b"HEAD"] = c3.id
  169. result, stdout, stderr = self._run_cli("log", "--reverse")
  170. # Check order - commit 1 should appear before commit 3
  171. pos1 = stdout.index("Commit 1")
  172. pos3 = stdout.index("Commit 3")
  173. self.assertLess(pos1, pos3)
  174. class StatusCommandTest(DulwichCliTestCase):
  175. """Tests for status command."""
  176. def test_status_empty(self):
  177. result, stdout, stderr = self._run_cli("status")
  178. # Should not crash on empty repo
  179. def test_status_with_untracked(self):
  180. # Create an untracked file
  181. test_file = os.path.join(self.repo_path, "untracked.txt")
  182. with open(test_file, "w") as f:
  183. f.write("untracked content")
  184. result, stdout, stderr = self._run_cli("status")
  185. self.assertIn("Untracked files:", stdout)
  186. self.assertIn("untracked.txt", stdout)
  187. class BranchCommandTest(DulwichCliTestCase):
  188. """Tests for branch command."""
  189. def test_branch_create(self):
  190. # Create initial commit
  191. test_file = os.path.join(self.repo_path, "test.txt")
  192. with open(test_file, "w") as f:
  193. f.write("test")
  194. self._run_cli("add", "test.txt")
  195. self._run_cli("commit", "--message=Initial")
  196. # Create branch
  197. result, stdout, stderr = self._run_cli("branch", "test-branch")
  198. self.assertIn(b"refs/heads/test-branch", self.repo.refs.keys())
  199. def test_branch_delete(self):
  200. # Create initial commit and branch
  201. test_file = os.path.join(self.repo_path, "test.txt")
  202. with open(test_file, "w") as f:
  203. f.write("test")
  204. self._run_cli("add", "test.txt")
  205. self._run_cli("commit", "--message=Initial")
  206. self._run_cli("branch", "test-branch")
  207. # Delete branch
  208. result, stdout, stderr = self._run_cli("branch", "-d", "test-branch")
  209. self.assertNotIn(b"refs/heads/test-branch", self.repo.refs.keys())
  210. class CheckoutCommandTest(DulwichCliTestCase):
  211. """Tests for checkout command."""
  212. def test_checkout_branch(self):
  213. # Create initial commit and branch
  214. test_file = os.path.join(self.repo_path, "test.txt")
  215. with open(test_file, "w") as f:
  216. f.write("test")
  217. self._run_cli("add", "test.txt")
  218. self._run_cli("commit", "--message=Initial")
  219. self._run_cli("branch", "test-branch")
  220. # Checkout branch
  221. result, stdout, stderr = self._run_cli("checkout", "test-branch")
  222. self.assertEqual(
  223. self.repo.refs.read_ref(b"HEAD"), b"ref: refs/heads/test-branch"
  224. )
  225. class TagCommandTest(DulwichCliTestCase):
  226. """Tests for tag command."""
  227. def test_tag_create(self):
  228. # Create initial commit
  229. test_file = os.path.join(self.repo_path, "test.txt")
  230. with open(test_file, "w") as f:
  231. f.write("test")
  232. self._run_cli("add", "test.txt")
  233. self._run_cli("commit", "--message=Initial")
  234. # Create tag
  235. result, stdout, stderr = self._run_cli("tag", "v1.0")
  236. self.assertIn(b"refs/tags/v1.0", self.repo.refs.keys())
  237. class FilterBranchCommandTest(DulwichCliTestCase):
  238. """Tests for filter-branch command."""
  239. def setUp(self):
  240. super().setUp()
  241. # Create a more complex repository structure for testing
  242. # Create some files in subdirectories
  243. os.makedirs(os.path.join(self.repo_path, "subdir"))
  244. os.makedirs(os.path.join(self.repo_path, "other"))
  245. # Create files
  246. files = {
  247. "README.md": "# Test Repo",
  248. "subdir/file1.txt": "File in subdir",
  249. "subdir/file2.txt": "Another file in subdir",
  250. "other/file3.txt": "File in other dir",
  251. "root.txt": "File at root",
  252. }
  253. for path, content in files.items():
  254. file_path = os.path.join(self.repo_path, path)
  255. with open(file_path, "w") as f:
  256. f.write(content)
  257. # Add all files and create initial commit
  258. self._run_cli("add", ".")
  259. self._run_cli("commit", "--message=Initial commit")
  260. # Create a second commit modifying subdir
  261. with open(os.path.join(self.repo_path, "subdir/file1.txt"), "a") as f:
  262. f.write("\nModified content")
  263. self._run_cli("add", "subdir/file1.txt")
  264. self._run_cli("commit", "--message=Modify subdir file")
  265. # Create a third commit in other dir
  266. with open(os.path.join(self.repo_path, "other/file3.txt"), "a") as f:
  267. f.write("\nMore content")
  268. self._run_cli("add", "other/file3.txt")
  269. self._run_cli("commit", "--message=Modify other file")
  270. # Create a branch
  271. self._run_cli("branch", "test-branch")
  272. # Create a tag
  273. self._run_cli("tag", "v1.0")
  274. def test_filter_branch_subdirectory_filter(self):
  275. """Test filter-branch with subdirectory filter."""
  276. # Run filter-branch to extract only the subdir
  277. result, stdout, stderr = self._run_cli(
  278. "filter-branch", "--subdirectory-filter", "subdir"
  279. )
  280. # Check that the operation succeeded
  281. self.assertEqual(result, 0)
  282. self.assertIn("Rewrite HEAD", stdout)
  283. # filter-branch rewrites history but doesn't update working tree
  284. # We need to check the commit contents, not the working tree
  285. # Reset to the rewritten HEAD to update working tree
  286. self._run_cli("reset", "--hard", "HEAD")
  287. # Now check that only files from subdir remain at root level
  288. self.assertTrue(os.path.exists(os.path.join(self.repo_path, "file1.txt")))
  289. self.assertTrue(os.path.exists(os.path.join(self.repo_path, "file2.txt")))
  290. self.assertFalse(os.path.exists(os.path.join(self.repo_path, "README.md")))
  291. self.assertFalse(os.path.exists(os.path.join(self.repo_path, "root.txt")))
  292. self.assertFalse(os.path.exists(os.path.join(self.repo_path, "other")))
  293. self.assertFalse(os.path.exists(os.path.join(self.repo_path, "subdir")))
  294. # Check that original refs were backed up
  295. original_refs = [
  296. ref for ref in self.repo.refs.keys() if ref.startswith(b"refs/original/")
  297. ]
  298. self.assertTrue(
  299. len(original_refs) > 0, "No original refs found after filter-branch"
  300. )
  301. @skipIf(sys.platform == "win32", "sed command not available on Windows")
  302. def test_filter_branch_msg_filter(self):
  303. """Test filter-branch with message filter."""
  304. # Run filter-branch to prepend [FILTERED] to commit messages
  305. result, stdout, stderr = self._run_cli(
  306. "filter-branch", "--msg-filter", "sed 's/^/[FILTERED] /'"
  307. )
  308. self.assertEqual(result, 0)
  309. # Check that commit messages were modified
  310. result, stdout, stderr = self._run_cli("log")
  311. self.assertIn("[FILTERED] Modify other file", stdout)
  312. self.assertIn("[FILTERED] Modify subdir file", stdout)
  313. self.assertIn("[FILTERED] Initial commit", stdout)
  314. def test_filter_branch_env_filter(self):
  315. """Test filter-branch with environment filter."""
  316. # Run filter-branch to change author email
  317. env_filter = """
  318. if [ "$GIT_AUTHOR_EMAIL" = "test@example.com" ]; then
  319. export GIT_AUTHOR_EMAIL="filtered@example.com"
  320. fi
  321. """
  322. result, stdout, stderr = self._run_cli(
  323. "filter-branch", "--env-filter", env_filter
  324. )
  325. self.assertEqual(result, 0)
  326. def test_filter_branch_prune_empty(self):
  327. """Test filter-branch with prune-empty option."""
  328. # Create a commit that only touches files outside subdir
  329. with open(os.path.join(self.repo_path, "root.txt"), "a") as f:
  330. f.write("\nNew line")
  331. self._run_cli("add", "root.txt")
  332. self._run_cli("commit", "--message=Modify root file only")
  333. # Run filter-branch to extract subdir with prune-empty
  334. result, stdout, stderr = self._run_cli(
  335. "filter-branch", "--subdirectory-filter", "subdir", "--prune-empty"
  336. )
  337. self.assertEqual(result, 0)
  338. # The last commit should have been pruned
  339. result, stdout, stderr = self._run_cli("log")
  340. self.assertNotIn("Modify root file only", stdout)
  341. @skipIf(sys.platform == "win32", "sed command not available on Windows")
  342. def test_filter_branch_force(self):
  343. """Test filter-branch with force option."""
  344. # Run filter-branch once with a filter that actually changes something
  345. result, stdout, stderr = self._run_cli(
  346. "filter-branch", "--msg-filter", "sed 's/^/[TEST] /'"
  347. )
  348. self.assertEqual(result, 0)
  349. # Check that backup refs were created
  350. # The implementation backs up refs under refs/original/
  351. original_refs = [
  352. ref for ref in self.repo.refs.keys() if ref.startswith(b"refs/original/")
  353. ]
  354. self.assertTrue(len(original_refs) > 0, "No original refs found")
  355. # Run again without force - should fail
  356. result, stdout, stderr = self._run_cli(
  357. "filter-branch", "--msg-filter", "sed 's/^/[TEST2] /'"
  358. )
  359. self.assertEqual(result, 1)
  360. self.assertIn("Cannot create a new backup", stdout)
  361. self.assertIn("refs/original", stdout)
  362. # Run with force - should succeed
  363. result, stdout, stderr = self._run_cli(
  364. "filter-branch", "--force", "--msg-filter", "sed 's/^/[TEST3] /'"
  365. )
  366. self.assertEqual(result, 0)
  367. @skipIf(sys.platform == "win32", "sed command not available on Windows")
  368. def test_filter_branch_specific_branch(self):
  369. """Test filter-branch on a specific branch."""
  370. # Switch to test-branch and add a commit
  371. self._run_cli("checkout", "test-branch")
  372. with open(os.path.join(self.repo_path, "branch-file.txt"), "w") as f:
  373. f.write("Branch specific file")
  374. self._run_cli("add", "branch-file.txt")
  375. self._run_cli("commit", "--message=Branch commit")
  376. # Run filter-branch on the test-branch
  377. result, stdout, stderr = self._run_cli(
  378. "filter-branch", "--msg-filter", "sed 's/^/[BRANCH] /'", "test-branch"
  379. )
  380. self.assertEqual(result, 0)
  381. self.assertIn("Ref 'refs/heads/test-branch' was rewritten", stdout)
  382. # Check that only test-branch was modified
  383. result, stdout, stderr = self._run_cli("log")
  384. self.assertIn("[BRANCH] Branch commit", stdout)
  385. # Switch to master and check it wasn't modified
  386. self._run_cli("checkout", "master")
  387. result, stdout, stderr = self._run_cli("log")
  388. self.assertNotIn("[BRANCH]", stdout)
  389. def test_filter_branch_tree_filter(self):
  390. """Test filter-branch with tree filter."""
  391. # Use a tree filter to remove a specific file
  392. tree_filter = "rm -f root.txt"
  393. result, stdout, stderr = self._run_cli(
  394. "filter-branch", "--tree-filter", tree_filter
  395. )
  396. self.assertEqual(result, 0)
  397. # Check that the file was removed from the latest commit
  398. # We need to check the commit tree, not the working directory
  399. result, stdout, stderr = self._run_cli("ls-tree", "HEAD")
  400. self.assertNotIn("root.txt", stdout)
  401. def test_filter_branch_index_filter(self):
  402. """Test filter-branch with index filter."""
  403. # Use an index filter to remove a file from the index
  404. index_filter = "git rm --cached --ignore-unmatch root.txt"
  405. result, stdout, stderr = self._run_cli(
  406. "filter-branch", "--index-filter", index_filter
  407. )
  408. self.assertEqual(result, 0)
  409. def test_filter_branch_parent_filter(self):
  410. """Test filter-branch with parent filter."""
  411. # Create a merge commit first
  412. self._run_cli("checkout", "HEAD", "-b", "feature")
  413. with open(os.path.join(self.repo_path, "feature.txt"), "w") as f:
  414. f.write("Feature")
  415. self._run_cli("add", "feature.txt")
  416. self._run_cli("commit", "--message=Feature commit")
  417. self._run_cli("checkout", "master")
  418. self._run_cli("merge", "feature", "--message=Merge feature")
  419. # Use parent filter to linearize history (remove second parent)
  420. parent_filter = "cut -d' ' -f1"
  421. result, stdout, stderr = self._run_cli(
  422. "filter-branch", "--parent-filter", parent_filter
  423. )
  424. self.assertEqual(result, 0)
  425. def test_filter_branch_commit_filter(self):
  426. """Test filter-branch with commit filter."""
  427. # Use commit filter to skip commits with certain messages
  428. commit_filter = """
  429. if grep -q "Modify other" <<< "$GIT_COMMIT_MESSAGE"; then
  430. skip_commit "$@"
  431. else
  432. git commit-tree "$@"
  433. fi
  434. """
  435. result, stdout, stderr = self._run_cli(
  436. "filter-branch", "--commit-filter", commit_filter
  437. )
  438. # Note: This test may fail because the commit filter syntax is simplified
  439. # In real Git, skip_commit is a function, but our implementation may differ
  440. def test_filter_branch_tag_name_filter(self):
  441. """Test filter-branch with tag name filter."""
  442. # Run filter-branch with tag name filter to rename tags
  443. result, stdout, stderr = self._run_cli(
  444. "filter-branch",
  445. "--tag-name-filter",
  446. "sed 's/^v/version-/'",
  447. "--msg-filter",
  448. "cat",
  449. )
  450. self.assertEqual(result, 0)
  451. # Check that tag was renamed
  452. self.assertIn(b"refs/tags/version-1.0", self.repo.refs.keys())
  453. def test_filter_branch_errors(self):
  454. """Test filter-branch error handling."""
  455. # Test with invalid subdirectory
  456. result, stdout, stderr = self._run_cli(
  457. "filter-branch", "--subdirectory-filter", "nonexistent"
  458. )
  459. # Should still succeed but produce empty history
  460. self.assertEqual(result, 0)
  461. def test_filter_branch_no_args(self):
  462. """Test filter-branch with no arguments."""
  463. # Should work as no-op
  464. result, stdout, stderr = self._run_cli("filter-branch")
  465. self.assertEqual(result, 0)
  466. class ShowCommandTest(DulwichCliTestCase):
  467. """Tests for show command."""
  468. def test_show_commit(self):
  469. # Create a commit
  470. test_file = os.path.join(self.repo_path, "test.txt")
  471. with open(test_file, "w") as f:
  472. f.write("test content")
  473. self._run_cli("add", "test.txt")
  474. self._run_cli("commit", "--message=Test commit")
  475. result, stdout, stderr = self._run_cli("show", "HEAD")
  476. self.assertIn("Test commit", stdout)
  477. class FormatPatchCommandTest(DulwichCliTestCase):
  478. """Tests for format-patch command."""
  479. def test_format_patch_single_commit(self):
  480. # Create a commit with actual content
  481. from dulwich.objects import Blob, Tree
  482. # Initial commit
  483. tree1 = Tree()
  484. self.repo.object_store.add_object(tree1)
  485. self.repo.do_commit(
  486. message=b"Initial commit",
  487. tree=tree1.id,
  488. )
  489. # Second commit with a file
  490. blob = Blob.from_string(b"Hello, World!\n")
  491. self.repo.object_store.add_object(blob)
  492. tree2 = Tree()
  493. tree2.add(b"hello.txt", 0o100644, blob.id)
  494. self.repo.object_store.add_object(tree2)
  495. self.repo.do_commit(
  496. message=b"Add hello.txt",
  497. tree=tree2.id,
  498. )
  499. # Test format-patch for last commit
  500. result, stdout, stderr = self._run_cli("format-patch", "-n", "1")
  501. self.assertEqual(result, None)
  502. self.assertIn("0001-Add-hello.txt.patch", stdout)
  503. # Check patch contents
  504. patch_file = os.path.join(self.repo_path, "0001-Add-hello.txt.patch")
  505. with open(patch_file, "rb") as f:
  506. content = f.read()
  507. # Check header
  508. self.assertIn(b"Subject: [PATCH 1/1] Add hello.txt", content)
  509. self.assertIn(b"From:", content)
  510. self.assertIn(b"Date:", content)
  511. # Check diff content
  512. self.assertIn(b"diff --git a/hello.txt b/hello.txt", content)
  513. self.assertIn(b"new file mode", content)
  514. self.assertIn(b"+Hello, World!", content)
  515. # Check footer
  516. self.assertIn(b"-- \nDulwich", content)
  517. # Clean up
  518. os.remove(patch_file)
  519. def test_format_patch_multiple_commits(self):
  520. from dulwich.objects import Blob, Tree
  521. # Initial commit
  522. tree1 = Tree()
  523. self.repo.object_store.add_object(tree1)
  524. self.repo.do_commit(
  525. message=b"Initial commit",
  526. tree=tree1.id,
  527. )
  528. # Second commit
  529. blob1 = Blob.from_string(b"File 1 content\n")
  530. self.repo.object_store.add_object(blob1)
  531. tree2 = Tree()
  532. tree2.add(b"file1.txt", 0o100644, blob1.id)
  533. self.repo.object_store.add_object(tree2)
  534. self.repo.do_commit(
  535. message=b"Add file1.txt",
  536. tree=tree2.id,
  537. )
  538. # Third commit
  539. blob2 = Blob.from_string(b"File 2 content\n")
  540. self.repo.object_store.add_object(blob2)
  541. tree3 = Tree()
  542. tree3.add(b"file1.txt", 0o100644, blob1.id)
  543. tree3.add(b"file2.txt", 0o100644, blob2.id)
  544. self.repo.object_store.add_object(tree3)
  545. self.repo.do_commit(
  546. message=b"Add file2.txt",
  547. tree=tree3.id,
  548. )
  549. # Test format-patch for last 2 commits
  550. result, stdout, stderr = self._run_cli("format-patch", "-n", "2")
  551. self.assertEqual(result, None)
  552. self.assertIn("0001-Add-file1.txt.patch", stdout)
  553. self.assertIn("0002-Add-file2.txt.patch", stdout)
  554. # Check first patch
  555. with open(os.path.join(self.repo_path, "0001-Add-file1.txt.patch"), "rb") as f:
  556. content = f.read()
  557. self.assertIn(b"Subject: [PATCH 1/2] Add file1.txt", content)
  558. self.assertIn(b"+File 1 content", content)
  559. # Check second patch
  560. with open(os.path.join(self.repo_path, "0002-Add-file2.txt.patch"), "rb") as f:
  561. content = f.read()
  562. self.assertIn(b"Subject: [PATCH 2/2] Add file2.txt", content)
  563. self.assertIn(b"+File 2 content", content)
  564. # Clean up
  565. os.remove(os.path.join(self.repo_path, "0001-Add-file1.txt.patch"))
  566. os.remove(os.path.join(self.repo_path, "0002-Add-file2.txt.patch"))
  567. def test_format_patch_output_directory(self):
  568. from dulwich.objects import Blob, Tree
  569. # Create a commit
  570. blob = Blob.from_string(b"Test content\n")
  571. self.repo.object_store.add_object(blob)
  572. tree = Tree()
  573. tree.add(b"test.txt", 0o100644, blob.id)
  574. self.repo.object_store.add_object(tree)
  575. self.repo.do_commit(
  576. message=b"Test commit",
  577. tree=tree.id,
  578. )
  579. # Create output directory
  580. output_dir = os.path.join(self.test_dir, "patches")
  581. os.makedirs(output_dir)
  582. # Test format-patch with output directory
  583. result, stdout, stderr = self._run_cli(
  584. "format-patch", "-o", output_dir, "-n", "1"
  585. )
  586. self.assertEqual(result, None)
  587. # Check that file was created in output directory with correct content
  588. patch_file = os.path.join(output_dir, "0001-Test-commit.patch")
  589. self.assertTrue(os.path.exists(patch_file))
  590. with open(patch_file, "rb") as f:
  591. content = f.read()
  592. self.assertIn(b"Subject: [PATCH 1/1] Test commit", content)
  593. self.assertIn(b"+Test content", content)
  594. def test_format_patch_commit_range(self):
  595. from dulwich.objects import Blob, Tree
  596. # Create commits with actual file changes
  597. commits = []
  598. trees = []
  599. # Initial empty commit
  600. tree0 = Tree()
  601. self.repo.object_store.add_object(tree0)
  602. trees.append(tree0)
  603. c0 = self.repo.do_commit(
  604. message=b"Initial commit",
  605. tree=tree0.id,
  606. )
  607. commits.append(c0)
  608. # Add three files in separate commits
  609. for i in range(1, 4):
  610. blob = Blob.from_string(f"Content {i}\n".encode())
  611. self.repo.object_store.add_object(blob)
  612. tree = Tree()
  613. # Copy previous files
  614. for j in range(1, i):
  615. prev_blob_id = trees[j][f"file{j}.txt".encode()][1]
  616. tree.add(f"file{j}.txt".encode(), 0o100644, prev_blob_id)
  617. # Add new file
  618. tree.add(f"file{i}.txt".encode(), 0o100644, blob.id)
  619. self.repo.object_store.add_object(tree)
  620. trees.append(tree)
  621. c = self.repo.do_commit(
  622. message=f"Add file{i}.txt".encode(),
  623. tree=tree.id,
  624. )
  625. commits.append(c)
  626. # Test format-patch with commit range (should get commits 2 and 3)
  627. result, stdout, stderr = self._run_cli(
  628. "format-patch", f"{commits[1].decode()}..{commits[3].decode()}"
  629. )
  630. self.assertEqual(result, None)
  631. # Should create patches for commits 2 and 3
  632. self.assertIn("0001-Add-file2.txt.patch", stdout)
  633. self.assertIn("0002-Add-file3.txt.patch", stdout)
  634. # Verify patch contents
  635. with open(os.path.join(self.repo_path, "0001-Add-file2.txt.patch"), "rb") as f:
  636. content = f.read()
  637. self.assertIn(b"Subject: [PATCH 1/2] Add file2.txt", content)
  638. self.assertIn(b"+Content 2", content)
  639. self.assertNotIn(b"file3.txt", content) # Should not include file3
  640. with open(os.path.join(self.repo_path, "0002-Add-file3.txt.patch"), "rb") as f:
  641. content = f.read()
  642. self.assertIn(b"Subject: [PATCH 2/2] Add file3.txt", content)
  643. self.assertIn(b"+Content 3", content)
  644. self.assertNotIn(b"file2.txt", content) # Should not modify file2
  645. # Clean up
  646. os.remove(os.path.join(self.repo_path, "0001-Add-file2.txt.patch"))
  647. os.remove(os.path.join(self.repo_path, "0002-Add-file3.txt.patch"))
  648. def test_format_patch_stdout(self):
  649. from dulwich.objects import Blob, Tree
  650. # Create a commit with modified file
  651. tree1 = Tree()
  652. blob1 = Blob.from_string(b"Original content\n")
  653. self.repo.object_store.add_object(blob1)
  654. tree1.add(b"file.txt", 0o100644, blob1.id)
  655. self.repo.object_store.add_object(tree1)
  656. self.repo.do_commit(
  657. message=b"Initial commit",
  658. tree=tree1.id,
  659. )
  660. tree2 = Tree()
  661. blob2 = Blob.from_string(b"Modified content\n")
  662. self.repo.object_store.add_object(blob2)
  663. tree2.add(b"file.txt", 0o100644, blob2.id)
  664. self.repo.object_store.add_object(tree2)
  665. self.repo.do_commit(
  666. message=b"Modify file.txt",
  667. tree=tree2.id,
  668. )
  669. # Mock stdout as a BytesIO for binary output
  670. stdout_stream = io.BytesIO()
  671. stdout_stream.buffer = stdout_stream
  672. # Run command with --stdout
  673. old_stdout = sys.stdout
  674. old_stderr = sys.stderr
  675. old_cwd = os.getcwd()
  676. try:
  677. sys.stdout = stdout_stream
  678. sys.stderr = io.StringIO()
  679. os.chdir(self.repo_path)
  680. cli.main(["format-patch", "--stdout", "-n", "1"])
  681. finally:
  682. sys.stdout = old_stdout
  683. sys.stderr = old_stderr
  684. os.chdir(old_cwd)
  685. # Check output
  686. stdout_stream.seek(0)
  687. output = stdout_stream.read()
  688. self.assertIn(b"Subject: [PATCH 1/1] Modify file.txt", output)
  689. self.assertIn(b"diff --git a/file.txt b/file.txt", output)
  690. self.assertIn(b"-Original content", output)
  691. self.assertIn(b"+Modified content", output)
  692. self.assertIn(b"-- \nDulwich", output)
  693. def test_format_patch_empty_repo(self):
  694. # Test with empty repository
  695. result, stdout, stderr = self._run_cli("format-patch", "-n", "5")
  696. self.assertEqual(result, None)
  697. # Should produce no output for empty repo
  698. self.assertEqual(stdout.strip(), "")
  699. class FetchPackCommandTest(DulwichCliTestCase):
  700. """Tests for fetch-pack command."""
  701. @patch("dulwich.cli.get_transport_and_path")
  702. def test_fetch_pack_basic(self, mock_transport):
  703. # Mock the transport
  704. mock_client = MagicMock()
  705. mock_transport.return_value = (mock_client, "/path/to/repo")
  706. mock_client.fetch.return_value = None
  707. result, stdout, stderr = self._run_cli(
  708. "fetch-pack", "git://example.com/repo.git"
  709. )
  710. mock_client.fetch.assert_called_once()
  711. class LsRemoteCommandTest(DulwichCliTestCase):
  712. """Tests for ls-remote command."""
  713. def test_ls_remote_basic(self):
  714. # Create a commit
  715. test_file = os.path.join(self.repo_path, "test.txt")
  716. with open(test_file, "w") as f:
  717. f.write("test")
  718. self._run_cli("add", "test.txt")
  719. self._run_cli("commit", "--message=Initial")
  720. # Test basic ls-remote
  721. result, stdout, stderr = self._run_cli("ls-remote", self.repo_path)
  722. lines = stdout.strip().split("\n")
  723. self.assertTrue(any("HEAD" in line for line in lines))
  724. self.assertTrue(any("refs/heads/master" in line for line in lines))
  725. def test_ls_remote_symref(self):
  726. # Create a commit
  727. test_file = os.path.join(self.repo_path, "test.txt")
  728. with open(test_file, "w") as f:
  729. f.write("test")
  730. self._run_cli("add", "test.txt")
  731. self._run_cli("commit", "--message=Initial")
  732. # Test ls-remote with --symref option
  733. result, stdout, stderr = self._run_cli("ls-remote", "--symref", self.repo_path)
  734. lines = stdout.strip().split("\n")
  735. # Should show symref for HEAD in exact format: "ref: refs/heads/master\tHEAD"
  736. expected_line = "ref: refs/heads/master\tHEAD"
  737. self.assertIn(
  738. expected_line,
  739. lines,
  740. f"Expected line '{expected_line}' not found in output: {lines}",
  741. )
  742. class PullCommandTest(DulwichCliTestCase):
  743. """Tests for pull command."""
  744. @patch("dulwich.porcelain.pull")
  745. def test_pull_basic(self, mock_pull):
  746. result, stdout, stderr = self._run_cli("pull", "origin")
  747. mock_pull.assert_called_once()
  748. @patch("dulwich.porcelain.pull")
  749. def test_pull_with_refspec(self, mock_pull):
  750. result, stdout, stderr = self._run_cli("pull", "origin", "master")
  751. mock_pull.assert_called_once()
  752. class PushCommandTest(DulwichCliTestCase):
  753. """Tests for push command."""
  754. @patch("dulwich.porcelain.push")
  755. def test_push_basic(self, mock_push):
  756. result, stdout, stderr = self._run_cli("push", "origin")
  757. mock_push.assert_called_once()
  758. @patch("dulwich.porcelain.push")
  759. def test_push_force(self, mock_push):
  760. result, stdout, stderr = self._run_cli("push", "-f", "origin")
  761. mock_push.assert_called_with(".", "origin", None, force=True)
  762. class ArchiveCommandTest(DulwichCliTestCase):
  763. """Tests for archive command."""
  764. def test_archive_basic(self):
  765. # Create a commit
  766. test_file = os.path.join(self.repo_path, "test.txt")
  767. with open(test_file, "w") as f:
  768. f.write("test content")
  769. self._run_cli("add", "test.txt")
  770. self._run_cli("commit", "--message=Initial")
  771. # Archive produces binary output, so use BytesIO
  772. result, stdout, stderr = self._run_cli(
  773. "archive", "HEAD", stdout_stream=io.BytesIO()
  774. )
  775. # Should complete without error and produce some binary output
  776. self.assertIsInstance(stdout, bytes)
  777. self.assertGreater(len(stdout), 0)
  778. class ForEachRefCommandTest(DulwichCliTestCase):
  779. """Tests for for-each-ref command."""
  780. def test_for_each_ref(self):
  781. # Create a commit
  782. test_file = os.path.join(self.repo_path, "test.txt")
  783. with open(test_file, "w") as f:
  784. f.write("test")
  785. self._run_cli("add", "test.txt")
  786. self._run_cli("commit", "--message=Initial")
  787. result, stdout, stderr = self._run_cli("for-each-ref")
  788. self.assertIn("refs/heads/master", stdout)
  789. class PackRefsCommandTest(DulwichCliTestCase):
  790. """Tests for pack-refs command."""
  791. def test_pack_refs(self):
  792. # Create some refs
  793. test_file = os.path.join(self.repo_path, "test.txt")
  794. with open(test_file, "w") as f:
  795. f.write("test")
  796. self._run_cli("add", "test.txt")
  797. self._run_cli("commit", "--message=Initial")
  798. self._run_cli("branch", "test-branch")
  799. result, stdout, stderr = self._run_cli("pack-refs", "--all")
  800. # Check that packed-refs file exists
  801. self.assertTrue(
  802. os.path.exists(os.path.join(self.repo_path, ".git", "packed-refs"))
  803. )
  804. class SubmoduleCommandTest(DulwichCliTestCase):
  805. """Tests for submodule commands."""
  806. def test_submodule_list(self):
  807. # Create an initial commit so repo has a HEAD
  808. test_file = os.path.join(self.repo_path, "test.txt")
  809. with open(test_file, "w") as f:
  810. f.write("test")
  811. self._run_cli("add", "test.txt")
  812. self._run_cli("commit", "--message=Initial")
  813. result, stdout, stderr = self._run_cli("submodule")
  814. # Should not crash on repo without submodules
  815. def test_submodule_init(self):
  816. # Create .gitmodules file for init to work
  817. gitmodules = os.path.join(self.repo_path, ".gitmodules")
  818. with open(gitmodules, "w") as f:
  819. f.write("") # Empty .gitmodules file
  820. result, stdout, stderr = self._run_cli("submodule", "init")
  821. # Should not crash
  822. class StashCommandTest(DulwichCliTestCase):
  823. """Tests for stash commands."""
  824. def test_stash_list_empty(self):
  825. result, stdout, stderr = self._run_cli("stash", "list")
  826. # Should not crash on empty stash
  827. def test_stash_push_pop(self):
  828. # Create a file and modify it
  829. test_file = os.path.join(self.repo_path, "test.txt")
  830. with open(test_file, "w") as f:
  831. f.write("initial")
  832. self._run_cli("add", "test.txt")
  833. self._run_cli("commit", "--message=Initial")
  834. # Modify file
  835. with open(test_file, "w") as f:
  836. f.write("modified")
  837. # Stash changes
  838. result, stdout, stderr = self._run_cli("stash", "push")
  839. self.assertIn("Saved working directory", stdout)
  840. # Note: Dulwich stash doesn't currently update the working tree
  841. # so the file remains modified after stash push
  842. # Note: stash pop is not fully implemented in Dulwich yet
  843. # so we only test stash push here
  844. class MergeCommandTest(DulwichCliTestCase):
  845. """Tests for merge command."""
  846. def test_merge_basic(self):
  847. # Create initial commit
  848. test_file = os.path.join(self.repo_path, "test.txt")
  849. with open(test_file, "w") as f:
  850. f.write("initial")
  851. self._run_cli("add", "test.txt")
  852. self._run_cli("commit", "--message=Initial")
  853. # Create and checkout new branch
  854. self._run_cli("branch", "feature")
  855. self._run_cli("checkout", "feature")
  856. # Make changes in feature branch
  857. with open(test_file, "w") as f:
  858. f.write("feature changes")
  859. self._run_cli("add", "test.txt")
  860. self._run_cli("commit", "--message=Feature commit")
  861. # Go back to main
  862. self._run_cli("checkout", "master")
  863. # Merge feature branch
  864. result, stdout, stderr = self._run_cli("merge", "feature")
  865. class HelpCommandTest(DulwichCliTestCase):
  866. """Tests for help command."""
  867. def test_help_basic(self):
  868. result, stdout, stderr = self._run_cli("help")
  869. self.assertIn("dulwich command line tool", stdout)
  870. def test_help_all(self):
  871. result, stdout, stderr = self._run_cli("help", "-a")
  872. self.assertIn("Available commands:", stdout)
  873. self.assertIn("add", stdout)
  874. self.assertIn("commit", stdout)
  875. class RemoteCommandTest(DulwichCliTestCase):
  876. """Tests for remote commands."""
  877. def test_remote_add(self):
  878. result, stdout, stderr = self._run_cli(
  879. "remote", "add", "origin", "https://github.com/example/repo.git"
  880. )
  881. # Check remote was added to config
  882. config = self.repo.get_config()
  883. self.assertEqual(
  884. config.get((b"remote", b"origin"), b"url"),
  885. b"https://github.com/example/repo.git",
  886. )
  887. class CheckIgnoreCommandTest(DulwichCliTestCase):
  888. """Tests for check-ignore command."""
  889. def test_check_ignore(self):
  890. # Create .gitignore
  891. gitignore = os.path.join(self.repo_path, ".gitignore")
  892. with open(gitignore, "w") as f:
  893. f.write("*.log\n")
  894. result, stdout, stderr = self._run_cli("check-ignore", "test.log", "test.txt")
  895. self.assertIn("test.log", stdout)
  896. self.assertNotIn("test.txt", stdout)
  897. class LsFilesCommandTest(DulwichCliTestCase):
  898. """Tests for ls-files command."""
  899. def test_ls_files(self):
  900. # Add some files
  901. for name in ["a.txt", "b.txt", "c.txt"]:
  902. path = os.path.join(self.repo_path, name)
  903. with open(path, "w") as f:
  904. f.write(f"content of {name}")
  905. self._run_cli("add", "a.txt", "b.txt", "c.txt")
  906. result, stdout, stderr = self._run_cli("ls-files")
  907. self.assertIn("a.txt", stdout)
  908. self.assertIn("b.txt", stdout)
  909. self.assertIn("c.txt", stdout)
  910. class LsTreeCommandTest(DulwichCliTestCase):
  911. """Tests for ls-tree command."""
  912. def test_ls_tree(self):
  913. # Create a directory structure
  914. os.mkdir(os.path.join(self.repo_path, "subdir"))
  915. with open(os.path.join(self.repo_path, "file.txt"), "w") as f:
  916. f.write("file content")
  917. with open(os.path.join(self.repo_path, "subdir", "nested.txt"), "w") as f:
  918. f.write("nested content")
  919. self._run_cli("add", ".")
  920. self._run_cli("commit", "--message=Initial")
  921. result, stdout, stderr = self._run_cli("ls-tree", "HEAD")
  922. self.assertIn("file.txt", stdout)
  923. self.assertIn("subdir", stdout)
  924. def test_ls_tree_recursive(self):
  925. # Create nested structure
  926. os.mkdir(os.path.join(self.repo_path, "subdir"))
  927. with open(os.path.join(self.repo_path, "subdir", "nested.txt"), "w") as f:
  928. f.write("nested")
  929. self._run_cli("add", ".")
  930. self._run_cli("commit", "--message=Initial")
  931. result, stdout, stderr = self._run_cli("ls-tree", "-r", "HEAD")
  932. self.assertIn("subdir/nested.txt", stdout)
  933. class DescribeCommandTest(DulwichCliTestCase):
  934. """Tests for describe command."""
  935. def test_describe(self):
  936. # Create tagged commit
  937. test_file = os.path.join(self.repo_path, "test.txt")
  938. with open(test_file, "w") as f:
  939. f.write("test")
  940. self._run_cli("add", "test.txt")
  941. self._run_cli("commit", "--message=Initial")
  942. self._run_cli("tag", "v1.0")
  943. result, stdout, stderr = self._run_cli("describe")
  944. self.assertIn("v1.0", stdout)
  945. class FsckCommandTest(DulwichCliTestCase):
  946. """Tests for fsck command."""
  947. def test_fsck(self):
  948. # Create a commit
  949. test_file = os.path.join(self.repo_path, "test.txt")
  950. with open(test_file, "w") as f:
  951. f.write("test")
  952. self._run_cli("add", "test.txt")
  953. self._run_cli("commit", "--message=Initial")
  954. result, stdout, stderr = self._run_cli("fsck")
  955. # Should complete without errors
  956. class RepackCommandTest(DulwichCliTestCase):
  957. """Tests for repack command."""
  958. def test_repack(self):
  959. # Create some objects
  960. for i in range(5):
  961. test_file = os.path.join(self.repo_path, f"test{i}.txt")
  962. with open(test_file, "w") as f:
  963. f.write(f"content {i}")
  964. self._run_cli("add", f"test{i}.txt")
  965. self._run_cli("commit", f"--message=Commit {i}")
  966. result, stdout, stderr = self._run_cli("repack")
  967. # Should create pack files
  968. pack_dir = os.path.join(self.repo_path, ".git", "objects", "pack")
  969. self.assertTrue(any(f.endswith(".pack") for f in os.listdir(pack_dir)))
  970. class ResetCommandTest(DulwichCliTestCase):
  971. """Tests for reset command."""
  972. def test_reset_soft(self):
  973. # Create commits
  974. test_file = os.path.join(self.repo_path, "test.txt")
  975. with open(test_file, "w") as f:
  976. f.write("first")
  977. self._run_cli("add", "test.txt")
  978. self._run_cli("commit", "--message=First")
  979. first_commit = self.repo.head()
  980. with open(test_file, "w") as f:
  981. f.write("second")
  982. self._run_cli("add", "test.txt")
  983. self._run_cli("commit", "--message=Second")
  984. # Reset soft
  985. result, stdout, stderr = self._run_cli("reset", "--soft", first_commit.decode())
  986. # HEAD should be at first commit
  987. self.assertEqual(self.repo.head(), first_commit)
  988. class WriteTreeCommandTest(DulwichCliTestCase):
  989. """Tests for write-tree command."""
  990. def test_write_tree(self):
  991. # Create and add files
  992. test_file = os.path.join(self.repo_path, "test.txt")
  993. with open(test_file, "w") as f:
  994. f.write("test")
  995. self._run_cli("add", "test.txt")
  996. result, stdout, stderr = self._run_cli("write-tree")
  997. # Should output tree SHA
  998. self.assertEqual(len(stdout.strip()), 40)
  999. class UpdateServerInfoCommandTest(DulwichCliTestCase):
  1000. """Tests for update-server-info command."""
  1001. def test_update_server_info(self):
  1002. result, stdout, stderr = self._run_cli("update-server-info")
  1003. # Should create info/refs file
  1004. info_refs = os.path.join(self.repo_path, ".git", "info", "refs")
  1005. self.assertTrue(os.path.exists(info_refs))
  1006. class SymbolicRefCommandTest(DulwichCliTestCase):
  1007. """Tests for symbolic-ref command."""
  1008. def test_symbolic_ref(self):
  1009. # Create a branch
  1010. test_file = os.path.join(self.repo_path, "test.txt")
  1011. with open(test_file, "w") as f:
  1012. f.write("test")
  1013. self._run_cli("add", "test.txt")
  1014. self._run_cli("commit", "--message=Initial")
  1015. self._run_cli("branch", "test-branch")
  1016. result, stdout, stderr = self._run_cli(
  1017. "symbolic-ref", "HEAD", "refs/heads/test-branch"
  1018. )
  1019. # HEAD should now point to test-branch
  1020. self.assertEqual(
  1021. self.repo.refs.read_ref(b"HEAD"), b"ref: refs/heads/test-branch"
  1022. )
  1023. class FormatBytesTestCase(TestCase):
  1024. """Tests for format_bytes function."""
  1025. def test_bytes(self):
  1026. """Test formatting bytes."""
  1027. self.assertEqual("0.0 B", format_bytes(0))
  1028. self.assertEqual("1.0 B", format_bytes(1))
  1029. self.assertEqual("512.0 B", format_bytes(512))
  1030. self.assertEqual("1023.0 B", format_bytes(1023))
  1031. def test_kilobytes(self):
  1032. """Test formatting kilobytes."""
  1033. self.assertEqual("1.0 KB", format_bytes(1024))
  1034. self.assertEqual("1.5 KB", format_bytes(1536))
  1035. self.assertEqual("2.0 KB", format_bytes(2048))
  1036. self.assertEqual("1023.0 KB", format_bytes(1024 * 1023))
  1037. def test_megabytes(self):
  1038. """Test formatting megabytes."""
  1039. self.assertEqual("1.0 MB", format_bytes(1024 * 1024))
  1040. self.assertEqual("1.5 MB", format_bytes(1024 * 1024 * 1.5))
  1041. self.assertEqual("10.0 MB", format_bytes(1024 * 1024 * 10))
  1042. self.assertEqual("1023.0 MB", format_bytes(1024 * 1024 * 1023))
  1043. def test_gigabytes(self):
  1044. """Test formatting gigabytes."""
  1045. self.assertEqual("1.0 GB", format_bytes(1024 * 1024 * 1024))
  1046. self.assertEqual("2.5 GB", format_bytes(1024 * 1024 * 1024 * 2.5))
  1047. self.assertEqual("1023.0 GB", format_bytes(1024 * 1024 * 1024 * 1023))
  1048. def test_terabytes(self):
  1049. """Test formatting terabytes."""
  1050. self.assertEqual("1.0 TB", format_bytes(1024 * 1024 * 1024 * 1024))
  1051. self.assertEqual("5.0 TB", format_bytes(1024 * 1024 * 1024 * 1024 * 5))
  1052. self.assertEqual("1000.0 TB", format_bytes(1024 * 1024 * 1024 * 1024 * 1000))
  1053. class ParseRelativeTimeTestCase(TestCase):
  1054. """Tests for parse_relative_time function."""
  1055. def test_now(self):
  1056. """Test parsing 'now'."""
  1057. self.assertEqual(0, parse_relative_time("now"))
  1058. def test_seconds(self):
  1059. """Test parsing seconds."""
  1060. self.assertEqual(1, parse_relative_time("1 second ago"))
  1061. self.assertEqual(5, parse_relative_time("5 seconds ago"))
  1062. self.assertEqual(30, parse_relative_time("30 seconds ago"))
  1063. def test_minutes(self):
  1064. """Test parsing minutes."""
  1065. self.assertEqual(60, parse_relative_time("1 minute ago"))
  1066. self.assertEqual(300, parse_relative_time("5 minutes ago"))
  1067. self.assertEqual(1800, parse_relative_time("30 minutes ago"))
  1068. def test_hours(self):
  1069. """Test parsing hours."""
  1070. self.assertEqual(3600, parse_relative_time("1 hour ago"))
  1071. self.assertEqual(7200, parse_relative_time("2 hours ago"))
  1072. self.assertEqual(86400, parse_relative_time("24 hours ago"))
  1073. def test_days(self):
  1074. """Test parsing days."""
  1075. self.assertEqual(86400, parse_relative_time("1 day ago"))
  1076. self.assertEqual(604800, parse_relative_time("7 days ago"))
  1077. self.assertEqual(2592000, parse_relative_time("30 days ago"))
  1078. def test_weeks(self):
  1079. """Test parsing weeks."""
  1080. self.assertEqual(604800, parse_relative_time("1 week ago"))
  1081. self.assertEqual(1209600, parse_relative_time("2 weeks ago"))
  1082. self.assertEqual(
  1083. 36288000, parse_relative_time("60 weeks ago")
  1084. ) # 60 * 7 * 24 * 60 * 60
  1085. def test_invalid_format(self):
  1086. """Test invalid time formats."""
  1087. with self.assertRaises(ValueError) as cm:
  1088. parse_relative_time("invalid")
  1089. self.assertIn("Invalid relative time format", str(cm.exception))
  1090. with self.assertRaises(ValueError) as cm:
  1091. parse_relative_time("2 weeks")
  1092. self.assertIn("Invalid relative time format", str(cm.exception))
  1093. with self.assertRaises(ValueError) as cm:
  1094. parse_relative_time("ago")
  1095. self.assertIn("Invalid relative time format", str(cm.exception))
  1096. with self.assertRaises(ValueError) as cm:
  1097. parse_relative_time("two weeks ago")
  1098. self.assertIn("Invalid number in relative time", str(cm.exception))
  1099. def test_invalid_unit(self):
  1100. """Test invalid time units."""
  1101. with self.assertRaises(ValueError) as cm:
  1102. parse_relative_time("5 months ago")
  1103. self.assertIn("Unknown time unit: months", str(cm.exception))
  1104. with self.assertRaises(ValueError) as cm:
  1105. parse_relative_time("2 years ago")
  1106. self.assertIn("Unknown time unit: years", str(cm.exception))
  1107. def test_singular_plural(self):
  1108. """Test that both singular and plural forms work."""
  1109. self.assertEqual(
  1110. parse_relative_time("1 second ago"), parse_relative_time("1 seconds ago")
  1111. )
  1112. self.assertEqual(
  1113. parse_relative_time("1 minute ago"), parse_relative_time("1 minutes ago")
  1114. )
  1115. self.assertEqual(
  1116. parse_relative_time("1 hour ago"), parse_relative_time("1 hours ago")
  1117. )
  1118. self.assertEqual(
  1119. parse_relative_time("1 day ago"), parse_relative_time("1 days ago")
  1120. )
  1121. self.assertEqual(
  1122. parse_relative_time("1 week ago"), parse_relative_time("1 weeks ago")
  1123. )
  1124. if __name__ == "__main__":
  1125. unittest.main()