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, stack};
8
9#[derive(Debug, clap::Args)]
11pub struct Repair {
12 #[arg(long, action = ArgAction::SetTrue)]
14 dry_run: bool,
15}
16
17impl Run for Repair {
18 fn run(self) -> Result<()> {
19 repair(self.dry_run)
20 }
21}
22
23pub fn repair(dry_run: bool) -> Result<()> {
28 let branches = git::local_branches()?;
29 let trunk = stack::trunk_branch(&branches);
30
31 let provider = detect_provider()
34 .ok()
35 .map(|provider| (provider.kind, review_provider(provider.kind)));
36
37 let mut repaired = 0;
38 let mut verified = 0;
39 let mut unresolved = 0;
40
41 for branch in &branches {
42 if Some(branch.as_str()) == trunk.as_deref() {
43 continue;
44 }
45
46 if let Some(parent) = stack::parent_for_branch(branch)? {
47 if !branches.contains(&parent) {
48 anstream::println!(
49 "{}",
50 style::warn(&format!(
51 "{branch}: parent {parent} does not exist locally; \
52 fix with `git stk adopt` or `git stk detach {branch}`"
53 ))
54 );
55 unresolved += 1;
56 continue;
57 }
58
59 let base_valid = matches!(
60 stack::base_for_branch(branch)?,
61 Some(base) if git::is_ancestor(&base, branch).unwrap_or(false)
62 );
63 if base_valid {
64 verified += 1;
65 } else {
66 anstream::println!(
67 "{}: {} fork point from {}",
68 style::branch(branch),
69 if dry_run {
70 "would re-record"
71 } else {
72 "re-recorded"
73 },
74 style::branch(&parent)
75 );
76 if !dry_run {
77 stack::record_base(branch, &parent);
78 }
79 repaired += 1;
80 }
81 continue;
82 }
83
84 let mut found: Option<(String, String)> = None;
85 if let Some((kind, review_provider)) = &provider
86 && let Ok(Some(review)) = review_provider.review_for_branch(branch)
87 && review.branch == *branch
88 && review.base != *branch
89 {
90 if branches.contains(&review.base) {
91 found = Some((review.base.clone(), format!("{kind} review {}", review.id)));
92 } else {
93 anstream::println!(
94 "{}",
95 style::warn(&format!(
96 "{branch}: review {} targets {}, which is not a local branch",
97 review.id, review.base
98 ))
99 );
100 }
101 }
102
103 if found.is_none() {
104 match nearest_ancestor_branch(branch, &branches)? {
105 Ancestry::One(parent) => found = Some((parent, "ancestry".to_owned())),
106 Ancestry::None => {
107 anstream::println!(
108 "{}",
109 style::warn(&format!(
110 "{branch}: no parent found; attach manually with \
111 `git stk adopt {branch} --parent <parent>`"
112 ))
113 );
114 }
115 Ancestry::Ambiguous(candidates) => {
116 anstream::println!(
117 "{}",
118 style::warn(&format!(
119 "{branch}: ambiguous parent candidates ({}); attach manually with \
120 `git stk adopt`",
121 candidates.join(", ")
122 ))
123 );
124 }
125 }
126 }
127
128 match found {
129 Some((parent, source)) => {
130 anstream::println!(
131 "{}: {} parent {} {}",
132 style::branch(branch),
133 if dry_run { "would set" } else { "set" },
134 style::branch(&parent),
135 style::dim(&format!("(from {source})"))
136 );
137 if !dry_run {
138 stack::set_parent_for_branch(branch, &parent)?;
139 stack::record_base(branch, &parent);
140 }
141 repaired += 1;
142 }
143 None => unresolved += 1,
144 }
145 }
146
147 anstream::println!(
148 "{}",
149 style::success(&format!(
150 "repair complete: {repaired} {}repaired, {verified} verified, {unresolved} unresolved",
151 if dry_run { "would be " } else { "" }
152 ))
153 );
154 Ok(())
155}
156
157enum Ancestry {
158 One(String),
159 None,
160 Ambiguous(Vec<String>),
161}
162
163fn nearest_ancestor_branch(branch: &str, branches: &[String]) -> Result<Ancestry> {
166 let tip = git::rev_parse(branch)?;
167
168 let mut candidates: Vec<(String, String)> = Vec::new();
169 for other in branches {
170 if other == branch {
171 continue;
172 }
173 let other_tip = git::rev_parse(other)?;
174 if other_tip != tip && git::is_ancestor(other, branch)? {
177 candidates.push((other.clone(), other_tip));
178 }
179 }
180
181 let nearest: Vec<String> = candidates
184 .iter()
185 .filter(|(candidate, candidate_tip)| {
186 !candidates.iter().any(|(other, other_tip)| {
187 other != candidate
188 && other_tip != candidate_tip
189 && git::is_ancestor(candidate, other).unwrap_or(false)
190 })
191 })
192 .map(|(candidate, _)| candidate.clone())
193 .collect();
194
195 Ok(match nearest.len() {
196 0 => Ancestry::None,
197 1 => Ancestry::One(nearest.into_iter().next().expect("one candidate")),
198 _ => Ancestry::Ambiguous(nearest),
199 })
200}