1use objects::object::{ChangeId, MarkerName, ThreadName};
5use refs::RefExpectation;
6use sley::{
7 ObjectId as SleyObjectId, RefPrecondition, ReferenceTarget, Repository as SleyRepository,
8};
9
10use crate::bridge::git_core::{
11 GitBridge, GitBridgeError, GitResult, git_err, thread_is_unclaimed_bootstrap,
12};
13
14pub fn sync_threads(bridge: &mut GitBridge) -> GitResult<usize> {
16 let repo = bridge.open_git_repo()?;
17 let mut stats = 0;
18
19 let threads = bridge.heddle_repo.refs().list_threads()?;
20 for track_name in threads {
21 if let Some(state_id) = bridge.heddle_repo.refs().get_thread(&track_name)?
22 && let Some(git_oid) = bridge.mapping.get_git(&state_id)
23 {
24 sync_track_to_branch(&repo, &track_name, git_oid)?;
25 stats += 1;
26 }
27 }
28
29 Ok(stats)
30}
31
32pub fn sync_markers(bridge: &mut GitBridge) -> GitResult<usize> {
34 let repo = bridge.open_git_repo()?;
35 let mut stats = 0;
36
37 let markers = bridge.heddle_repo.refs().list_markers()?;
38 for marker_name in markers {
39 if let Some(state_id) = bridge.heddle_repo.refs().get_marker(&marker_name)?
40 && let Some(git_oid) = bridge.mapping.get_git(&state_id)
41 {
42 sync_marker_to_tag(&repo, &marker_name, git_oid)?;
43 stats += 1;
44 }
45 }
46
47 Ok(stats)
48}
49
50pub fn sync_branches(bridge: &mut GitBridge) -> GitResult<usize> {
52 let repo = bridge.open_git_repo()?;
53 let mut stats = 0;
54
55 for reference in repo.references().list_refs().map_err(git_err)? {
56 let Some(name) = reference.name.strip_prefix("refs/heads/") else {
57 continue;
58 };
59 let Some(target) = peeled_oid(&repo, &reference.name, &reference.target)? else {
60 continue;
61 };
62 if let Some(change_id) = bridge.mapping.get_heddle(target) {
63 let tn = ThreadName::new(name);
64 if let Some(existing) = bridge.heddle_repo.refs().get_thread(&tn)?
65 && !thread_can_adopt_change(bridge, &existing, &change_id)?
66 {
67 return Err(GitBridgeError::GitHeddleThreadDiverged {
68 thread: name.to_string(),
69 branch: name.to_string(),
70 thread_change: existing,
71 branch_change: change_id,
72 });
73 }
74
75 bridge.heddle_repo.refs().set_thread(&tn, &change_id)?;
76 stats += 1;
77 }
78 }
79
80 Ok(stats)
81}
82
83fn thread_can_adopt_change(
84 bridge: &GitBridge<'_>,
85 existing: &ChangeId,
86 change_id: &ChangeId,
87) -> GitResult<bool> {
88 if existing == change_id {
89 return Ok(true);
90 }
91 if thread_is_unclaimed_bootstrap(bridge.heddle_repo, existing)? {
92 return Ok(true);
93 }
94 wire::is_ancestor(bridge.heddle_repo.store(), *existing, *change_id)
95 .map_err(|err| GitBridgeError::InvalidMapping(err.to_string()))
96}
97
98pub fn sync_tags(bridge: &mut GitBridge) -> GitResult<usize> {
100 let repo = bridge.open_git_repo()?;
101 let mut stats = 0;
102
103 for reference in repo.references().list_refs().map_err(git_err)? {
104 let Some(name) = reference.name.strip_prefix("refs/tags/") else {
105 continue;
106 };
107 let Some(oid) = peeled_oid(&repo, &reference.name, &reference.target)? else {
108 continue;
109 };
110
111 if let Some(change_id) = bridge.mapping.get_heddle(oid) {
112 let mn = MarkerName::new(name);
113 match bridge.heddle_repo.refs().get_marker(&mn) {
114 Ok(Some(existing)) if existing != change_id => bridge
115 .heddle_repo
116 .refs()
117 .set_marker_cas(&mn, RefExpectation::Any, &change_id)?,
118 Ok(_) => {}
119 Err(err) => return Err(err.into()),
120 }
121
122 if bridge.heddle_repo.refs().get_marker(&mn)?.is_none() {
123 bridge.heddle_repo.refs().create_marker(&mn, &change_id)?;
124 }
125 stats += 1;
126 }
127 }
128
129 Ok(stats)
130}
131
132pub fn sync_track_to_branch(
134 repo: &SleyRepository,
135 track_name: &str,
136 git_oid: SleyObjectId,
137) -> GitResult<()> {
138 let branch_ref = format!("refs/heads/{}", track_name);
139
140 if let Some(branch) = repo.find_reference(&branch_ref).map_err(git_err)? {
141 let existing = branch.peeled_oid(repo).map_err(git_err)?;
142 let Some(existing) = existing else {
143 return set_ref(
144 repo,
145 &branch_ref,
146 git_oid,
147 RefPrecondition::Any,
148 "heddle: sync thread",
149 );
150 };
151 if existing != git_oid {
152 ensure_commit_update_fast_forward(repo, &branch_ref, existing, git_oid)?;
153 set_ref(
154 repo,
155 &branch_ref,
156 git_oid,
157 RefPrecondition::MustExistAndMatch(ReferenceTarget::Direct(existing)),
158 "heddle: sync thread",
159 )?;
160 }
161 return Ok(());
162 }
163
164 set_ref(
165 repo,
166 &branch_ref,
167 git_oid,
168 RefPrecondition::MustNotExist,
169 "heddle: sync thread",
170 )
171}
172
173pub fn sync_marker_to_tag(
175 repo: &SleyRepository,
176 marker_name: &str,
177 git_oid: SleyObjectId,
178) -> GitResult<()> {
179 let tag_ref = format!("refs/tags/{}", marker_name);
180 if let Some(reference) = repo.find_reference(&tag_ref).map_err(git_err)? {
181 let existing = peeled_oid(repo, &tag_ref, &reference.target)?;
182 let Some(existing) = existing else {
183 return set_ref(
184 repo,
185 &tag_ref,
186 git_oid,
187 RefPrecondition::Any,
188 "heddle: sync marker",
189 );
190 };
191 if existing != git_oid {
192 set_ref(
199 repo,
200 &tag_ref,
201 git_oid,
202 RefPrecondition::Any,
203 "heddle: sync marker",
204 )?;
205 }
206 return Ok(());
207 }
208
209 set_ref(
210 repo,
211 &tag_ref,
212 git_oid,
213 RefPrecondition::MustNotExist,
214 "heddle: sync marker",
215 )
216}
217
218fn set_ref(
219 repo: &SleyRepository,
220 name: &str,
221 oid: SleyObjectId,
222 precondition: RefPrecondition,
223 message: &str,
224) -> GitResult<()> {
225 let old_oid = match &precondition {
226 RefPrecondition::MustExistAndMatch(ReferenceTarget::Direct(oid))
227 | RefPrecondition::ExistingMustMatch(ReferenceTarget::Direct(oid)) => *oid,
228 _ => SleyObjectId::null(repo.object_format()),
229 };
230 let refs = repo.references();
231 let mut tx = refs.transaction();
232 tx.update_to(
233 name,
234 ReferenceTarget::Direct(oid),
235 precondition,
236 Some(sley::plumbing::sley_refs::ReflogEntry {
237 old_oid,
238 new_oid: oid,
239 committer: bridge_identity(),
240 message: message.as_bytes().to_vec(),
241 }),
242 );
243 tx.commit().map_err(git_err)
244}
245
246fn ensure_commit_update_fast_forward(
247 repo: &SleyRepository,
248 ref_name: &str,
249 old: SleyObjectId,
250 new: SleyObjectId,
251) -> GitResult<()> {
252 if sley::plumbing::sley_rev::is_ancestor(
253 repo.git_dir(),
254 repo.object_format(),
255 repo.objects().as_ref(),
256 &old,
257 &new,
258 )
259 .map_err(git_err)?
260 {
261 Ok(())
262 } else {
263 Err(GitBridgeError::NonFastForwardRef {
264 name: ref_name.to_string(),
265 old,
266 new,
267 })
268 }
269}
270
271fn peeled_oid(
272 repo: &SleyRepository,
273 name: &str,
274 target: &ReferenceTarget,
275) -> GitResult<Option<SleyObjectId>> {
276 let Some(oid) = (match target {
277 ReferenceTarget::Direct(oid) => Ok(Some(*oid)),
278 ReferenceTarget::Symbolic(_) => {
279 let Some(reference) = repo.find_reference(name).map_err(git_err)? else {
280 return Ok(None);
281 };
282 reference.peeled_oid(repo).map_err(git_err)
283 }
284 })?
285 else {
286 return Ok(None);
287 };
288 match sley::plumbing::sley_rev::peel_to_commit(
289 repo.objects().as_ref(),
290 repo.object_format(),
291 &oid,
292 ) {
293 Ok(commit_oid) => Ok(Some(commit_oid)),
294 Err(_) => Ok(None),
295 }
296}
297
298fn bridge_identity() -> Vec<u8> {
299 let seconds = std::time::SystemTime::now()
300 .duration_since(std::time::UNIX_EPOCH)
301 .map(|d| d.as_secs() as i64)
302 .unwrap_or(0);
303 format!("Heddle <heddle@local> {seconds} +0000").into_bytes()
304}