1use objects::{
5 error::HeddleError,
6 object::{ChangeId, ContentHash, FileMode},
7};
8use repo::Repository as HeddleRepository;
9
10use crate::bridge::{
11 git_core::{GitBridge, GitBridgeError, GitResult, SyncMapping, git_err},
12 git_notes,
13 git_sync::{sync_marker_to_tag, sync_track_to_branch},
14 git_util::ExportStats,
15};
16
17const SUBMODULE_PREFIX: &str = "heddle-submodule:";
18
19pub fn export_state(
21 mapping: &mut SyncMapping,
22 heddle_repo: &HeddleRepository,
23 repo: &gix::Repository,
24 state_id: &ChangeId,
25) -> GitResult<gix::hash::ObjectId> {
26 let state = heddle_repo
27 .store()
28 .get_state(state_id)?
29 .ok_or(GitBridgeError::StateNotFound(*state_id))?;
30
31 let git_tree_oid = export_tree(heddle_repo, repo, &state.tree)?;
32 let hosted_url = heddle_repo
39 .config()
40 .hosted
41 .upstream_url
42 .as_deref()
43 .filter(|s| !s.is_empty());
44 let message =
45 GitBridge::build_commit_message_with_footer(&state, hosted_url, 0);
46 let parent_oids: Vec<gix::hash::ObjectId> = state
47 .parents
48 .iter()
49 .map(|parent_id| {
50 mapping
51 .get_git(parent_id)
52 .ok_or(GitBridgeError::StateNotFound(*parent_id))
53 })
54 .collect::<GitResult<Vec<_>>>()?;
55
56 let sig = GitBridge::state_to_signature(&state);
57 let mut committer_buf = gix::date::parse::TimeBuf::default();
58 let mut author_buf = gix::date::parse::TimeBuf::default();
59 let commit = repo
60 .new_commit_as(
61 sig.to_ref(&mut committer_buf),
62 sig.to_ref(&mut author_buf),
63 &message,
64 git_tree_oid,
65 parent_oids,
66 )
67 .map_err(git_err)?;
68 Ok(commit.id)
69}
70
71pub fn export_tree(
73 heddle_repo: &HeddleRepository,
74 repo: &gix::Repository,
75 tree_hash: &ContentHash,
76) -> GitResult<gix::hash::ObjectId> {
77 let tree = heddle_repo
78 .store()
79 .get_tree(tree_hash)?
80 .ok_or_else(|| HeddleError::NotFound(format!("tree {}", tree_hash)))?;
81
82 let empty_tree = gix::hash::ObjectId::empty_tree(repo.object_hash());
83 let mut editor = repo.edit_tree(empty_tree).map_err(git_err)?;
84
85 for entry in tree.entries() {
86 let (kind, id) = if entry.is_tree() {
87 (
88 gix::object::tree::EntryKind::Tree,
89 export_tree(heddle_repo, repo, &entry.hash)?,
90 )
91 } else {
92 let stub = heddle_repo
103 .redaction_stub_for_blob(&entry.hash)
104 .map_err(|err| HeddleError::Config(format!("redaction lookup failed: {err}")))?;
105
106 if let Some(stub_text) = stub {
107 let kind = match entry.mode {
110 FileMode::Symlink => gix::object::tree::EntryKind::Link,
111 FileMode::Executable => gix::object::tree::EntryKind::BlobExecutable,
112 _ => gix::object::tree::EntryKind::Blob,
113 };
114 let oid = repo
115 .write_blob(stub_text.as_bytes())
116 .map_err(git_err)?
117 .detach();
118 (kind, oid)
119 } else {
120 let blob = heddle_repo
121 .store()
122 .get_blob(&entry.hash)?
123 .ok_or_else(|| HeddleError::NotFound(format!("blob {}", entry.hash)))?;
124
125 if entry.mode == FileMode::Normal
126 && let Some(oid) = submodule_oid_from_blob(blob.content())
127 {
128 (gix::object::tree::EntryKind::Commit, oid)
129 } else {
130 let kind = match entry.mode {
131 FileMode::Normal => gix::object::tree::EntryKind::Blob,
132 FileMode::Executable => gix::object::tree::EntryKind::BlobExecutable,
133 FileMode::Symlink => gix::object::tree::EntryKind::Link,
134 };
135 let oid = repo.write_blob(blob.content()).map_err(git_err)?.detach();
136 (kind, oid)
137 }
138 }
139 };
140
141 editor.upsert(&entry.name, kind, id).map_err(git_err)?;
142 }
143
144 Ok(editor.write().map_err(git_err)?.detach())
145}
146
147pub fn export_all(bridge: &mut GitBridge) -> GitResult<ExportStats> {
149 bridge.init_mirror()?;
150
151 let states = bridge.heddle_repo.store().list_states()?;
152 let mut stats = ExportStats::default();
153
154 bridge.build_existing_mapping(None)?;
155
156 let sorted_states = bridge.sort_states_topologically(&states)?;
157 let repo = bridge.open_git_repo()?;
158 bridge.mapping.retain_git_objects(&repo);
159
160 for state_id in sorted_states {
161 if bridge.mapping.has_heddle(&state_id) {
166 continue;
167 }
168 let git_oid = export_state(&mut bridge.mapping, bridge.heddle_repo, &repo, &state_id)?;
169 bridge.mapping.insert(state_id, git_oid);
170 stats.states_exported += 1;
171
172 if let Some(state) = bridge.heddle_repo.store().get_state(&state_id)? {
176 let note = git_notes::HeddleNote::from_state(&state);
177 git_notes::write_note(&repo, git_oid, ¬e)?;
178 }
179 }
180
181 let note_targets: Vec<(ChangeId, gix::hash::ObjectId)> =
186 bridge.mapping.iter().map(|(c, o)| (*c, *o)).collect();
187 for (change_id, git_oid) in note_targets {
188 if git_notes::read_note(&repo, git_oid)?.is_none()
189 && let Some(state) = bridge.heddle_repo.store().get_state(&change_id)?
190 {
191 let note = git_notes::HeddleNote::from_state(&state);
192 git_notes::write_note(&repo, git_oid, ¬e)?;
193 }
194 }
195
196 let threads = bridge.heddle_repo.refs().list_threads()?;
197 for track_name in threads {
198 if let Some(state_id) = bridge.heddle_repo.refs().get_thread(&track_name)?
199 && let Some(git_oid) = bridge.mapping.get_git(&state_id)
200 {
201 sync_track_to_branch(&repo, &track_name, git_oid)?;
202 stats.threads_synced += 1;
203 }
204 }
205
206 let markers = bridge.heddle_repo.refs().list_markers()?;
207 for marker_name in markers {
208 if let Some(state_id) = bridge.heddle_repo.refs().get_marker(&marker_name)?
209 && let Some(git_oid) = bridge.mapping.get_git(&state_id)
210 {
211 sync_marker_to_tag(&repo, &marker_name, git_oid)?;
212 stats.markers_synced += 1;
213 }
214 }
215
216 bridge.save_mapping_to_disk()?;
217
218 Ok(stats)
219}
220
221fn submodule_oid_from_blob(content: &[u8]) -> Option<gix::hash::ObjectId> {
222 let text = std::str::from_utf8(content).ok()?;
223 let text = text.trim();
224 let trimmed = text.strip_prefix(SUBMODULE_PREFIX)?.trim();
225
226 trimmed.parse().ok()
227}