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
1 change: 1 addition & 0 deletions completion/stgit.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
4 changes: 4 additions & 0 deletions src/cmd/branch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ mod describe;
mod list;
mod protect;
mod rename;
mod reset;
mod unprotect;

use anyhow::Result;
Expand Down Expand Up @@ -59,6 +60,7 @@ fn make() -> clap::Command {
"{--delete,-D} [--force] [branch]",
"--cleanup [--force] [branch]",
"{--describe,-d} <description> [branch]",
"--reset [branch]",
],
))
.subcommand(self::list::command())
Expand All @@ -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")
Expand Down Expand Up @@ -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::<BranchLocator>("branch-any") {
Expand Down
83 changes: 83 additions & 0 deletions src/cmd/branch/reset.rs
Original file line number Diff line number Diff line change
@@ -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::<crate::branchloc::BranchLocator>("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(())
}
17 changes: 17 additions & 0 deletions src/stack/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<gix::Commit<'repo>>,
prev_state: Rc<gix::Commit<'repo>>,
) -> 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
Expand Down
Loading