git_stk/commands/
repair.rs1use anyhow::Result;
2use clap::ArgAction;
3
4use crate::commands::Run;
5use crate::providers::{detect_provider, review_provider};
6use crate::style;
7use crate::{git, settings, stack};
8
9#[derive(Debug, clap::Args)]
11pub struct Repair {
12 #[arg(long, 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_provider()
56 .ok()
57 .map(|provider| (provider.kind, review_provider(provider.kind)));
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_for_branch(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 let base_valid = matches!(
82 stack::base_for_branch(branch)?,
83 Some(base) if git::is_ancestor(&base, branch).unwrap_or(false)
84 );
85 if base_valid {
86 verified += 1;
87 } else {
88 anstream::println!(
89 "{}: {} fork point from {}",
90 style::branch(branch),
91 if dry_run {
92 "would re-record"
93 } else {
94 "re-recorded"
95 },
96 style::branch(&parent)
97 );
98 if !dry_run {
99 stack::record_base(branch, &parent);
100 }
101 repaired += 1;
102 }
103 continue;
104 }
105
106 let mut found: Option<(String, String)> = None;
107 if let Some((kind, review_provider)) = &provider
108 && let Ok(Some(review)) = review_provider.review_for_branch(branch)
109 && review.branch == *branch
110 && review.base != *branch
111 {
112 if branches.contains(&review.base) {
113 found = Some((review.base.clone(), format!("{kind} review {}", review.id)));
114 } else {
115 anstream::println!(
116 "{}",
117 style::warn(&format!(
118 "{branch}: review {} targets {}, which is not a local branch",
119 review.id, review.base
120 ))
121 );
122 }
123 }
124
125 if found.is_none() {
126 match nearest_ancestor_branch(branch, &branches)? {
127 Ancestry::One(parent) => found = Some((parent, "ancestry".to_owned())),
128 Ancestry::None => {
129 anstream::println!(
130 "{}",
131 style::warn(&format!(
132 "{branch}: no parent found; attach manually with \
133 `git stk adopt {branch} --parent <parent>`"
134 ))
135 );
136 }
137 Ancestry::Ambiguous(candidates) => {
138 anstream::println!(
139 "{}",
140 style::warn(&format!(
141 "{branch}: ambiguous parent candidates ({}); attach manually with \
142 `git stk adopt`",
143 candidates.join(", ")
144 ))
145 );
146 }
147 }
148 }
149
150 match found {
151 Some((parent, source)) => {
152 anstream::println!(
153 "{}: {} parent {} {}",
154 style::branch(branch),
155 if dry_run { "would set" } else { "set" },
156 style::branch(&parent),
157 style::dim(&format!("(from {source})"))
158 );
159 if !dry_run {
160 stack::set_parent_for_branch(branch, &parent)?;
161 stack::record_base(branch, &parent);
162 }
163 repaired += 1;
164 }
165 None => unresolved += 1,
166 }
167 }
168
169 anstream::println!(
170 "{}",
171 style::success(&format!(
172 "repair complete: {repaired} {}repaired, {verified} verified, {unresolved} unresolved",
173 if dry_run { "would be " } else { "" }
174 ))
175 );
176 Ok(())
177}
178
179enum Ancestry {
180 One(String),
181 None,
182 Ambiguous(Vec<String>),
183}
184
185fn nearest_ancestor_branch(branch: &str, branches: &[String]) -> Result<Ancestry> {
188 let tip = git::rev_parse(branch)?;
189
190 let mut candidates: Vec<(String, String)> = Vec::new();
191 for other in branches {
192 if other == branch {
193 continue;
194 }
195 let other_tip = git::rev_parse(other)?;
196 if other_tip != tip && git::is_ancestor(other, branch)? {
199 candidates.push((other.clone(), other_tip));
200 }
201 }
202
203 let nearest: Vec<String> = candidates
206 .iter()
207 .filter(|(candidate, candidate_tip)| {
208 !candidates.iter().any(|(other, other_tip)| {
209 other != candidate
210 && other_tip != candidate_tip
211 && git::is_ancestor(candidate, other).unwrap_or(false)
212 })
213 })
214 .map(|(candidate, _)| candidate.clone())
215 .collect();
216
217 Ok(match nearest.len() {
218 0 => Ancestry::None,
219 1 => Ancestry::One(nearest.into_iter().next().expect("one candidate")),
220 _ => Ancestry::Ambiguous(nearest),
221 })
222}