1use std::{ffi::CString, fmt::Debug};
2
3use anyhow::Context;
4use anyhow::Ok;
5use git2::ApplyOptions;
6use git2::Diff;
7use git2::DiffDelta;
8use git2::Index;
9use git2::MergeOptions;
10use git2::{Branch, Commit, Oid, Repository};
11use indoc::formatdoc;
12use tracing::info;
13
14use crate::git::SyncState;
15
16use super::CommitMetadata;
17use super::GitRepo;
18use super::UnTrackedCommit;
19
20#[derive(Clone)]
21pub struct TrackedCommit<'repo> {
22 repo: &'repo Repository,
23 git_repo: &'repo GitRepo,
24 commit: Commit<'repo>,
25 meta_data: CommitMetadata<'repo>,
26}
27
28impl<'repo> TrackedCommit<'repo> {
29 pub fn new(
30 repo: &'repo Repository,
31 git_repo: &'repo GitRepo,
32 commit: Commit<'repo>,
33 meta_data: CommitMetadata<'repo>,
34 ) -> Self {
35 Self {
36 repo,
37 git_repo,
38 commit,
39 meta_data,
40 }
41 }
42
43 pub fn remote_branch(&self) -> anyhow::Result<Branch> {
44 let remote_branch = self
45 .repo
46 .find_branch(
47 &format!("origin/{}", self.meta_data.remote_branch_name),
48 git2::BranchType::Remote,
49 )
50 .context("Find the remote branch")?;
51 Ok(remote_branch)
52 }
53
54 pub fn local_branch_head(&self) -> anyhow::Result<Commit> {
55 let commit_meta_data = &self.meta_data;
56 Ok(self.repo.find_commit(commit_meta_data.remote_commit)?)
57 }
58
59 pub fn as_commit(&self) -> &Commit {
60 &self.commit
61 }
62
63 pub fn commit(self) -> Commit<'repo> {
64 self.commit
65 }
66
67 pub fn meta_data(&self) -> &CommitMetadata {
68 &self.meta_data
69 }
70
71 pub fn update_local_branch_head(self) -> anyhow::Result<Self> {
87 let remote_commit = self.repo.find_commit(self.meta_data().remote_commit)?;
88
89 info!("Sync with branch head: {}", remote_commit.id());
90
91 let origin_main_commit = self.git_repo.base_commit()?;
92 let complete_index = self
93 .repo
94 .cherrypick_commit(
95 self.as_commit(),
96 &origin_main_commit,
97 0,
98 Some(MergeOptions::default().file_favor(git2::FileFavor::Theirs)),
99 )
100 .context("Cherry picking directly on master")?;
101
102 if complete_index.has_conflicts() {
103 anyhow::bail!("There are conflicts");
104 }
105
106 let patch = self.repo.diff_tree_to_index(
107 Some(&remote_commit.tree()?),
108 Some(&complete_index),
109 None,
110 )?;
111 let main_sync_patch = self.repo.diff_tree_to_tree(
113 Some(&remote_commit.tree()?),
114 Some(&origin_main_commit.tree()?),
115 None,
116 )?;
117
118 let mut files_in_main_patch = Vec::new();
119 main_sync_patch.foreach(
120 &mut |file_delta, _| {
121 files_in_main_patch.push((file_delta.old_file().id(), file_delta.new_file().id()));
122 true
123 },
124 None,
125 Some(&mut |_, _| true),
126 None,
127 )?;
128
129 println!("Main patch files: {:?}", files_in_main_patch);
130
131 let new_commit = self.split_and_apply_patch(remote_commit, &patch, |delta| {
132 if let Some(delta) = delta {
133 files_in_main_patch.contains(&(delta.old_file().id(), delta.new_file().id()))
134 } else {
135 panic!("delta callback without any DiffDelta");
136 }
137 })?;
138
139 if new_commit.is_none() {
140 drop(new_commit);
141 return std::result::Result::Ok(self);
142 }
143
144 let new_commit = new_commit.unwrap();
145 let new_commit_id = new_commit.id();
146 drop(new_commit);
147 info!("New patch commit {}", new_commit_id);
148 let new_meta = self.meta_data.update_commit(new_commit_id);
149 self.git_repo.save_meta_data(&self.commit, &new_meta)?;
150 std::result::Result::Ok(TrackedCommit {
151 repo: self.repo,
152 git_repo: self.git_repo,
153 commit: self.commit,
154 meta_data: new_meta,
155 })
156 }
157
158 fn split_and_apply_patch<F>(
159 &self,
160 parent: Commit,
161 patch: &Diff,
162 mut delta_cb: F,
163 ) -> anyhow::Result<Option<Commit<'_>>>
164 where
165 F: FnMut(Option<DiffDelta<'_>>) -> bool,
166 {
167 let mut new_index = self
168 .repo
169 .apply_to_tree(
170 &parent.tree()?,
171 patch,
172 Some(ApplyOptions::new().delta_callback(|delta| delta_cb(delta))),
173 )
174 .context("Apply commit patch to old branch")?;
175
176 let main_sync_commit = self
177 .commit_index(&mut new_index, &parent, "Sync with main!")?
178 .unwrap_or(parent);
179
180 let mut index2 = self
181 .repo
182 .apply_to_tree(
183 &main_sync_commit.tree()?,
184 patch,
185 Some(ApplyOptions::new().delta_callback(|delta| !delta_cb(delta))),
186 )
187 .context("Apply commit patch to old branch")?;
188
189 self.commit_index(&mut index2, &main_sync_commit, "Fixup!")
190 }
191
192 fn commit_index(
193 &self,
194 index: &mut Index,
195 parent: &Commit,
196 msg: &str,
197 ) -> anyhow::Result<Option<Commit<'_>>> {
198 if index.has_conflicts() {
199 for c in index.conflicts()? {
200 let c = c?;
201 println!(
202 "{} {} {}",
203 c.our
204 .as_ref()
205 .map(|our| String::from_utf8(our.path.clone()).unwrap())
206 .unwrap_or("NONE".to_string()),
207 c.their
208 .map(|our| String::from_utf8(our.path).unwrap())
209 .unwrap_or("NONE".to_string()),
210 c.ancestor
211 .map(|our| String::from_utf8(our.path).unwrap())
212 .unwrap_or("NONE".to_string())
213 );
214 }
215 panic!("Conflicts while cherry-picking");
216 }
217 if index.is_empty() {
218 return std::result::Result::Ok(None);
219 }
220 let tree_id = index.write_tree_to(self.repo)?;
221 if tree_id == parent.tree()?.id() {
222 return std::result::Result::Ok(None);
223 }
224 let tree = self.repo.find_tree(tree_id)?;
225 let new_commit = {
226 let signature = self.as_commit().author();
227 self.repo
228 .commit(None, &signature, &signature, msg, &tree, &[parent])?
229 };
230
231 std::result::Result::Ok(Some(self.repo.find_commit(new_commit)?))
232 }
233
234 pub fn merge_remote_head(self, new_parent: Option<&Commit>) -> anyhow::Result<Self> {
252 let remote_branch_commit = self.remote_branch()?.get().peel_to_commit()?;
254 let remote_branch_head = remote_branch_commit.id();
255 let local_branch_head = self.meta_data().remote_commit;
256 let merge_base = self
257 .repo
258 .merge_base(local_branch_head, remote_branch_head)?;
259
260 let new_remote_commit = if merge_base == local_branch_head {
261 self.repo.find_commit(remote_branch_head)?
262 } else if merge_base == remote_branch_head {
263 drop(remote_branch_commit);
264 return Ok(self);
265 } else {
266 let local_branch_commit = self.repo.find_commit(local_branch_head)?;
267 let oid = self.merge(&local_branch_commit, &remote_branch_commit)?;
268 self.repo.find_commit(oid)?
269 };
270
271 let new_remote_tree = new_remote_commit.tree()?;
272 let diff = self.repo.diff_tree_to_tree(
273 Some(&self.git_repo.base_commit()?.tree()?),
274 Some(&new_remote_tree),
275 None,
276 )?;
277
278 let parent_commit = if let Some(parent) = new_parent {
279 parent.clone()
280 } else {
281 self.commit.parent(0)?
282 };
283 let mut index = self
284 .repo
285 .apply_to_tree(&parent_commit.tree()?, &diff, None)?;
286 let tree_id = index.write_tree_to(self.repo)?;
287 let tree = self.repo.find_tree(tree_id)?;
288
289 let new_commit = {
290 let signature = self.as_commit().author();
291 self.repo.commit(
292 None,
293 &signature,
294 &signature,
295 self.commit.message().expect("Not valid UTF-8"),
296 &tree,
297 &[&parent_commit],
298 )?
299 };
300
301 drop(remote_branch_commit);
302 let new_commit = self.repo.find_commit(new_commit)?;
303 let new_meta_data = self.meta_data.update_commit(new_remote_commit.id());
304 self.git_repo.save_meta_data(&new_commit, &new_meta_data)?;
305
306 Ok(TrackedCommit::new(
307 self.repo,
308 self.git_repo,
309 new_commit,
310 new_meta_data,
311 ))
312 }
313
314 pub fn sync_with_main(mut self) -> anyhow::Result<Self> {
329 let local_branch_head = self.meta_data().remote_commit;
330 let merge_base = self
331 .repo
332 .merge_base(local_branch_head, self.as_commit().id())
333 .context("Find merge base of remote and main")?;
334 if merge_base == self.git_repo.base_commit()?.id() || merge_base == self.commit.id() {
335 Ok(self)
336 } else {
337 let local_branch_commit = self.repo.find_commit(local_branch_head)?;
338 let merge_oid = self
339 .merge(&self.git_repo.base_commit()?, &local_branch_commit)
340 .context("Merge origin/main with local_branch_head")?;
341
342 let _ = std::mem::replace(&mut self.meta_data.remote_commit, merge_oid);
343 self.git_repo
344 .save_meta_data(self.as_commit(), &self.meta_data)?;
345 Ok(self)
346 }
347 }
348
349 pub fn cont(
350 self,
351 new_remote_commit: &Commit<'repo>,
352 new_parent: Option<&Commit<'repo>>,
353 ) -> anyhow::Result<Self> {
354 let new_remote_tree = new_remote_commit.tree()?;
355 let diff = self.repo.diff_tree_to_tree(
356 Some(&self.git_repo.base_commit()?.tree()?),
357 Some(&new_remote_tree),
358 None,
359 )?;
360
361 let parent_commit = if let Some(parent) = new_parent {
362 parent.clone()
363 } else {
364 self.commit.parent(0)?
365 };
366 let mut index = self
367 .repo
368 .apply_to_tree(&parent_commit.tree()?, &diff, None)?;
369 let tree_id = index.write_tree_to(self.repo)?;
370 let tree = self.repo.find_tree(tree_id)?;
371
372 let new_commit = {
373 let signature = self.as_commit().author();
374 self.repo.commit(
375 None,
376 &signature,
377 &signature,
378 self.commit.message().expect("Not valid UTF-8"),
379 &tree,
380 &[&parent_commit],
381 )?
382 };
383
384 let new_commit = self.repo.find_commit(new_commit)?;
385 let new_meta_data = self.meta_data.update_commit(new_remote_commit.id());
386 self.git_repo.save_meta_data(&new_commit, &new_meta_data)?;
387
388 Ok(TrackedCommit::new(
389 self.repo,
390 self.git_repo,
391 new_commit,
392 new_meta_data,
393 ))
394 }
395
396 pub fn update_remote(self, new_remote_head: Oid) -> Self {
397 TrackedCommit {
398 repo: self.repo,
399 git_repo: self.git_repo,
400 commit: self.commit,
401 meta_data: self.meta_data.update_commit(new_remote_head),
402 }
403 }
404
405 fn merge(&self, commit1: &Commit, commit2: &Commit) -> anyhow::Result<Oid> {
406 let mut merge_index = self.repo.merge_commits(commit1, commit2, None)?;
407
408 if merge_index.has_conflicts() {
410 for c in merge_index.conflicts()? {
411 let c = c?;
412 println!("Conclict {:?}", CString::new(c.our.unwrap().path).unwrap())
413 }
414
415 self.repo.checkout_tree(commit1.tree()?.as_object(), None)?;
416 self.repo
417 .set_head_detached(commit1.id())
418 .context("Detach HEAD")?;
419 self.repo.merge(
420 &[&self.repo.find_annotated_commit(commit2.id())?],
421 None,
422 None,
423 )?;
424 self.git_repo.save_sync_state(&SyncState {
425 main_commit_id: self.commit.id().into(),
426 remote_commit_id: commit2.id().into(),
427 main_commit_parent_id: self.commit.parent(0)?.id().into(),
428 main_branch_name: self.git_repo.current_branch_name.clone(),
429 })?;
430 let message = formatdoc! {"
431 Unable to merge local commit ({local}) with commit from remote ({remote})
432 Once all the conflicts has been resolved, run 'ubr sync --continue'
433 ",
434 local = commit1.id(),
435 remote = commit2.id(),
436 };
437 anyhow::bail!(message);
438 }
439 if merge_index.is_empty() {
440 anyhow::bail!("Index is empty");
441 }
442 let tree = merge_index
443 .write_tree_to(self.repo)
444 .context("write index to tree")?;
445 let oid = self.repo.commit(
446 None,
447 &self.repo.signature().context("No signature")?,
448 &self.repo.signature()?,
449 "Merge",
450 &self.repo.find_tree(tree)?,
451 &[commit1, commit2],
452 )?;
453
454 Ok(oid)
455 }
456
457 pub(crate) fn untrack(self) -> anyhow::Result<UnTrackedCommit<'repo>> {
458 self.git_repo.remove_meta_data(&self.commit)?;
459 Ok(UnTrackedCommit::new(self.repo, self.git_repo, self.commit))
462 }
463}
464
465impl Debug for TrackedCommit<'_> {
466 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
467 let commit = &self.commit;
468 write!(
469 f,
470 "Tracked Commit: {:?} {:?}",
471 commit.id(),
472 commit.message()
473 )
474 }
475}