loftspec_store/repository/
base.rs1use std::io::Write;
6use std::path::Path;
7use std::process::{Output, Stdio};
8use std::{io, thread};
9
10use loftspec::codec::{Files, SpecPath};
11
12use super::{GitCommand, succeeded, text};
13use crate::StoreError;
14use crate::fs::{SPEC, hidden};
15
16pub const MERGE_BASE: [&str; 2] = ["merge-base", "HEAD"];
18pub const LIST_TREE: [&str; 3] = ["ls-tree", "-r", "-z"];
20pub const BATCH: [&str; 2] = ["cat-file", "--batch"];
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum MergeBase {
26 Commit(String),
28 Unrelated,
30}
31
32impl GitCommand {
33 pub fn merge_base(&self, root: &Path, revision: &str) -> Result<MergeBase, StoreError> {
39 if revision.is_empty() || revision.starts_with('-') {
40 return Err(StoreError::UnknownRevision(revision.to_owned()));
41 }
42 let call: Vec<&str> = MERGE_BASE.into_iter().chain([revision]).collect();
43 let output = self.run(root, &call)?;
44 match output.status.code() {
45 Some(1) => return Ok(MergeBase::Unrelated),
46 Some(128)
47 if String::from_utf8_lossy(&output.stderr).contains("Not a valid object name") =>
48 {
49 return Err(StoreError::UnknownRevision(revision.to_owned()));
50 }
51 _ => {}
52 }
53 Ok(MergeBase::Commit(text(&succeeded(&call, output)?)))
54 }
55
56 pub fn spec_files_at(&self, root: &Path, commit: &str) -> Result<Files, StoreError> {
63 let call: Vec<&str> = LIST_TREE.into_iter().chain([commit, "--", SPEC]).collect();
64 let listed = succeeded(&call, self.run(root, &call)?)?;
65 let blobs = blobs(&listed, &call)?;
66 let mut input = String::new();
67 for (object, _) in &blobs {
68 input.push_str(object);
69 input.push('\n');
70 }
71 let call = BATCH;
72 let output = self.run_with_input(root, &call, input.as_bytes())?;
73 let answer = succeeded(&call, output)?;
74 let mut files = Files::new();
75 let mut at = 0;
76 for (object, path) in blobs {
77 let (bytes, next) = object_at(&answer, at, &object).ok_or_else(|| StoreError::Git {
78 call: call.join(" "),
79 message: format!("no object {object} for {path}"),
80 })?;
81 files.insert(SpecPath::new(&path), bytes.to_vec());
82 at = next;
83 }
84 Ok(files)
85 }
86
87 fn run_with_input(
90 &self,
91 root: &Path,
92 call: &[&str],
93 input: &[u8],
94 ) -> Result<Output, StoreError> {
95 let failed = |error: io::Error| match error.kind() {
96 io::ErrorKind::NotFound => StoreError::GitUnavailable(error.to_string()),
97 _ => StoreError::Git {
98 call: call.join(" "),
99 message: error.to_string(),
100 },
101 };
102 let mut child = (self.command)()
103 .arg("-C")
104 .arg(root)
105 .args(call)
106 .env("GIT_OPTIONAL_LOCKS", "0")
107 .stdin(Stdio::piped())
108 .stdout(Stdio::piped())
109 .stderr(Stdio::piped())
110 .spawn()
111 .map_err(failed)?;
112 let mut stdin = child.stdin.take().expect("the input is piped");
113 thread::scope(|scope| {
114 scope.spawn(move || stdin.write_all(input));
115 child.wait_with_output()
116 })
117 .map_err(failed)
118 }
119}
120
121fn blobs(listed: &[u8], call: &[&str]) -> Result<Vec<(String, String)>, StoreError> {
124 let unread = |record: &str| StoreError::Git {
125 call: call.join(" "),
126 message: format!("a record the port does not read: {record:?}"),
127 };
128 let mut blobs = Vec::new();
129 for record in listed
130 .split(|byte| *byte == 0)
131 .filter(|record| !record.is_empty())
132 {
133 let record = String::from_utf8_lossy(record);
134 let (header, path) = record.split_once('\t').ok_or_else(|| unread(&record))?;
135 let mut fields = header.split(' ');
136 let (Some(_mode), Some(kind), Some(object)) = (fields.next(), fields.next(), fields.next())
137 else {
138 return Err(unread(&record));
139 };
140 let Some(path) = path
141 .strip_prefix(SPEC)
142 .and_then(|rest| rest.strip_prefix('/'))
143 else {
144 return Err(unread(&record));
145 };
146 if kind != "blob" || path.split('/').any(hidden) {
147 continue;
148 }
149 blobs.push((object.to_owned(), path.to_owned()));
150 }
151 Ok(blobs)
152}
153
154fn object_at<'a>(answer: &'a [u8], at: usize, object: &str) -> Option<(&'a [u8], usize)> {
157 let rest = answer.get(at..)?;
158 let end = rest.iter().position(|byte| *byte == b'\n')?;
159 let header = String::from_utf8_lossy(&rest[..end]);
160 let mut fields = header.split(' ');
161 let (Some(id), Some(_kind), Some(size)) = (fields.next(), fields.next(), fields.next()) else {
162 return None;
163 };
164 let size: usize = size.parse().ok()?;
165 let start = end + 1;
166 if id != object || rest.get(start + size) != Some(&b'\n') {
167 return None;
168 }
169 Some((&rest[start..start + size], at + start + size + 1))
170}