@@ -6,12 +6,27 @@ use gitbutler_edit_mode::commands::{
66} ;
77use gitbutler_operating_modes:: { EDIT_BRANCH_REF , WORKSPACE_BRANCH_REF } ;
88use gitbutler_stack:: VirtualBranchesHandle ;
9+ use std:: process:: Command ;
910use tempfile:: TempDir ;
1011
1112fn command_ctx ( folder : & str ) -> Result < ( Context , TempDir ) > {
1213 gitbutler_testsupport:: writable:: fixture ( "edit_mode.sh" , folder)
1314}
1415
16+ fn run_git ( cwd : & std:: path:: Path , args : & [ & str ] ) -> Result < String > {
17+ let output = Command :: new ( "git" ) . args ( args) . current_dir ( cwd) . output ( ) ?;
18+ if !output. status . success ( ) {
19+ anyhow:: bail!(
20+ "git command failed in {}: git {}\n stderr: {}" ,
21+ cwd. display( ) ,
22+ args. join( " " ) ,
23+ String :: from_utf8_lossy( & output. stderr)
24+ ) ;
25+ }
26+
27+ Ok ( String :: from_utf8_lossy ( & output. stdout ) . trim ( ) . to_string ( ) )
28+ }
29+
1530// Fixture:
1631// * xxx (HEAD -> gitbutler/workspace) GitButler Workspace Commit
1732// * xxx foobar
@@ -86,6 +101,8 @@ fn abort_requires_force_when_changes_were_made() -> Result<()> {
86101 drop ( repo) ;
87102
88103 std:: fs:: write ( worktree_dir. join ( "file" ) , "edited during edit mode\n " ) ?;
104+ let untracked_path = worktree_dir. join ( "new-untracked-during-edit-mode.txt" ) ;
105+ std:: fs:: write ( & untracked_path, "temporary file\n " ) ?;
89106
90107 let result = abort_and_return_to_workspace ( & mut ctx, false ) ;
91108 assert ! ( result. is_err( ) ) ;
@@ -101,6 +118,10 @@ fn abort_requires_force_when_changes_were_made() -> Result<()> {
101118 ctx. git2_repo. get( ) ?. head( ) ?. name( ) ,
102119 Some ( WORKSPACE_BRANCH_REF )
103120 ) ;
121+ assert ! (
122+ !untracked_path. exists( ) ,
123+ "forced abort should clean untracked files in non-submodule repos"
124+ ) ;
104125
105126 Ok ( ( ) )
106127}
@@ -136,3 +157,110 @@ fn save_and_return_to_workspace_preserves_submodule_worktree() -> Result<()> {
136157
137158 Ok ( ( ) )
138159}
160+
161+ #[ test]
162+ fn abort_preserves_preexisting_dirty_and_diverged_submodule_state ( ) -> Result < ( ) > {
163+ let ( mut ctx, _tempdir) = command_ctx ( "save_and_return_to_workspace_preserves_submodule_worktree" ) ?;
164+ let ( foobar, worktree_dir) = {
165+ let repo = ctx. git2_repo . get ( ) ?;
166+ let foobar = repo. head ( ) ?. peel_to_commit ( ) ?. parent ( 0 ) ?. id ( ) ;
167+ ( foobar, repo. path ( ) . parent ( ) . unwrap ( ) . to_path_buf ( ) )
168+ } ;
169+ let submodule_dir = worktree_dir. join ( "submodules/test-module" ) ;
170+
171+ run_git (
172+ & submodule_dir,
173+ & [ "config" , "user.name" , "Submodule Author" ] ,
174+ ) ?;
175+ run_git (
176+ & submodule_dir,
177+ & [ "config" , "user.email" , "submodule@example.com" ] ,
178+ ) ?;
179+ std:: fs:: write ( submodule_dir. join ( "diverged.txt" ) , "diverged commit\n " ) ?;
180+ run_git ( & submodule_dir, & [ "add" , "diverged.txt" ] ) ?;
181+ run_git ( & submodule_dir, & [ "commit" , "-m" , "local diverged commit" ] ) ?;
182+
183+ std:: fs:: write ( submodule_dir. join ( "dirty.txt" ) , "dirty worktree change\n " ) ?;
184+
185+ let baseline_submodule_head = run_git ( & submodule_dir, & [ "rev-parse" , "HEAD" ] ) ?;
186+ let baseline_submodule_status = run_git ( & submodule_dir, & [ "status" , "--porcelain" ] ) ?;
187+ let baseline_superproject_submodule_status =
188+ run_git ( & worktree_dir, & [ "status" , "--porcelain" , "submodules/test-module" ] ) ?;
189+
190+ let vb_state = VirtualBranchesHandle :: new ( ctx. project_data_dir ( ) ) ;
191+ let stacks = vb_state. list_stacks_in_workspace ( ) ?;
192+ let stack = stacks. first ( ) . unwrap ( ) ;
193+
194+ enter_edit_mode ( & mut ctx, foobar, stack. id ) ?;
195+ abort_and_return_to_workspace ( & mut ctx, true ) ?;
196+
197+ let final_submodule_head = run_git ( & submodule_dir, & [ "rev-parse" , "HEAD" ] ) ?;
198+ let final_submodule_status = run_git ( & submodule_dir, & [ "status" , "--porcelain" ] ) ?;
199+ let final_superproject_submodule_status =
200+ run_git ( & worktree_dir, & [ "status" , "--porcelain" , "submodules/test-module" ] ) ?;
201+
202+ assert_eq ! (
203+ final_submodule_head, baseline_submodule_head,
204+ "abort should preserve pre-existing submodule commit divergence"
205+ ) ;
206+ assert_eq ! (
207+ final_submodule_status, baseline_submodule_status,
208+ "abort should preserve pre-existing dirty submodule working tree state"
209+ ) ;
210+ assert_eq ! (
211+ final_superproject_submodule_status, baseline_superproject_submodule_status,
212+ "abort should restore the same superproject-visible submodule state"
213+ ) ;
214+
215+ Ok ( ( ) )
216+ }
217+
218+ #[ test]
219+ fn abort_requires_force_warns_about_submodule_and_gitlink_reversion ( ) -> Result < ( ) > {
220+ let ( mut ctx, _tempdir) = command_ctx ( "save_and_return_to_workspace_preserves_submodule_worktree" ) ?;
221+ let ( foobar, worktree_dir) = {
222+ let repo = ctx. git2_repo . get ( ) ?;
223+ let foobar = repo. head ( ) ?. peel_to_commit ( ) ?. parent ( 0 ) ?. id ( ) ;
224+ ( foobar, repo. path ( ) . parent ( ) . unwrap ( ) . to_path_buf ( ) )
225+ } ;
226+ let submodule_dir = worktree_dir. join ( "submodules/test-module" ) ;
227+
228+ let vb_state = VirtualBranchesHandle :: new ( ctx. project_data_dir ( ) ) ;
229+ let stacks = vb_state. list_stacks_in_workspace ( ) ?;
230+ let stack = stacks. first ( ) . unwrap ( ) ;
231+
232+ enter_edit_mode ( & mut ctx, foobar, stack. id ) ?;
233+
234+ run_git (
235+ & submodule_dir,
236+ & [ "config" , "user.name" , "Submodule Author" ] ,
237+ ) ?;
238+ run_git (
239+ & submodule_dir,
240+ & [ "config" , "user.email" , "submodule@example.com" ] ,
241+ ) ?;
242+ std:: fs:: write (
243+ submodule_dir. join ( "during-edit-mode-gitlink-change.txt" ) ,
244+ "changed during edit mode\n " ,
245+ ) ?;
246+ run_git ( & submodule_dir, & [ "add" , "during-edit-mode-gitlink-change.txt" ] ) ?;
247+ run_git ( & submodule_dir, & [ "commit" , "-m" , "gitlink change during edit mode" ] ) ?;
248+
249+ // Stage the submodule entry so superproject tree diff includes the gitlink change.
250+ run_git ( & worktree_dir, & [ "add" , "submodules/test-module" ] ) ?;
251+
252+ let result = abort_and_return_to_workspace ( & mut ctx, false ) ;
253+ assert ! ( result. is_err( ) ) ;
254+ let err = result. err ( ) . unwrap ( ) . to_string ( ) ;
255+ assert ! (
256+ err. contains( "including submodule or gitlink changes" ) ,
257+ "expected submodule/gitlink warning in abort error, got: {err}"
258+ ) ;
259+ assert ! (
260+ err. contains( "will revert changes made during edit mode" ) ,
261+ "expected explicit reversion warning in abort error, got: {err}"
262+ ) ;
263+
264+ Ok ( ( ) )
265+ }
266+
0 commit comments