Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion src/cli/sync/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<CommandOutcome> {
Expand Down Expand Up @@ -261,6 +265,13 @@ pub fn execute(args: SyncArgs) -> io::Result<CommandOutcome> {
}
}

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());
Expand Down Expand Up @@ -434,6 +445,7 @@ impl From<SyncArgs> for SyncOptions {
fn from(args: SyncArgs) -> Self {
Self {
continue_operation: args.continue_operation,
abort_operation: args.abort_operation,
}
}
}
Expand Down Expand Up @@ -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);
}
}
8 changes: 8 additions & 0 deletions src/core/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,14 @@ pub fn continue_rebase() -> io::Result<GitCommandOutput> {
output_to_git_command_output(output)
}

pub fn abort_rebase() -> io::Result<GitCommandOutput> {
let output = Command::new("git")
.args(["rebase", "--abort"])
.output()?;

output_to_git_command_output(output)
}

pub fn init_repository() -> io::Result<ExitStatus> {
Command::new("git").args(["init", "--quiet"]).status()
}
Expand Down
95 changes: 95 additions & 0 deletions src/core/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Comment on lines 18 to 22
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding abort_operation to SyncOptions requires updating all SyncOptions { ... } struct literals. There is at least one existing literal later in this file (in the tests) that now omits abort_operation, which will not compile. Consider either adding the field at that call site(s) or switching those literals to ..Default::default() to make future option additions non-breaking.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed — verified no other SyncOptions literals exist in the codebase besides the test, which already has the field. Added the missing abort_operation: false to the test struct literal. The other usages all go through SyncOptions::default() which handles it via the derived Default impl.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b5801c8. All SyncOptions struct literals in production and test code now include the abort_operation field. The struct derives Default, so the three call sites using SyncOptions::default() correctly get abort_operation: false. The one explicit struct literal (in the resume test) already had the field from the prior commit. The new abort tests also use explicit struct literals with both fields.


#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -185,6 +186,10 @@ pub fn run_with_reporter<F>(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);
}
Expand Down Expand Up @@ -307,6 +312,35 @@ where
}
}

fn abort_sync() -> io::Result<SyncOutcome> {
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 {
Comment on lines +315 to +325
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new --abort/abort_sync path doesn’t appear to have coverage in the existing src/core/sync.rs test module (which already exercises --continue resume behavior). Add tests for: (1) erroring when there’s no paused operation, (2) aborting an in-progress rebase, and (3) clearing operation.json on success.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Integration test coverage for abort requires a full conflicting restack setup. Added as a follow-up TODO. The core abort logic is straightforward (check pending op, abort rebase, clear operation file) and is covered by the existing operation persistence tests for the underlying primitives.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b5801c8. Added two tests for the abort path:

  1. abort_cancels_paused_sync_and_clears_pending_operation -- sets up a real conflict (divergent shared.txt on parent and child branches), verifies sync pauses with a pending operation, then calls abort and asserts: the outcome succeeds, paused is false, and the pending operation file is cleared.

  2. abort_returns_error_when_no_pending_operation -- calls abort on an initialized repo with no paused operation and verifies it returns the expected error message.

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<F>(reporter: &mut F) -> io::Result<SyncOutcome>
where
F: FnMut(SyncEvent) -> io::Result<()>,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1623,6 +1658,7 @@ mod tests {
let outcome = run_with_reporter(
&SyncOptions {
continue_operation: true,
abort_operation: false,
},
&mut |event| {
events.push(event.clone());
Expand All @@ -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")
);
});
}
}
2 changes: 1 addition & 1 deletion src/core/workflow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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'"
))
}

Expand Down