@@ -1693,3 +1693,387 @@ fn rebase_squashed_merge_force_rebase() {
16931693
16941694 teardown_git_repo ( repo_name) ;
16951695}
1696+
1697+ #[ test]
1698+ fn rebase_continue_with_remaining_branches ( ) {
1699+ let repo_name = "rebase_continue_with_remaining_branches" ;
1700+ let repo = setup_git_repo ( repo_name) ;
1701+ let path_to_repo = generate_path_to_repo ( repo_name) ;
1702+
1703+ {
1704+ create_new_file ( & path_to_repo, "hello_world.txt" , "Hello, world!" ) ;
1705+ first_commit_all ( & repo, "first commit" ) ;
1706+ } ;
1707+
1708+ assert_eq ! ( & get_current_branch_name( & repo) , "master" ) ;
1709+
1710+ // Create branch_1
1711+ {
1712+ let branch_name = "branch_1" ;
1713+ create_branch ( & repo, branch_name) ;
1714+ checkout_branch ( & repo, branch_name) ;
1715+ create_new_file ( & path_to_repo, "file_1.txt" , "contents 1" ) ;
1716+ commit_all ( & repo, "branch 1 commit" ) ;
1717+ } ;
1718+
1719+ // Create branch_2
1720+ {
1721+ let branch_name = "branch_2" ;
1722+ create_branch ( & repo, branch_name) ;
1723+ checkout_branch ( & repo, branch_name) ;
1724+ create_new_file ( & path_to_repo, "file_2.txt" , "contents 2" ) ;
1725+ commit_all ( & repo, "branch 2 commit" ) ;
1726+ } ;
1727+
1728+ // Create branch_3
1729+ {
1730+ let branch_name = "branch_3" ;
1731+ create_branch ( & repo, branch_name) ;
1732+ checkout_branch ( & repo, branch_name) ;
1733+ create_new_file ( & path_to_repo, "file_3.txt" , "contents 3" ) ;
1734+ commit_all ( & repo, "branch 3 commit" ) ;
1735+ } ;
1736+
1737+ // Set up chain
1738+ let args: Vec < & str > = vec ! [
1739+ "setup" ,
1740+ "chain_name" ,
1741+ "master" ,
1742+ "branch_1" ,
1743+ "branch_2" ,
1744+ "branch_3" ,
1745+ ] ;
1746+ run_test_bin_expect_ok ( & path_to_repo, args) ;
1747+
1748+ // Create conflict: branch_1 modifies file_2.txt (which branch_2 also has)
1749+ {
1750+ checkout_branch ( & repo, "branch_1" ) ;
1751+ create_new_file ( & path_to_repo, "file_2.txt" , "conflict from branch 1" ) ;
1752+ commit_all ( & repo, "add conflict" ) ;
1753+ } ;
1754+
1755+ // Record original branch_3 position
1756+ let branch_3_oid_before = repo. revparse_single ( "branch_3" ) . unwrap ( ) . id ( ) . to_string ( ) ;
1757+
1758+ // Run git chain rebase — should fail on branch_2
1759+ let args: Vec < & str > = vec ! [ "rebase" ] ;
1760+ let output = run_test_bin_expect_err ( & path_to_repo, args) ;
1761+
1762+ let stderr = console:: strip_ansi_codes ( & String :: from_utf8_lossy ( & output. stderr ) )
1763+ . trim ( )
1764+ . to_string ( ) ;
1765+ println ! ( "STDERR: {}" , stderr) ;
1766+
1767+ assert ! (
1768+ stderr. contains( "Unable to completely rebase branch_2 to branch_1" ) ,
1769+ "stderr should contain rebase failure message, got: {}" ,
1770+ stderr
1771+ ) ;
1772+
1773+ // Verify state file exists
1774+ let state_file = path_to_repo. join ( ".git/chain-rebase-state.json" ) ;
1775+ assert ! (
1776+ state_file. exists( ) ,
1777+ "chain rebase state file should exist after conflict"
1778+ ) ;
1779+
1780+ // Verify branch_3 was NOT rebased yet (still at original position)
1781+ let branch_3_oid_during = repo. revparse_single ( "branch_3" ) . unwrap ( ) . id ( ) . to_string ( ) ;
1782+ assert_eq ! (
1783+ branch_3_oid_before, branch_3_oid_during,
1784+ "branch_3 should not have been rebased yet"
1785+ ) ;
1786+
1787+ // Resolve conflict and complete git-level rebase
1788+ commit_all ( & repo, "resolve conflict" ) ;
1789+ run_git_command ( & path_to_repo, vec ! [ "rebase" , "--continue" ] ) ;
1790+
1791+ assert_eq ! ( repo. state( ) , RepositoryState :: Clean ) ;
1792+
1793+ // Run git chain rebase --continue — should rebase branch_3
1794+ let args: Vec < & str > = vec ! [ "rebase" , "--continue" ] ;
1795+ let output = run_test_bin_for_rebase ( & path_to_repo, args) ;
1796+
1797+ let stdout = String :: from_utf8_lossy ( & output. stdout ) . to_string ( ) ;
1798+ println ! ( "CONTINUE STDOUT: {}" , stdout) ;
1799+
1800+ assert ! (
1801+ stdout. contains( "Continuing chain rebase" ) ,
1802+ "should show continue message, got: {}" ,
1803+ stdout
1804+ ) ;
1805+
1806+ // Verify branch_3 was rebased (position changed)
1807+ let branch_3_oid_after = repo. revparse_single ( "branch_3" ) . unwrap ( ) . id ( ) . to_string ( ) ;
1808+ assert_ne ! (
1809+ branch_3_oid_before, branch_3_oid_after,
1810+ "branch_3 should have been rebased during --continue"
1811+ ) ;
1812+
1813+ // Verify state file was cleaned up
1814+ assert ! (
1815+ !state_file. exists( ) ,
1816+ "chain rebase state file should be cleaned up after successful continue"
1817+ ) ;
1818+
1819+ // Verify we're back on the original branch (branch_1)
1820+ assert_eq ! ( & get_current_branch_name( & repo) , "branch_1" ) ;
1821+
1822+ teardown_git_repo ( repo_name) ;
1823+ }
1824+
1825+ #[ test]
1826+ fn rebase_abort_after_conflict ( ) {
1827+ let repo_name = "rebase_abort_after_conflict" ;
1828+ let repo = setup_git_repo ( repo_name) ;
1829+ let path_to_repo = generate_path_to_repo ( repo_name) ;
1830+
1831+ {
1832+ create_new_file ( & path_to_repo, "hello_world.txt" , "Hello, world!" ) ;
1833+ first_commit_all ( & repo, "first commit" ) ;
1834+ } ;
1835+
1836+ assert_eq ! ( & get_current_branch_name( & repo) , "master" ) ;
1837+
1838+ // Create branch_1
1839+ {
1840+ let branch_name = "branch_1" ;
1841+ create_branch ( & repo, branch_name) ;
1842+ checkout_branch ( & repo, branch_name) ;
1843+ create_new_file ( & path_to_repo, "file_1.txt" , "contents 1" ) ;
1844+ commit_all ( & repo, "branch 1 commit" ) ;
1845+ } ;
1846+
1847+ // Create branch_2
1848+ {
1849+ let branch_name = "branch_2" ;
1850+ create_branch ( & repo, branch_name) ;
1851+ checkout_branch ( & repo, branch_name) ;
1852+ create_new_file ( & path_to_repo, "file_2.txt" , "contents 2" ) ;
1853+ commit_all ( & repo, "branch 2 commit" ) ;
1854+ } ;
1855+
1856+ // Set up chain
1857+ let args: Vec < & str > = vec ! [ "setup" , "chain_name" , "master" , "branch_1" , "branch_2" ] ;
1858+ run_test_bin_expect_ok ( & path_to_repo, args) ;
1859+
1860+ // Create conflict: branch_1 modifies file_2.txt (which branch_2 also has)
1861+ {
1862+ checkout_branch ( & repo, "branch_1" ) ;
1863+ create_new_file ( & path_to_repo, "file_2.txt" , "conflict from branch 1" ) ;
1864+ commit_all ( & repo, "add conflict" ) ;
1865+ } ;
1866+
1867+ // Record original branch positions
1868+ let branch_2_oid_before = repo. revparse_single ( "branch_2" ) . unwrap ( ) . id ( ) . to_string ( ) ;
1869+
1870+ // Run git chain rebase — should fail on branch_2
1871+ let args: Vec < & str > = vec ! [ "rebase" ] ;
1872+ let output = run_test_bin_expect_err ( & path_to_repo, args) ;
1873+
1874+ let stderr = console:: strip_ansi_codes ( & String :: from_utf8_lossy ( & output. stderr ) )
1875+ . trim ( )
1876+ . to_string ( ) ;
1877+ assert ! (
1878+ stderr. contains( "Unable to completely rebase branch_2 to branch_1" ) ,
1879+ "stderr should contain rebase failure message, got: {}" ,
1880+ stderr
1881+ ) ;
1882+
1883+ // Verify state file exists
1884+ let state_file = path_to_repo. join ( ".git/chain-rebase-state.json" ) ;
1885+ assert ! (
1886+ state_file. exists( ) ,
1887+ "chain rebase state file should exist after conflict"
1888+ ) ;
1889+
1890+ // Run git chain rebase --abort (while git rebase is still in progress)
1891+ let args: Vec < & str > = vec ! [ "rebase" , "--abort" ] ;
1892+ let output = run_test_bin_for_rebase ( & path_to_repo, args) ;
1893+
1894+ let stdout = String :: from_utf8_lossy ( & output. stdout ) . to_string ( ) ;
1895+ println ! ( "ABORT STDOUT: {}" , stdout) ;
1896+
1897+ assert ! (
1898+ stdout. contains( "Aborted chain rebase" ) ,
1899+ "should show abort message, got: {}" ,
1900+ stdout
1901+ ) ;
1902+ assert ! (
1903+ stdout. contains( "All branches restored" ) ,
1904+ "should show restoration message, got: {}" ,
1905+ stdout
1906+ ) ;
1907+
1908+ // Verify state file was cleaned up
1909+ assert ! (
1910+ !state_file. exists( ) ,
1911+ "chain rebase state file should be cleaned up after abort"
1912+ ) ;
1913+
1914+ // Verify repo is in clean state
1915+ assert_eq ! ( repo. state( ) , RepositoryState :: Clean ) ;
1916+
1917+ // Verify we're back on the original branch
1918+ assert_eq ! ( & get_current_branch_name( & repo) , "branch_1" ) ;
1919+
1920+ // Verify branch_2 is restored to its original position
1921+ let branch_2_oid_after = repo. revparse_single ( "branch_2" ) . unwrap ( ) . id ( ) . to_string ( ) ;
1922+ assert_eq ! (
1923+ branch_2_oid_before, branch_2_oid_after,
1924+ "branch_2 should be restored to its original position after abort"
1925+ ) ;
1926+
1927+ teardown_git_repo ( repo_name) ;
1928+ }
1929+
1930+ #[ test]
1931+ fn rebase_continue_no_state ( ) {
1932+ let repo_name = "rebase_continue_no_state" ;
1933+ let repo = setup_git_repo ( repo_name) ;
1934+ let path_to_repo = generate_path_to_repo ( repo_name) ;
1935+
1936+ {
1937+ create_new_file ( & path_to_repo, "hello_world.txt" , "Hello, world!" ) ;
1938+ first_commit_all ( & repo, "first commit" ) ;
1939+ } ;
1940+
1941+ // Suppress unused variable warning
1942+ let _ = & repo;
1943+
1944+ // Try --continue with no state file — should fail
1945+ let args: Vec < & str > = vec ! [ "rebase" , "--continue" ] ;
1946+ let output = run_test_bin_expect_err ( & path_to_repo, args) ;
1947+
1948+ let stderr = console:: strip_ansi_codes ( & String :: from_utf8_lossy ( & output. stderr ) )
1949+ . trim ( )
1950+ . to_string ( ) ;
1951+ println ! ( "STDERR: {}" , stderr) ;
1952+
1953+ assert ! (
1954+ stderr. contains( "No chain rebase in progress" ) ,
1955+ "stderr should indicate no rebase in progress, got: {}" ,
1956+ stderr
1957+ ) ;
1958+
1959+ teardown_git_repo ( repo_name) ;
1960+ }
1961+
1962+ #[ test]
1963+ fn rebase_abort_no_state ( ) {
1964+ let repo_name = "rebase_abort_no_state" ;
1965+ let repo = setup_git_repo ( repo_name) ;
1966+ let path_to_repo = generate_path_to_repo ( repo_name) ;
1967+
1968+ {
1969+ create_new_file ( & path_to_repo, "hello_world.txt" , "Hello, world!" ) ;
1970+ first_commit_all ( & repo, "first commit" ) ;
1971+ } ;
1972+
1973+ // Suppress unused variable warning
1974+ let _ = & repo;
1975+
1976+ // Try --abort with no state file — should fail
1977+ let args: Vec < & str > = vec ! [ "rebase" , "--abort" ] ;
1978+ let output = run_test_bin_expect_err ( & path_to_repo, args) ;
1979+
1980+ let stderr = console:: strip_ansi_codes ( & String :: from_utf8_lossy ( & output. stderr ) )
1981+ . trim ( )
1982+ . to_string ( ) ;
1983+ println ! ( "STDERR: {}" , stderr) ;
1984+
1985+ assert ! (
1986+ stderr. contains( "No chain rebase in progress" ) ,
1987+ "stderr should indicate no rebase in progress, got: {}" ,
1988+ stderr
1989+ ) ;
1990+
1991+ teardown_git_repo ( repo_name) ;
1992+ }
1993+
1994+ #[ test]
1995+ fn rebase_blocked_when_state_exists ( ) {
1996+ let repo_name = "rebase_blocked_when_state_exists" ;
1997+ let repo = setup_git_repo ( repo_name) ;
1998+ let path_to_repo = generate_path_to_repo ( repo_name) ;
1999+
2000+ {
2001+ create_new_file ( & path_to_repo, "hello_world.txt" , "Hello, world!" ) ;
2002+ first_commit_all ( & repo, "first commit" ) ;
2003+ } ;
2004+
2005+ assert_eq ! ( & get_current_branch_name( & repo) , "master" ) ;
2006+
2007+ // Create branch_1
2008+ {
2009+ let branch_name = "branch_1" ;
2010+ create_branch ( & repo, branch_name) ;
2011+ checkout_branch ( & repo, branch_name) ;
2012+ create_new_file ( & path_to_repo, "file_1.txt" , "contents 1" ) ;
2013+ commit_all ( & repo, "branch 1 commit" ) ;
2014+ } ;
2015+
2016+ // Create branch_2
2017+ {
2018+ let branch_name = "branch_2" ;
2019+ create_branch ( & repo, branch_name) ;
2020+ checkout_branch ( & repo, branch_name) ;
2021+ create_new_file ( & path_to_repo, "file_2.txt" , "contents 2" ) ;
2022+ commit_all ( & repo, "branch 2 commit" ) ;
2023+ } ;
2024+
2025+ // Set up chain
2026+ let args: Vec < & str > = vec ! [ "setup" , "chain_name" , "master" , "branch_1" , "branch_2" ] ;
2027+ run_test_bin_expect_ok ( & path_to_repo, args) ;
2028+
2029+ // Create conflict
2030+ {
2031+ checkout_branch ( & repo, "branch_1" ) ;
2032+ create_new_file ( & path_to_repo, "file_2.txt" , "conflict from branch 1" ) ;
2033+ commit_all ( & repo, "add conflict" ) ;
2034+ } ;
2035+
2036+ // Run git chain rebase — should fail on branch_2
2037+ let args: Vec < & str > = vec ! [ "rebase" ] ;
2038+ run_test_bin_expect_err ( & path_to_repo, args) ;
2039+
2040+ // Resolve git rebase conflict
2041+ commit_all ( & repo, "resolve conflict" ) ;
2042+ run_git_command ( & path_to_repo, vec ! [ "rebase" , "--continue" ] ) ;
2043+
2044+ // Verify state file still exists
2045+ let state_file = path_to_repo. join ( ".git/chain-rebase-state.json" ) ;
2046+ assert ! ( state_file. exists( ) , "chain rebase state file should exist" ) ;
2047+
2048+ // Try to run git chain rebase again — should fail because state exists
2049+ let args: Vec < & str > = vec ! [ "rebase" ] ;
2050+ let output = run_test_bin_expect_err ( & path_to_repo, args) ;
2051+
2052+ let stderr = console:: strip_ansi_codes ( & String :: from_utf8_lossy ( & output. stderr ) )
2053+ . trim ( )
2054+ . to_string ( ) ;
2055+ println ! ( "STDERR: {}" , stderr) ;
2056+
2057+ assert ! (
2058+ stderr. contains( "chain rebase is already in progress" ) ,
2059+ "stderr should indicate existing rebase state, got: {}" ,
2060+ stderr
2061+ ) ;
2062+ assert ! (
2063+ stderr. contains( "--continue" ) && stderr. contains( "--abort" ) ,
2064+ "stderr should suggest --continue and --abort, got: {}" ,
2065+ stderr
2066+ ) ;
2067+
2068+ // Clean up by running --continue
2069+ let args: Vec < & str > = vec ! [ "rebase" , "--continue" ] ;
2070+ run_test_bin_expect_ok ( & path_to_repo, args) ;
2071+
2072+ // Verify state file is now cleaned up
2073+ assert ! (
2074+ !state_file. exists( ) ,
2075+ "chain rebase state file should be cleaned up"
2076+ ) ;
2077+
2078+ teardown_git_repo ( repo_name) ;
2079+ }
0 commit comments