Skip to main content

cli/bridge/
git_sync.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Sync threads/markers functionality for Git bridge.
3
4use 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
14/// Sync Heddle threads to Git branches.
15pub 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
32/// Sync Heddle markers to Git tags.
33pub 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
50/// Sync Git branches to Heddle threads.
51pub 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
98/// Sync Git tags to Heddle markers.
99pub 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
132/// Sync a Heddle thread to a Git branch.
133pub 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
173/// Sync a Heddle marker to a Git tag.
174pub 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            // A marker is a free-move ref (`classify_tag_move`): a legitimate
193            // RETARGET to a new served+minted OID must FORCE-set the mirror tag,
194            // not abort the whole export with a conflict (heddle#316 S1). The
195            // mirror is heddle-owned, so there is no out-of-band tip to spare
196            // here; the destination-side ownership gate (`classify_tag_move`,
197            // `recorded == old`) still spares an out-of-band DESTINATION tag.
198            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}