git_policy_cli/
shadow_refs.rs1use super::shadow_git::shadow_request;
20use super::transaction_git::{GitContext, GitFailure, GitOutput, GitRequest, run_git_checked};
21use super::private_storage::write_private_file;
23use super::transaction_journal::{Base, RefFormat, SymbolicHead};
25use std::path::{Path, PathBuf};
27
28pub const PACKED_REFS_HEADER: &str = "# pack-refs with: sorted \n";
31
32#[derive(Clone, Debug, Eq, PartialEq)]
42pub struct RefSnapshotEntry {
43 pub name: String,
45 pub oid: String,
47 pub symref: String,
49}
50
51pub fn list_real_refs(context: &GitContext, cwd: &Path) -> Result<Vec<RefSnapshotEntry>, GitFailure> {
61 let mut request: GitRequest = GitRequest::new(
62 cwd,
63 &["for-each-ref", "--format=%(refname)%00%(objectname)%00%(symref)"],
64 );
65 request.without_prefix = true;
66 let output: GitOutput = run_git_checked(context, &request)?;
67 let text: &str = std::str::from_utf8(output.stdout.as_slice())
68 .map_err(|_| { return GitFailure(String::from("git for-each-ref returned non-UTF-8 output.")); })?;
69 let mut refs: Vec<RefSnapshotEntry> = Vec::new();
70 for line in text.split('\n').filter(|line| { return !line.is_empty(); }) {
71 let fields: Vec<&str> = line.split('\0').collect();
72 let [name, oid, symref] = fields.as_slice() else {
73 return Err(GitFailure(format!(
74 "git for-each-ref returned a malformed line: {line:?}"
75 )));
76 };
77 refs.push(RefSnapshotEntry {
78 name: String::from(*name),
79 oid: String::from(*oid),
80 symref: String::from(*symref),
81 });
82 }
83 refs.sort_by(|left, right| { return left.name.as_bytes().cmp(right.name.as_bytes()); });
84 return Ok(refs);
85}
86
87pub fn format_packed_refs(refs: &[RefSnapshotEntry], target_ref: &str) -> String {
97 let mut text: String = String::from(PACKED_REFS_HEADER);
98 for entry in refs
99 .iter()
100 .filter(|entry| { return entry.symref.is_empty() && entry.name != target_ref; })
101 {
102 text.push_str(format!("{} {}\n", entry.oid, entry.name).as_str());
103 }
104 return text;
105}
106
107fn write_shadow_head(
118 context: &GitContext,
119 shadow: &Path,
120 symbolic_head: &SymbolicHead,
121 base: &Base,
122) -> Result<(), GitFailure> {
123 match symbolic_head {
124 SymbolicHead::Branch(reference) => {
125 if let Base::Commit(oid) = base {
126 run_git_checked(
127 context,
128 &shadow_request(shadow, shadow, &["update-ref", reference.as_str(), oid.as_str()]),
129 )?;
130 }
131 run_git_checked(
132 context,
133 &shadow_request(shadow, shadow, &["symbolic-ref", "HEAD", reference.as_str()]),
134 )?;
135 return Ok(());
136 }
137 SymbolicHead::Detached => {
138 let Base::Commit(oid) = base else {
139 return Err(GitFailure(String::from(
140 "A detached HEAD has no unborn preparation base.",
141 )));
142 };
143 run_git_checked(
144 context,
145 &shadow_request(shadow, shadow, &["update-ref", "--no-deref", "HEAD", oid.as_str()]),
146 )?;
147 return Ok(());
148 }
149 }
150}
151
152pub fn write_shadow_refs(
164 context: &GitContext,
165 cwd: &Path,
166 shadow: &Path,
167 ref_format: RefFormat,
168 target_ref: &str,
169 symbolic_head: &SymbolicHead,
170 base: &Base,
171) -> Result<(), GitFailure> {
172 let refs: Vec<RefSnapshotEntry> = list_real_refs(context, cwd)?;
173 match ref_format {
174 RefFormat::Files => {
175 write_private_file(
176 PathBuf::from(shadow).join("packed-refs").as_path(),
177 format_packed_refs(&refs, target_ref).as_bytes(),
178 )
179 .map_err(|error| { return GitFailure(format!("writing packed-refs failed: {error}")); })?;
180 }
181 RefFormat::Reftable => {
182 let mut lines: String = String::new();
183 for entry in refs
184 .iter()
185 .filter(|entry| { return entry.symref.is_empty() && entry.name != target_ref; })
186 {
187 lines.push_str(format!("create {} {}\n", entry.name, entry.oid).as_str());
188 }
189 let mut request: GitRequest = shadow_request(shadow, shadow, &["update-ref", "--stdin"]);
190 request.input = Some(lines.into_bytes());
191 run_git_checked(context, &request)?;
192 }
193 }
194 for entry in refs.iter().filter(|entry| { return !entry.symref.is_empty(); }) {
195 run_git_checked(
196 context,
197 &shadow_request(shadow, shadow, &["symbolic-ref", entry.name.as_str(), entry.symref.as_str()]),
198 )?;
199 }
200 return write_shadow_head(context, shadow, symbolic_head, base);
201}
202
203#[cfg(test)]
205#[path = "shadow_refs_tests.rs"]
206mod tests;