cli/git_projection_engine/
git_mapping.rs1use std::{
5 collections::HashSet,
6 fs::{self, File},
7 io::Write,
8 path::{Path, PathBuf},
9};
10
11use objects::{object::ChangeId, store::ObjectStore};
12use serde::{Deserialize, Serialize};
13use sley::{ObjectFormat, ObjectId as SleyObjectId, ReferenceTarget, Repository as SleyRepository};
14
15use super::git_core::{
16 GitProjection, GitProjectionError, GitProjectionResult, SyncMapping, git_err,
17};
18
19#[derive(Debug, Serialize, Deserialize)]
20struct MappingEntry {
21 change_id: String,
22 git_oid: String,
23}
24
25#[derive(Debug, Serialize, Deserialize, Default)]
26struct MappingFile {
27 entries: Vec<MappingEntry>,
28}
29
30#[derive(Debug, Default)]
31struct GitIdentityIndex {
32 mapping: SyncMapping,
33}
34
35impl GitIdentityIndex {
36 fn from_notes(repo: &SleyRepository) -> GitProjectionResult<Self> {
37 let mut index = Self::default();
38 for (change_id, git_oid) in super::git_notes::read_identity_mappings(repo)? {
39 index.mapping.insert_checked(change_id, git_oid)?;
40 }
41 Ok(index)
42 }
43
44 fn fill_gaps_from_cache(&mut self, cache: &SyncMapping) {
45 for (change_id, git_oid) in cache.iter() {
46 if self.mapping.get_git(change_id) == Some(*git_oid) {
47 continue;
48 }
49 if self.mapping.has_heddle(change_id) || self.mapping.has_git(*git_oid) {
50 continue;
51 }
52 self.mapping.insert(*change_id, *git_oid);
53 }
54 }
55
56 fn into_mapping(self) -> SyncMapping {
57 self.mapping
58 }
59}
60
61impl<'a> GitProjection<'a> {
62 pub(crate) fn mapping_path(&self) -> PathBuf {
63 self.heddle_repo
64 .heddle_dir()
65 .join("git-projection")
66 .join("git-projection-mapping.json")
67 }
68
69 pub(crate) fn mapping_tmp_path(&self) -> PathBuf {
70 self.mapping_path().with_extension("json.tmp")
71 }
72
73 fn read_mapping_cache_from_disk(&self) -> GitProjectionResult<SyncMapping> {
74 self.recover_mapping_tmp()?;
75 let path = self.mapping_path();
76 if !path.exists() {
77 return Ok(SyncMapping::new());
78 }
79
80 let data = fs::read_to_string(&path)?;
81 let file: MappingFile = serde_json::from_str(&data)
82 .map_err(|err| GitProjectionError::InvalidMapping(err.to_string()))?;
83
84 let mut mapping = SyncMapping::new();
85 for entry in file.entries {
86 let change_id = ChangeId::parse(&entry.change_id)?;
87 let git_oid = parse_stored_git_oid(&entry.git_oid)?;
88 mapping.insert_checked(change_id, git_oid)?;
89 }
90
91 Ok(mapping)
92 }
93
94 fn recover_mapping_tmp(&self) -> GitProjectionResult<()> {
95 let path = self.mapping_path();
96 let tmp_path = self.mapping_tmp_path();
97 if !tmp_path.exists() {
98 return Ok(());
99 }
100 if !path.exists() {
101 fs::rename(&tmp_path, &path)?;
102 } else {
103 fs::remove_file(&tmp_path)?;
104 }
105 Ok(())
106 }
107
108 fn mapping_bytes(mapping: &SyncMapping) -> GitProjectionResult<Vec<u8>> {
109 let entries = mapping
110 .iter()
111 .map(|(change_id, git_oid)| MappingEntry {
112 change_id: change_id.to_string_full(),
113 git_oid: git_oid.to_string(),
114 })
115 .collect();
116
117 let file = MappingFile { entries };
118 serde_json::to_vec_pretty(&file)
119 .map_err(|err| GitProjectionError::InvalidMapping(err.to_string()))
120 }
121
122 pub(crate) fn write_mapping_tmp_to_disk(&self) -> GitProjectionResult<PathBuf> {
123 self.write_mapping_tmp_value_to_disk(&self.mapping)
124 }
125
126 fn write_mapping_tmp_value_to_disk(
127 &self,
128 mapping: &SyncMapping,
129 ) -> GitProjectionResult<PathBuf> {
130 let path = self.mapping_path();
131 let tmp_path = self.mapping_tmp_path();
132 if let Some(parent) = path.parent() {
133 fs::create_dir_all(parent)?;
134 let parent_file = File::open(parent)?;
135 parent_file.sync_all()?;
136 }
137
138 let data = Self::mapping_bytes(mapping)?;
139 let mut file = File::create(&tmp_path)?;
140 file.write_all(&data)?;
141 file.sync_all()?;
142 Ok(tmp_path)
143 }
144
145 pub(crate) fn commit_mapping_tmp_to_disk(&self) -> GitProjectionResult<()> {
146 let path = self.mapping_path();
147 let tmp_path = self.mapping_tmp_path();
148 if !tmp_path.exists() {
149 return Err(GitProjectionError::InvalidMapping(format!(
150 "mapping temp file is missing: {}",
151 tmp_path.display()
152 )));
153 }
154 fs::rename(&tmp_path, &path)?;
155 if let Some(parent) = path.parent() {
156 let parent_file = File::open(parent)?;
157 parent_file.sync_all()?;
158 }
159 Ok(())
160 }
161
162 pub(crate) fn save_mapping_to_disk(&self) -> GitProjectionResult<()> {
163 self.write_mapping_tmp_to_disk()?;
164 objects::fault_inject::maybe_panic_at("mapping_after_tmp_before_commit");
170 self.commit_mapping_tmp_to_disk()
171 }
172
173 pub(crate) fn build_existing_mapping(
179 &mut self,
180 git_repo_path: Option<&Path>,
181 ) -> GitProjectionResult<()> {
182 let repo = match git_repo_path {
183 Some(path) => super::git_core::open_repo(path)?,
184 None => self.open_git_repo()?,
185 };
186
187 let cache = self.read_mapping_cache_from_disk()?;
188 let live_cache = self.mapping.clone();
189 let mut index = GitIdentityIndex::from_notes(&repo)?;
190 index.fill_gaps_from_cache(&live_cache);
191 index.fill_gaps_from_cache(&cache);
192 self.mapping = index.into_mapping();
193 Ok(())
194 }
195
196 pub(crate) fn seed_ingest_identity_mappings_from_mirror(
197 &mut self,
198 repo: &SleyRepository,
199 ) -> GitProjectionResult<()> {
200 let ingest = self.heddle_repo.git_overlay_ingest_commit_mapping()?;
201 for (git_sha, change_id) in ingest {
202 let change_id = ChangeId::parse(&change_id)?;
203 if self.heddle_repo.store().get_state(&change_id)?.is_none() {
204 continue;
205 }
206 if self.mapping.has_heddle(&change_id) {
207 continue;
208 }
209 let git_oid = parse_stored_git_oid(&git_sha)?;
210 if self.mapping.has_git(git_oid) || repo.read_object(&git_oid).is_err() {
211 continue;
212 }
213 self.mapping.insert(change_id, git_oid);
214 }
215 Ok(())
216 }
217
218 #[cfg_attr(not(feature = "git-overlay"), allow(dead_code))]
219 pub(crate) fn prune_unreachable_mapping_entries(&mut self) -> GitProjectionResult<usize> {
220 let repo = self.open_git_repo()?;
221 self.mapping = self.read_mapping_cache_from_disk()?;
222 let reachable: HashSet<_> = collect_commit_oids(&repo)?.into_iter().collect();
223 let removed = self.mapping.retain_git_object_set(&reachable);
224 if removed > 0 {
225 self.save_mapping_to_disk()?;
226 }
227 Ok(removed)
228 }
229
230 #[cfg_attr(not(feature = "git-overlay"), allow(dead_code))]
257 pub(crate) fn consolidate_mirror(&self) -> GitProjectionResult<usize> {
258 use sley::plumbing::sley_odb::{install_repack_result, repack_all_objects};
259
260 let repo = self.open_git_repo()?;
261 let git_dir = repo.git_dir().to_path_buf();
262 let format = repo.object_format();
263
264 let Some(result) = repack_all_objects(&git_dir, format).map_err(git_err)? else {
265 return Ok(0);
266 };
267 let pruned_loose = result.packed_loose.len();
268 install_repack_result(&git_dir, format, &result, true).map_err(git_err)?;
272 Ok(pruned_loose)
273 }
274}
275
276fn collect_commit_oids(repo: &SleyRepository) -> GitProjectionResult<Vec<SleyObjectId>> {
280 let mut tips = Vec::new();
281
282 for reference in repo.references().list_refs().map_err(git_err)? {
283 if !(reference.name.starts_with("refs/heads/") || reference.name.starts_with("refs/tags/"))
284 {
285 continue;
286 }
287 let oid = match reference.target {
288 ReferenceTarget::Direct(oid) => oid,
289 ReferenceTarget::Symbolic(_) => {
290 let Some(reference) = repo.find_reference(&reference.name).map_err(git_err)? else {
291 continue;
292 };
293 let Some(oid) = reference.peeled_oid(repo).map_err(git_err)? else {
294 continue;
295 };
296 oid
297 }
298 };
299 if let Ok(commit_oid) = sley::plumbing::sley_rev::peel_to_commit(
300 repo.objects().as_ref(),
301 repo.object_format(),
302 &oid,
303 ) {
304 tips.push(commit_oid);
305 }
306 }
307
308 let mut seen = HashSet::new();
309 let mut stack = tips;
310 while let Some(oid) = stack.pop() {
311 if !seen.insert(oid) {
312 continue;
313 }
314 let commit = repo.read_commit(&oid).map_err(git_err)?;
315 stack.extend(commit.parents);
316 }
317
318 Ok(seen.into_iter().collect())
319}
320
321fn parse_stored_git_oid(value: &str) -> GitProjectionResult<SleyObjectId> {
322 let format = match value.len() {
323 40 => ObjectFormat::Sha1,
324 64 => ObjectFormat::Sha256,
325 _ => {
326 return Err(GitProjectionError::InvalidMapping(format!(
327 "invalid git oid length for {value}"
328 )));
329 }
330 };
331 SleyObjectId::from_hex(format, value)
332 .map_err(|err| GitProjectionError::InvalidMapping(err.to_string()))
333}