up_rs/tasks/git/
fetch.rs

1//! Fetch updates to a branch.
2use crate::tasks::git::branch::shorten_branch_ref;
3use crate::tasks::git::errors::GitError as E;
4use color_eyre::eyre::Result;
5use git2::Cred;
6use git2::CredentialType;
7use git2::ErrorClass;
8use git2::ErrorCode;
9use git2::Remote;
10use git2::RemoteCallbacks;
11use git2::Repository;
12use std::thread;
13use std::time::Duration;
14use tracing::debug;
15use tracing::warn;
16
17/// Number of times to try authenticating when fetching.
18const AUTH_RETRY_COUNT: usize = 10;
19/// Length of time to sleep after multiple fetch failures.
20const RETRY_SLEEP_INTERVAL_S: u64 = 2;
21
22/// Prepare the remote authentication callbacks for fetching.
23///
24/// Refs: <https://github.com/rust-lang/cargo/blob/2f115a76e5a1e5eb11cd29e95f972ed107267847/src/cargo/sources/git/utils.rs#L588>
25pub(super) fn remote_callbacks(count: &mut usize) -> RemoteCallbacks {
26    let mut remote_callbacks = RemoteCallbacks::new();
27    remote_callbacks.credentials(move |url, username_from_url, allowed_types| {
28        *count += 1;
29        if *count > 2 {
30            thread::sleep(Duration::from_secs(RETRY_SLEEP_INTERVAL_S));
31        }
32        if *count > AUTH_RETRY_COUNT {
33            let extra = if allowed_types.contains(CredentialType::SSH_KEY) {
34                // On macOS ssh-add takes a -K argument to automatically add the ssh key's password
35                // to the system keychain. This argument isn't present on other platforms.
36                let ssh_add_keychain = if cfg!(target_os = "macos") { "-K " } else { "" };
37                format!(
38                    "\nIf 'git clone {url}' works, you probably need to add your ssh keys to the \
39                     ssh-agent. Try running 'ssh-add {ssh_add_keychain}-A' or 'ssh-add \
40                     {ssh_add_keychain}~/.ssh/*id_{{rsa,ed25519}}'."
41                )
42            } else {
43                String::new()
44            };
45            let message = format!(
46                "Authentication failure while trying to fetch git repository.{extra}\nurl: {url}, \
47                 username_from_url: {username_from_url:?}, allowed_types: {allowed_types:?}"
48            );
49            return Err(git2::Error::new(ErrorCode::Auth, ErrorClass::Ssh, message));
50        }
51        debug!("SSH_AUTH_SOCK: {:?}", std::env::var("SSH_AUTH_SOCK"));
52        debug!(
53            "Fetching credentials, url: {url}, username_from_url: {username_from_url:?}, count: \
54             {count}, allowed_types: {allowed_types:?}"
55        );
56        let username = username_from_url.unwrap_or("git");
57        if allowed_types.contains(CredentialType::USERNAME) {
58            Cred::username(username)
59        } else if allowed_types.contains(CredentialType::SSH_KEY) {
60            Cred::ssh_key_from_agent(username)
61        } else if allowed_types.contains(CredentialType::USER_PASS_PLAINTEXT) {
62            let git_config = git2::Config::open_default()?;
63            git2::Cred::credential_helper(&git_config, url, None)
64        } else {
65            Cred::default()
66        }
67    });
68    remote_callbacks
69}
70
71/// Equivalent of: `git remote set-head --auto <remote>`
72/// Find remote HEAD, then set the symbolic-ref `refs/remotes/<remote>/HEAD` to
73/// `refs/remotes/<remote>/<branch>`
74pub(super) fn set_remote_head(
75    repo: &Repository,
76    remote: &Remote,
77    default_branch: &str,
78) -> Result<bool> {
79    let mut did_work = false;
80    let remote_name = remote.name().ok_or(E::RemoteNameMissing)?;
81    let remote_ref = format!("refs/remotes/{remote_name}/HEAD");
82    let short_branch = shorten_branch_ref(default_branch);
83    let remote_head = format!("refs/remotes/{remote_name}/{short_branch}",);
84    debug!("Setting remote head for remote {remote_name}: {remote_ref} => {remote_head}",);
85    match repo.find_reference(&remote_ref) {
86        Ok(reference) => {
87            if matches!(reference.symbolic_target(), Some(target) if target == remote_head) {
88                debug!("Ref {remote_ref} already points to {remote_head}.",);
89            } else {
90                warn!(
91                    "Overwriting existing {remote_ref} to point to {remote_head} instead of
92                    {symbolic_target:?}",
93                    symbolic_target = reference.symbolic_target(),
94                );
95                repo.reference_symbolic(
96                    &remote_ref,
97                    &remote_head,
98                    true,
99                    "up overwrite remote head",
100                )?;
101                did_work = true;
102            }
103        }
104        Err(e) if e.code() == ErrorCode::NotFound => {
105            repo.reference_symbolic(&remote_ref, &remote_head, false, "up set remote head")?;
106            did_work = true;
107        }
108        Err(e) => return Err(e.into()),
109    }
110    Ok(did_work)
111}