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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
use anyhow::Result;
use clap_complete::engine::ArgValueCompleter;
use crate::commands::Run;
use crate::completions;
use crate::providers::{BaseGap, CheckStatus, ReviewState, detect_review_provider};
use crate::style;
use crate::{git, stack};
/// Print local and remote stack status for a branch.
#[derive(Debug, clap::Args)]
pub struct Status {
/// Branch to report on (defaults to the current branch).
#[arg(add = ArgValueCompleter::new(completions::branch_candidates))]
branch: Option<String>,
}
impl Run for Status {
fn run(self) -> Result<()> {
print_status(self.branch.as_deref())
}
}
pub fn print_status(branch: Option<&str>) -> Result<()> {
let branch = branch
.map(str::to_owned)
.map_or_else(git::current_branch, Ok)?;
// Marker-aware, like every other reader: a base with a stray `stkParent`
// has no parent for any purpose, and reporting one here would produce a
// "run `git stk restack`" hint that `restack` cannot act on.
let is_base = stack::is_floor(&branch)?;
let parent = stack::stacked_parent_of(&branch)?;
let children = stack::children_of(&branch)?;
anstream::println!("branch: {}", style::paint(style::CURRENT, &branch));
// Where the branch actually lives, when that is not here. Best effort: a
// failed listing just omits the line.
if let Some(path) = git::worktree_holding(&branch).ok().flatten() {
anstream::println!("worktree: {}", git::display_path(&path));
}
match parent.as_deref() {
Some(parent) => anstream::println!("parent: {}", style::paint(style::BRANCH, parent)),
// A recorded base has no parent by design, not by omission - say which,
// so it is not read as a branch whose metadata went missing.
None if is_base => anstream::println!("parent: none (this stack's base)"),
None => anstream::println!("parent: none"),
}
if children.is_empty() {
anstream::println!("children: none");
} else {
let children: Vec<String> = children
.iter()
.map(|child| style::paint(style::BRANCH, child))
.collect();
anstream::println!("children: {}", children.join(", "));
}
// Provider state is best-effort: a repo with no remote (or no provider
// configured) still shows its local stack rather than hard-failing.
let detected = detect_review_provider().ok();
let review = match &detected {
Some((provider, review_provider)) => {
anstream::println!("provider: {} ({})", provider.kind, provider.source);
// Closed-inclusive: a review closed without merging is part of the
// branch's story, not "no review".
let review = review_provider.review_for_branch_including_closed(&branch)?;
match &review {
Some(review) => {
// The queue state, the CI rollup, and the platform stack
// in one ask. Each provider takes its own cheapest route:
// GitHub folds all three into the query `list` already
// makes, everyone else keeps the per-call path. Best
// effort throughout - a failed lookup just omits a marker.
let annotation = review_provider.annotate_review(review, false).ok();
// A queued review shows just the clock (it is waiting to
// land); otherwise the CI dot.
let queued = annotation.as_ref().is_some_and(|found| found.queued);
let marker = if queued {
crate::providers::QUEUED_MARK
} else {
annotation
.as_ref()
.map_or(CheckStatus::None, |found| found.checks)
.dot()
};
anstream::println!(
"review: {marker}{} {} {} -> {}",
review.id,
style::state(&review.state),
style::paint(style::BRANCH, &review.branch),
style::paint(style::BRANCH, &review.base)
);
anstream::println!("url: {}", style::paint(style::DIM, &review.url));
// The platform's own stack, when it holds this review -
// which is what makes GitHub, not git-stk, the one that
// merges and retargets it. Position and size come from
// whichever source answered, never one from each.
if let Some(at) = annotation.as_ref().and_then(|found| found.stack) {
anstream::println!(
"stack: {} stack {} ({} of {})",
provider.kind,
at.number,
at.position,
at.size
);
}
// A base and a local parent that disagree while the
// platform's stack can still close the gap is a chain
// part-way through unwinding, not a fault - and `submit`,
// which the warning names, refuses a review in a stack.
if let Some(parent) = parent.as_deref()
&& parent != review.base
{
// Asked only on a disagreement, which is rare: the
// lookup costs a call, and the annotation cannot
// answer it.
match review_provider.base_gap(review, parent).unwrap_or(None) {
Some(BaseGap::Platform) => anstream::println!(
"{}",
style::paint(
style::DIM,
&format!(
"review base is {}, local parent is {parent} - \
the platform retargets it as the layer below lands",
review.base
)
)
),
Some(BaseGap::Sync) => anstream::println!(
"{} review base is {}, local parent is {parent} - the \
platform moved it when {parent} landed; run `git stk sync`",
style::paint(style::WARN, "warning:"),
review.base
),
Some(BaseGap::Neither) => anstream::println!(
"{} review base is {}, local parent is {parent} - the \
platform will not move it there and refuses a change by \
hand; run `git stk unstack`, then \
`git stk submit`",
style::paint(style::WARN, "warning:"),
review.base
),
None => anstream::println!(
"{} review base is {}, local parent is {parent} - run `git stk submit`",
style::paint(style::WARN, "warning:"),
review.base
),
}
}
}
None => anstream::println!("review: none"),
}
review
}
None => {
anstream::println!("{}", style::dim("provider: not detected (no review info)"));
None
}
};
// Teach the loop: the next command, derived from review states and
// local drift. A sync covers the restack, so the nudges don't stack.
let mut hints = Vec::new();
if is_base {
hints.push(format!(
"{branch} is this stack's base, so nothing rebases, submits, or lands it - \
`git stk detach {branch}` if it should be"
));
}
match &review {
// `sync` and `cleanup` both skip a base on purpose, so the usual
// remedies can never be satisfied here. Say what actually happened
// rather than reprint a dead end every run.
Some(review)
if is_base && matches!(review.state, ReviewState::Merged | ReviewState::Closed) =>
{
hints.push(format!(
"review {} is {} - git-stk leaves a stack's base alone, so this is yours to \
finish; `git stk detach {branch}` first if it should be managed",
review.id,
style::state(&review.state)
));
}
Some(review) if review.state == ReviewState::Merged => {
hints.push(format!(
"review {} is merged - run `git stk sync`",
review.id
));
}
Some(review) if review.state == ReviewState::Closed => {
hints.push(format!(
"review {} was closed without merging - `git stk submit` opens a new review",
review.id
));
}
_ => {}
}
if let Some(parent) = parent.as_deref() {
if let Some((_, review_provider)) = &detected {
match review_provider.review_for_branch_including_closed(parent) {
Ok(Some(parent_review)) if parent_review.branch == parent => {
// `sync` skips a base before `landing_for`, so it never
// retargets a layer off one - pointing there would reprint
// every run. Name the re-root instead.
let parent_is_base = stack::is_floor(parent)?;
match parent_review.state {
// Only the states `landing_for` would have acted on:
// `Unknown(_)` covers things like GitLab's `locked`,
// which is still running, and printed nothing before.
_ if parent_is_base
&& matches!(
parent_review.state,
ReviewState::Merged | ReviewState::Closed
) =>
{
hints.push(format!(
"parent review {} is {} - {parent} is this stack's base, so \
git-stk does not retarget off it; re-root with \
`git stk adopt {branch} --parent <parent>`",
parent_review.id,
style::state(&parent_review.state)
));
}
ReviewState::Merged => hints.push(format!(
"parent review {} is merged - run `git stk sync`",
parent_review.id
)),
ReviewState::Closed => hints.push(format!(
"parent review {} was closed without merging - \
retarget with `git stk adopt {branch} --parent <parent>`",
parent_review.id
)),
_ => {}
}
}
_ => {}
}
}
if hints.is_empty()
&& let Some(hint) = stack::behind_parent_hint(&branch, parent)
{
hints.push(hint);
}
}
for hint in hints {
anstream::println!("{} {hint}", style::paint(style::HINT, "hint:"));
}
Ok(())
}