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