up_rs/tasks/git/
checkout.rs

1//! Checkout a git branch or ref.
2use crate::tasks::git::fetch::remote_callbacks;
3use crate::tasks::git::status::ensure_repo_clean;
4use color_eyre::eyre::bail;
5use color_eyre::eyre::eyre;
6use color_eyre::eyre::Result;
7use git2::build::CheckoutBuilder;
8use git2::BranchType;
9use git2::ErrorCode;
10use git2::FetchOptions;
11use git2::Repository;
12use git2::SubmoduleUpdateOptions;
13use std::convert::Into;
14use std::str;
15use tracing::debug;
16use tracing::trace;
17
18/// Checkout the branch if necessary (branch isn't the current branch).
19///
20/// By default this function will skip checking out the branch when we're
21/// already on the branch, and error if the repo isn't clean. To always checkout
22/// and ignore issues set `force` to `true`.
23pub(super) fn checkout_branch(
24    repo: &Repository,
25    branch_name: &str,
26    short_branch: &str,
27    upstream_remote_name: &str,
28    force: bool,
29) -> Result<()> {
30    match repo.find_branch(short_branch, BranchType::Local) {
31        Ok(_) => (),
32        Err(e) if e.code() == ErrorCode::NotFound => {
33            debug!("Branch {short_branch} doesn't exist, creating it...",);
34            let branch_target = format!("{upstream_remote_name}/{short_branch}");
35            let branch_commit = repo
36                .find_branch(&branch_target, BranchType::Remote)?
37                .get()
38                .peel_to_commit()?;
39            let mut branch = repo.branch(short_branch, &branch_commit, false)?;
40            branch.set_upstream(Some(&branch_target))?;
41        }
42        Err(e) => return Err(e.into()),
43    };
44    match repo.head() {
45        Ok(current_head) => {
46            // A branch is currently checked out.
47            let current_head = current_head.name();
48            trace!("Current head is {current_head:?}, branch_name is {branch_name}",);
49            if !force && !repo.head_detached()? && current_head == Some(branch_name) {
50                debug!("Repo head is already {branch_name}, skipping branch checkout...",);
51                return Ok(());
52            }
53        }
54        Err(e) if e.code() == ErrorCode::UnbornBranch => {
55            // We just initialized the repo and haven't yet checked out a branch.
56            trace!("No current head, continuing with branch checkout...");
57        }
58        Err(e) => {
59            bail!(e);
60        }
61    }
62    if !force {
63        ensure_repo_clean(repo)?;
64    }
65    debug!("Setting head to {branch_name}");
66    set_and_checkout_head(repo, branch_name, force)?;
67    Ok(())
68}
69
70/// Set repo head if the branch is clean, then checkout the head directly.
71///
72/// Use force to always check out the branch whether or not it's clean.
73///
74/// The head checkout:
75/// Updates files in the index and the working tree to match the content of
76/// the commit pointed at by HEAD.
77/// Wraps git2's function with a different set of checkout options to the
78/// default.
79pub(super) fn set_and_checkout_head(
80    repo: &Repository,
81    branch_name: &str,
82    force: bool,
83) -> Result<()> {
84    if force {
85        debug!("Force checking out {branch_name}");
86    } else {
87        ensure_repo_clean(repo)?;
88    }
89    repo.set_head(branch_name)?;
90    force_checkout_head(repo)?;
91    Ok(())
92}
93
94/// Checkout head without checking that the repo is clean.
95///
96/// Private so users don't accidentally use this.
97///
98/// Note that this function force-overwrites the current working tree and index,
99/// so before calling this function ensure that the repository doesn't have
100/// uncommitted changes (e.g. by erroring if `ensure_clean()` returns false),
101/// or work could be lost.
102fn force_checkout_head(repo: &Repository) -> Result<()> {
103    debug!("Force checking out HEAD.");
104    repo.checkout_head(Some(
105        CheckoutBuilder::new()
106            .force()
107            .allow_conflicts(true)
108            .recreate_missing(true)
109            .conflict_style_diff3(true)
110            .conflict_style_merge(true),
111    ))?;
112
113    for mut submodule in repo.submodules()? {
114        trace!("Updating submodule: {:?}", submodule.name());
115
116        let mut checkout_builder = CheckoutBuilder::new();
117        checkout_builder
118            .force()
119            .allow_conflicts(true)
120            .recreate_missing(true)
121            .conflict_style_diff3(true)
122            .conflict_style_merge(true);
123
124        // Update the submodule's head.
125        let mut count = 0;
126        let mut fetch_options = FetchOptions::new();
127        fetch_options.remote_callbacks(remote_callbacks(&mut count));
128
129        submodule.update(
130            false,
131            Some(
132                SubmoduleUpdateOptions::new()
133                    .fetch(fetch_options)
134                    .checkout(checkout_builder),
135            ),
136        )?;
137
138        // Open the submodule and force checkout its head too (recurses into nested submodules).
139        let submodule_repo = submodule.open()?;
140        force_checkout_head(&submodule_repo)?;
141    }
142    Ok(())
143}
144
145/// Work out whether we need to checkout a branch (usually because the repo was newly-created).
146pub(super) fn needs_checkout(repo: &Repository, branch_name: &str) -> bool {
147    match repo.head().map_err(Into::into).and_then(|h| {
148        h.shorthand()
149            .map(ToOwned::to_owned)
150            .ok_or_else(|| eyre!("Current branch is not valid UTF-8"))
151    }) {
152        Ok(current_branch) if current_branch == branch_name => {
153            debug!("Already on branch: '{branch_name}'");
154            false
155        }
156        Ok(current_branch) => {
157            debug!("Current branch: {current_branch}");
158            true
159        }
160        Err(e) => {
161            debug!("Current branch errored: {e}");
162            true
163        }
164    }
165}