diff --git a/src/cli/sync/mod.rs b/src/cli/sync/mod.rs index 56da08a..d2eb188 100644 --- a/src/cli/sync/mod.rs +++ b/src/cli/sync/mod.rs @@ -19,8 +19,12 @@ use super::common; #[derive(Args, Debug, Clone, Default)] pub struct SyncArgs { /// Continue a paused restack rebase sequence - #[arg(short = 'c', long = "continue")] + #[arg(short = 'c', long = "continue", conflicts_with = "abort_operation")] pub continue_operation: bool, + + /// Abort a paused restack rebase sequence + #[arg(long = "abort", conflicts_with = "continue_operation")] + pub abort_operation: bool, } pub fn execute(args: SyncArgs) -> io::Result { @@ -261,6 +265,13 @@ pub fn execute(args: SyncArgs) -> io::Result { } } + if args.abort_operation && outcome.status.success() { + println!("Aborted paused restack operation."); + return Ok(CommandOutcome { + status: outcome.status, + }); + } + if !outcome.status.success() { if outcome.paused { common::print_restack_pause_guidance(outcome.failure_output.as_deref()); @@ -434,6 +445,7 @@ impl From for SyncOptions { fn from(args: SyncArgs) -> Self { Self { continue_operation: args.continue_operation, + abort_operation: args.abort_operation, } } } @@ -550,8 +562,21 @@ mod tests { fn converts_cli_args_into_core_sync_options() { let options = SyncOptions::from(SyncArgs { continue_operation: true, + abort_operation: false, }); assert!(options.continue_operation); + assert!(!options.abort_operation); + } + + #[test] + fn converts_abort_cli_args_into_core_sync_options() { + let options = SyncOptions::from(SyncArgs { + continue_operation: false, + abort_operation: true, + }); + + assert!(!options.continue_operation); + assert!(options.abort_operation); } } diff --git a/src/core/git.rs b/src/core/git.rs index 5be1336..c3257ec 100644 --- a/src/core/git.rs +++ b/src/core/git.rs @@ -234,6 +234,14 @@ pub fn continue_rebase() -> io::Result { output_to_git_command_output(output) } +pub fn abort_rebase() -> io::Result { + let output = Command::new("git") + .args(["rebase", "--abort"]) + .output()?; + + output_to_git_command_output(output) +} + pub fn init_repository() -> io::Result { Command::new("git").args(["init", "--quiet"]).status() } diff --git a/src/core/sync.rs b/src/core/sync.rs index 56f73fd..aa8a4da 100644 --- a/src/core/sync.rs +++ b/src/core/sync.rs @@ -18,6 +18,7 @@ use crate::core::{adopt, commit, git, merge, orphan, reparent}; #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct SyncOptions { pub continue_operation: bool, + pub abort_operation: bool, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -185,6 +186,10 @@ pub fn run_with_reporter(options: &SyncOptions, reporter: &mut F) -> io::Resu where F: FnMut(SyncEvent) -> io::Result<()>, { + if options.abort_operation { + return abort_sync(); + } + if !options.continue_operation { return run_full_sync_with_reporter(reporter); } @@ -307,6 +312,35 @@ where } } +fn abort_sync() -> io::Result { + let session = open_initialized("dagger is not initialized; run 'dgr init' first")?; + + if load_operation(&session.paths)?.is_none() { + return Err(io::Error::other("no paused dgr operation to abort")); + } + + if git::is_rebase_in_progress(&session.repo) { + let abort_output = git::abort_rebase()?; + if !abort_output.status.success() { + return Ok(SyncOutcome { + status: abort_output.status, + completion: None, + failure_output: Some(abort_output.combined_output()), + paused: true, + }); + } + } + + clear_operation(&session.paths)?; + + Ok(SyncOutcome { + status: git::success_status()?, + completion: None, + failure_output: None, + paused: false, + }) +} + fn run_full_sync_with_reporter(reporter: &mut F) -> io::Result where F: FnMut(SyncEvent) -> io::Result<()>, @@ -1346,6 +1380,7 @@ mod tests { pull_request_needs_repair, run, run_with_reporter, }; use crate::core::gh::{PullRequestState, PullRequestStatus}; + use crate::core::store::load_operation; use crate::core::test_support::{ append_file, commit_file, create_tracked_branch, git_ok, initialize_main_repo, with_temp_repo, @@ -1623,6 +1658,7 @@ mod tests { let outcome = run_with_reporter( &SyncOptions { continue_operation: true, + abort_operation: false, }, &mut |event| { events.push(event.clone()); @@ -1646,4 +1682,63 @@ mod tests { )); }); } + + #[test] + fn abort_cancels_paused_sync_and_clears_pending_operation() { + with_temp_repo("dgr-sync-abort", |repo| { + initialize_main_repo(repo); + crate::core::init::run(&crate::core::init::InitOptions::default()).unwrap(); + create_tracked_branch("feat/auth"); + commit_file(repo, "auth.txt", "auth\n", "feat: auth"); + create_tracked_branch("feat/auth-ui"); + commit_file(repo, "shared.txt", "child\n", "feat: ui"); + git_ok(repo, &["checkout", "main"]); + commit_file(repo, "shared.txt", "main\n", "feat: trunk"); + git_ok(repo, &["checkout", "feat/auth"]); + + // Trigger a conflict so sync pauses + let paused = run(&SyncOptions::default()).unwrap(); + assert!(!paused.status.success()); + assert!(paused.paused); + + // Verify a pending operation exists + let session = + crate::core::store::open_initialized("should be initialized").unwrap(); + assert!(load_operation(&session.paths).unwrap().is_some()); + + // Abort the paused sync + let outcome = run(&SyncOptions { + continue_operation: false, + abort_operation: true, + }) + .unwrap(); + + assert!(outcome.status.success()); + assert!(!outcome.paused); + + // Verify the pending operation has been cleared + assert!(load_operation(&session.paths).unwrap().is_none()); + }); + } + + #[test] + fn abort_returns_error_when_no_pending_operation() { + with_temp_repo("dgr-sync-abort-no-op", |repo| { + initialize_main_repo(repo); + crate::core::init::run(&crate::core::init::InitOptions::default()).unwrap(); + + let result = run(&SyncOptions { + continue_operation: false, + abort_operation: true, + }); + + assert!(result.is_err()); + assert!( + result + .unwrap_err() + .to_string() + .contains("no paused dgr operation to abort") + ); + }); + } } diff --git a/src/core/workflow.rs b/src/core/workflow.rs index f6018a5..7c64e70 100644 --- a/src/core/workflow.rs +++ b/src/core/workflow.rs @@ -74,7 +74,7 @@ pub(crate) fn ensure_no_pending_operation( fn pending_operation_error(command_name: &str, paused_origin: &str) -> io::Error { io::Error::other(format!( - "dgr {command_name} cannot run while a dgr {paused_origin} operation is paused; run 'dgr sync --continue'" + "dgr {command_name} cannot run while a dgr {paused_origin} operation is paused; run 'dgr sync --continue' or 'dgr sync --abort'" )) }