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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
use std::str;

use anyhow::{anyhow, bail, Result};
use git2::{build::CheckoutBuilder, BranchType, ErrorCode, Repository, SubmoduleUpdateOptions};
use log::{debug, trace};

use crate::tasks::git::status::ensure_repo_clean;

/// Checkout the branch if necessary (branch isn't the current branch).
///
/// By default this function will skip checking out the branch when we're
/// already on the branch, and error if the repo isn't clean. To always checkout
/// and ignore issues set `force` to `true`.
pub(super) fn checkout_branch(
    repo: &Repository,
    branch_name: &str,
    short_branch: &str,
    upstream_remote: &str,
    force: bool,
) -> Result<()> {
    match repo.find_branch(short_branch, BranchType::Local) {
        Ok(_) => (),
        Err(e) if e.code() == ErrorCode::NotFound => {
            debug!(
                "Branch {short_branch} doesn't exist, creating it...",
                short_branch = short_branch,
            );
            let branch_target = format!("{}/{}", upstream_remote, short_branch);
            let branch_commit = repo
                .find_branch(&branch_target, BranchType::Remote)?
                .get()
                .peel_to_commit()?;
            let mut branch = repo.branch(short_branch, &branch_commit, false)?;
            branch.set_upstream(Some(&branch_target))?;
        }
        Err(e) => return Err(e.into()),
    };
    match repo.head() {
        Ok(current_head) => {
            // A branch is currently checked out.
            let current_head = current_head.name();
            trace!(
                "Current head is {:?}, branch_name is {}",
                current_head,
                branch_name
            );
            if !force && !repo.head_detached()? && current_head == Some(branch_name) {
                debug!(
                    "Repo head is already {}, skipping branch checkout...",
                    branch_name,
                );
                return Ok(());
            }
        }
        Err(e) if e.code() == ErrorCode::UnbornBranch => {
            // We just initialized the repo and haven't yet checked out a branch.
            trace!("No current head, continuing with branch checkout...");
        }
        Err(e) => {
            bail!(e);
        }
    }
    if !force {
        ensure_repo_clean(repo)?;
    }
    debug!("Setting head to {branch_name}", branch_name = branch_name);
    set_and_checkout_head(repo, branch_name, force)?;
    Ok(())
}

/// Set repo head if the branch is clean, then checkout the head directly.
///
/// Use force to always check out the branch whether or not it's clean.
///
/// The head checkout:
/// Updates files in the index and the working tree to match the content of
/// the commit pointed at by HEAD.
/// Wraps git2's function with a different set of checkout options to the
/// default.
pub(super) fn set_and_checkout_head(
    repo: &Repository,
    branch_name: &str,
    force: bool,
) -> Result<()> {
    if force {
        debug!("Force checking out {}", branch_name);
    } else {
        ensure_repo_clean(repo)?;
    }
    repo.set_head(branch_name)?;
    force_checkout_head(repo)?;
    Ok(())
}

/// Checkout head without checking that the repo is clean.
///
/// Private so users don't accidentally use this.
///
/// Note that this function force-overwrites the current working tree and index,
/// so before calling this function ensure that the repository doesn't have
/// uncommitted changes (e.g. by erroring if `ensure_clean()` returns false),
/// or work could be lost.
fn force_checkout_head(repo: &Repository) -> Result<()> {
    debug!("Force checking out HEAD.");
    repo.checkout_head(Some(
        CheckoutBuilder::new()
            .force()
            .allow_conflicts(true)
            .recreate_missing(true)
            .conflict_style_diff3(true)
            .conflict_style_merge(true),
    ))?;

    for mut submodule in repo.submodules()? {
        trace!("Updating submodule: {:?}", submodule.name());

        let mut checkout_builder = CheckoutBuilder::new();
        checkout_builder
            .force()
            .allow_conflicts(true)
            .recreate_missing(true)
            .conflict_style_diff3(true)
            .conflict_style_merge(true);

        // Update the submodule's head. Doesn't fetch as it assumes that the parent repo was already
        // fetched.
        submodule.update(
            false,
            Some(SubmoduleUpdateOptions::new().checkout(checkout_builder)),
        )?;

        // Open the submodule and force checkout its head too (recurses into nested submodules).
        let submodule_repo = submodule.open()?;
        force_checkout_head(&submodule_repo)?;
    }
    Ok(())
}

pub(super) fn needs_checkout(repo: &Repository, branch_name: &str) -> bool {
    match repo.head().map_err(|e| e.into()).and_then(|h| {
        h.shorthand()
            .map(ToOwned::to_owned)
            .ok_or_else(|| anyhow!("Current branch is not valid UTF-8"))
    }) {
        Ok(current_branch) if current_branch == branch_name => {
            debug!("Already on branch: '{}'", branch_name);
            false
        }
        Ok(current_branch) => {
            debug!("Current branch: {}", current_branch);
            true
        }
        Err(e) => {
            debug!("Current branch errored: {}", e);
            true
        }
    }
}