diff --git a/completion/stgit.zsh b/completion/stgit.zsh index 3a9d4d56..b414b354 100644 --- a/completion/stgit.zsh +++ b/completion/stgit.zsh @@ -40,6 +40,7 @@ _stg-branch() { {-D,--delete}':delete branch' '--cleanup:cleanup stg metadata for branch' {-d,--describe}':set branch description' + '--reset:soft reset the stack marking all patches as unapplied' ) switch_options=( '--merge:merge worktree changes into other branch' diff --git a/src/cmd/branch/mod.rs b/src/cmd/branch/mod.rs index 1234780c..c09e7219 100644 --- a/src/cmd/branch/mod.rs +++ b/src/cmd/branch/mod.rs @@ -10,6 +10,7 @@ mod describe; mod list; mod protect; mod rename; +mod reset; mod unprotect; use anyhow::Result; @@ -59,6 +60,7 @@ fn make() -> clap::Command { "{--delete,-D} [--force] [branch]", "--cleanup [--force] [branch]", "{--describe,-d} [branch]", + "--reset [branch]", ], )) .subcommand(self::list::command()) @@ -70,6 +72,7 @@ fn make() -> clap::Command { .subcommand(self::delete::command()) .subcommand(self::cleanup::command()) .subcommand(self::describe::command()) + .subcommand(self::reset::command()) .arg( clap::Arg::new("merge") .long("merge") @@ -98,6 +101,7 @@ fn run(matches: &clap::ArgMatches) -> Result<()> { "--delete" => self::delete::dispatch(&repo, submatches), "--cleanup" => self::cleanup::dispatch(&repo, submatches), "--describe" => self::describe::dispatch(&repo, submatches), + "--reset" => self::reset::dispatch(&repo, submatches), s => panic!("unhandled branch subcommand {s}"), } } else if let Some(target_branch_loc) = matches.get_one::("branch-any") { diff --git a/src/cmd/branch/reset.rs b/src/cmd/branch/reset.rs new file mode 100644 index 00000000..3d3772fc --- /dev/null +++ b/src/cmd/branch/reset.rs @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-2.0-only + +//! `stg branch --reset` implementation. + +use std::rc::Rc; + +use anyhow::{anyhow, Result}; + +use crate::{ + color::get_color_stdout, + print_info_message, + stack::{InitializationPolicy, Stack, StackAccess, StackState, StackStateAccess}, +}; + +pub(super) fn command() -> clap::Command { + clap::Command::new("--reset") + .about("Soft reset the stack marking all patches as unapplied") + .long_about( + "Reset the stack head to the current git HEAD and mark all patches as \ + unapplied.\n\ + \n\ + This command is useful when the branch has diverged from the stack state \ + and you want to reset the stack without losing patches. After running this \ + command, you can reconcile the state by hand either by iteratively running \ + `stg push --merged` or by scrapping the patches and starting anew with \ + `stg uncommit`.", + ) + .arg( + clap::Arg::new("branch-any") + .help("Branch to reset (defaults to current branch)") + .value_name("branch") + .value_parser(clap::value_parser!(crate::branchloc::BranchLocator)), + ) +} + +pub(super) fn dispatch(repo: &gix::Repository, matches: &clap::ArgMatches) -> Result<()> { + let stack = if let Some(branch_loc) = + matches.get_one::("branch-any") + { + let branch = branch_loc.resolve(repo)?; + Stack::from_branch(repo, branch, InitializationPolicy::RequireInitialized)? + } else { + Stack::current(repo, InitializationPolicy::RequireInitialized)? + }; + + let config = repo.config_snapshot(); + if stack.is_protected(&config) { + return Err(anyhow!( + "this branch is protected; modification is not permitted." + )); + } + + if stack.get_branch_head().id == stack.head().id { + print_info_message( + matches, + "git head already matching stack state, doing nothing", + ); + return Ok(()); + } + + stack + .setup_transaction() + .use_index_and_worktree(false) + .with_output_stream(get_color_stdout(matches)) + .transact(|trans| { + let commit = trans.stack().get_branch_head().to_owned(); + + let stack = trans.stack(); + let repo = stack.repo; + let stack_state_commit = repo + .find_reference(stack.get_stack_refname())? + .peel_to_commit() + .map(Rc::new)?; + + let new_stack_state = StackState::from_commit(trans.stack().repo, &stack_state_commit)? + .reset_branch_state(commit, stack_state_commit); + + trans.reset_to_state(new_stack_state) + }) + .execute("branch-reset")?; + + Ok(()) +} diff --git a/src/stack/state.rs b/src/stack/state.rs index fd92b447..aa09b8fb 100644 --- a/src/stack/state.rs +++ b/src/stack/state.rs @@ -187,6 +187,23 @@ impl<'repo> StackState<'repo> { } } + /// Create updated state with new `head` and `prev` commits and all applied patches marked as unapplied. + /// + /// This functionality can be used to fully reset stack state without losing any patches. + pub(crate) fn reset_branch_state( + self, + new_head: Rc>, + prev_state: Rc>, + ) -> Self { + Self { + prev: Some(prev_state), + head: new_head, + applied: vec![], + unapplied: [self.applied, self.unapplied].concat(), + ..self + } + } + /// Commit stack state to repository. /// /// The stack state content exists in a tree that is unrelated to the