Skip to main content

workon/
fetch.rs

1//! Prune-fetch helpers for keeping remote-tracking refs in sync.
2//!
3//! "Gone" upstream detection in `prune --gone` relies on `Branch::upstream()` returning
4//! `Err` for a branch whose tracking ref was deleted on the remote. That only happens
5//! after a *prune-fetch* — a fetch that also removes stale `refs/remotes/<remote>/*`
6//! entries. Nothing in the codebase performed such a fetch before this module.
7//!
8//! Public functions:
9//!
10//! - [`remotes_tracked_by_worktrees`] — discover which remotes are relevant (deduplicated
11//!   list of `branch.<name>.remote` values across all worktrees).
12//! - [`remotes_tracked_by_branches`] — the same discovery for branches with no worktree.
13//! - [`prune_fetch`] — run the equivalent of `git fetch --prune <remote>` for a single
14//!   remote, deleting stale remote-tracking refs and then fetching.
15
16use git2::{FetchOptions, FetchPrune};
17
18use crate::error::Result;
19use crate::get_remote_callbacks::get_remote_callbacks;
20use crate::worktree::WorktreeDescriptor;
21
22/// Returns the deduplicated list of remote names tracked by the given worktrees.
23///
24/// For each worktree with a local branch that has an upstream configured, reads
25/// `branch.<name>.remote` from git config. Results are deduplicated and returned in
26/// stable (first-seen) order. Worktrees with detached HEAD or no upstream are
27/// silently skipped.
28///
29/// Accepts any iterator of worktree references, so callers can pass a full
30/// `&[WorktreeDescriptor]` or a filtered subset of borrowed descriptors.
31pub fn remotes_tracked_by_worktrees<'a>(
32    repo: &git2::Repository,
33    worktrees: impl IntoIterator<Item = &'a WorktreeDescriptor>,
34) -> Result<Vec<String>> {
35    let config = repo.config()?;
36    let mut remotes: Vec<String> = Vec::new();
37
38    for wt in worktrees {
39        let branch_name = match wt.branch()? {
40            Some(name) => name,
41            None => continue, // detached HEAD
42        };
43
44        let remote_key = format!("branch.{}.remote", branch_name);
45        if let Ok(remote) = config.get_string(&remote_key) {
46            if !remotes.contains(&remote) {
47                remotes.push(remote);
48            }
49        }
50    }
51
52    Ok(remotes)
53}
54
55/// Returns the deduplicated list of remote names tracked by the given branch names.
56///
57/// Sibling of [`remotes_tracked_by_worktrees`] for branches with no worktree: reads
58/// `branch.<name>.remote` from git config for each name. Results are deduplicated and
59/// returned in stable (first-seen) order. Branches with no upstream configured are
60/// silently skipped.
61pub fn remotes_tracked_by_branches<'a>(
62    repo: &git2::Repository,
63    names: impl IntoIterator<Item = &'a str>,
64) -> Result<Vec<String>> {
65    let config = repo.config()?;
66    let mut remotes: Vec<String> = Vec::new();
67
68    for name in names {
69        let remote_key = format!("branch.{}.remote", name);
70        if let Ok(remote) = config.get_string(&remote_key) {
71            if !remotes.contains(&remote) {
72                remotes.push(remote);
73            }
74        }
75    }
76
77    Ok(remotes)
78}
79
80/// Prune-fetch from a single remote: remove stale remote-tracking refs, then fetch.
81///
82/// Equivalent to `git fetch --prune <remote>`. After this call, any
83/// `refs/remotes/<remote>/<branch>` ref that no longer exists on the remote is
84/// deleted locally, making `Branch::upstream()` correctly return `Err` (i.e. "gone")
85/// for branches that were deleted upstream.
86///
87/// Returns `Err` on auth failure, network error, or unknown remote. Callers that
88/// want warn-and-continue behaviour should catch the error and proceed without
89/// aborting the rest of the prune.
90pub fn prune_fetch(repo: &git2::Repository, remote_name: &str) -> Result<()> {
91    let mut remote = repo.find_remote(remote_name)?;
92
93    // Stash the URL before we borrow remote mutably.
94    // url() returns Result<&str, _> in this git2 version; ignore errors (URL may be absent).
95    let remote_url = remote.url().ok().map(str::to_owned);
96    let auth = get_remote_callbacks(repo, remote_url.as_deref())?;
97
98    let mut fetch_opts = FetchOptions::new();
99    fetch_opts.prune(FetchPrune::On); // equivalent to --prune: delete stale tracking refs
100    fetch_opts.remote_callbacks(auth.callbacks());
101
102    // Empty refspec slice = use the remote's configured refspecs (same as `git fetch origin`).
103    remote.fetch(&[] as &[&str], Some(&mut fetch_opts), None)?;
104
105    Ok(())
106}