test_rebase.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678
  1. # test_rebase.py -- rebase tests
  2. # Copyright (C) 2025 Dulwich contributors
  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.rebase."""
  22. import importlib.util
  23. import os
  24. import tempfile
  25. from dulwich.objects import Blob, Commit, Tree
  26. from dulwich.rebase import (
  27. RebaseConflict,
  28. Rebaser,
  29. RebaseTodo,
  30. RebaseTodoCommand,
  31. RebaseTodoEntry,
  32. process_interactive_rebase,
  33. rebase,
  34. start_interactive,
  35. )
  36. from dulwich.repo import MemoryRepo, Repo
  37. from dulwich.tests.utils import make_commit
  38. from . import DependencyMissing, TestCase
  39. class RebaserTestCase(TestCase):
  40. """Tests for the Rebaser class."""
  41. def setUp(self):
  42. """Set up test repository."""
  43. super().setUp()
  44. self.repo = MemoryRepo()
  45. self.addCleanup(self.repo.close)
  46. def _setup_initial_commit(self):
  47. """Set up initial commit for tests."""
  48. # Create initial commit
  49. blob = Blob.from_string(b"Initial content\n")
  50. self.repo.object_store.add_object(blob)
  51. tree = Tree()
  52. tree.add(b"file.txt", 0o100644, blob.id)
  53. self.repo.object_store.add_object(tree)
  54. self.initial_commit = make_commit(
  55. tree=tree.id,
  56. parents=[],
  57. message=b"Initial commit",
  58. committer=b"Test User <test@example.com>",
  59. author=b"Test User <test@example.com>",
  60. commit_time=1000000,
  61. author_time=1000000,
  62. commit_timezone=0,
  63. author_timezone=0,
  64. )
  65. self.repo.object_store.add_object(self.initial_commit)
  66. # Set up branches
  67. self.repo.refs[b"refs/heads/master"] = self.initial_commit.id
  68. self.repo.refs.set_symbolic_ref(b"HEAD", b"refs/heads/master")
  69. def test_simple_rebase(self):
  70. """Test simple rebase with no conflicts."""
  71. self._setup_initial_commit()
  72. # Create feature branch with one commit
  73. feature_blob = Blob.from_string(b"Feature content\n")
  74. self.repo.object_store.add_object(feature_blob)
  75. feature_tree = Tree()
  76. feature_tree.add(b"feature.txt", 0o100644, feature_blob.id)
  77. feature_tree.add(
  78. b"file.txt", 0o100644, self.repo[self.initial_commit.tree][b"file.txt"][1]
  79. )
  80. self.repo.object_store.add_object(feature_tree)
  81. feature_commit = Commit()
  82. feature_commit.tree = feature_tree.id
  83. feature_commit.parents = [self.initial_commit.id]
  84. feature_commit.message = b"Add feature"
  85. feature_commit.committer = b"Test User <test@example.com>"
  86. feature_commit.author = b"Test User <test@example.com>"
  87. feature_commit.commit_time = 1000100
  88. feature_commit.author_time = 1000100
  89. feature_commit.commit_timezone = 0
  90. feature_commit.author_timezone = 0
  91. self.repo.object_store.add_object(feature_commit)
  92. self.repo.refs[b"refs/heads/feature"] = feature_commit.id
  93. # Create main branch advancement
  94. main_blob = Blob.from_string(b"Main advancement\n")
  95. self.repo.object_store.add_object(main_blob)
  96. main_tree = Tree()
  97. main_tree.add(b"main.txt", 0o100644, main_blob.id)
  98. main_tree.add(
  99. b"file.txt", 0o100644, self.repo[self.initial_commit.tree][b"file.txt"][1]
  100. )
  101. self.repo.object_store.add_object(main_tree)
  102. main_commit = Commit()
  103. main_commit.tree = main_tree.id
  104. main_commit.parents = [self.initial_commit.id]
  105. main_commit.message = b"Main advancement"
  106. main_commit.committer = b"Test User <test@example.com>"
  107. main_commit.author = b"Test User <test@example.com>"
  108. main_commit.commit_time = 1000200
  109. main_commit.author_time = 1000200
  110. main_commit.commit_timezone = 0
  111. main_commit.author_timezone = 0
  112. self.repo.object_store.add_object(main_commit)
  113. self.repo.refs[b"refs/heads/master"] = main_commit.id
  114. # Switch to feature branch
  115. self.repo.refs.set_symbolic_ref(b"HEAD", b"refs/heads/feature")
  116. # Check refs before rebase
  117. main_ref_before = self.repo.refs[b"refs/heads/master"]
  118. feature_ref_before = self.repo.refs[b"refs/heads/feature"]
  119. # Double check that refs are correctly set up
  120. self.assertEqual(main_ref_before, main_commit.id)
  121. self.assertEqual(feature_ref_before, feature_commit.id)
  122. # Perform rebase
  123. rebaser = Rebaser(self.repo)
  124. commits = rebaser.start(b"refs/heads/master", branch=b"refs/heads/feature")
  125. self.assertEqual(len(commits), 1)
  126. self.assertEqual(commits[0].id, feature_commit.id)
  127. # Continue rebase
  128. result = rebaser.continue_()
  129. self.assertIsNone(result) # Rebase complete
  130. # Check that feature branch was updated
  131. new_feature_head = self.repo.refs[b"refs/heads/feature"]
  132. new_commit = self.repo[new_feature_head]
  133. # Should have main commit as parent
  134. self.assertEqual(new_commit.parents, [main_commit.id])
  135. # Should have same tree as original (both files present)
  136. new_tree = self.repo[new_commit.tree]
  137. self.assertIn(b"feature.txt", new_tree)
  138. self.assertIn(b"main.txt", new_tree)
  139. self.assertIn(b"file.txt", new_tree)
  140. def test_rebase_with_conflicts(self):
  141. # Check if merge3 module is available
  142. if importlib.util.find_spec("merge3") is None:
  143. raise DependencyMissing("merge3")
  144. """Test rebase with merge conflicts."""
  145. self._setup_initial_commit()
  146. # Create feature branch with conflicting change
  147. feature_blob = Blob.from_string(b"Feature change to file\n")
  148. self.repo.object_store.add_object(feature_blob)
  149. feature_tree = Tree()
  150. feature_tree.add(b"file.txt", 0o100644, feature_blob.id)
  151. self.repo.object_store.add_object(feature_tree)
  152. feature_commit = Commit()
  153. feature_commit.tree = feature_tree.id
  154. feature_commit.parents = [self.initial_commit.id]
  155. feature_commit.message = b"Feature change"
  156. feature_commit.committer = b"Test User <test@example.com>"
  157. feature_commit.author = b"Test User <test@example.com>"
  158. feature_commit.commit_time = 1000100
  159. feature_commit.author_time = 1000100
  160. feature_commit.commit_timezone = 0
  161. feature_commit.author_timezone = 0
  162. self.repo.object_store.add_object(feature_commit)
  163. self.repo.refs[b"refs/heads/feature"] = feature_commit.id
  164. # Create main branch with conflicting change
  165. main_blob = Blob.from_string(b"Main change to file\n")
  166. self.repo.object_store.add_object(main_blob)
  167. main_tree = Tree()
  168. main_tree.add(b"file.txt", 0o100644, main_blob.id)
  169. self.repo.object_store.add_object(main_tree)
  170. main_commit = Commit()
  171. main_commit.tree = main_tree.id
  172. main_commit.parents = [self.initial_commit.id]
  173. main_commit.message = b"Main change"
  174. main_commit.committer = b"Test User <test@example.com>"
  175. main_commit.author = b"Test User <test@example.com>"
  176. main_commit.commit_time = 1000200
  177. main_commit.author_time = 1000200
  178. main_commit.commit_timezone = 0
  179. main_commit.author_timezone = 0
  180. self.repo.object_store.add_object(main_commit)
  181. self.repo.refs[b"refs/heads/master"] = main_commit.id
  182. # Attempt rebase - should fail with conflicts
  183. with self.assertRaises(RebaseConflict) as cm:
  184. rebase(self.repo, b"refs/heads/master", branch=b"refs/heads/feature")
  185. self.assertIn(b"file.txt", cm.exception.conflicted_files)
  186. def test_abort_rebase(self):
  187. """Test aborting a rebase."""
  188. self._setup_initial_commit()
  189. # Set up branches similar to simple rebase test
  190. feature_blob = Blob.from_string(b"Feature content\n")
  191. self.repo.object_store.add_object(feature_blob)
  192. feature_tree = Tree()
  193. feature_tree.add(b"feature.txt", 0o100644, feature_blob.id)
  194. feature_tree.add(
  195. b"file.txt", 0o100644, self.repo[self.initial_commit.tree][b"file.txt"][1]
  196. )
  197. self.repo.object_store.add_object(feature_tree)
  198. feature_commit = Commit()
  199. feature_commit.tree = feature_tree.id
  200. feature_commit.parents = [self.initial_commit.id]
  201. feature_commit.message = b"Add feature"
  202. feature_commit.committer = b"Test User <test@example.com>"
  203. feature_commit.author = b"Test User <test@example.com>"
  204. feature_commit.commit_time = 1000100
  205. feature_commit.author_time = 1000100
  206. feature_commit.commit_timezone = 0
  207. feature_commit.author_timezone = 0
  208. self.repo.object_store.add_object(feature_commit)
  209. self.repo.refs[b"refs/heads/feature"] = feature_commit.id
  210. self.repo.refs.set_symbolic_ref(b"HEAD", b"refs/heads/feature")
  211. # Start rebase
  212. rebaser = Rebaser(self.repo)
  213. rebaser.start(b"refs/heads/master")
  214. # Abort rebase
  215. rebaser.abort()
  216. # Check that HEAD is restored
  217. self.assertEqual(self.repo.refs.read_ref(b"HEAD"), b"ref: refs/heads/feature")
  218. self.assertEqual(self.repo.refs[b"refs/heads/feature"], feature_commit.id)
  219. # Check that REBASE_HEAD is cleaned up
  220. self.assertNotIn(b"REBASE_HEAD", self.repo.refs)
  221. def test_rebase_no_commits(self):
  222. """Test rebase when already up to date."""
  223. self._setup_initial_commit()
  224. # Both branches point to same commit
  225. self.repo.refs[b"refs/heads/feature"] = self.initial_commit.id
  226. self.repo.refs.set_symbolic_ref(b"HEAD", b"refs/heads/feature")
  227. # Perform rebase
  228. result = rebase(self.repo, b"refs/heads/master")
  229. # Should return empty list (no new commits)
  230. self.assertEqual(result, [])
  231. def test_rebase_onto(self):
  232. """Test rebase with --onto option."""
  233. self._setup_initial_commit()
  234. # Create a chain of commits: initial -> A -> B -> C
  235. blob_a = Blob.from_string(b"Commit A\n")
  236. self.repo.object_store.add_object(blob_a)
  237. tree_a = Tree()
  238. tree_a.add(b"a.txt", 0o100644, blob_a.id)
  239. tree_a.add(
  240. b"file.txt", 0o100644, self.repo[self.initial_commit.tree][b"file.txt"][1]
  241. )
  242. self.repo.object_store.add_object(tree_a)
  243. commit_a = make_commit(
  244. id=b"a" * 40,
  245. tree=tree_a.id,
  246. parents=[self.initial_commit.id],
  247. message=b"Commit A",
  248. committer=b"Test User <test@example.com>",
  249. author=b"Test User <test@example.com>",
  250. commit_time=1000100,
  251. author_time=1000100,
  252. )
  253. self.repo.object_store.add_object(commit_a)
  254. blob_b = Blob.from_string(b"Commit B\n")
  255. self.repo.object_store.add_object(blob_b)
  256. tree_b = Tree()
  257. tree_b.add(b"b.txt", 0o100644, blob_b.id)
  258. tree_b.add(b"a.txt", 0o100644, blob_a.id)
  259. tree_b.add(
  260. b"file.txt", 0o100644, self.repo[self.initial_commit.tree][b"file.txt"][1]
  261. )
  262. self.repo.object_store.add_object(tree_b)
  263. commit_b = make_commit(
  264. id=b"b" * 40,
  265. tree=tree_b.id,
  266. parents=[commit_a.id],
  267. message=b"Commit B",
  268. committer=b"Test User <test@example.com>",
  269. author=b"Test User <test@example.com>",
  270. commit_time=1000200,
  271. author_time=1000200,
  272. )
  273. self.repo.object_store.add_object(commit_b)
  274. blob_c = Blob.from_string(b"Commit C\n")
  275. self.repo.object_store.add_object(blob_c)
  276. tree_c = Tree()
  277. tree_c.add(b"c.txt", 0o100644, blob_c.id)
  278. tree_c.add(b"b.txt", 0o100644, blob_b.id)
  279. tree_c.add(b"a.txt", 0o100644, blob_a.id)
  280. tree_c.add(
  281. b"file.txt", 0o100644, self.repo[self.initial_commit.tree][b"file.txt"][1]
  282. )
  283. self.repo.object_store.add_object(tree_c)
  284. commit_c = make_commit(
  285. id=b"c" * 40,
  286. tree=tree_c.id,
  287. parents=[commit_b.id],
  288. message=b"Commit C",
  289. committer=b"Test User <test@example.com>",
  290. author=b"Test User <test@example.com>",
  291. commit_time=1000300,
  292. author_time=1000300,
  293. )
  294. self.repo.object_store.add_object(commit_c)
  295. # Create separate branch at commit A
  296. self.repo.refs[b"refs/heads/topic"] = commit_c.id
  297. self.repo.refs[b"refs/heads/newbase"] = commit_a.id
  298. self.repo.refs.set_symbolic_ref(b"HEAD", b"refs/heads/topic")
  299. # Rebase B and C onto initial commit (skipping A)
  300. rebaser = Rebaser(self.repo)
  301. commits = rebaser.start(
  302. upstream=commit_a.id,
  303. onto=self.initial_commit.id,
  304. branch=b"refs/heads/topic",
  305. )
  306. # Should rebase commits B and C
  307. self.assertEqual(len(commits), 2)
  308. self.assertEqual(commits[0].id, commit_b.id)
  309. self.assertEqual(commits[1].id, commit_c.id)
  310. # Continue rebase
  311. result = rebaser.continue_()
  312. self.assertIsNone(result)
  313. # Check result
  314. new_head = self.repo.refs[b"refs/heads/topic"]
  315. new_c = self.repo[new_head]
  316. new_b = self.repo[new_c.parents[0]]
  317. # B should now have initial commit as parent (not A)
  318. self.assertEqual(new_b.parents, [self.initial_commit.id])
  319. # Trees should still have b.txt and c.txt but not a.txt
  320. new_b_tree = self.repo[new_b.tree]
  321. self.assertIn(b"b.txt", new_b_tree)
  322. self.assertNotIn(b"a.txt", new_b_tree)
  323. new_c_tree = self.repo[new_c.tree]
  324. self.assertIn(b"c.txt", new_c_tree)
  325. self.assertIn(b"b.txt", new_c_tree)
  326. self.assertNotIn(b"a.txt", new_c_tree)
  327. class InteractiveRebaseTestCase(TestCase):
  328. """Tests for interactive rebase functionality."""
  329. def setUp(self):
  330. """Set up test repository."""
  331. super().setUp()
  332. self.repo = MemoryRepo()
  333. self.addCleanup(self.repo.close)
  334. self._setup_initial_commit()
  335. def _setup_initial_commit(self):
  336. """Set up initial commit for tests."""
  337. # Create initial commit
  338. blob = Blob.from_string(b"Initial content\n")
  339. self.repo.object_store.add_object(blob)
  340. tree = Tree()
  341. tree.add(b"file.txt", 0o100644, blob.id)
  342. self.repo.object_store.add_object(tree)
  343. self.initial_commit = make_commit(
  344. tree=tree.id,
  345. parents=[],
  346. message=b"Initial commit",
  347. committer=b"Test User <test@example.com>",
  348. author=b"Test User <test@example.com>",
  349. commit_time=1000000,
  350. author_time=1000000,
  351. commit_timezone=0,
  352. author_timezone=0,
  353. )
  354. self.repo.object_store.add_object(self.initial_commit)
  355. # Set up branches
  356. self.repo.refs[b"refs/heads/master"] = self.initial_commit.id
  357. self.repo.refs.set_symbolic_ref(b"HEAD", b"refs/heads/master")
  358. def _create_test_commits(self):
  359. """Create a series of test commits for interactive rebase."""
  360. commits = []
  361. parent = self.initial_commit.id
  362. for i in range(3):
  363. blob = Blob.from_string(f"Content {i}\n".encode())
  364. self.repo.object_store.add_object(blob)
  365. tree = Tree()
  366. tree.add(f"file{i}.txt".encode(), 0o100644, blob.id)
  367. self.repo.object_store.add_object(tree)
  368. commit = Commit()
  369. commit.tree = tree.id
  370. commit.parents = [parent]
  371. commit.message = f"Commit {i}".encode()
  372. commit.committer = b"Test User <test@example.com>"
  373. commit.author = b"Test User <test@example.com>"
  374. commit.commit_time = 1000000 + i * 100
  375. commit.author_time = 1000000 + i * 100
  376. commit.commit_timezone = 0
  377. commit.author_timezone = 0
  378. self.repo.object_store.add_object(commit)
  379. commits.append(commit)
  380. parent = commit.id
  381. self.repo.refs[b"refs/heads/feature"] = commits[-1].id
  382. return commits
  383. def test_todo_parsing(self):
  384. """Test parsing of todo file format."""
  385. todo_content = """pick 1234567 First commit
  386. reword 2345678 Second commit
  387. edit 3456789 Third commit
  388. squash 4567890 Fourth commit
  389. fixup 5678901 Fifth commit
  390. drop 6789012 Sixth commit
  391. exec echo "Running test"
  392. break
  393. # This is a comment
  394. """
  395. todo = RebaseTodo.from_string(todo_content)
  396. self.assertEqual(len(todo.entries), 8)
  397. # Check first entry
  398. self.assertEqual(todo.entries[0].command, RebaseTodoCommand.PICK)
  399. self.assertEqual(todo.entries[0].commit_sha, b"1234567")
  400. self.assertEqual(todo.entries[0].short_message, "First commit")
  401. # Check reword
  402. self.assertEqual(todo.entries[1].command, RebaseTodoCommand.REWORD)
  403. # Check exec
  404. self.assertEqual(todo.entries[6].command, RebaseTodoCommand.EXEC)
  405. self.assertEqual(todo.entries[6].arguments, 'echo "Running test"')
  406. # Check break
  407. self.assertEqual(todo.entries[7].command, RebaseTodoCommand.BREAK)
  408. def test_todo_generation(self):
  409. """Test generation of todo list from commits."""
  410. commits = self._create_test_commits()
  411. todo = RebaseTodo.from_commits(commits)
  412. # Should have one pick entry per commit
  413. self.assertEqual(len(todo.entries), 3)
  414. for i, entry in enumerate(todo.entries):
  415. self.assertEqual(entry.command, RebaseTodoCommand.PICK)
  416. # commit_sha stores the full hex SHA as bytes
  417. self.assertEqual(entry.commit_sha, commits[i].id)
  418. self.assertIn(f"Commit {i}", entry.short_message)
  419. def test_todo_serialization(self):
  420. """Test serialization of todo list."""
  421. entries = [
  422. RebaseTodoEntry(
  423. command=RebaseTodoCommand.PICK,
  424. commit_sha=b"1234567890abcdef",
  425. short_message="First commit",
  426. ),
  427. RebaseTodoEntry(
  428. command=RebaseTodoCommand.SQUASH,
  429. commit_sha=b"fedcba0987654321",
  430. short_message="Second commit",
  431. ),
  432. RebaseTodoEntry(command=RebaseTodoCommand.EXEC, arguments="make test"),
  433. ]
  434. todo = RebaseTodo(entries)
  435. content = todo.to_string(include_comments=False)
  436. lines = content.strip().split("\n")
  437. self.assertEqual(len(lines), 3)
  438. self.assertIn("pick 1234567", lines[0])
  439. self.assertIn("squash fedcba0", lines[1])
  440. self.assertIn("exec make test", lines[2])
  441. def test_start_interactive_no_editor(self):
  442. """Test starting interactive rebase without editor."""
  443. self._create_test_commits()
  444. # Start interactive rebase
  445. todo = start_interactive(
  446. self.repo,
  447. b"refs/heads/master",
  448. branch=b"refs/heads/feature",
  449. editor_callback=None,
  450. )
  451. # Should have generated todo list
  452. self.assertEqual(len(todo.entries), 3)
  453. for entry in todo.entries:
  454. self.assertEqual(entry.command, RebaseTodoCommand.PICK)
  455. def test_start_interactive_with_editor(self):
  456. """Test starting interactive rebase with editor callback."""
  457. self._create_test_commits()
  458. def mock_editor(content):
  459. # Simulate user changing pick to squash for second commit
  460. lines = content.decode().splitlines()
  461. new_lines = []
  462. for i, line in enumerate(lines):
  463. if i == 1 and line.startswith("pick"):
  464. new_lines.append(line.replace("pick", "squash"))
  465. else:
  466. new_lines.append(line)
  467. return "\n".join(new_lines).encode()
  468. todo = start_interactive(
  469. self.repo,
  470. b"refs/heads/master",
  471. branch=b"refs/heads/feature",
  472. editor_callback=mock_editor,
  473. )
  474. # Second entry should be squash
  475. self.assertEqual(todo.entries[0].command, RebaseTodoCommand.PICK)
  476. self.assertEqual(todo.entries[1].command, RebaseTodoCommand.SQUASH)
  477. self.assertEqual(todo.entries[2].command, RebaseTodoCommand.PICK)
  478. def test_process_drop_command(self):
  479. """Test processing DROP command in interactive rebase."""
  480. commits = self._create_test_commits()
  481. # Create todo with drop command
  482. entries = [
  483. RebaseTodoEntry(
  484. command=RebaseTodoCommand.PICK,
  485. commit_sha=commits[0].id,
  486. short_message="Commit 0",
  487. ),
  488. RebaseTodoEntry(
  489. command=RebaseTodoCommand.DROP,
  490. commit_sha=commits[1].id,
  491. short_message="Commit 1",
  492. ),
  493. RebaseTodoEntry(
  494. command=RebaseTodoCommand.PICK,
  495. commit_sha=commits[2].id,
  496. short_message="Commit 2",
  497. ),
  498. ]
  499. todo = RebaseTodo(entries)
  500. is_complete, pause_reason = process_interactive_rebase(self.repo, todo)
  501. # Should complete successfully
  502. self.assertTrue(is_complete)
  503. self.assertIsNone(pause_reason)
  504. # Should have only picked 2 commits (dropped one)
  505. # Note: _done list would contain the rebased commits
  506. def test_process_break_command(self):
  507. """Test processing BREAK command in interactive rebase."""
  508. commits = self._create_test_commits()
  509. entries = [
  510. RebaseTodoEntry(
  511. command=RebaseTodoCommand.PICK,
  512. commit_sha=commits[0].id,
  513. short_message="Commit 0",
  514. ),
  515. RebaseTodoEntry(command=RebaseTodoCommand.BREAK),
  516. RebaseTodoEntry(
  517. command=RebaseTodoCommand.PICK,
  518. commit_sha=commits[1].id,
  519. short_message="Commit 1",
  520. ),
  521. ]
  522. todo = RebaseTodo(entries)
  523. is_complete, pause_reason = process_interactive_rebase(self.repo, todo)
  524. # Should pause at break
  525. self.assertFalse(is_complete)
  526. self.assertEqual(pause_reason, "break")
  527. # Todo should be at position after break
  528. self.assertEqual(todo.current_index, 2)
  529. def test_process_edit_command(self):
  530. """Test processing EDIT command in interactive rebase."""
  531. commits = self._create_test_commits()
  532. entries = [
  533. RebaseTodoEntry(
  534. command=RebaseTodoCommand.PICK,
  535. commit_sha=commits[0].id,
  536. short_message="Commit 0",
  537. ),
  538. RebaseTodoEntry(
  539. command=RebaseTodoCommand.EDIT,
  540. commit_sha=commits[1].id,
  541. short_message="Commit 1",
  542. ),
  543. ]
  544. todo = RebaseTodo(entries)
  545. is_complete, pause_reason = process_interactive_rebase(self.repo, todo)
  546. # Should pause for editing
  547. self.assertFalse(is_complete)
  548. self.assertEqual(pause_reason, "edit")
  549. def test_abbreviations(self):
  550. """Test parsing abbreviated commands."""
  551. todo_content = """p 1234567 Pick
  552. r 2345678 Reword
  553. e 3456789 Edit
  554. s 4567890 Squash
  555. f 5678901 Fixup
  556. d 6789012 Drop
  557. x echo test
  558. b
  559. """
  560. todo = RebaseTodo.from_string(todo_content)
  561. self.assertEqual(todo.entries[0].command, RebaseTodoCommand.PICK)
  562. self.assertEqual(todo.entries[1].command, RebaseTodoCommand.REWORD)
  563. self.assertEqual(todo.entries[2].command, RebaseTodoCommand.EDIT)
  564. self.assertEqual(todo.entries[3].command, RebaseTodoCommand.SQUASH)
  565. self.assertEqual(todo.entries[4].command, RebaseTodoCommand.FIXUP)
  566. self.assertEqual(todo.entries[5].command, RebaseTodoCommand.DROP)
  567. self.assertEqual(todo.entries[6].command, RebaseTodoCommand.EXEC)
  568. self.assertEqual(todo.entries[7].command, RebaseTodoCommand.BREAK)