git_stk/commands/
repair.rs1use anyhow::Result;
2use clap::ArgAction;
3
4use crate::commands::Run;
5use crate::providers::{detect_review_provider, owned_review_for_branch};
6use crate::style;
7use crate::{git, settings, stack};
8
9#[derive(Debug, clap::Args)]
11pub struct Repair {
12 #[arg(long, short = 'n', action = ArgAction::SetTrue)]
14 dry_run: bool,
15 #[arg(long, action = ArgAction::SetTrue, conflicts_with = "dry_run")]
18 from_remote: bool,
19}
20
21impl Run for Repair {
22 fn run(self) -> Result<()> {
23 if self.from_remote {
24 repair_from_remote()
25 } else {
26 repair(self.dry_run)
27 }
28 }
29}
30
31fn repair_from_remote() -> Result<()> {
33 let remote = settings::remote()?;
34 let attached = stack::apply_remote_metadata(&remote)?;
35 anstream::println!(
36 "{}",
37 style::success(&format!(
38 "rebuilt {attached} branch{} from {remote}",
39 if attached == 1 { "" } else { "es" }
40 ))
41 );
42 Ok(())
43}
44
45pub fn repair(dry_run: bool) -> Result<()> {
50 let branches = git::local_branches()?;
51 let trunk = stack::trunk_branch(&branches);
52
53 let provider = detect_review_provider()
56 .ok()
57 .map(|(detected, client)| (detected.kind, client));
58
59 let mut repaired = 0;
60 let mut verified = 0;
61 let mut unresolved = 0;
62
63 for branch in &branches {
64 if Some(branch.as_str()) == trunk.as_deref() {
65 continue;
66 }
67
68 if let Some(parent) = stack::parent_of(branch)? {
69 if !branches.contains(&parent) {
70 anstream::println!(
71 "{}",
72 style::warn(&format!(
73 "{branch}: parent {parent} does not exist locally; \
74 fix with `git stk adopt` or `git stk detach {branch}`"
75 ))
76 );
77 unresolved += 1;
78 continue;
79 }
80
81 if stack::base_is_current(branch, &parent)? {
82 verified += 1;
83 } else {
84 anstream::println!(
85 "{}: {} fork point from {}",
86 style::branch(branch),
87 if dry_run {
88 "would re-record"
89 } else {
90 "re-recorded"
91 },
92 style::branch(&parent)
93 );
94 if !dry_run {
95 stack::record_base(branch, &parent);
96 }
97 repaired += 1;
98 }
99 continue;
100 }
101
102 let mut found: Option<(String, String)> = None;
103 if let Some((kind, review_provider)) = &provider
104 && let Ok(Some(review)) = owned_review_for_branch(&**review_provider, branch)
105 && review.base != *branch
106 {
107 if branches.contains(&review.base) {
108 found = Some((review.base.clone(), format!("{kind} review {}", review.id)));
109 } else {
110 anstream::println!(
111 "{}",
112 style::warn(&format!(
113 "{branch}: review {} targets {}, which is not a local branch",
114 review.id, review.base
115 ))
116 );
117 }
118 }
119
120 if found.is_none() {
121 match nearest_ancestor_branch(branch, &branches)? {
122 Ancestry::One(parent) => found = Some((parent, "ancestry".to_owned())),
123 Ancestry::None => {
124 anstream::println!(
125 "{}",
126 style::warn(&format!(
127 "{branch}: no parent found; attach manually with \
128 `git stk adopt {branch} --parent <parent>`"
129 ))
130 );
131 }
132 Ancestry::Ambiguous(candidates) => {
133 anstream::println!(
134 "{}",
135 style::warn(&format!(
136 "{branch}: ambiguous parent candidates ({}); attach manually with \
137 `git stk adopt`",
138 candidates.join(", ")
139 ))
140 );
141 }
142 }
143 }
144
145 match found {
146 Some((parent, source)) => {
147 anstream::println!(
148 "{}: {} parent {} {}",
149 style::branch(branch),
150 if dry_run { "would set" } else { "set" },
151 style::branch(&parent),
152 style::dim(&format!("(from {source})"))
153 );
154 if !dry_run {
155 stack::set_parent(branch, &parent)?;
156 stack::record_base(branch, &parent);
157 }
158 repaired += 1;
159 }
160 None => unresolved += 1,
161 }
162 }
163
164 anstream::println!(
165 "{}",
166 style::success(&format!(
167 "repair complete: {repaired} {}repaired, {verified} verified, {unresolved} unresolved",
168 if dry_run { "would be " } else { "" }
169 ))
170 );
171 Ok(())
172}
173
174enum Ancestry {
175 One(String),
176 None,
177 Ambiguous(Vec<String>),
178}
179
180fn nearest_ancestor_branch(branch: &str, branches: &[String]) -> Result<Ancestry> {
183 let tip = git::rev_parse(branch)?;
184
185 let mut candidates: Vec<(String, String)> = Vec::new();
186 for other in branches {
187 if other == branch {
188 continue;
189 }
190 let other_tip = git::rev_parse(other)?;
191 if other_tip != tip && git::is_ancestor(other, branch)? {
194 candidates.push((other.clone(), other_tip));
195 }
196 }
197
198 let nearest: Vec<String> = candidates
201 .iter()
202 .filter(|(candidate, candidate_tip)| {
203 !candidates.iter().any(|(other, other_tip)| {
204 other != candidate
205 && other_tip != candidate_tip
206 && git::is_ancestor(candidate, other).unwrap_or(false)
207 })
208 })
209 .map(|(candidate, _)| candidate.clone())
210 .collect();
211
212 Ok(match nearest.len() {
213 0 => Ancestry::None,
214 1 => Ancestry::One(nearest.into_iter().next().expect("one candidate")),
215 _ => Ancestry::Ambiguous(nearest),
216 })
217}