test_rebase.py 24 KB

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