1pub mod args;
2mod branch;
3pub mod config;
4mod core;
5mod merge_tracker;
6mod simple_glob;
7mod subprocess;
8mod util;
9
10use std::collections::{HashMap, HashSet};
11use std::convert::TryFrom;
12
13use anyhow::{Context, Result};
14use git2::{Config as GitConfig, Error as GitError, ErrorCode, Repository};
15use log::*;
16
17use crate::args::DeleteFilter;
18use crate::branch::RemoteTrackingBranchStatus;
19pub use crate::branch::{
20 LocalBranch, Refname, RemoteBranch, RemoteBranchError, RemoteTrackingBranch,
21};
22use crate::core::{
23 get_direct_fetch_branches, get_non_tracking_local_branches,
24 get_non_upstream_remote_tracking_branches, get_remote_heads, get_tracking_branches, Classifier,
25 DirectFetchClassificationRequest, NonTrackingBranchClassificationRequest,
26 NonUpstreamBranchClassificationRequest, TrackingBranchClassificationRequest,
27};
28pub use crate::core::{ClassifiedBranch, SkipSuggestion, TrimPlan};
29use crate::merge_tracker::MergeTracker;
30pub use crate::subprocess::{ls_remote_head, remote_update, RemoteHead};
31pub use crate::util::ForceSendSync;
32
33pub struct Git {
34 pub repo: Repository,
35 pub config: GitConfig,
36}
37
38impl TryFrom<Repository> for Git {
39 type Error = GitError;
40
41 fn try_from(repo: Repository) -> Result<Self, Self::Error> {
42 let config = repo.config()?.snapshot()?;
43 Ok(Self { repo, config })
44 }
45}
46
47pub struct PlanParam<'a> {
48 pub bases: Vec<&'a str>,
49 pub protected_patterns: Vec<&'a str>,
50 pub delete: DeleteFilter,
51 pub detach: bool,
52}
53
54pub fn get_trim_plan(git: &Git, param: &PlanParam) -> Result<TrimPlan> {
55 let bases = resolve_bases(&git.repo, &git.config, ¶m.bases)?;
56 let base_upstreams: Vec<_> = bases
57 .iter()
58 .map(|b| match b {
59 BaseSpec::Local { upstream, .. } => upstream.clone(),
60 BaseSpec::Remote { remote, .. } => remote.clone(),
61 })
62 .collect();
63 trace!("bases: {:#?}", bases);
64
65 let tracking_branches = get_tracking_branches(git)?;
66 debug!("tracking_branches: {:#?}", tracking_branches);
67
68 let direct_fetch_branches = get_direct_fetch_branches(git)?;
69 debug!("direct_fetch_branches: {:#?}", direct_fetch_branches);
70
71 let non_tracking_branches = get_non_tracking_local_branches(git)?;
72 debug!("non_tracking_branches: {:#?}", non_tracking_branches);
73
74 let non_upstream_branches = get_non_upstream_remote_tracking_branches(git)?;
75 debug!("non_upstream_branches: {:#?}", non_upstream_branches);
76
77 let remote_heads = if param.delete.scan_tracking() {
78 let remotes: Vec<_> = direct_fetch_branches
79 .iter()
80 .map(|(_, r)| r.clone())
81 .collect();
82 get_remote_heads(git, &remotes)?
83 } else {
84 Vec::new()
85 };
86 debug!("remote_heads: {:#?}", remote_heads);
87
88 let merge_tracker = MergeTracker::with_base_upstreams(&git.repo, &git.config, &base_upstreams)?;
89 let mut classifier = Classifier::new(git, &merge_tracker);
90 let mut skipped = HashMap::new();
91
92 info!("Enqueue classification requests");
93 if param.delete.scan_tracking() {
94 for (local, upstream) in &tracking_branches {
95 for base in &base_upstreams {
96 classifier.queue_request(TrackingBranchClassificationRequest {
97 base,
98 local,
99 upstream: upstream.as_ref(),
100 });
101 }
102 }
103
104 for (local, remote) in &direct_fetch_branches {
105 for base in &base_upstreams {
106 classifier.queue_request_with_context(
107 DirectFetchClassificationRequest {
108 base,
109 local,
110 remote,
111 },
112 &remote_heads,
113 );
114 }
115 }
116 } else {
117 for (local, upstream) in &tracking_branches {
118 if let Some(upstream) = upstream {
119 let remote = upstream.to_remote_branch(&git.repo)?.remote;
120 let suggestion = SkipSuggestion::TrackingRemote(remote);
121 skipped.insert(local.refname.clone(), suggestion.clone());
122 skipped.insert(upstream.refname.clone(), suggestion.clone());
123 } else {
124 skipped.insert(local.refname.clone(), SkipSuggestion::Tracking);
125 }
126 }
127
128 for (local, _) in &direct_fetch_branches {
129 skipped.insert(local.refname.clone(), SkipSuggestion::Tracking);
130 }
131 }
132
133 if param.delete.scan_non_tracking_local() {
134 for base in &base_upstreams {
135 for local in &non_tracking_branches {
136 classifier.queue_request(NonTrackingBranchClassificationRequest { base, local });
137 }
138 }
139 } else {
140 for local in &non_tracking_branches {
141 skipped.insert(local.refname.clone(), SkipSuggestion::NonTracking);
142 }
143 }
144
145 for base in &base_upstreams {
146 for remote_tracking in &non_upstream_branches {
147 let remote = remote_tracking.to_remote_branch(&git.repo)?;
148 if param.delete.scan_non_upstream_remote(&remote.remote) {
149 classifier.queue_request(NonUpstreamBranchClassificationRequest {
150 base,
151 remote: remote_tracking,
152 });
153 } else {
154 let remote = remote_tracking.to_remote_branch(&git.repo)?.remote;
155 skipped.insert(
156 remote_tracking.refname.clone(),
157 SkipSuggestion::NonUpstream(remote),
158 );
159 }
160 }
161 }
162
163 let classifications = classifier.classify()?;
164
165 let mut result = TrimPlan {
166 skipped,
167 to_delete: HashSet::new(),
168 preserved: Vec::new(),
169 };
170 for classification in classifications {
171 result.to_delete.extend(classification.result);
172 }
173
174 result.preserve_bases(&git.repo, &git.config, &bases)?;
175 result.preserve_protected(&git.repo, ¶m.protected_patterns)?;
176 result.preserve_non_heads_remotes(&git.repo)?;
177 result.preserve_worktree(&git.repo)?;
178 result.apply_delete_range_filter(&git.repo, ¶m.delete)?;
179
180 if !param.detach {
181 result.adjust_not_to_detach(&git.repo)?;
182 }
183
184 Ok(result)
185}
186
187#[derive(Debug)]
188pub(crate) enum BaseSpec<'a> {
189 Local {
190 #[allow(dead_code)] pattern: &'a str,
192 local: LocalBranch,
193 upstream: RemoteTrackingBranch,
194 },
195 Remote {
196 pattern: &'a str,
197 remote: RemoteTrackingBranch,
198 },
199}
200
201impl<'a> BaseSpec<'a> {
202 fn is_local(&self, branch: &LocalBranch) -> bool {
203 matches!(self, BaseSpec::Local { local, .. } if local == branch)
204 }
205
206 fn covers_remote(&self, refname: &str) -> bool {
207 match self {
208 BaseSpec::Local { upstream, .. } if upstream.refname() == refname => true,
209 BaseSpec::Remote { remote, .. } if remote.refname() == refname => true,
210 _ => false,
211 }
212 }
213
214 fn remote_pattern(&self, refname: &str) -> Option<&str> {
215 match self {
216 BaseSpec::Remote { pattern, remote } if remote.refname() == refname => Some(pattern),
217 _ => None,
218 }
219 }
220}
221
222pub(crate) fn resolve_bases<'a>(
223 repo: &Repository,
224 config: &GitConfig,
225 bases: &[&'a str],
226) -> Result<Vec<BaseSpec<'a>>> {
227 let mut result = Vec::new();
228 for base in bases {
229 let reference = match repo.resolve_reference_from_short_name(base) {
230 Ok(reference) => reference,
231 Err(err) if err.code() == ErrorCode::NotFound => continue,
232 Err(err) => return Err(err.into()),
233 };
234
235 if reference.is_branch() {
236 let local = LocalBranch::try_from(&reference)?;
237 if let RemoteTrackingBranchStatus::Exists(upstream) =
238 local.fetch_upstream(repo, config)?
239 {
240 result.push(BaseSpec::Local {
241 pattern: base,
242 local,
243 upstream,
244 })
245 }
246 } else {
247 let remote = RemoteTrackingBranch::try_from(&reference)?;
248 result.push(BaseSpec::Remote {
249 pattern: base,
250 remote,
251 })
252 }
253 }
254
255 Ok(result)
256}
257
258pub fn delete_local_branches(
259 repo: &Repository,
260 branches: &[&LocalBranch],
261 dry_run: bool,
262) -> Result<()> {
263 if branches.is_empty() {
264 return Ok(());
265 }
266
267 let detach_to = if repo.head_detached()? {
268 None
269 } else {
270 let head = repo.head()?;
271 let head_refname = head.name().context("non-utf8 head ref name")?;
272 if branches.iter().any(|branch| branch.refname == head_refname) {
273 Some(head)
274 } else {
275 None
276 }
277 };
278
279 if let Some(head) = detach_to {
280 subprocess::checkout(repo, head, dry_run)?;
281 }
282 subprocess::branch_delete(repo, branches, dry_run)?;
283
284 Ok(())
285}
286
287pub fn delete_remote_branches(
288 repo: &Repository,
289 remote_branches: &[RemoteBranch],
290 dry_run: bool,
291) -> Result<()> {
292 if remote_branches.is_empty() {
293 return Ok(());
294 }
295 let mut per_remote = HashMap::new();
296 for remote_branch in remote_branches {
297 let entry = per_remote
298 .entry(&remote_branch.remote)
299 .or_insert_with(Vec::new);
300 entry.push(remote_branch);
301 }
302 for (remote_name, remote_refnames) in per_remote.iter() {
303 subprocess::push_delete(repo, remote_name, remote_refnames, dry_run)?;
304 }
305 Ok(())
306}