git_trim/
core.rs

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    /// `hub-cli` can checkout pull request branch. However they are stored in `refs/pulls/`.
201    /// This prevents to remove them.
202    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)] // used in `Debug`
606    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/// `hub-cli` style branch classification request.
691/// `hub-cli` sets config `branch.{branch_name}.remote` as URL without `remote.{remote}` entry.
692/// However we can try manual classification without `remote.{remote}` entry.
693#[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
827/// Get `hub-cli` style direct fetched branches
828pub 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
859/// Get local branches that doesn't track any branch.
860pub 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
875/// Get remote tracking branches that doesn't tracked by any branch.
876pub 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}