test_cli.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862
  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.mock import MagicMock, patch
  31. from dulwich import cli
  32. from dulwich.cli import format_bytes, parse_relative_time
  33. from dulwich.repo import Repo
  34. from dulwich.tests.utils import (
  35. build_commit_graph,
  36. )
  37. from . import TestCase
  38. class DulwichCliTestCase(TestCase):
  39. """Base class for CLI tests."""
  40. def setUp(self) -> None:
  41. super().setUp()
  42. self.test_dir = tempfile.mkdtemp()
  43. self.addCleanup(shutil.rmtree, self.test_dir)
  44. self.repo_path = os.path.join(self.test_dir, "repo")
  45. os.mkdir(self.repo_path)
  46. self.repo = Repo.init(self.repo_path)
  47. self.addCleanup(self.repo.close)
  48. def _run_cli(self, *args, stdout_stream=None):
  49. """Run CLI command and capture output."""
  50. class MockStream:
  51. def __init__(self):
  52. self._buffer = io.BytesIO()
  53. self.buffer = self._buffer
  54. def write(self, data):
  55. if isinstance(data, bytes):
  56. self._buffer.write(data)
  57. else:
  58. self._buffer.write(data.encode("utf-8"))
  59. def getvalue(self):
  60. value = self._buffer.getvalue()
  61. try:
  62. return value.decode("utf-8")
  63. except UnicodeDecodeError:
  64. return value
  65. def __getattr__(self, name):
  66. return getattr(self._buffer, name)
  67. old_stdout = sys.stdout
  68. old_stderr = sys.stderr
  69. old_cwd = os.getcwd()
  70. try:
  71. # Use custom stdout_stream if provided, otherwise use MockStream
  72. if stdout_stream:
  73. sys.stdout = stdout_stream
  74. if not hasattr(sys.stdout, "buffer"):
  75. sys.stdout.buffer = sys.stdout
  76. else:
  77. sys.stdout = MockStream()
  78. sys.stderr = MockStream()
  79. os.chdir(self.repo_path)
  80. result = cli.main(list(args))
  81. return result, sys.stdout.getvalue(), sys.stderr.getvalue()
  82. finally:
  83. sys.stdout = old_stdout
  84. sys.stderr = old_stderr
  85. os.chdir(old_cwd)
  86. class InitCommandTest(DulwichCliTestCase):
  87. """Tests for init command."""
  88. def test_init_basic(self):
  89. # Create a new directory for init
  90. new_repo_path = os.path.join(self.test_dir, "new_repo")
  91. result, stdout, stderr = self._run_cli("init", new_repo_path)
  92. self.assertTrue(os.path.exists(os.path.join(new_repo_path, ".git")))
  93. def test_init_bare(self):
  94. # Create a new directory for bare repo
  95. bare_repo_path = os.path.join(self.test_dir, "bare_repo")
  96. result, stdout, stderr = self._run_cli("init", "--bare", bare_repo_path)
  97. self.assertTrue(os.path.exists(os.path.join(bare_repo_path, "HEAD")))
  98. self.assertFalse(os.path.exists(os.path.join(bare_repo_path, ".git")))
  99. class AddCommandTest(DulwichCliTestCase):
  100. """Tests for add command."""
  101. def test_add_single_file(self):
  102. # Create a file to add
  103. test_file = os.path.join(self.repo_path, "test.txt")
  104. with open(test_file, "w") as f:
  105. f.write("test content")
  106. result, stdout, stderr = self._run_cli("add", "test.txt")
  107. # Check that file is in index
  108. self.assertIn(b"test.txt", self.repo.open_index())
  109. def test_add_multiple_files(self):
  110. # Create multiple files
  111. for i in range(3):
  112. test_file = os.path.join(self.repo_path, f"test{i}.txt")
  113. with open(test_file, "w") as f:
  114. f.write(f"content {i}")
  115. result, stdout, stderr = self._run_cli(
  116. "add", "test0.txt", "test1.txt", "test2.txt"
  117. )
  118. index = self.repo.open_index()
  119. self.assertIn(b"test0.txt", index)
  120. self.assertIn(b"test1.txt", index)
  121. self.assertIn(b"test2.txt", index)
  122. class RmCommandTest(DulwichCliTestCase):
  123. """Tests for rm command."""
  124. def test_rm_file(self):
  125. # Create, add and commit a file first
  126. test_file = os.path.join(self.repo_path, "test.txt")
  127. with open(test_file, "w") as f:
  128. f.write("test content")
  129. self._run_cli("add", "test.txt")
  130. self._run_cli("commit", "--message=Add test file")
  131. # Now remove it from index and working directory
  132. result, stdout, stderr = self._run_cli("rm", "test.txt")
  133. # Check that file is not in index
  134. self.assertNotIn(b"test.txt", self.repo.open_index())
  135. class CommitCommandTest(DulwichCliTestCase):
  136. """Tests for commit command."""
  137. def test_commit_basic(self):
  138. # Create and add a file
  139. test_file = os.path.join(self.repo_path, "test.txt")
  140. with open(test_file, "w") as f:
  141. f.write("test content")
  142. self._run_cli("add", "test.txt")
  143. # Commit
  144. result, stdout, stderr = self._run_cli("commit", "--message=Initial commit")
  145. # Check that HEAD points to a commit
  146. self.assertIsNotNone(self.repo.head())
  147. class LogCommandTest(DulwichCliTestCase):
  148. """Tests for log command."""
  149. def test_log_empty_repo(self):
  150. result, stdout, stderr = self._run_cli("log")
  151. # Empty repo should not crash
  152. def test_log_with_commits(self):
  153. # Create some commits
  154. c1, c2, c3 = build_commit_graph(
  155. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  156. )
  157. self.repo.refs[b"HEAD"] = c3.id
  158. result, stdout, stderr = self._run_cli("log")
  159. self.assertIn("Commit 3", stdout)
  160. self.assertIn("Commit 2", stdout)
  161. self.assertIn("Commit 1", stdout)
  162. def test_log_reverse(self):
  163. # Create some commits
  164. c1, c2, c3 = build_commit_graph(
  165. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  166. )
  167. self.repo.refs[b"HEAD"] = c3.id
  168. result, stdout, stderr = self._run_cli("log", "--reverse")
  169. # Check order - commit 1 should appear before commit 3
  170. pos1 = stdout.index("Commit 1")
  171. pos3 = stdout.index("Commit 3")
  172. self.assertLess(pos1, pos3)
  173. class StatusCommandTest(DulwichCliTestCase):
  174. """Tests for status command."""
  175. def test_status_empty(self):
  176. result, stdout, stderr = self._run_cli("status")
  177. # Should not crash on empty repo
  178. def test_status_with_untracked(self):
  179. # Create an untracked file
  180. test_file = os.path.join(self.repo_path, "untracked.txt")
  181. with open(test_file, "w") as f:
  182. f.write("untracked content")
  183. result, stdout, stderr = self._run_cli("status")
  184. self.assertIn("Untracked files:", stdout)
  185. self.assertIn("untracked.txt", stdout)
  186. class BranchCommandTest(DulwichCliTestCase):
  187. """Tests for branch command."""
  188. def test_branch_create(self):
  189. # Create initial commit
  190. test_file = os.path.join(self.repo_path, "test.txt")
  191. with open(test_file, "w") as f:
  192. f.write("test")
  193. self._run_cli("add", "test.txt")
  194. self._run_cli("commit", "--message=Initial")
  195. # Create branch
  196. result, stdout, stderr = self._run_cli("branch", "test-branch")
  197. self.assertIn(b"refs/heads/test-branch", self.repo.refs.keys())
  198. def test_branch_delete(self):
  199. # Create initial commit and branch
  200. test_file = os.path.join(self.repo_path, "test.txt")
  201. with open(test_file, "w") as f:
  202. f.write("test")
  203. self._run_cli("add", "test.txt")
  204. self._run_cli("commit", "--message=Initial")
  205. self._run_cli("branch", "test-branch")
  206. # Delete branch
  207. result, stdout, stderr = self._run_cli("branch", "-d", "test-branch")
  208. self.assertNotIn(b"refs/heads/test-branch", self.repo.refs.keys())
  209. class CheckoutCommandTest(DulwichCliTestCase):
  210. """Tests for checkout command."""
  211. def test_checkout_branch(self):
  212. # Create initial commit and branch
  213. test_file = os.path.join(self.repo_path, "test.txt")
  214. with open(test_file, "w") as f:
  215. f.write("test")
  216. self._run_cli("add", "test.txt")
  217. self._run_cli("commit", "--message=Initial")
  218. self._run_cli("branch", "test-branch")
  219. # Checkout branch
  220. result, stdout, stderr = self._run_cli("checkout", "test-branch")
  221. self.assertEqual(
  222. self.repo.refs.read_ref(b"HEAD"), b"ref: refs/heads/test-branch"
  223. )
  224. class TagCommandTest(DulwichCliTestCase):
  225. """Tests for tag command."""
  226. def test_tag_create(self):
  227. # Create initial commit
  228. test_file = os.path.join(self.repo_path, "test.txt")
  229. with open(test_file, "w") as f:
  230. f.write("test")
  231. self._run_cli("add", "test.txt")
  232. self._run_cli("commit", "--message=Initial")
  233. # Create tag
  234. result, stdout, stderr = self._run_cli("tag", "v1.0")
  235. self.assertIn(b"refs/tags/v1.0", self.repo.refs.keys())
  236. class ShowCommandTest(DulwichCliTestCase):
  237. """Tests for show command."""
  238. def test_show_commit(self):
  239. # Create a commit
  240. test_file = os.path.join(self.repo_path, "test.txt")
  241. with open(test_file, "w") as f:
  242. f.write("test content")
  243. self._run_cli("add", "test.txt")
  244. self._run_cli("commit", "--message=Test commit")
  245. result, stdout, stderr = self._run_cli("show", "HEAD")
  246. self.assertIn("Test commit", stdout)
  247. class FetchPackCommandTest(DulwichCliTestCase):
  248. """Tests for fetch-pack command."""
  249. @patch("dulwich.cli.get_transport_and_path")
  250. def test_fetch_pack_basic(self, mock_transport):
  251. # Mock the transport
  252. mock_client = MagicMock()
  253. mock_transport.return_value = (mock_client, "/path/to/repo")
  254. mock_client.fetch.return_value = None
  255. result, stdout, stderr = self._run_cli(
  256. "fetch-pack", "git://example.com/repo.git"
  257. )
  258. mock_client.fetch.assert_called_once()
  259. class LsRemoteCommandTest(DulwichCliTestCase):
  260. """Tests for ls-remote command."""
  261. def test_ls_remote_basic(self):
  262. # Create a commit
  263. test_file = os.path.join(self.repo_path, "test.txt")
  264. with open(test_file, "w") as f:
  265. f.write("test")
  266. self._run_cli("add", "test.txt")
  267. self._run_cli("commit", "--message=Initial")
  268. # Test basic ls-remote
  269. result, stdout, stderr = self._run_cli("ls-remote", self.repo_path)
  270. lines = stdout.strip().split("\n")
  271. self.assertTrue(any("HEAD" in line for line in lines))
  272. self.assertTrue(any("refs/heads/master" in line for line in lines))
  273. def test_ls_remote_symref(self):
  274. # Create a commit
  275. test_file = os.path.join(self.repo_path, "test.txt")
  276. with open(test_file, "w") as f:
  277. f.write("test")
  278. self._run_cli("add", "test.txt")
  279. self._run_cli("commit", "--message=Initial")
  280. # Test ls-remote with --symref option
  281. result, stdout, stderr = self._run_cli("ls-remote", "--symref", self.repo_path)
  282. lines = stdout.strip().split("\n")
  283. # Should show symref for HEAD in exact format: "ref: refs/heads/master\tHEAD"
  284. expected_line = "ref: refs/heads/master\tHEAD"
  285. self.assertIn(
  286. expected_line,
  287. lines,
  288. f"Expected line '{expected_line}' not found in output: {lines}",
  289. )
  290. class PullCommandTest(DulwichCliTestCase):
  291. """Tests for pull command."""
  292. @patch("dulwich.porcelain.pull")
  293. def test_pull_basic(self, mock_pull):
  294. result, stdout, stderr = self._run_cli("pull", "origin")
  295. mock_pull.assert_called_once()
  296. @patch("dulwich.porcelain.pull")
  297. def test_pull_with_refspec(self, mock_pull):
  298. result, stdout, stderr = self._run_cli("pull", "origin", "master")
  299. mock_pull.assert_called_once()
  300. class PushCommandTest(DulwichCliTestCase):
  301. """Tests for push command."""
  302. @patch("dulwich.porcelain.push")
  303. def test_push_basic(self, mock_push):
  304. result, stdout, stderr = self._run_cli("push", "origin")
  305. mock_push.assert_called_once()
  306. @patch("dulwich.porcelain.push")
  307. def test_push_force(self, mock_push):
  308. result, stdout, stderr = self._run_cli("push", "-f", "origin")
  309. mock_push.assert_called_with(".", "origin", None, force=True)
  310. class ArchiveCommandTest(DulwichCliTestCase):
  311. """Tests for archive command."""
  312. def test_archive_basic(self):
  313. # Create a commit
  314. test_file = os.path.join(self.repo_path, "test.txt")
  315. with open(test_file, "w") as f:
  316. f.write("test content")
  317. self._run_cli("add", "test.txt")
  318. self._run_cli("commit", "--message=Initial")
  319. # Archive produces binary output, so use BytesIO
  320. result, stdout, stderr = self._run_cli(
  321. "archive", "HEAD", stdout_stream=io.BytesIO()
  322. )
  323. # Should complete without error and produce some binary output
  324. self.assertIsInstance(stdout, bytes)
  325. self.assertGreater(len(stdout), 0)
  326. class ForEachRefCommandTest(DulwichCliTestCase):
  327. """Tests for for-each-ref command."""
  328. def test_for_each_ref(self):
  329. # Create a commit
  330. test_file = os.path.join(self.repo_path, "test.txt")
  331. with open(test_file, "w") as f:
  332. f.write("test")
  333. self._run_cli("add", "test.txt")
  334. self._run_cli("commit", "--message=Initial")
  335. result, stdout, stderr = self._run_cli("for-each-ref")
  336. self.assertIn("refs/heads/master", stdout)
  337. class PackRefsCommandTest(DulwichCliTestCase):
  338. """Tests for pack-refs command."""
  339. def test_pack_refs(self):
  340. # Create some refs
  341. test_file = os.path.join(self.repo_path, "test.txt")
  342. with open(test_file, "w") as f:
  343. f.write("test")
  344. self._run_cli("add", "test.txt")
  345. self._run_cli("commit", "--message=Initial")
  346. self._run_cli("branch", "test-branch")
  347. result, stdout, stderr = self._run_cli("pack-refs", "--all")
  348. # Check that packed-refs file exists
  349. self.assertTrue(
  350. os.path.exists(os.path.join(self.repo_path, ".git", "packed-refs"))
  351. )
  352. class SubmoduleCommandTest(DulwichCliTestCase):
  353. """Tests for submodule commands."""
  354. def test_submodule_list(self):
  355. # Create an initial commit so repo has a HEAD
  356. test_file = os.path.join(self.repo_path, "test.txt")
  357. with open(test_file, "w") as f:
  358. f.write("test")
  359. self._run_cli("add", "test.txt")
  360. self._run_cli("commit", "--message=Initial")
  361. result, stdout, stderr = self._run_cli("submodule")
  362. # Should not crash on repo without submodules
  363. def test_submodule_init(self):
  364. # Create .gitmodules file for init to work
  365. gitmodules = os.path.join(self.repo_path, ".gitmodules")
  366. with open(gitmodules, "w") as f:
  367. f.write("") # Empty .gitmodules file
  368. result, stdout, stderr = self._run_cli("submodule", "init")
  369. # Should not crash
  370. class StashCommandTest(DulwichCliTestCase):
  371. """Tests for stash commands."""
  372. def test_stash_list_empty(self):
  373. result, stdout, stderr = self._run_cli("stash", "list")
  374. # Should not crash on empty stash
  375. def test_stash_push_pop(self):
  376. # Create a file and modify it
  377. test_file = os.path.join(self.repo_path, "test.txt")
  378. with open(test_file, "w") as f:
  379. f.write("initial")
  380. self._run_cli("add", "test.txt")
  381. self._run_cli("commit", "--message=Initial")
  382. # Modify file
  383. with open(test_file, "w") as f:
  384. f.write("modified")
  385. # Stash changes
  386. result, stdout, stderr = self._run_cli("stash", "push")
  387. self.assertIn("Saved working directory", stdout)
  388. # Note: Dulwich stash doesn't currently update the working tree
  389. # so the file remains modified after stash push
  390. # Note: stash pop is not fully implemented in Dulwich yet
  391. # so we only test stash push here
  392. class MergeCommandTest(DulwichCliTestCase):
  393. """Tests for merge command."""
  394. def test_merge_basic(self):
  395. # Create initial commit
  396. test_file = os.path.join(self.repo_path, "test.txt")
  397. with open(test_file, "w") as f:
  398. f.write("initial")
  399. self._run_cli("add", "test.txt")
  400. self._run_cli("commit", "--message=Initial")
  401. # Create and checkout new branch
  402. self._run_cli("branch", "feature")
  403. self._run_cli("checkout", "feature")
  404. # Make changes in feature branch
  405. with open(test_file, "w") as f:
  406. f.write("feature changes")
  407. self._run_cli("add", "test.txt")
  408. self._run_cli("commit", "--message=Feature commit")
  409. # Go back to main
  410. self._run_cli("checkout", "master")
  411. # Merge feature branch
  412. result, stdout, stderr = self._run_cli("merge", "feature")
  413. class HelpCommandTest(DulwichCliTestCase):
  414. """Tests for help command."""
  415. def test_help_basic(self):
  416. result, stdout, stderr = self._run_cli("help")
  417. self.assertIn("dulwich command line tool", stdout)
  418. def test_help_all(self):
  419. result, stdout, stderr = self._run_cli("help", "-a")
  420. self.assertIn("Available commands:", stdout)
  421. self.assertIn("add", stdout)
  422. self.assertIn("commit", stdout)
  423. class RemoteCommandTest(DulwichCliTestCase):
  424. """Tests for remote commands."""
  425. def test_remote_add(self):
  426. result, stdout, stderr = self._run_cli(
  427. "remote", "add", "origin", "https://github.com/example/repo.git"
  428. )
  429. # Check remote was added to config
  430. config = self.repo.get_config()
  431. self.assertEqual(
  432. config.get((b"remote", b"origin"), b"url"),
  433. b"https://github.com/example/repo.git",
  434. )
  435. class CheckIgnoreCommandTest(DulwichCliTestCase):
  436. """Tests for check-ignore command."""
  437. def test_check_ignore(self):
  438. # Create .gitignore
  439. gitignore = os.path.join(self.repo_path, ".gitignore")
  440. with open(gitignore, "w") as f:
  441. f.write("*.log\n")
  442. result, stdout, stderr = self._run_cli("check-ignore", "test.log", "test.txt")
  443. self.assertIn("test.log", stdout)
  444. self.assertNotIn("test.txt", stdout)
  445. class LsFilesCommandTest(DulwichCliTestCase):
  446. """Tests for ls-files command."""
  447. def test_ls_files(self):
  448. # Add some files
  449. for name in ["a.txt", "b.txt", "c.txt"]:
  450. path = os.path.join(self.repo_path, name)
  451. with open(path, "w") as f:
  452. f.write(f"content of {name}")
  453. self._run_cli("add", "a.txt", "b.txt", "c.txt")
  454. result, stdout, stderr = self._run_cli("ls-files")
  455. self.assertIn("a.txt", stdout)
  456. self.assertIn("b.txt", stdout)
  457. self.assertIn("c.txt", stdout)
  458. class LsTreeCommandTest(DulwichCliTestCase):
  459. """Tests for ls-tree command."""
  460. def test_ls_tree(self):
  461. # Create a directory structure
  462. os.mkdir(os.path.join(self.repo_path, "subdir"))
  463. with open(os.path.join(self.repo_path, "file.txt"), "w") as f:
  464. f.write("file content")
  465. with open(os.path.join(self.repo_path, "subdir", "nested.txt"), "w") as f:
  466. f.write("nested content")
  467. self._run_cli("add", ".")
  468. self._run_cli("commit", "--message=Initial")
  469. result, stdout, stderr = self._run_cli("ls-tree", "HEAD")
  470. self.assertIn("file.txt", stdout)
  471. self.assertIn("subdir", stdout)
  472. def test_ls_tree_recursive(self):
  473. # Create nested structure
  474. os.mkdir(os.path.join(self.repo_path, "subdir"))
  475. with open(os.path.join(self.repo_path, "subdir", "nested.txt"), "w") as f:
  476. f.write("nested")
  477. self._run_cli("add", ".")
  478. self._run_cli("commit", "--message=Initial")
  479. result, stdout, stderr = self._run_cli("ls-tree", "-r", "HEAD")
  480. self.assertIn("subdir/nested.txt", stdout)
  481. class DescribeCommandTest(DulwichCliTestCase):
  482. """Tests for describe command."""
  483. def test_describe(self):
  484. # Create tagged commit
  485. test_file = os.path.join(self.repo_path, "test.txt")
  486. with open(test_file, "w") as f:
  487. f.write("test")
  488. self._run_cli("add", "test.txt")
  489. self._run_cli("commit", "--message=Initial")
  490. self._run_cli("tag", "v1.0")
  491. result, stdout, stderr = self._run_cli("describe")
  492. self.assertIn("v1.0", stdout)
  493. class FsckCommandTest(DulwichCliTestCase):
  494. """Tests for fsck command."""
  495. def test_fsck(self):
  496. # Create a commit
  497. test_file = os.path.join(self.repo_path, "test.txt")
  498. with open(test_file, "w") as f:
  499. f.write("test")
  500. self._run_cli("add", "test.txt")
  501. self._run_cli("commit", "--message=Initial")
  502. result, stdout, stderr = self._run_cli("fsck")
  503. # Should complete without errors
  504. class RepackCommandTest(DulwichCliTestCase):
  505. """Tests for repack command."""
  506. def test_repack(self):
  507. # Create some objects
  508. for i in range(5):
  509. test_file = os.path.join(self.repo_path, f"test{i}.txt")
  510. with open(test_file, "w") as f:
  511. f.write(f"content {i}")
  512. self._run_cli("add", f"test{i}.txt")
  513. self._run_cli("commit", f"--message=Commit {i}")
  514. result, stdout, stderr = self._run_cli("repack")
  515. # Should create pack files
  516. pack_dir = os.path.join(self.repo_path, ".git", "objects", "pack")
  517. self.assertTrue(any(f.endswith(".pack") for f in os.listdir(pack_dir)))
  518. class ResetCommandTest(DulwichCliTestCase):
  519. """Tests for reset command."""
  520. def test_reset_soft(self):
  521. # Create commits
  522. test_file = os.path.join(self.repo_path, "test.txt")
  523. with open(test_file, "w") as f:
  524. f.write("first")
  525. self._run_cli("add", "test.txt")
  526. self._run_cli("commit", "--message=First")
  527. first_commit = self.repo.head()
  528. with open(test_file, "w") as f:
  529. f.write("second")
  530. self._run_cli("add", "test.txt")
  531. self._run_cli("commit", "--message=Second")
  532. # Reset soft
  533. result, stdout, stderr = self._run_cli("reset", "--soft", first_commit.decode())
  534. # HEAD should be at first commit
  535. self.assertEqual(self.repo.head(), first_commit)
  536. class WriteTreeCommandTest(DulwichCliTestCase):
  537. """Tests for write-tree command."""
  538. def test_write_tree(self):
  539. # Create and add files
  540. test_file = os.path.join(self.repo_path, "test.txt")
  541. with open(test_file, "w") as f:
  542. f.write("test")
  543. self._run_cli("add", "test.txt")
  544. result, stdout, stderr = self._run_cli("write-tree")
  545. # Should output tree SHA
  546. self.assertEqual(len(stdout.strip()), 40)
  547. class UpdateServerInfoCommandTest(DulwichCliTestCase):
  548. """Tests for update-server-info command."""
  549. def test_update_server_info(self):
  550. result, stdout, stderr = self._run_cli("update-server-info")
  551. # Should create info/refs file
  552. info_refs = os.path.join(self.repo_path, ".git", "info", "refs")
  553. self.assertTrue(os.path.exists(info_refs))
  554. class SymbolicRefCommandTest(DulwichCliTestCase):
  555. """Tests for symbolic-ref command."""
  556. def test_symbolic_ref(self):
  557. # Create a branch
  558. test_file = os.path.join(self.repo_path, "test.txt")
  559. with open(test_file, "w") as f:
  560. f.write("test")
  561. self._run_cli("add", "test.txt")
  562. self._run_cli("commit", "--message=Initial")
  563. self._run_cli("branch", "test-branch")
  564. result, stdout, stderr = self._run_cli(
  565. "symbolic-ref", "HEAD", "refs/heads/test-branch"
  566. )
  567. # HEAD should now point to test-branch
  568. self.assertEqual(
  569. self.repo.refs.read_ref(b"HEAD"), b"ref: refs/heads/test-branch"
  570. )
  571. class FormatBytesTestCase(TestCase):
  572. """Tests for format_bytes function."""
  573. def test_bytes(self):
  574. """Test formatting bytes."""
  575. self.assertEqual("0.0 B", format_bytes(0))
  576. self.assertEqual("1.0 B", format_bytes(1))
  577. self.assertEqual("512.0 B", format_bytes(512))
  578. self.assertEqual("1023.0 B", format_bytes(1023))
  579. def test_kilobytes(self):
  580. """Test formatting kilobytes."""
  581. self.assertEqual("1.0 KB", format_bytes(1024))
  582. self.assertEqual("1.5 KB", format_bytes(1536))
  583. self.assertEqual("2.0 KB", format_bytes(2048))
  584. self.assertEqual("1023.0 KB", format_bytes(1024 * 1023))
  585. def test_megabytes(self):
  586. """Test formatting megabytes."""
  587. self.assertEqual("1.0 MB", format_bytes(1024 * 1024))
  588. self.assertEqual("1.5 MB", format_bytes(1024 * 1024 * 1.5))
  589. self.assertEqual("10.0 MB", format_bytes(1024 * 1024 * 10))
  590. self.assertEqual("1023.0 MB", format_bytes(1024 * 1024 * 1023))
  591. def test_gigabytes(self):
  592. """Test formatting gigabytes."""
  593. self.assertEqual("1.0 GB", format_bytes(1024 * 1024 * 1024))
  594. self.assertEqual("2.5 GB", format_bytes(1024 * 1024 * 1024 * 2.5))
  595. self.assertEqual("1023.0 GB", format_bytes(1024 * 1024 * 1024 * 1023))
  596. def test_terabytes(self):
  597. """Test formatting terabytes."""
  598. self.assertEqual("1.0 TB", format_bytes(1024 * 1024 * 1024 * 1024))
  599. self.assertEqual("5.0 TB", format_bytes(1024 * 1024 * 1024 * 1024 * 5))
  600. self.assertEqual("1000.0 TB", format_bytes(1024 * 1024 * 1024 * 1024 * 1000))
  601. class ParseRelativeTimeTestCase(TestCase):
  602. """Tests for parse_relative_time function."""
  603. def test_now(self):
  604. """Test parsing 'now'."""
  605. self.assertEqual(0, parse_relative_time("now"))
  606. def test_seconds(self):
  607. """Test parsing seconds."""
  608. self.assertEqual(1, parse_relative_time("1 second ago"))
  609. self.assertEqual(5, parse_relative_time("5 seconds ago"))
  610. self.assertEqual(30, parse_relative_time("30 seconds ago"))
  611. def test_minutes(self):
  612. """Test parsing minutes."""
  613. self.assertEqual(60, parse_relative_time("1 minute ago"))
  614. self.assertEqual(300, parse_relative_time("5 minutes ago"))
  615. self.assertEqual(1800, parse_relative_time("30 minutes ago"))
  616. def test_hours(self):
  617. """Test parsing hours."""
  618. self.assertEqual(3600, parse_relative_time("1 hour ago"))
  619. self.assertEqual(7200, parse_relative_time("2 hours ago"))
  620. self.assertEqual(86400, parse_relative_time("24 hours ago"))
  621. def test_days(self):
  622. """Test parsing days."""
  623. self.assertEqual(86400, parse_relative_time("1 day ago"))
  624. self.assertEqual(604800, parse_relative_time("7 days ago"))
  625. self.assertEqual(2592000, parse_relative_time("30 days ago"))
  626. def test_weeks(self):
  627. """Test parsing weeks."""
  628. self.assertEqual(604800, parse_relative_time("1 week ago"))
  629. self.assertEqual(1209600, parse_relative_time("2 weeks ago"))
  630. self.assertEqual(
  631. 36288000, parse_relative_time("60 weeks ago")
  632. ) # 60 * 7 * 24 * 60 * 60
  633. def test_invalid_format(self):
  634. """Test invalid time formats."""
  635. with self.assertRaises(ValueError) as cm:
  636. parse_relative_time("invalid")
  637. self.assertIn("Invalid relative time format", str(cm.exception))
  638. with self.assertRaises(ValueError) as cm:
  639. parse_relative_time("2 weeks")
  640. self.assertIn("Invalid relative time format", str(cm.exception))
  641. with self.assertRaises(ValueError) as cm:
  642. parse_relative_time("ago")
  643. self.assertIn("Invalid relative time format", str(cm.exception))
  644. with self.assertRaises(ValueError) as cm:
  645. parse_relative_time("two weeks ago")
  646. self.assertIn("Invalid number in relative time", str(cm.exception))
  647. def test_invalid_unit(self):
  648. """Test invalid time units."""
  649. with self.assertRaises(ValueError) as cm:
  650. parse_relative_time("5 months ago")
  651. self.assertIn("Unknown time unit: months", str(cm.exception))
  652. with self.assertRaises(ValueError) as cm:
  653. parse_relative_time("2 years ago")
  654. self.assertIn("Unknown time unit: years", str(cm.exception))
  655. def test_singular_plural(self):
  656. """Test that both singular and plural forms work."""
  657. self.assertEqual(
  658. parse_relative_time("1 second ago"), parse_relative_time("1 seconds ago")
  659. )
  660. self.assertEqual(
  661. parse_relative_time("1 minute ago"), parse_relative_time("1 minutes ago")
  662. )
  663. self.assertEqual(
  664. parse_relative_time("1 hour ago"), parse_relative_time("1 hours ago")
  665. )
  666. self.assertEqual(
  667. parse_relative_time("1 day ago"), parse_relative_time("1 days ago")
  668. )
  669. self.assertEqual(
  670. parse_relative_time("1 week ago"), parse_relative_time("1 weeks ago")
  671. )
  672. if __name__ == "__main__":
  673. unittest.main()