gnostr_asyncgit/sync/branch/
merge_commit.rs

1//! merging from upstream
2
3use git2::Commit;
4use scopetime::scope_time;
5
6use super::BranchType;
7use crate::{
8    error::{Error, Result},
9    sync::{merge_msg, repository::repo, CommitId, RepoPath},
10};
11
12/// merge upstream using a merge commit if we did not create
13/// conflicts. if we did not create conflicts we create a merge commit
14/// and return the commit id. Otherwise we return `None`
15pub fn merge_upstream_commit(repo_path: &RepoPath, branch_name: &str) -> Result<Option<CommitId>> {
16    scope_time!("merge_upstream_commit");
17
18    let repo = repo(repo_path)?;
19
20    let branch = repo.find_branch(branch_name, BranchType::Local)?;
21    let upstream = branch.upstream()?;
22
23    let upstream_commit = upstream.get().peel_to_commit()?;
24
25    let annotated_upstream = repo.reference_to_annotated_commit(&upstream.into_reference())?;
26
27    let (analysis, pref) = repo.merge_analysis(&[&annotated_upstream])?;
28
29    if !analysis.is_normal() {
30        return Err(Error::Generic("normal merge not possible".into()));
31    }
32
33    if analysis.is_fast_forward() && pref.is_fastforward_only() {
34        return Err(Error::Generic("ff merge would be possible".into()));
35    }
36
37    //TODO: support merge on unborn?
38    if analysis.is_unborn() {
39        return Err(Error::Generic("head is unborn".into()));
40    }
41
42    repo.merge(&[&annotated_upstream], None, None)?;
43
44    if !repo.index()?.has_conflicts() {
45        let msg = merge_msg(repo_path)?;
46
47        let commit_id = commit_merge_with_head(&repo, &[upstream_commit], &msg)?;
48
49        return Ok(Some(commit_id));
50    }
51
52    Ok(None)
53}
54
55pub(crate) fn commit_merge_with_head(
56    repo: &git2::Repository,
57    commits: &[Commit],
58    msg: &str,
59) -> Result<CommitId> {
60    let signature = crate::sync::commit::signature_allow_undefined_name(repo)?;
61    let mut index = repo.index()?;
62    let tree_id = index.write_tree()?;
63    let tree = repo.find_tree(tree_id)?;
64    let head_commit = repo.find_commit(crate::sync::utils::get_head_repo(repo)?.into())?;
65
66    let mut parents = vec![&head_commit];
67    parents.extend(commits);
68
69    let commit_id = repo
70        .commit(
71            Some("HEAD"),
72            &signature,
73            &signature,
74            msg,
75            &tree,
76            parents.as_slice(),
77        )?
78        .into();
79    repo.cleanup_state()?;
80    Ok(commit_id)
81}
82
83#[cfg(test)]
84mod test {
85    use git2::Time;
86
87    use super::*;
88    use crate::sync::{
89        branch_compare_upstream,
90        remotes::{fetch, push::push_branch},
91        tests::{
92            debug_cmd_print, get_commit_ids, repo_clone, repo_init_bare, write_commit_file,
93            write_commit_file_at,
94        },
95        RepoState,
96    };
97
98    #[test]
99    fn test_merge_normal() {
100        let (r1_dir, _repo) = repo_init_bare().unwrap();
101
102        let (clone1_dir, clone1) = repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
103
104        let (clone2_dir, clone2) = repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
105
106        let clone2_dir = clone2_dir.path().to_str().unwrap();
107
108        // clone1
109
110        let commit1 = write_commit_file_at(&clone1, "test.txt", "test", "commit1", Time::new(1, 0));
111
112        push_branch(
113            &clone1_dir.path().to_str().unwrap().into(),
114            "origin",
115            "master",
116            false,
117            false,
118            None,
119            None,
120        )
121        .unwrap();
122
123        // clone2
124
125        let commit2 =
126            write_commit_file_at(&clone2, "test2.txt", "test", "commit2", Time::new(2, 0));
127
128        //push should fail since origin diverged
129        assert!(push_branch(
130            &clone2_dir.into(),
131            "origin",
132            "master",
133            false,
134            false,
135            None,
136            None,
137        )
138        .is_err());
139
140        //lets fetch from origin
141        let bytes = fetch(&clone2_dir.into(), "master", None, None).unwrap();
142        assert!(bytes > 0);
143
144        //we should be one commit behind
145        assert_eq!(
146            branch_compare_upstream(&clone2_dir.into(), "master")
147                .unwrap()
148                .behind,
149            1
150        );
151
152        let merge_commit = merge_upstream_commit(&clone2_dir.into(), "master")
153            .unwrap()
154            .unwrap();
155
156        let state = crate::sync::repo_state(&clone2_dir.into()).unwrap();
157        assert_eq!(state, RepoState::Clean);
158
159        assert!(!clone2.head_detached().unwrap());
160
161        let commits = get_commit_ids(&clone2, 10);
162        assert_eq!(commits.len(), 3);
163        assert_eq!(commits[0], merge_commit);
164        assert_eq!(commits[1], commit2);
165        assert_eq!(commits[2], commit1);
166
167        //verify commit msg
168        let details = crate::sync::get_commit_details(&clone2_dir.into(), merge_commit).unwrap();
169        assert_eq!(
170            details.message.unwrap().combine(),
171            String::from("Merge remote-tracking branch 'refs/remotes/origin/master'")
172        );
173    }
174
175    #[test]
176    fn test_merge_normal_non_ff() {
177        let (r1_dir, _repo) = repo_init_bare().unwrap();
178
179        let (clone1_dir, clone1) = repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
180
181        let (clone2_dir, clone2) = repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
182
183        // clone1
184
185        write_commit_file(&clone1, "test.bin", "test\nfooo", "commit1");
186
187        debug_cmd_print(&clone2_dir.path().to_str().unwrap().into(), "git status");
188
189        push_branch(
190            &clone1_dir.path().to_str().unwrap().into(),
191            "origin",
192            "master",
193            false,
194            false,
195            None,
196            None,
197        )
198        .unwrap();
199
200        // clone2
201
202        write_commit_file(&clone2, "test.bin", "foobar\ntest", "commit2");
203
204        let bytes = fetch(
205            &clone2_dir.path().to_str().unwrap().into(),
206            "master",
207            None,
208            None,
209        )
210        .unwrap();
211        assert!(bytes > 0);
212
213        let res =
214            merge_upstream_commit(&clone2_dir.path().to_str().unwrap().into(), "master").unwrap();
215
216        //this should not have committed cause we left conflicts
217        // behind
218        assert_eq!(res, None);
219
220        let state = crate::sync::repo_state(&clone2_dir.path().to_str().unwrap().into()).unwrap();
221
222        //validate the repo is in a merge state now
223        assert_eq!(state, RepoState::Merge);
224
225        //check that we still only have the first commit
226        let commits = get_commit_ids(&clone1, 10);
227        assert_eq!(commits.len(), 1);
228    }
229}