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}