ubr/git/local_commit/
tracked_commit.rs

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    //
72    // Apply the diff between this commit and the self.meta_data.remote_commit
73    // and return the new TrackedCommit
74    //
75    //
76    //              *
77    //              |    * (Merge)
78    //              |   / \
79    //              *  /   * (remote_branch_head)
80    //              | * <-/------------------------ cherry-pick c1 local_branch_head (resolve conflicts by accepting theirs)
81    //              |  \ /
82    //        c1    *   * (local_branch_head)
83    //              |  /
84    //              | /
85    //  (origin)    *
86    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        // Split the patch
112        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    ///
235    /// Merge remote_branch_head with local_branch_head unless remote_branch_head any
236    /// of those are a direct dependant on the other.
237    ///
238    /// Will not update from remote.
239    /// ```text
240    ///                 *
241    ///                 |    * (Merge) <---- Produces this merge unless.
242    ///                 |   / \
243    ///                 *  /   * (remote_branch_head)
244    ///                 | * <-/------------------------ (local_branch_head)
245    ///                 |  \ /
246    ///           c1    *   *
247    ///                 |  /
248    ///                 | /
249    ///     (origin)    *
250    /// ```
251    pub fn merge_remote_head(self, new_parent: Option<&Commit>) -> anyhow::Result<Self> {
252        // TODO: This should not take in a parent. The rebase should happen after
253        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    //
315    //
316    //                         * (Merge with 'main') <---- Produces this merge
317    //                 *      /  \
318    //                 |     /    * (Merge)
319    //                 |    /    / \
320    //           c1    *   /    /   * (remote_branch_head)
321    //                 |  /    * <-/------------------------(local_branch_head)
322    //                 | /      \ /
323    //     (origin)    *         *
324    //                 |        /
325    //                 |       /
326    //                 *------/
327    //
328    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        //self.repo.merge_analysis_for_ref
409        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        //self.git_repo.remove_remote_branch(&self.meta_data.remote_branch_name)?;
460
461        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}