tame_index/utils/
git.rs

1//! Utilities for working with gix that might be useful for downstream users
2
3use crate::{error::GitError, Error};
4
5/// Writes the `FETCH_HEAD` for the specified fetch outcome to the specified git
6/// repository
7///
8/// This function is narrowly focused on on writing a `FETCH_HEAD` that contains
9/// exactly two pieces of information, the id of the commit pointed to by the
10/// remote `HEAD`, and, if it exists, the same id with the remote branch whose
11/// `HEAD` is the same. This focus gives use two things:
12///     1. `FETCH_HEAD` that can be parsed to the correct remote HEAD by
13/// [`gix`](https://github.com/Byron/gitoxide/commit/eb2b513bd939f6b59891d0a4cf5465b1c1e458b3)
14///     1. A `FETCH_HEAD` that closely (or even exactly) matches that created by
15/// cargo via git or git2 when fetching only `+HEAD:refs/remotes/origin/HEAD`
16///
17/// Calling this function for the fetch outcome of a clone will write `FETCH_HEAD`
18/// just as if a normal fetch had occurred, but note that AFAICT neither git nor
19/// git2 does this, ie. a fresh clone will not have a `FETCH_HEAD` present. I don't
20/// _think_ that has negative implications, but if it does...just don't call this
21/// function on the result of a clone :)
22///
23/// Note that the remote provided should be the same remote used for the fetch
24/// operation. The reason this is not just grabbed from the repo is because
25/// repositories may not have the configured remote, or the remote was modified
26/// (eg. replacing refspecs) before the fetch operation
27pub fn write_fetch_head(
28    repo: &gix::Repository,
29    fetch: &gix::remote::fetch::Outcome,
30    remote: &gix::Remote<'_>,
31) -> Result<gix::ObjectId, Error> {
32    use gix::{bstr::ByteSlice, protocol::handshake::Ref};
33    use std::fmt::Write;
34
35    // Find the remote head commit
36    let (head_target_branch, oid) = fetch
37        .ref_map
38        .mappings
39        .iter()
40        .find_map(|mapping| {
41            let gix::remote::fetch::refmap::Source::Ref(rref) = &mapping.remote else {
42                return None;
43            };
44
45            let Ref::Symbolic {
46                full_ref_name,
47                target,
48                object,
49                ..
50            } = rref
51            else {
52                return None;
53            };
54
55            (full_ref_name == "HEAD").then_some((target, object))
56        })
57        .ok_or_else(|| GitError::UnableToFindRemoteHead)?;
58
59    let remote_url = {
60        let ru = remote
61            .url(gix::remote::Direction::Fetch)
62            .expect("can't fetch without a fetch url");
63        let s = ru.to_bstring();
64        let v = s.into();
65        String::from_utf8(v).expect("remote url was not utf-8 :-/")
66    };
67
68    let fetch_head = {
69        let mut hex_id = [0u8; 40];
70        let gix::ObjectId::Sha1(sha1) = oid;
71        let commit_id = crate::utils::encode_hex(sha1, &mut hex_id);
72
73        let mut fetch_head = String::new();
74
75        let remote_name = remote
76            .name()
77            .and_then(|n| {
78                let gix::remote::Name::Symbol(name) = n else {
79                    return None;
80                };
81                Some(name.as_ref())
82            })
83            .unwrap_or("origin");
84
85        // We write the remote HEAD first, but _only_ if it was explicitly requested
86        if remote
87            .refspecs(gix::remote::Direction::Fetch)
88            .iter()
89            .any(|rspec| {
90                let rspec = rspec.to_ref();
91                if !rspec.remote().map_or(false, |r| r.ends_with(b"HEAD")) {
92                    return false;
93                }
94
95                rspec.local().map_or(false, |l| {
96                    l.to_str().ok().and_then(|l| {
97                        l.strip_prefix("refs/remotes/")
98                            .and_then(|l| l.strip_suffix("/HEAD"))
99                    }) == Some(remote_name)
100                })
101            })
102        {
103            writeln!(&mut fetch_head, "{commit_id}\t\t{remote_url}").unwrap();
104        }
105
106        // Attempt to get the branch name, but if it looks suspect just skip this,
107        // it _should_ be fine, or at least, we've already written the only thing
108        // that gix can currently parse
109        if let Some(branch_name) = head_target_branch
110            .to_str()
111            .ok()
112            .and_then(|s| s.strip_prefix("refs/heads/"))
113        {
114            writeln!(
115                &mut fetch_head,
116                "{commit_id}\t\tbranch '{branch_name}' of {remote_url}"
117            )
118            .unwrap();
119        }
120
121        fetch_head
122    };
123
124    // We _could_ also emit other branches/tags like git does, however it's more
125    // complicated than just our limited use case of writing remote HEAD
126    //
127    // 1. Remote branches are always emitted, however in gix those aren't part
128    // of the ref mappings if they haven't been updated since the last fetch
129    // 2. Conversely, tags are _not_ written by git unless they have been changed
130    // added, but gix _does_ always place those in the fetch mappings
131
132    if fetch_head.is_empty() {
133        return Err(GitError::UnableToFindRemoteHead.into());
134    }
135
136    let fetch_head_path = crate::PathBuf::from_path_buf(repo.path().join("FETCH_HEAD"))?;
137    std::fs::write(&fetch_head_path, fetch_head)
138        .map_err(|io| Error::IoPath(io, fetch_head_path))?;
139
140    Ok(*oid)
141}