git_stk/commands/unstack.rs
1use anyhow::{Result, bail};
2use clap::ArgAction;
3
4use crate::commands::Run;
5use crate::prompt::confirm;
6use crate::providers::detect_review_provider;
7use crate::stack;
8use crate::style;
9
10/// Dissolve the platform's own stack for the current stack, leaving its
11/// reviews open and standalone.
12///
13/// Only GitHub keeps stacks. Registering one is opt-in via `stk.githubStacks`;
14/// dissolving one is not, because a stack outlives the setting that created it
15/// and may have been made outside git-stk entirely.
16#[derive(Debug, clap::Args)]
17pub struct Unstack {
18 /// Look the stack up and print what would be dissolved, without
19 /// dissolving it.
20 #[arg(long, short = 'n', action = ArgAction::SetTrue)]
21 dry_run: bool,
22 /// Skip the confirmation prompt.
23 #[arg(long, short = 'y', action = ArgAction::SetTrue)]
24 yes: bool,
25}
26
27impl Run for Unstack {
28 fn run(self) -> Result<()> {
29 let current = crate::git::current_branch()?;
30 let line = stack::stack_line(¤t)?;
31 if line.is_empty() {
32 bail!("not on a stacked branch; nothing to unstack");
33 }
34
35 // The whole line, not just the layers git-stk records a parent for:
36 // a stack need not begin where your local line does, one made outside
37 // git-stk need not align with it at all or be adopted here, and the
38 // line's own base can be a layer of the platform's. A failed lookup is
39 // an error here, not "no stack" - this is the whole command, so
40 // answering "already gone" for an expired token would be a lie.
41 let (_, review_provider) = detect_review_provider()?;
42 let found = review_provider.native_stacks_covering(&line)?;
43 if found.is_empty() {
44 anstream::println!(
45 "{}",
46 style::dim("no platform stack recorded for this stack; nothing to dissolve")
47 );
48 return Ok(());
49 }
50
51 // What is about to happen, before any of it does. A stack is dissolved
52 // whole, several can cover one line, and the line reaches the whole
53 // subtree above you - so this can take apart reviews that are nowhere
54 // on screen. There is no undo: `undo` restores local metadata, and
55 // this is a `POST`.
56 for stack in &found {
57 anstream::println!(
58 "{} dissolve stack {} ({})",
59 if self.dry_run { "would" } else { "will" },
60 stack.number,
61 stack
62 .layers
63 .iter()
64 .map(|layer| layer.id.as_str())
65 .collect::<Vec<_>>()
66 .join(" ")
67 );
68 }
69 if self.dry_run {
70 return Ok(());
71 }
72
73 let reviews: usize = found.iter().map(|stack| stack.layers.len()).sum();
74 if !self.yes
75 && !confirm(&format!(
76 "dissolve {} stack{}, leaving {reviews} review{} standalone? [y/N] ",
77 found.len(),
78 if found.len() == 1 { "" } else { "s" },
79 if reviews == 1 { "" } else { "s" }
80 ))?
81 {
82 anstream::println!("unstack cancelled");
83 return Ok(());
84 }
85
86 // Keep going after a failure rather than leaving the rest of the line
87 // silently still stacked: which ones survived is the whole answer for
88 // a command someone reaches for to get unstuck.
89 let mut failed: Vec<(u64, anyhow::Error)> = Vec::new();
90 for stack in &found {
91 match review_provider.unstack_reviews(stack) {
92 Ok(Some(report)) => anstream::println!("{report}"),
93 Ok(None) => anstream::println!(
94 "{}",
95 style::dim("this provider does not keep stacks; nothing to dissolve")
96 ),
97 Err(error) => failed.push((stack.number, error)),
98 }
99 }
100
101 // Every failure reported the same way, and the summary - not an
102 // arbitrary one of them - is the error. Promoting one would make the
103 // headline depend on iteration order: a 404 for a stack a teammate
104 // already dissolved would outrank the expired token that is the real
105 // reason the rest did not go.
106 if !failed.is_empty() {
107 for (_, error) in &failed {
108 anstream::eprintln!("{}", style::warn(&format!("{error:#}")));
109 }
110 bail!(
111 "{} stack{} still registered: {}",
112 failed.len(),
113 if failed.len() == 1 { "" } else { "s" },
114 failed
115 .iter()
116 .map(|(number, _)| number.to_string())
117 .collect::<Vec<_>>()
118 .join(", ")
119 );
120 }
121 Ok(())
122 }
123}