|
| 1 | +//! merging from upstream |
| 2 | +
|
| 3 | +use super::BranchType; |
| 4 | +use crate::{ |
| 5 | + error::{Error, Result}, |
| 6 | + sync::{utils, CommitId}, |
| 7 | +}; |
| 8 | +use git2::MergeOptions; |
| 9 | +use scopetime::scope_time; |
| 10 | + |
| 11 | +/// merge upstream using a merge commit without conflicts. fails if not possible without conflicts |
| 12 | +pub fn merge_upstream_commit( |
| 13 | + repo_path: &str, |
| 14 | + branch_name: &str, |
| 15 | +) -> Result<CommitId> { |
| 16 | + scope_time!("merge_upstream_commit"); |
| 17 | + |
| 18 | + let repo = utils::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 = |
| 26 | + repo.find_annotated_commit(upstream_commit.id())?; |
| 27 | + |
| 28 | + let (analysis, _) = |
| 29 | + repo.merge_analysis(&[&annotated_upstream])?; |
| 30 | + |
| 31 | + if !analysis.is_normal() { |
| 32 | + return Err(Error::Generic( |
| 33 | + "normal merge not possible".into(), |
| 34 | + )); |
| 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 | + let mut opt = MergeOptions::default(); |
| 43 | + opt.fail_on_conflict(true); |
| 44 | + |
| 45 | + repo.merge(&[&annotated_upstream], Some(&mut opt), None)?; |
| 46 | + |
| 47 | + assert!(!repo.index()?.has_conflicts()); |
| 48 | + |
| 49 | + let signature = |
| 50 | + crate::sync::commit::signature_allow_undefined_name(&repo)?; |
| 51 | + let mut index = repo.index()?; |
| 52 | + let tree_id = index.write_tree()?; |
| 53 | + let tree = repo.find_tree(tree_id)?; |
| 54 | + |
| 55 | + let head_commit = repo.find_commit( |
| 56 | + crate::sync::utils::get_head_repo(&repo)?.into(), |
| 57 | + )?; |
| 58 | + let parents = vec![&head_commit, &upstream_commit]; |
| 59 | + |
| 60 | + //find remote url for this branch |
| 61 | + let remote_url = { |
| 62 | + let branch_refname = |
| 63 | + branch.get().name().ok_or_else(|| { |
| 64 | + Error::Generic(String::from( |
| 65 | + "branch refname not found", |
| 66 | + )) |
| 67 | + })?; |
| 68 | + let buf = repo.branch_upstream_remote(branch_refname)?; |
| 69 | + let remote = |
| 70 | + repo.find_remote(buf.as_str().ok_or_else(|| { |
| 71 | + Error::Generic(String::from("remote name not found")) |
| 72 | + })?)?; |
| 73 | + remote.url().unwrap_or_default().to_string() |
| 74 | + }; |
| 75 | + |
| 76 | + let commit_id = repo |
| 77 | + .commit( |
| 78 | + Some("HEAD"), |
| 79 | + &signature, |
| 80 | + &signature, |
| 81 | + format!("Merge '{}' from {}", branch_name, remote_url) |
| 82 | + .as_str(), |
| 83 | + &tree, |
| 84 | + parents.as_slice(), |
| 85 | + )? |
| 86 | + .into(); |
| 87 | + |
| 88 | + repo.cleanup_state()?; |
| 89 | + |
| 90 | + Ok(commit_id) |
| 91 | +} |
| 92 | + |
| 93 | +#[cfg(test)] |
| 94 | +mod test { |
| 95 | + use super::super::merge_ff::test::write_commit_file; |
| 96 | + use super::*; |
| 97 | + use crate::sync::{ |
| 98 | + branch_compare_upstream, |
| 99 | + remotes::{fetch_origin, push::push}, |
| 100 | + tests::{ |
| 101 | + debug_cmd_print, get_commit_ids, repo_clone, |
| 102 | + repo_init_bare, |
| 103 | + }, |
| 104 | + RepoState, |
| 105 | + }; |
| 106 | + |
| 107 | + #[test] |
| 108 | + fn test_merge_normal() { |
| 109 | + let (r1_dir, _repo) = repo_init_bare().unwrap(); |
| 110 | + |
| 111 | + let (clone1_dir, clone1) = |
| 112 | + repo_clone(r1_dir.path().to_str().unwrap()).unwrap(); |
| 113 | + |
| 114 | + let (clone2_dir, clone2) = |
| 115 | + repo_clone(r1_dir.path().to_str().unwrap()).unwrap(); |
| 116 | + |
| 117 | + let clone2_dir = clone2_dir.path().to_str().unwrap(); |
| 118 | + |
| 119 | + // clone1 |
| 120 | + |
| 121 | + let commit1 = |
| 122 | + write_commit_file(&clone1, "test.txt", "test", "commit1"); |
| 123 | + |
| 124 | + push( |
| 125 | + clone1_dir.path().to_str().unwrap(), |
| 126 | + "origin", |
| 127 | + "master", |
| 128 | + false, |
| 129 | + None, |
| 130 | + None, |
| 131 | + ) |
| 132 | + .unwrap(); |
| 133 | + |
| 134 | + // clone2 |
| 135 | + |
| 136 | + let commit2 = write_commit_file( |
| 137 | + &clone2, |
| 138 | + "test2.txt", |
| 139 | + "test", |
| 140 | + "commit2", |
| 141 | + ); |
| 142 | + |
| 143 | + //push should fail since origin diverged |
| 144 | + assert!(push( |
| 145 | + clone2_dir, "origin", "master", false, None, None, |
| 146 | + ) |
| 147 | + .is_err()); |
| 148 | + |
| 149 | + //lets fetch from origin |
| 150 | + let bytes = |
| 151 | + fetch_origin(clone2_dir, "master", None, None).unwrap(); |
| 152 | + assert!(bytes > 0); |
| 153 | + |
| 154 | + //we should be one commit behind |
| 155 | + assert_eq!( |
| 156 | + branch_compare_upstream(clone2_dir, "master") |
| 157 | + .unwrap() |
| 158 | + .behind, |
| 159 | + 1 |
| 160 | + ); |
| 161 | + |
| 162 | + let merge_commit = |
| 163 | + merge_upstream_commit(clone2_dir, "master").unwrap(); |
| 164 | + |
| 165 | + let state = crate::sync::repo_state(clone2_dir).unwrap(); |
| 166 | + |
| 167 | + assert_eq!(state, RepoState::Clean); |
| 168 | + |
| 169 | + let commits = get_commit_ids(&clone2, 10); |
| 170 | + assert_eq!(commits.len(), 3); |
| 171 | + assert_eq!(commits[0], merge_commit); |
| 172 | + assert_eq!(commits[1], commit2); |
| 173 | + assert_eq!(commits[2], commit1); |
| 174 | + |
| 175 | + //verify commit msg |
| 176 | + let details = |
| 177 | + crate::sync::get_commit_details(clone2_dir, merge_commit) |
| 178 | + .unwrap(); |
| 179 | + assert_eq!( |
| 180 | + details.message.unwrap().combine(), |
| 181 | + format!( |
| 182 | + "Merge 'master' from {}", |
| 183 | + r1_dir.path().to_str().unwrap() |
| 184 | + ) |
| 185 | + ); |
| 186 | + } |
| 187 | + |
| 188 | + #[test] |
| 189 | + fn test_merge_normal_conflict() { |
| 190 | + let (r1_dir, _repo) = repo_init_bare().unwrap(); |
| 191 | + |
| 192 | + let (clone1_dir, clone1) = |
| 193 | + repo_clone(r1_dir.path().to_str().unwrap()).unwrap(); |
| 194 | + |
| 195 | + let (clone2_dir, clone2) = |
| 196 | + repo_clone(r1_dir.path().to_str().unwrap()).unwrap(); |
| 197 | + |
| 198 | + // clone1 |
| 199 | + |
| 200 | + write_commit_file(&clone1, "test.bin", "test", "commit1"); |
| 201 | + |
| 202 | + debug_cmd_print( |
| 203 | + clone2_dir.path().to_str().unwrap(), |
| 204 | + "git status", |
| 205 | + ); |
| 206 | + |
| 207 | + push( |
| 208 | + clone1_dir.path().to_str().unwrap(), |
| 209 | + "origin", |
| 210 | + "master", |
| 211 | + false, |
| 212 | + None, |
| 213 | + None, |
| 214 | + ) |
| 215 | + .unwrap(); |
| 216 | + |
| 217 | + // clone2 |
| 218 | + |
| 219 | + write_commit_file(&clone2, "test.bin", "foobar", "commit2"); |
| 220 | + |
| 221 | + let bytes = fetch_origin( |
| 222 | + clone2_dir.path().to_str().unwrap(), |
| 223 | + "master", |
| 224 | + None, |
| 225 | + None, |
| 226 | + ) |
| 227 | + .unwrap(); |
| 228 | + assert!(bytes > 0); |
| 229 | + |
| 230 | + let res = merge_upstream_commit( |
| 231 | + clone2_dir.path().to_str().unwrap(), |
| 232 | + "master", |
| 233 | + ); |
| 234 | + |
| 235 | + //this should have failed cause it would create a conflict |
| 236 | + assert!(res.is_err()); |
| 237 | + |
| 238 | + let state = crate::sync::repo_state( |
| 239 | + clone2_dir.path().to_str().unwrap(), |
| 240 | + ) |
| 241 | + .unwrap(); |
| 242 | + |
| 243 | + //make sure we left the repo not in some merging state |
| 244 | + assert_eq!(state, RepoState::Clean); |
| 245 | + |
| 246 | + //check that we still only have the first commit |
| 247 | + let commits = get_commit_ids(&clone1, 10); |
| 248 | + assert_eq!(commits.len(), 1); |
| 249 | + } |
| 250 | +} |
0 commit comments