Skip to content

Commit f349858

Browse files
author
Stephan Dilly
committed
branch merge from upstream (#384)
* better structure of all branch functions * support and unittest fast forward merge
1 parent 1f8295b commit f349858

File tree

7 files changed

+361
-92
lines changed

7 files changed

+361
-92
lines changed

Diff for: asyncgit/src/sync/branch/merge.rs

+162
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
//! merging from upstream
2+
3+
use super::BranchType;
4+
use crate::{
5+
error::{Error, Result},
6+
sync::utils,
7+
};
8+
use scopetime::scope_time;
9+
10+
///
11+
pub fn branch_merge_upstream_fastforward(
12+
repo_path: &str,
13+
branch: &str,
14+
) -> Result<()> {
15+
scope_time!("branch_merge_upstream");
16+
17+
let repo = utils::repo(repo_path)?;
18+
19+
let branch = repo.find_branch(branch, BranchType::Local)?;
20+
let upstream = branch.upstream()?;
21+
22+
let upstream_commit =
23+
upstream.into_reference().peel_to_commit()?;
24+
25+
let annotated =
26+
repo.find_annotated_commit(upstream_commit.id())?;
27+
28+
let (analysis, _) = repo.merge_analysis(&[&annotated])?;
29+
30+
if !analysis.is_fast_forward() {
31+
return Err(Error::Generic(
32+
"fast forward merge not possible".into(),
33+
));
34+
}
35+
36+
if analysis.is_unborn() {
37+
return Err(Error::Generic("head is unborn".into()));
38+
}
39+
40+
repo.checkout_tree(upstream_commit.as_object(), None)?;
41+
42+
repo.head()?.set_target(annotated.id(), "")?;
43+
44+
Ok(())
45+
}
46+
47+
#[cfg(test)]
48+
mod test {
49+
use super::*;
50+
use crate::sync::{
51+
commit, fetch_origin,
52+
remotes::push::push,
53+
stage_add_file,
54+
tests::{
55+
debug_cmd_print, get_commit_ids, repo_clone,
56+
repo_init_bare,
57+
},
58+
CommitId,
59+
};
60+
use git2::Repository;
61+
use std::{fs::File, io::Write, path::Path};
62+
63+
// write, stage and commit a file
64+
fn write_commit_file(
65+
repo: &Repository,
66+
file: &str,
67+
content: &str,
68+
commit_name: &str,
69+
) -> CommitId {
70+
File::create(
71+
repo.workdir().unwrap().join(file).to_str().unwrap(),
72+
)
73+
.unwrap()
74+
.write_all(content.as_bytes())
75+
.unwrap();
76+
77+
stage_add_file(
78+
repo.workdir().unwrap().to_str().unwrap(),
79+
Path::new(file),
80+
)
81+
.unwrap();
82+
83+
commit(repo.workdir().unwrap().to_str().unwrap(), commit_name)
84+
.unwrap()
85+
}
86+
87+
#[test]
88+
fn test_merge() {
89+
let (r1_dir, _repo) = repo_init_bare().unwrap();
90+
91+
let (clone1_dir, clone1) =
92+
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
93+
94+
let (clone2_dir, clone2) =
95+
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
96+
97+
// clone1
98+
99+
let commit1 =
100+
write_commit_file(&clone1, "test.txt", "test", "commit1");
101+
102+
push(
103+
clone1_dir.path().to_str().unwrap(),
104+
"origin",
105+
"master",
106+
false,
107+
None,
108+
None,
109+
)
110+
.unwrap();
111+
112+
// clone2
113+
debug_cmd_print(
114+
clone2_dir.path().to_str().unwrap(),
115+
"git pull --ff",
116+
);
117+
118+
let commit2 = write_commit_file(
119+
&clone2,
120+
"test2.txt",
121+
"test",
122+
"commit2",
123+
);
124+
125+
push(
126+
clone2_dir.path().to_str().unwrap(),
127+
"origin",
128+
"master",
129+
false,
130+
None,
131+
None,
132+
)
133+
.unwrap();
134+
135+
// clone1 again
136+
137+
let bytes = fetch_origin(
138+
clone1_dir.path().to_str().unwrap(),
139+
"master",
140+
)
141+
.unwrap();
142+
assert!(bytes > 0);
143+
144+
let bytes = fetch_origin(
145+
clone1_dir.path().to_str().unwrap(),
146+
"master",
147+
)
148+
.unwrap();
149+
assert_eq!(bytes, 0);
150+
151+
branch_merge_upstream_fastforward(
152+
clone1_dir.path().to_str().unwrap(),
153+
"master",
154+
)
155+
.unwrap();
156+
157+
let commits = get_commit_ids(&clone1, 10);
158+
assert_eq!(commits.len(), 2);
159+
assert_eq!(commits[1], commit1);
160+
assert_eq!(commits[0], commit2);
161+
}
162+
}

Diff for: asyncgit/src/sync/branch.rs renamed to asyncgit/src/sync/branch/mod.rs

+4-63
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
//!
1+
//! branch functions
2+
3+
pub mod merge;
4+
pub mod rename;
25

36
use super::{
47
remotes::get_default_remote_in_repo, utils::bytes2string,
@@ -182,22 +185,6 @@ pub fn delete_branch(
182185
Ok(())
183186
}
184187

185-
/// Rename the branch reference
186-
pub fn rename_branch(
187-
repo_path: &str,
188-
branch_ref: &str,
189-
new_name: &str,
190-
) -> Result<()> {
191-
scope_time!("delete_branch");
192-
193-
let repo = utils::repo(repo_path)?;
194-
let branch_as_ref = repo.find_reference(branch_ref)?;
195-
let mut branch = git2::Branch::wrap(branch_as_ref);
196-
branch.rename(new_name, true)?;
197-
198-
Ok(())
199-
}
200-
201188
/// creates a new branch pointing to current HEAD commit and updating HEAD to new branch
202189
pub fn create_branch(repo_path: &str, name: &str) -> Result<()> {
203190
scope_time!("create_branch");
@@ -404,49 +391,3 @@ mod test_delete_branch {
404391
);
405392
}
406393
}
407-
408-
#[cfg(test)]
409-
mod test_rename_branch {
410-
use super::*;
411-
use crate::sync::tests::repo_init;
412-
413-
#[test]
414-
fn test_rename_branch() {
415-
let (_td, repo) = repo_init().unwrap();
416-
let root = repo.path().parent().unwrap();
417-
let repo_path = root.as_os_str().to_str().unwrap();
418-
419-
create_branch(repo_path, "branch1").unwrap();
420-
421-
checkout_branch(repo_path, "refs/heads/branch1").unwrap();
422-
423-
assert_eq!(
424-
repo.branches(None)
425-
.unwrap()
426-
.nth(0)
427-
.unwrap()
428-
.unwrap()
429-
.0
430-
.name()
431-
.unwrap()
432-
.unwrap(),
433-
"branch1"
434-
);
435-
436-
rename_branch(repo_path, "refs/heads/branch1", "AnotherName")
437-
.unwrap();
438-
439-
assert_eq!(
440-
repo.branches(None)
441-
.unwrap()
442-
.nth(0)
443-
.unwrap()
444-
.unwrap()
445-
.0
446-
.name()
447-
.unwrap()
448-
.unwrap(),
449-
"AnotherName"
450-
);
451-
}
452-
}

Diff for: asyncgit/src/sync/branch/rename.rs

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//! renaming of branches
2+
3+
use crate::{error::Result, sync::utils};
4+
use scopetime::scope_time;
5+
6+
/// Rename the branch reference
7+
pub fn rename_branch(
8+
repo_path: &str,
9+
branch_ref: &str,
10+
new_name: &str,
11+
) -> Result<()> {
12+
scope_time!("delete_branch");
13+
14+
let repo = utils::repo(repo_path)?;
15+
let branch_as_ref = repo.find_reference(branch_ref)?;
16+
let mut branch = git2::Branch::wrap(branch_as_ref);
17+
branch.rename(new_name, true)?;
18+
19+
Ok(())
20+
}
21+
22+
#[cfg(test)]
23+
mod test {
24+
use super::super::*;
25+
use super::rename_branch;
26+
use crate::sync::tests::repo_init;
27+
28+
#[test]
29+
fn test_rename_branch() {
30+
let (_td, repo) = repo_init().unwrap();
31+
let root = repo.path().parent().unwrap();
32+
let repo_path = root.as_os_str().to_str().unwrap();
33+
34+
create_branch(repo_path, "branch1").unwrap();
35+
36+
checkout_branch(repo_path, "refs/heads/branch1").unwrap();
37+
38+
assert_eq!(
39+
repo.branches(None)
40+
.unwrap()
41+
.nth(0)
42+
.unwrap()
43+
.unwrap()
44+
.0
45+
.name()
46+
.unwrap()
47+
.unwrap(),
48+
"branch1"
49+
);
50+
51+
rename_branch(repo_path, "refs/heads/branch1", "AnotherName")
52+
.unwrap();
53+
54+
assert_eq!(
55+
repo.branches(None)
56+
.unwrap()
57+
.nth(0)
58+
.unwrap()
59+
.unwrap()
60+
.0
61+
.name()
62+
.unwrap()
63+
.unwrap(),
64+
"AnotherName"
65+
);
66+
}
67+
}

Diff for: asyncgit/src/sync/mod.rs

+37-3
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,16 @@ mod logwalker;
1717
pub mod remotes;
1818
mod reset;
1919
mod stash;
20+
mod state;
2021
pub mod status;
2122
mod tags;
2223
pub mod utils;
2324

2425
pub use branch::{
2526
branch_compare_upstream, checkout_branch, create_branch,
26-
delete_branch, get_branches_info, rename_branch, BranchCompare,
27-
BranchInfo,
27+
delete_branch, get_branches_info,
28+
merge::branch_merge_upstream_fastforward, rename::rename_branch,
29+
BranchCompare, BranchInfo,
2830
};
2931
pub use commit::{amend, commit, tag};
3032
pub use commit_details::{
@@ -42,6 +44,7 @@ pub use logwalker::LogWalker;
4244
pub use remotes::{fetch_origin, get_default_remote, get_remotes};
4345
pub use reset::{reset_stage, reset_workdir};
4446
pub use stash::{get_stashes, stash_apply, stash_drop, stash_save};
47+
pub use state::{repo_state, RepoState};
4548
pub use tags::{get_tags, CommitTags, Tags};
4649
pub use utils::{
4750
get_head, get_head_tuple, is_bare_repo, is_repo, stage_add_all,
@@ -50,7 +53,10 @@ pub use utils::{
5053

5154
#[cfg(test)]
5255
mod tests {
53-
use super::status::{get_status, StatusType};
56+
use super::{
57+
status::{get_status, StatusType},
58+
CommitId, LogWalker,
59+
};
5460
use crate::error::Result;
5561
use git2::Repository;
5662
use std::process::Command;
@@ -120,6 +126,23 @@ mod tests {
120126
Ok((td, repo))
121127
}
122128

129+
///
130+
pub fn repo_clone(p: &str) -> Result<(TempDir, Repository)> {
131+
sandbox_config_files();
132+
133+
let td = TempDir::new()?;
134+
135+
let td_path = td.path().as_os_str().to_str().unwrap();
136+
137+
let repo = Repository::clone(p, td_path).unwrap();
138+
139+
let mut config = repo.config()?;
140+
config.set_str("user.name", "name")?;
141+
config.set_str("user.email", "email")?;
142+
143+
Ok((td, repo))
144+
}
145+
123146
/// Same as repo_init, but the repo is a bare repo (--bare)
124147
pub fn repo_init_bare() -> Result<(TempDir, Repository)> {
125148
let tmp_repo_dir = TempDir::new()?;
@@ -145,6 +168,17 @@ mod tests {
145168
eprintln!("\n----\n{}", cmd);
146169
}
147170

171+
/// helper to fetch commmit details using log walker
172+
pub fn get_commit_ids(
173+
r: &Repository,
174+
max_count: usize,
175+
) -> Vec<CommitId> {
176+
let mut commit_ids = Vec::<CommitId>::new();
177+
LogWalker::new(r).read(&mut commit_ids, max_count).unwrap();
178+
179+
commit_ids
180+
}
181+
148182
fn debug_cmd(path: &str, cmd: &str) -> String {
149183
let output = if cfg!(target_os = "windows") {
150184
Command::new("cmd")

0 commit comments

Comments
 (0)