1use std::collections::{HashMap, HashSet};
2use std::convert::TryFrom;
3use std::fmt::Debug;
4
5use anyhow::{Context, Result};
6use crossbeam_channel::unbounded;
7use git2::{BranchType, Config, Repository};
8use log::*;
9use rayon::prelude::*;
10
11use crate::args::DeleteFilter;
12use crate::branch::{
13 LocalBranch, Refname, RemoteBranch, RemoteTrackingBranch, RemoteTrackingBranchStatus,
14};
15use crate::merge_tracker::MergeTracker;
16use crate::subprocess::{self, get_worktrees, RemoteHead};
17use crate::util::ForceSendSync;
18use crate::{config, BaseSpec, Git};
19
20pub struct TrimPlan {
21 pub skipped: HashMap<String, SkipSuggestion>,
22 pub to_delete: HashSet<ClassifiedBranch>,
23 pub preserved: Vec<Preserved>,
24}
25
26pub struct Preserved {
27 pub branch: ClassifiedBranch,
28 pub reason: String,
29 pub base: bool,
30}
31
32impl TrimPlan {
33 pub fn locals_to_delete(&self) -> Vec<&LocalBranch> {
34 let mut result = Vec::new();
35 for branch in &self.to_delete {
36 if let Some(local) = branch.local() {
37 result.push(local)
38 }
39 }
40 result
41 }
42
43 pub fn remotes_to_delete(&self, repo: &Repository) -> Result<Vec<RemoteBranch>> {
44 let mut result = Vec::new();
45 for branch in &self.to_delete {
46 if let Some(remote) = branch.remote(repo)? {
47 result.push(remote);
48 }
49 }
50 Ok(result)
51 }
52}
53
54impl TrimPlan {
55 pub(crate) fn preserve_bases(
56 &mut self,
57 repo: &Repository,
58 config: &Config,
59 base_specs: &[BaseSpec],
60 ) -> Result<()> {
61 fn local_is_or_tracks_base(
62 repo: &Repository,
63 config: &Config,
64 base_specs: &[BaseSpec],
65 local: &LocalBranch,
66 ) -> Result<Option<String>> {
67 if base_specs.iter().any(|spec| spec.is_local(local)) {
68 return Ok(Some("base".to_owned()));
69 }
70
71 match local.fetch_upstream(repo, config)? {
72 RemoteTrackingBranchStatus::Exists(upstream) => {
73 if let Some(pattern) = base_specs
74 .iter()
75 .find_map(|spec| spec.remote_pattern(upstream.refname()))
76 {
77 return Ok(Some(format!("tracks base `{}`", pattern)));
78 }
79 }
80 RemoteTrackingBranchStatus::Gone(upstream) => {
81 if let Some(pattern) = base_specs
82 .iter()
83 .find_map(|spec| spec.remote_pattern(&upstream))
84 {
85 return Ok(Some(format!("tracked base `{}`", pattern)));
86 }
87 }
88 _ => {}
89 }
90 Ok(None)
91 }
92
93 let mut preserve = Vec::new();
94 for branch in &self.to_delete {
95 match &branch {
96 ClassifiedBranch::MergedLocal(local)
97 | ClassifiedBranch::Stray(local)
98 | ClassifiedBranch::MergedDirectFetch { local, .. }
99 | ClassifiedBranch::DivergedDirectFetch { local, .. }
100 | ClassifiedBranch::MergedNonTrackingLocal(local) => {
101 if let Some(reason) = local_is_or_tracks_base(repo, config, base_specs, local)?
102 {
103 preserve.push(Preserved {
104 branch: branch.clone(),
105 reason,
106 base: true,
107 });
108 continue;
109 }
110 }
111 ClassifiedBranch::MergedRemoteTracking(upstream)
112 | ClassifiedBranch::MergedNonUpstreamRemoteTracking(upstream) => {
113 if base_specs
114 .iter()
115 .any(|spec| spec.covers_remote(upstream.refname()))
116 {
117 preserve.push(Preserved {
118 branch: branch.clone(),
119 reason: "base".to_owned(),
120 base: true,
121 });
122 continue;
123 }
124 }
125 ClassifiedBranch::DivergedRemoteTracking { local, upstream } => {
126 if let Some(reason) = local_is_or_tracks_base(repo, config, base_specs, local)?
127 {
128 preserve.push(Preserved {
129 branch: branch.clone(),
130 reason,
131 base: true,
132 });
133 continue;
134 } else if base_specs
135 .iter()
136 .any(|spec| spec.covers_remote(upstream.refname()))
137 {
138 preserve.push(Preserved {
139 branch: branch.clone(),
140 reason: "base".to_owned(),
141 base: true,
142 });
143 continue;
144 }
145 }
146 };
147 }
148
149 for preserved in &preserve {
150 self.to_delete.remove(&preserved.branch);
151 }
152 self.preserved.extend(preserve);
153
154 Ok(())
155 }
156
157 pub fn preserve_protected(
158 &mut self,
159 repo: &Repository,
160 preserved_patterns: &[&str],
161 ) -> Result<()> {
162 let mut preserve = Vec::new();
163 for branch in &self.to_delete {
164 let pattern =
165 match &branch {
166 ClassifiedBranch::MergedLocal(local)
167 | ClassifiedBranch::Stray(local)
168 | ClassifiedBranch::MergedDirectFetch { local, .. }
169 | ClassifiedBranch::DivergedDirectFetch { local, .. }
170 | ClassifiedBranch::MergedNonTrackingLocal(local) => {
171 get_protect_pattern(repo, preserved_patterns, local)?
172 }
173 ClassifiedBranch::MergedRemoteTracking(upstream)
174 | ClassifiedBranch::MergedNonUpstreamRemoteTracking(upstream) => {
175 get_protect_pattern(repo, preserved_patterns, upstream)?
176 }
177 ClassifiedBranch::DivergedRemoteTracking { local, upstream } => {
178 get_protect_pattern(repo, preserved_patterns, local)?
179 .or(get_protect_pattern(repo, preserved_patterns, upstream)?)
180 }
181 };
182
183 if let Some(pattern) = pattern {
184 preserve.push(Preserved {
185 branch: branch.clone(),
186 reason: format!("protected by a pattern `{}`", pattern),
187 base: false,
188 });
189 }
190 }
191
192 for preserved in &preserve {
193 self.to_delete.remove(&preserved.branch);
194 }
195 self.preserved.extend(preserve);
196
197 Ok(())
198 }
199
200 pub fn preserve_non_heads_remotes(&mut self, repo: &Repository) -> Result<()> {
203 let mut preserve = Vec::new();
204
205 for branch in &self.to_delete {
206 let remote = if let Some(remote) = branch.remote(repo)? {
207 remote
208 } else {
209 continue;
210 };
211
212 if !remote.refname.starts_with("refs/heads/") {
213 trace!("filter-out: remote ref {}", remote);
214 preserve.push(Preserved {
215 branch: branch.clone(),
216 reason: "a non-heads remote".to_owned(),
217 base: false,
218 });
219 }
220 }
221
222 for preserved in &preserve {
223 self.to_delete.remove(&preserved.branch);
224 }
225 self.preserved.extend(preserve);
226
227 Ok(())
228 }
229
230 pub fn preserve_worktree(&mut self, repo: &Repository) -> Result<()> {
231 let worktrees = get_worktrees(repo)?;
232 let mut preserve = Vec::new();
233 for branch in &self.to_delete {
234 let local = if let Some(local) = branch.local() {
235 local
236 } else {
237 continue;
238 };
239 if let Some(path) = worktrees.get(local) {
240 preserve.push(Preserved {
241 branch: branch.clone(),
242 reason: format!("worktree at {}", path),
243 base: false,
244 });
245 }
246 }
247
248 for preserved in &preserve {
249 self.to_delete.remove(&preserved.branch);
250 }
251 self.preserved.extend(preserve);
252
253 Ok(())
254 }
255
256 pub fn apply_delete_range_filter(
257 &mut self,
258 repo: &Repository,
259 filter: &DeleteFilter,
260 ) -> Result<()> {
261 let mut preserve = Vec::new();
262
263 for branch in &self.to_delete {
264 let range = match branch {
265 ClassifiedBranch::MergedLocal(_) => {
266 if !filter.delete_merged_local() {
267 Some("merged-local".to_owned())
268 } else {
269 None
270 }
271 }
272 ClassifiedBranch::Stray(_) => {
273 if !filter.delete_stray() {
274 Some("stray".to_owned())
275 } else {
276 None
277 }
278 }
279 ClassifiedBranch::MergedRemoteTracking(upstream) => {
280 let remote = upstream.to_remote_branch(repo)?;
281 if !filter.delete_merged_remote(&remote.remote) {
282 Some(format!("merged-remote:{}", &remote.remote))
283 } else {
284 None
285 }
286 }
287 ClassifiedBranch::DivergedRemoteTracking { upstream, .. } => {
288 let remote = upstream.to_remote_branch(repo)?;
289 if !filter.delete_diverged(&remote.remote) {
290 Some(format!("diverged:{}", &remote.remote))
291 } else {
292 None
293 }
294 }
295
296 ClassifiedBranch::MergedDirectFetch { remote, .. } => {
297 if !filter.delete_merged_remote(&remote.remote) {
298 Some(format!("merged-remote:{}", &remote.remote))
299 } else {
300 None
301 }
302 }
303 ClassifiedBranch::DivergedDirectFetch { remote, .. } => {
304 if !filter.delete_diverged(&remote.remote) {
305 Some(format!("diverged:{}", &remote.remote))
306 } else {
307 None
308 }
309 }
310
311 ClassifiedBranch::MergedNonTrackingLocal(_) => {
312 if !filter.delete_merged_non_tracking_local() {
313 Some("local".to_owned())
314 } else {
315 None
316 }
317 }
318 ClassifiedBranch::MergedNonUpstreamRemoteTracking(upstream) => {
319 let remote = upstream.to_remote_branch(repo)?;
320 if !filter.delete_merged_non_upstream_remote_tracking(&remote.remote) {
321 Some(format!("remote:{}", &remote.remote))
322 } else {
323 None
324 }
325 }
326 };
327
328 trace!("Delete range result: {:?} => {:?}", branch, range);
329
330 if let Some(range) = range {
331 preserve.push(Preserved {
332 branch: branch.clone(),
333 reason: format!("delete range `{}` was not given", range),
334 base: false,
335 });
336 }
337 }
338
339 for preserved in &preserve {
340 self.to_delete.remove(&preserved.branch);
341 }
342 self.preserved.extend(preserve);
343
344 Ok(())
345 }
346
347 pub fn adjust_not_to_detach(&mut self, repo: &Repository) -> Result<()> {
348 if repo.head_detached()? {
349 return Ok(());
350 }
351 let head = repo.head()?;
352 let head_name = head.name().context("non-utf8 head ref name")?;
353 let head_branch = LocalBranch::new(head_name);
354
355 let mut preserve = Vec::new();
356
357 for branch in &self.to_delete {
358 if branch.local() == Some(&head_branch) {
359 preserve.push(Preserved {
360 branch: branch.clone(),
361 reason: "HEAD".to_owned(),
362 base: false,
363 });
364 }
365 }
366
367 for preserved in &preserve {
368 self.to_delete.remove(&preserved.branch);
369 }
370 self.preserved.extend(preserve);
371 Ok(())
372 }
373
374 pub fn get_preserved_local(&self, target: &LocalBranch) -> Option<&Preserved> {
375 self.preserved
376 .iter()
377 .find(|&preserved| preserved.branch.local() == Some(target))
378 }
379
380 pub fn get_preserved_upstream(&self, target: &RemoteTrackingBranch) -> Option<&Preserved> {
381 self.preserved
382 .iter()
383 .find(|&preserved| preserved.branch.upstream() == Some(target))
384 }
385}
386
387#[derive(Clone, Eq, PartialEq)]
388pub enum SkipSuggestion {
389 Tracking,
390 TrackingRemote(String),
391 NonTracking,
392 NonUpstream(String),
393}
394
395impl SkipSuggestion {
396 pub const KIND_TRACKING: i32 = 1;
397 pub const KIND_NON_TRACKING: i32 = 2;
398 pub const KIND_NON_UPSTREAM: i32 = 3;
399
400 pub fn kind(&self) -> i32 {
401 match self {
402 SkipSuggestion::Tracking => Self::KIND_TRACKING,
403 SkipSuggestion::TrackingRemote(_) => Self::KIND_TRACKING,
404 SkipSuggestion::NonTracking => Self::KIND_NON_TRACKING,
405 SkipSuggestion::NonUpstream(_) => Self::KIND_NON_UPSTREAM,
406 }
407 }
408}
409
410fn get_protect_pattern<'a, B: Refname>(
411 repo: &Repository,
412 protected_patterns: &[&'a str],
413 branch: &B,
414) -> Result<Option<&'a str>> {
415 let prefixes = &["", "refs/remotes/", "refs/heads/"];
416 let target_refname = branch.refname();
417 for protected_pattern in protected_patterns {
418 for prefix in prefixes {
419 for reference in repo.references_glob(&format!("{}{}", prefix, protected_pattern))? {
420 let reference = reference?;
421 let refname = reference.name().context("non utf-8 refname")?;
422 if target_refname == refname {
423 return Ok(Some(protected_pattern));
424 }
425 }
426 }
427 }
428 Ok(None)
429}
430
431#[derive(Hash, Eq, PartialEq, Debug, Clone)]
432pub enum ClassifiedBranch {
433 MergedLocal(LocalBranch),
434 Stray(LocalBranch),
435 MergedRemoteTracking(RemoteTrackingBranch),
436 DivergedRemoteTracking {
437 local: LocalBranch,
438 upstream: RemoteTrackingBranch,
439 },
440
441 MergedDirectFetch {
442 local: LocalBranch,
443 remote: RemoteBranch,
444 },
445 DivergedDirectFetch {
446 local: LocalBranch,
447 remote: RemoteBranch,
448 },
449
450 MergedNonTrackingLocal(LocalBranch),
451 MergedNonUpstreamRemoteTracking(RemoteTrackingBranch),
452}
453
454impl ClassifiedBranch {
455 pub fn local(&self) -> Option<&LocalBranch> {
456 match self {
457 ClassifiedBranch::MergedLocal(local)
458 | ClassifiedBranch::Stray(local)
459 | ClassifiedBranch::DivergedRemoteTracking { local, .. }
460 | ClassifiedBranch::MergedDirectFetch { local, .. }
461 | ClassifiedBranch::DivergedDirectFetch { local, .. }
462 | ClassifiedBranch::MergedNonTrackingLocal(local) => Some(local),
463 _ => None,
464 }
465 }
466
467 pub fn upstream(&self) -> Option<&RemoteTrackingBranch> {
468 match self {
469 ClassifiedBranch::MergedRemoteTracking(upstream)
470 | ClassifiedBranch::DivergedRemoteTracking { upstream, .. }
471 | ClassifiedBranch::MergedNonUpstreamRemoteTracking(upstream) => Some(upstream),
472 _ => None,
473 }
474 }
475
476 pub fn remote(&self, repo: &Repository) -> Result<Option<RemoteBranch>> {
477 match self {
478 ClassifiedBranch::MergedRemoteTracking(upstream)
479 | ClassifiedBranch::DivergedRemoteTracking { upstream, .. }
480 | ClassifiedBranch::MergedNonUpstreamRemoteTracking(upstream) => {
481 let remote = upstream.to_remote_branch(repo)?;
482 Ok(Some(remote))
483 }
484 ClassifiedBranch::MergedDirectFetch { remote, .. }
485 | ClassifiedBranch::DivergedDirectFetch { remote, .. } => Ok(Some(remote.clone())),
486 _ => Ok(None),
487 }
488 }
489
490 pub fn message_local(&self) -> String {
491 match self {
492 ClassifiedBranch::MergedLocal(_) | ClassifiedBranch::MergedDirectFetch { .. } => {
493 "merged".to_owned()
494 }
495 ClassifiedBranch::MergedNonTrackingLocal(_) => "merged non-tracking".to_owned(),
496 ClassifiedBranch::Stray(_) => "stray".to_owned(),
497 ClassifiedBranch::DivergedRemoteTracking {
498 upstream: remote, ..
499 } => format!("diverged with {}", remote.refname),
500 ClassifiedBranch::DivergedDirectFetch { remote, .. } => {
501 format!("diverged with {}", remote)
502 }
503 _ => "If you see this message, report this as a bug".to_owned(),
504 }
505 }
506
507 pub fn message_remote(&self) -> String {
508 match self {
509 ClassifiedBranch::MergedRemoteTracking(_)
510 | ClassifiedBranch::MergedDirectFetch { .. } => "merged".to_owned(),
511 ClassifiedBranch::MergedNonUpstreamRemoteTracking(_) => {
512 "merged non-upstream".to_owned()
513 }
514 ClassifiedBranch::DivergedRemoteTracking { local, .. } => {
515 format!("diverged with {}", local.refname)
516 }
517 ClassifiedBranch::DivergedDirectFetch { local, .. } => {
518 format!("diverged with {}", local.short_name())
519 }
520 _ => "If you see this message, report this as a bug".to_owned(),
521 }
522 }
523}
524
525pub struct Classifier<'a> {
526 git: &'a Git,
527 merge_tracker: &'a MergeTracker,
528 tasks: Vec<Box<dyn FnOnce() -> Result<ClassificationResponseWithId> + Send + Sync + 'a>>,
529}
530
531impl<'a> Classifier<'a> {
532 pub fn new(git: &'a Git, merge_tracker: &'a MergeTracker) -> Self {
533 Self {
534 git,
535 merge_tracker,
536 tasks: Vec::new(),
537 }
538 }
539
540 pub fn queue_request<R: ClassificationRequest + Send + Sync + Debug + 'a>(&mut self, req: R) {
541 let id = self.tasks.len();
542 trace!("Enqueue #{}: {:#?}", id, req);
543 let git = ForceSendSync::new(self.git);
544 let merge_tracker = self.merge_tracker;
545 self.tasks.push(Box::new(move || {
546 req.classify(git, merge_tracker)
547 .with_context(|| format!("Failed to classify #{}: {:#?}", id, req))
548 .map(|response| ClassificationResponseWithId { id, response })
549 }));
550 }
551
552 pub fn queue_request_with_context<
553 R: ClassificationRequestWithContext<C> + Send + Sync + Debug + 'a,
554 C: Send + Sync + 'a,
555 >(
556 &mut self,
557 req: R,
558 context: C,
559 ) {
560 let id = self.tasks.len();
561 trace!("Enqueue #{}: {:#?}", id, req);
562 let git = ForceSendSync::new(self.git);
563 let merge_tracker = self.merge_tracker;
564 self.tasks.push(Box::new(move || {
565 req.classify_with_context(git, merge_tracker, context)
566 .with_context(|| format!("Failed to classify #{}: {:#?}", id, req))
567 .map(|response| ClassificationResponseWithId { id, response })
568 }));
569 }
570
571 pub fn classify(self) -> Result<Vec<ClassificationResponse>> {
572 info!("Classify {} requests", self.tasks.len());
573 let tasks = self.tasks;
574 let receiver = rayon::scope(move |scope| {
575 let (sender, receiver) = unbounded();
576 for tasks in tasks {
577 let sender = sender.clone();
578 scope.spawn(move |_| {
579 let result = tasks();
580 sender.send(result).unwrap();
581 })
582 }
583 receiver
584 });
585
586 let mut results = Vec::new();
587 for result in receiver {
588 let ClassificationResponseWithId { id, response } = result?;
589 debug!("Result #{}: {:#?}", id, response);
590
591 results.push(response);
592 }
593
594 Ok(results)
595 }
596}
597
598struct ClassificationResponseWithId {
599 id: usize,
600 response: ClassificationResponse,
601}
602
603#[derive(Debug)]
604pub struct ClassificationResponse {
605 #[allow(dead_code)] message: &'static str,
607 pub result: Vec<ClassifiedBranch>,
608}
609
610pub trait ClassificationRequest {
611 fn classify(
612 &self,
613 git: ForceSendSync<&Git>,
614 merge_tracker: &MergeTracker,
615 ) -> Result<ClassificationResponse>;
616}
617
618pub trait ClassificationRequestWithContext<C> {
619 fn classify_with_context(
620 &self,
621 git: ForceSendSync<&Git>,
622 merge_tracker: &MergeTracker,
623 context: C,
624 ) -> Result<ClassificationResponse>;
625}
626
627#[derive(Debug)]
628pub struct TrackingBranchClassificationRequest<'a> {
629 pub base: &'a RemoteTrackingBranch,
630 pub local: &'a LocalBranch,
631 pub upstream: Option<&'a RemoteTrackingBranch>,
632}
633
634impl<'a> ClassificationRequest for TrackingBranchClassificationRequest<'a> {
635 fn classify(
636 &self,
637 git: ForceSendSync<&Git>,
638 merge_tracker: &MergeTracker,
639 ) -> Result<ClassificationResponse> {
640 let local = merge_tracker.check_and_track(&git.repo, &self.base.refname, self.local)?;
641 let upstream = if let Some(upstream) = self.upstream {
642 merge_tracker.check_and_track(&git.repo, &self.base.refname, upstream)?
643 } else {
644 let result = if local.merged {
645 ClassificationResponse {
646 message: "local is merged but remote is gone",
647 result: vec![ClassifiedBranch::MergedLocal(local.branch)],
648 }
649 } else {
650 ClassificationResponse {
651 message: "local is stray but remote is gone",
652 result: vec![ClassifiedBranch::Stray(local.branch)],
653 }
654 };
655 return Ok(result);
656 };
657
658 let result = match (local.merged, upstream.merged) {
659 (true, true) => ClassificationResponse {
660 message: "local & upstream are merged",
661 result: vec![
662 ClassifiedBranch::MergedLocal(local.branch),
663 ClassifiedBranch::MergedRemoteTracking(upstream.branch),
664 ],
665 },
666 (true, false) => ClassificationResponse {
667 message: "local is merged but diverged with upstream",
668 result: vec![ClassifiedBranch::DivergedRemoteTracking {
669 local: local.branch,
670 upstream: upstream.branch,
671 }],
672 },
673 (false, true) => ClassificationResponse {
674 message: "upstream is merged, but the local strays",
675 result: vec![
676 ClassifiedBranch::Stray(local.branch),
677 ClassifiedBranch::MergedRemoteTracking(upstream.branch),
678 ],
679 },
680 (false, false) => ClassificationResponse {
681 message: "local & upstream are not merged yet",
682 result: vec![],
683 },
684 };
685
686 Ok(result)
687 }
688}
689
690#[derive(Debug)]
694pub struct DirectFetchClassificationRequest<'a> {
695 pub base: &'a RemoteTrackingBranch,
696 pub local: &'a LocalBranch,
697 pub remote: &'a RemoteBranch,
698}
699
700impl<'a> ClassificationRequestWithContext<&'a [RemoteHead]>
701 for DirectFetchClassificationRequest<'a>
702{
703 fn classify_with_context(
704 &self,
705 git: ForceSendSync<&Git>,
706 merge_tracker: &MergeTracker,
707 remote_heads: &[RemoteHead],
708 ) -> Result<ClassificationResponse> {
709 let local = merge_tracker.check_and_track(&git.repo, &self.base.refname, self.local)?;
710 let remote_head = remote_heads
711 .iter()
712 .find(|h| h.remote == self.remote.remote && h.refname == self.remote.refname)
713 .map(|h| &h.commit);
714
715 let result = match (local.merged, remote_head) {
716 (true, Some(head)) if head == &local.commit => ClassificationResponse {
717 message: "local & remote are merged",
718 result: vec![ClassifiedBranch::MergedDirectFetch {
719 local: local.branch,
720 remote: self.remote.clone(),
721 }],
722 },
723 (true, Some(_)) => ClassificationResponse {
724 message: "local is merged, but diverged with upstream",
725 result: vec![ClassifiedBranch::DivergedDirectFetch {
726 local: local.branch,
727 remote: self.remote.clone(),
728 }],
729 },
730 (true, None) => ClassificationResponse {
731 message: "local is merged and its upstream is gone",
732 result: vec![ClassifiedBranch::MergedLocal(local.branch)],
733 },
734 (false, None) => ClassificationResponse {
735 message: "local is not merged but the remote is gone somehow",
736 result: vec![ClassifiedBranch::Stray(local.branch)],
737 },
738 (false, _) => ClassificationResponse {
739 message: "local is not merged yet",
740 result: vec![],
741 },
742 };
743
744 Ok(result)
745 }
746}
747
748#[derive(Debug)]
749pub struct NonTrackingBranchClassificationRequest<'a> {
750 pub base: &'a RemoteTrackingBranch,
751 pub local: &'a LocalBranch,
752}
753
754impl<'a> ClassificationRequest for NonTrackingBranchClassificationRequest<'a> {
755 fn classify(
756 &self,
757 git: ForceSendSync<&Git>,
758 merge_tracker: &MergeTracker,
759 ) -> Result<ClassificationResponse> {
760 let local = merge_tracker.check_and_track(&git.repo, &self.base.refname, self.local)?;
761 let result = if local.merged {
762 ClassificationResponse {
763 message: "non-tracking local is merged",
764 result: vec![ClassifiedBranch::MergedNonTrackingLocal(local.branch)],
765 }
766 } else {
767 ClassificationResponse {
768 message: "non-tracking local is not merged",
769 result: vec![],
770 }
771 };
772 Ok(result)
773 }
774}
775
776#[derive(Debug)]
777pub struct NonUpstreamBranchClassificationRequest<'a> {
778 pub base: &'a RemoteTrackingBranch,
779 pub remote: &'a RemoteTrackingBranch,
780}
781
782impl<'a> ClassificationRequest for NonUpstreamBranchClassificationRequest<'a> {
783 fn classify(
784 &self,
785 git: ForceSendSync<&Git>,
786 merge_tracker: &MergeTracker,
787 ) -> Result<ClassificationResponse> {
788 let remote = merge_tracker.check_and_track(&git.repo, &self.base.refname, self.remote)?;
789 let result = if remote.merged {
790 ClassificationResponse {
791 message: "non-upstream local is merged",
792 result: vec![ClassifiedBranch::MergedNonUpstreamRemoteTracking(
793 remote.branch,
794 )],
795 }
796 } else {
797 ClassificationResponse {
798 message: "non-upstream local is not merged",
799 result: vec![],
800 }
801 };
802 Ok(result)
803 }
804}
805
806pub fn get_tracking_branches(
807 git: &Git,
808) -> Result<Vec<(LocalBranch, Option<RemoteTrackingBranch>)>> {
809 let mut result = Vec::new();
810 for branch in git.repo.branches(Some(BranchType::Local))? {
811 let local = LocalBranch::try_from(&branch?.0)?;
812
813 match local.fetch_upstream(&git.repo, &git.config)? {
814 RemoteTrackingBranchStatus::Exists(upstream) => {
815 result.push((local, Some(upstream)));
816 }
817 RemoteTrackingBranchStatus::Gone(_) => result.push((local, None)),
818 _ => {
819 continue;
820 }
821 };
822 }
823
824 Ok(result)
825}
826
827pub fn get_direct_fetch_branches(git: &Git) -> Result<Vec<(LocalBranch, RemoteBranch)>> {
829 let mut result = Vec::new();
830 for branch in git.repo.branches(Some(BranchType::Local))? {
831 let local = LocalBranch::try_from(&branch?.0)?;
832
833 let remote = if let Some(remote) = config::get_remote_name(&git.config, &local)? {
834 remote
835 } else {
836 continue;
837 };
838
839 if config::get_remote(&git.repo, &remote)?.is_some() {
840 continue;
841 }
842
843 let merge = config::get_merge(&git.config, &local)?.context(format!(
844 "Should have `branch.{}.merge` entry on git config",
845 local.short_name()
846 ))?;
847
848 let remote = RemoteBranch {
849 remote,
850 refname: merge,
851 };
852
853 result.push((local, remote));
854 }
855
856 Ok(result)
857}
858
859pub fn get_non_tracking_local_branches(git: &Git) -> Result<Vec<LocalBranch>> {
861 let mut result = Vec::new();
862 for branch in git.repo.branches(Some(BranchType::Local))? {
863 let branch = LocalBranch::try_from(&branch?.0)?;
864
865 if config::get_remote_name(&git.config, &branch)?.is_some() {
866 continue;
867 }
868
869 result.push(branch);
870 }
871
872 Ok(result)
873}
874
875pub fn get_non_upstream_remote_tracking_branches(git: &Git) -> Result<Vec<RemoteTrackingBranch>> {
877 let mut upstreams = HashSet::new();
878
879 let tracking_branches = get_tracking_branches(git)?;
880 for (_local, upstream) in tracking_branches {
881 if let Some(upstream) = upstream {
882 upstreams.insert(upstream);
883 }
884 }
885
886 let mut result = Vec::new();
887 for branch in git.repo.branches(Some(BranchType::Remote))? {
888 let (branch, _) = branch?;
889 if branch.get().symbolic_target_bytes().is_some() {
890 continue;
891 }
892
893 let branch = RemoteTrackingBranch::try_from(&branch)?;
894
895 if upstreams.contains(&branch) {
896 continue;
897 }
898
899 result.push(branch);
900 }
901
902 Ok(result)
903}
904
905pub fn get_remote_heads(git: &Git, branches: &[RemoteBranch]) -> Result<Vec<RemoteHead>> {
906 let mut remote_urls = Vec::new();
907
908 for branch in branches {
909 remote_urls.push(&branch.remote);
910 }
911
912 Ok(remote_urls
913 .into_par_iter()
914 .map({
915 let git = ForceSendSync::new(git);
916 move |remote_url| {
917 subprocess::ls_remote_heads(&git.repo, remote_url)
918 .with_context(|| format!("remote_url={}", remote_url))
919 }
920 })
921 .collect::<Result<Vec<Vec<RemoteHead>>, _>>()?
922 .into_iter()
923 .flatten()
924 .collect::<Vec<RemoteHead>>())
925}