cli/bridge/
git_mapping.rs1use std::{
5 collections::HashSet,
6 fs::{self, File},
7 io::Write,
8 path::{Path, PathBuf},
9};
10
11use objects::object::ChangeId;
12use serde::{Deserialize, Serialize};
13
14use super::git_core::{GitBridge, GitBridgeError, GitResult, git_err};
15
16#[derive(Debug, Serialize, Deserialize)]
17struct MappingEntry {
18 change_id: String,
19 git_oid: String,
20}
21
22#[derive(Debug, Serialize, Deserialize, Default)]
23struct MappingFile {
24 entries: Vec<MappingEntry>,
25}
26
27impl<'a> GitBridge<'a> {
28 pub(crate) fn mapping_path(&self) -> PathBuf {
29 self.heddle_repo
30 .heddle_dir()
31 .join("git-bridge")
32 .join("bridge-mapping.json")
33 }
34
35 pub(crate) fn mapping_tmp_path(&self) -> PathBuf {
36 self.mapping_path().with_extension("json.tmp")
37 }
38
39 fn legacy_mapping_path(&self) -> PathBuf {
40 self.heddle_repo
41 .heddle_dir()
42 .join("git")
43 .join("bridge-mapping.json")
44 }
45
46 fn remove_legacy_mapping_file(&self) -> GitResult<()> {
47 let legacy_path = self.legacy_mapping_path();
48 if !legacy_path.exists() {
49 return Ok(());
50 }
51
52 fs::remove_file(&legacy_path)?;
53 Ok(())
54 }
55
56 fn migrate_legacy_mapping_if_needed(&self) -> GitResult<PathBuf> {
57 let path = self.mapping_path();
58 let legacy_path = self.legacy_mapping_path();
59
60 if path.exists() {
61 self.remove_legacy_mapping_file()?;
62 return Ok(path);
63 }
64
65 if !legacy_path.exists() {
66 return Ok(path);
67 }
68
69 if let Some(parent) = path.parent() {
70 fs::create_dir_all(parent)?;
71 }
72
73 fs::rename(&legacy_path, &path)?;
74 Ok(path)
75 }
76
77 pub(crate) fn load_mapping_from_disk(&mut self) -> GitResult<()> {
78 self.recover_mapping_tmp()?;
79 let path = self.migrate_legacy_mapping_if_needed()?;
80 if !path.exists() {
81 return Ok(());
82 }
83
84 let data = fs::read_to_string(&path)?;
85 let file: MappingFile = serde_json::from_str(&data)
86 .map_err(|err| GitBridgeError::InvalidMapping(err.to_string()))?;
87
88 for entry in file.entries {
89 let change_id = ChangeId::parse(&entry.change_id)?;
90 let git_oid = entry
91 .git_oid
92 .parse::<gix::hash::ObjectId>()
93 .map_err(|err| GitBridgeError::InvalidMapping(err.to_string()))?;
94 self.mapping.insert_checked(change_id, git_oid)?;
95 }
96
97 Ok(())
98 }
99
100 fn recover_mapping_tmp(&self) -> GitResult<()> {
101 let path = self.mapping_path();
102 let tmp_path = self.mapping_tmp_path();
103 if !tmp_path.exists() {
104 return Ok(());
105 }
106 if !path.exists() {
107 fs::rename(&tmp_path, &path)?;
108 } else {
109 fs::remove_file(&tmp_path)?;
110 }
111 Ok(())
112 }
113
114 fn mapping_bytes(&self) -> GitResult<Vec<u8>> {
115 let entries = self
116 .mapping
117 .iter()
118 .map(|(change_id, git_oid)| MappingEntry {
119 change_id: change_id.to_string_full(),
120 git_oid: git_oid.to_string(),
121 })
122 .collect();
123
124 let file = MappingFile { entries };
125 serde_json::to_vec_pretty(&file)
126 .map_err(|err| GitBridgeError::InvalidMapping(err.to_string()))
127 }
128
129 pub(crate) fn write_mapping_tmp_to_disk(&self) -> GitResult<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()?;
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) -> GitResult<()> {
146 let path = self.mapping_path();
147 let tmp_path = self.mapping_tmp_path();
148 if !tmp_path.exists() {
149 return Err(GitBridgeError::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 self.remove_legacy_mapping_file()?;
160 Ok(())
161 }
162
163 pub(crate) fn save_mapping_to_disk(&self) -> GitResult<()> {
164 self.write_mapping_tmp_to_disk()?;
165 objects::fault_inject::maybe_panic_at("mapping_after_tmp_before_commit");
172 self.commit_mapping_tmp_to_disk()
173 }
174
175 pub(crate) fn build_existing_mapping(&mut self, git_repo_path: Option<&Path>) -> GitResult<()> {
185 self.load_mapping_from_disk()?;
186
187 let repo = match git_repo_path {
188 Some(path) => super::git_core::open_repo(path)?,
189 None => self.open_git_repo()?,
190 };
191
192 let notes = super::git_notes::read_all_notes(&repo)?;
196 for (oid, note) in ¬es {
197 let change_id = ChangeId::parse(¬e.change_id)?;
198 self.mapping.insert_checked(change_id, *oid)?;
199 }
200
201 let commit_oids = collect_commit_oids(&repo)?;
205 for oid in commit_oids {
206 if self.mapping.has_git(oid) {
207 continue;
208 }
209 let commit = repo.find_commit(oid).map_err(git_err)?;
210 let message = commit.message_raw_sloppy();
211 let trailers = GitBridge::parse_trailers(&message.to_string());
212 if let Some(change_id) = trailers.get(GitBridge::TRAILER_CHANGE_ID) {
213 let change_id = ChangeId::parse(change_id)?;
214 self.mapping.insert_checked(change_id, oid)?;
215 }
216 }
217
218 self.save_mapping_to_disk()?;
219 Ok(())
220 }
221
222 #[cfg_attr(not(feature = "git-overlay"), allow(dead_code))]
223 pub(crate) fn prune_unreachable_mapping_entries(&mut self) -> GitResult<usize> {
224 let repo = self.open_git_repo()?;
225 self.load_mapping_from_disk()?;
226 let reachable: HashSet<_> = collect_commit_oids(&repo)?.into_iter().collect();
227 let removed = self.mapping.retain_git_object_set(&reachable);
228 if removed > 0 {
229 self.save_mapping_to_disk()?;
230 }
231 Ok(removed)
232 }
233}
234
235fn collect_commit_oids(repo: &gix::Repository) -> GitResult<Vec<gix::hash::ObjectId>> {
240 let mut tips = Vec::new();
241 for reference in repo
242 .references()
243 .map_err(git_err)?
244 .local_branches()
245 .map_err(git_err)?
246 {
247 let mut reference = reference.map_err(git_err)?;
248 let oid = reference.peel_to_id().map_err(git_err)?.detach();
249 if let Ok(object) = repo.find_object(oid)
250 && object.kind == gix::objs::Kind::Commit
251 {
252 tips.push(oid);
253 }
254 }
255 for reference in repo
256 .references()
257 .map_err(git_err)?
258 .tags()
259 .map_err(git_err)?
260 {
261 let mut reference = reference.map_err(git_err)?;
262 let oid = reference.peel_to_id().map_err(git_err)?.detach();
263 if let Ok(object) = repo.find_object(oid)
264 && object.kind == gix::objs::Kind::Commit
265 {
266 tips.push(oid);
267 }
268 }
269
270 let mut seen = HashSet::new();
271 for info in repo.rev_walk(tips).all().map_err(git_err)? {
272 seen.insert(info.map_err(git_err)?.id);
273 }
274
275 Ok(seen.into_iter().collect())
276}