gnostr_asyncgit/sync/branch/
merge_commit.rs1use 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
12pub 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 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 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 let commit2 =
126 write_commit_file_at(&clone2, "test2.txt", "test", "commit2", Time::new(2, 0));
127
128 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 let bytes = fetch(&clone2_dir.into(), "master", None, None).unwrap();
142 assert!(bytes > 0);
143
144 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 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 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 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 assert_eq!(res, None);
219
220 let state = crate::sync::repo_state(&clone2_dir.path().to_str().unwrap().into()).unwrap();
221
222 assert_eq!(state, RepoState::Merge);
224
225 let commits = get_commit_ids(&clone1, 10);
227 assert_eq!(commits.len(), 1);
228 }
229}