Skip to content

Commit fbd6c60

Browse files
committed
feat: add stg branch --reset command to soft reset the stack marking all patches as unapplied
* Introduce alternative to `stg repair` for more manual control over stack metadata repair process. * Run `stg branch --reset` to force mark all patches in the stack as unapplied without changing HEAD. * Manually reconcile the stack state by running `stg push --merged` or `stg uncommit`.
1 parent f88c7f0 commit fbd6c60

File tree

4 files changed

+105
-0
lines changed

4 files changed

+105
-0
lines changed

completion/stgit.zsh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ _stg-branch() {
4040
{-D,--delete}':delete branch'
4141
'--cleanup:cleanup stg metadata for branch'
4242
{-d,--describe}':set branch description'
43+
'--reset:soft reset the stack marking all patches as unapplied'
4344
)
4445
switch_options=(
4546
'--merge:merge worktree changes into other branch'

src/cmd/branch/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ mod describe;
1010
mod list;
1111
mod protect;
1212
mod rename;
13+
mod reset;
1314
mod unprotect;
1415

1516
use anyhow::Result;
@@ -59,6 +60,7 @@ fn make() -> clap::Command {
5960
"{--delete,-D} [--force] [branch]",
6061
"--cleanup [--force] [branch]",
6162
"{--describe,-d} <description> [branch]",
63+
"--reset [branch]",
6264
],
6365
))
6466
.subcommand(self::list::command())
@@ -70,6 +72,7 @@ fn make() -> clap::Command {
7072
.subcommand(self::delete::command())
7173
.subcommand(self::cleanup::command())
7274
.subcommand(self::describe::command())
75+
.subcommand(self::reset::command())
7376
.arg(
7477
clap::Arg::new("merge")
7578
.long("merge")
@@ -98,6 +101,7 @@ fn run(matches: &clap::ArgMatches) -> Result<()> {
98101
"--delete" => self::delete::dispatch(&repo, submatches),
99102
"--cleanup" => self::cleanup::dispatch(&repo, submatches),
100103
"--describe" => self::describe::dispatch(&repo, submatches),
104+
"--reset" => self::reset::dispatch(&repo, submatches),
101105
s => panic!("unhandled branch subcommand {s}"),
102106
}
103107
} else if let Some(target_branch_loc) = matches.get_one::<BranchLocator>("branch-any") {

src/cmd/branch/reset.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// SPDX-License-Identifier: GPL-2.0-only
2+
3+
//! `stg branch --reset` implementation.
4+
5+
use std::rc::Rc;
6+
7+
use anyhow::{anyhow, Result};
8+
9+
use crate::{
10+
color::get_color_stdout,
11+
print_info_message,
12+
stack::{InitializationPolicy, Stack, StackAccess, StackState, StackStateAccess},
13+
};
14+
15+
pub(super) fn command() -> clap::Command {
16+
clap::Command::new("--reset")
17+
.about("Soft reset the stack marking all patches as unapplied")
18+
.long_about(
19+
"Reset the stack head to the current git HEAD and mark all patches as \
20+
unapplied.\n\
21+
\n\
22+
This command is useful when the branch has diverged from the stack state \
23+
and you want to reset the stack without losing patches. After running this \
24+
command, you can reconcile the state by hand either by iteratively running \
25+
`stg push --merged` or by scrapping the patches and starting anew with \
26+
`stg uncommit`.",
27+
)
28+
.arg(
29+
clap::Arg::new("branch-any")
30+
.help("Branch to reset (defaults to current branch)")
31+
.value_name("branch")
32+
.value_parser(clap::value_parser!(crate::branchloc::BranchLocator)),
33+
)
34+
}
35+
36+
pub(super) fn dispatch(repo: &gix::Repository, matches: &clap::ArgMatches) -> Result<()> {
37+
let stack = if let Some(branch_loc) =
38+
matches.get_one::<crate::branchloc::BranchLocator>("branch-any")
39+
{
40+
let branch = branch_loc.resolve(repo)?;
41+
Stack::from_branch(repo, branch, InitializationPolicy::RequireInitialized)?
42+
} else {
43+
Stack::current(repo, InitializationPolicy::RequireInitialized)?
44+
};
45+
46+
let config = repo.config_snapshot();
47+
if stack.is_protected(&config) {
48+
return Err(anyhow!(
49+
"this branch is protected; modification is not permitted."
50+
));
51+
}
52+
53+
if stack.get_branch_head().id == stack.head().id {
54+
print_info_message(
55+
matches,
56+
"git head already matching stack state, doing nothing",
57+
);
58+
return Ok(());
59+
}
60+
61+
stack
62+
.setup_transaction()
63+
.use_index_and_worktree(false)
64+
.with_output_stream(get_color_stdout(matches))
65+
.transact(|trans| {
66+
let commit = trans.stack().get_branch_head().to_owned();
67+
68+
let stack = trans.stack();
69+
let repo = stack.repo;
70+
let stack_state_commit = repo
71+
.find_reference(stack.get_stack_refname())?
72+
.peel_to_commit()
73+
.map(Rc::new)?;
74+
75+
let new_stack_state = StackState::from_commit(trans.stack().repo, &stack_state_commit)?
76+
.reset_branch_state(commit, stack_state_commit);
77+
78+
trans.reset_to_state(new_stack_state)
79+
})
80+
.execute("branch-reset")?;
81+
82+
Ok(())
83+
}

src/stack/state.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,23 @@ impl<'repo> StackState<'repo> {
187187
}
188188
}
189189

190+
/// Create updated state with new `head` and `prev` commits and all applied patches marked as unapplied.
191+
///
192+
/// This functionality can be used to fully reset stack state without losing any patches.
193+
pub(crate) fn reset_branch_state(
194+
self,
195+
new_head: Rc<gix::Commit<'repo>>,
196+
prev_state: Rc<gix::Commit<'repo>>,
197+
) -> Self {
198+
Self {
199+
prev: Some(prev_state),
200+
head: new_head,
201+
applied: vec![],
202+
unapplied: [self.applied, self.unapplied].concat(),
203+
..self
204+
}
205+
}
206+
190207
/// Commit stack state to repository.
191208
///
192209
/// The stack state content exists in a tree that is unrelated to the

0 commit comments

Comments
 (0)