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