1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
use anyhow::Result;
use git2::{Cred, CredentialType, ErrorClass, ErrorCode, Remote, RemoteCallbacks, Repository};
use log::{debug, warn};

use crate::git::{branch::shorten_branch_ref, errors::GitError as E};

/// Number of times to try authenticating when fetching.
const AUTH_RETRY_COUNT: usize = 6;

/// Prepare the remote authentication callbacks for fetching.
///
/// Refs: <https://github.com/rust-lang/cargo/blob/2f115a76e5a1e5eb11cd29e95f972ed107267847/src/cargo/sources/git/utils.rs#L588>
pub(super) fn remote_callbacks(count: &mut usize) -> RemoteCallbacks {
    let mut remote_callbacks = RemoteCallbacks::new();
    remote_callbacks.credentials(move |url, username_from_url, allowed_types| {
        *count += 1;
        if *count > AUTH_RETRY_COUNT {
            let extra = if allowed_types.contains(CredentialType::SSH_KEY) {
                // On macOS ssh-add takes a -K argument to automatically add the ssh key's password
                // to the system keychain. This argument isn't present on other platforms.
                let ssh_add_keychain = if cfg!(target_os = "macos") { "-K " } else { "" };
                format!(
                    "\nIf 'git clone {url}' works, you probably need to add your ssh keys to the ssh-agent. \
                    Try running 'ssh-add {ssh_add_keychain}-A' or 'ssh-add {ssh_add_keychain}~/.ssh/*id_{{rsa,ed25519}}'.",
                    url=url, ssh_add_keychain=ssh_add_keychain
                 )
            } else {
                String::new()
            };
            let message = format!("Authentication failure while trying to fetch git repository.{extra}\n\
            url: {url}, username_from_url: {username_from_url:?}, allowed_types: {allowed_types:?}",
                extra = extra,
                url = url,
                username_from_url = username_from_url,
                allowed_types= allowed_types);
            return Err(git2::Error::new(ErrorCode::Auth, ErrorClass::Ssh, message));
        }
        debug!("SSH_AUTH_SOCK: {:?}", std::env::var("SSH_AUTH_SOCK"));
        debug!(
            "Fetching credentials, url: {url}, username_from_url: {username_from_url:?}, count: {count}, allowed_types: {allowed_types:?}",
            url = &url,
            username_from_url = &username_from_url,
            count = count,
            allowed_types= allowed_types,
        );
        let username = username_from_url.unwrap_or("git");
        if allowed_types.contains(CredentialType::USERNAME) {
            Cred::username(username)
        } else if allowed_types.contains(CredentialType::SSH_KEY) {
            Cred::ssh_key_from_agent(username)
        } else if allowed_types.contains(CredentialType::USER_PASS_PLAINTEXT) {
            let git_config = git2::Config::open_default()?;
            git2::Cred::credential_helper(&git_config, url, None)
        } else {
            Cred::default()
        }
    });
    remote_callbacks
}

/// Equivalent of: git remote set-head --auto <remote>
/// Find remote HEAD, then set the symbolic-ref refs/remotes/<remote>/HEAD to
/// refs/remotes/<remote>/<branch>
pub(super) fn set_remote_head(
    repo: &Repository,
    remote: &Remote,
    default_branch: &str,
) -> Result<()> {
    let remote_name = remote.name().ok_or(E::RemoteNameMissing)?;
    let remote_ref = format!("refs/remotes/{remote_name}/HEAD", remote_name = remote_name);
    let short_branch = shorten_branch_ref(default_branch);
    let remote_head = format!(
        "refs/remotes/{remote_name}/{short_branch}",
        remote_name = remote_name,
        short_branch = short_branch,
    );
    debug!(
        "Setting remote head for remote {remote_name}: {remote_ref} => {remote_head}",
        remote_name = remote_name,
        remote_ref = remote_ref,
        remote_head = remote_head,
    );
    match repo.find_reference(&remote_ref) {
        Ok(reference) => {
            if matches!(reference.symbolic_target(), Some(target) if target == remote_head) {
                debug!(
                    "Ref {remote_ref} already points to {remote_head}.",
                    remote_ref = remote_ref,
                    remote_head = remote_head
                );
            } else {
                warn!(
                    "Overwriting existing {remote_ref} to point to {remote_head} instead of
                    {symbolic_target:?}",
                    remote_ref = remote_ref,
                    remote_head = remote_head,
                    symbolic_target = reference.symbolic_target(),
                );
                repo.reference_symbolic(
                    &remote_ref,
                    &remote_head,
                    true,
                    "up-rs overwrite remote head",
                )?;
            }
        }
        Err(e) if e.code() == ErrorCode::NotFound => {
            repo.reference_symbolic(&remote_ref, &remote_head, false, "up-rs set remote head")?;
        }
        Err(e) => return Err(e.into()),
    }
    Ok(())
}