Skip to content

Commit b5f1e76

Browse files
author
Stephan Dilly
authored
Remote branches (#618)
* allow checking out remote branch * set tracking branch on checking out remote * fix unittests by making branch list stable sorted by name
1 parent 6e231ad commit b5f1e76

File tree

9 files changed

+343
-61
lines changed

9 files changed

+343
-61
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111
- `[w]` key to toggle between staging/workdir [[@terhechte](https://github.com/terhechte)] ([#595](https://github.com/extrawurst/gitui/issues/595))
12+
- view/checkout remote branches ([#617](https://github.com/extrawurst/gitui/issues/617))
1213

1314
### Fixed
1415
- push branch to its tracking remote ([#597](https://github.com/extrawurst/gitui/issues/597))

assets/vim_style_key_config.ron

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
rename_branch: ( code: Char('r'), modifiers: ( bits: 0,),),
7171
select_branch: ( code: Char('b'), modifiers: ( bits: 0,),),
7272
delete_branch: ( code: Char('D'), modifiers: ( bits: 1,),),
73+
toggle_remote_branches: ( code: Char('t'), modifiers: ( bits: 0,),),
7374
push: ( code: Char('p'), modifiers: ( bits: 0,),),
7475
force_push: ( code: Char('P'), modifiers: ( bits: 1,),),
7576
pull: ( code: Char('f'), modifiers: ( bits: 0,),),

asyncgit/src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ pub enum Error {
1818
#[error("git: work dir error")]
1919
NoWorkDir,
2020

21+
#[error("git: uncommitted changes")]
22+
UncommittedChanges,
23+
2124
#[error("io error:{0}")]
2225
Io(#[from] std::io::Error),
2326

asyncgit/src/sync/branch/mod.rs

Lines changed: 219 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,27 @@ pub(crate) fn get_branch_name_repo(
4545
}
4646

4747
///
48+
#[derive(Debug)]
49+
pub struct LocalBranch {
50+
///
51+
pub is_head: bool,
52+
///
53+
pub has_upstream: bool,
54+
///
55+
pub remote: Option<String>,
56+
}
57+
58+
///
59+
#[derive(Debug)]
60+
pub enum BranchDetails {
61+
///
62+
Local(LocalBranch),
63+
///
64+
Remote,
65+
}
66+
67+
///
68+
#[derive(Debug)]
4869
pub struct BranchInfo {
4970
///
5071
pub name: String,
@@ -55,20 +76,37 @@ pub struct BranchInfo {
5576
///
5677
pub top_commit: CommitId,
5778
///
58-
pub is_head: bool,
59-
///
60-
pub has_upstream: bool,
61-
///
62-
pub remote: Option<String>,
79+
pub details: BranchDetails,
80+
}
81+
82+
impl BranchInfo {
83+
/// returns details about local branch or None
84+
pub fn local_details(&self) -> Option<&LocalBranch> {
85+
if let BranchDetails::Local(details) = &self.details {
86+
return Some(details);
87+
}
88+
89+
None
90+
}
6391
}
6492

65-
/// returns a list of `BranchInfo` with a simple summary of info about a single branch
66-
pub fn get_branches_info(repo_path: &str) -> Result<Vec<BranchInfo>> {
93+
/// returns a list of `BranchInfo` with a simple summary on each branch
94+
/// `local` filters for local branches otherwise remote branches will be returned
95+
pub fn get_branches_info(
96+
repo_path: &str,
97+
local: bool,
98+
) -> Result<Vec<BranchInfo>> {
6799
scope_time!("get_branches_info");
68100

101+
let filter = if local {
102+
BranchType::Local
103+
} else {
104+
BranchType::Remote
105+
};
106+
69107
let repo = utils::repo(repo_path)?;
70-
let branches_for_display = repo
71-
.branches(Some(BranchType::Local))?
108+
let mut branches_for_display: Vec<BranchInfo> = repo
109+
.branches(Some(filter))?
72110
.map(|b| {
73111
let branch = b?.0;
74112
let top_commit = branch.get().peel_to_commit()?;
@@ -82,21 +120,31 @@ pub fn get_branches_info(repo_path: &str) -> Result<Vec<BranchInfo>> {
82120
.and_then(|buf| buf.as_str())
83121
.map(String::from);
84122

123+
let details = if local {
124+
BranchDetails::Local(LocalBranch {
125+
is_head: branch.is_head(),
126+
has_upstream: upstream.is_ok(),
127+
remote,
128+
})
129+
} else {
130+
BranchDetails::Remote
131+
};
132+
85133
Ok(BranchInfo {
86134
name: bytes2string(branch.name_bytes()?)?,
87135
reference,
88136
top_commit_message: bytes2string(
89137
top_commit.summary_bytes().unwrap_or_default(),
90138
)?,
91139
top_commit: top_commit.id().into(),
92-
is_head: branch.is_head(),
93-
has_upstream: upstream.is_ok(),
94-
remote,
140+
details,
95141
})
96142
})
97143
.filter_map(Result::ok)
98144
.collect();
99145

146+
branches_for_display.sort_by(|a, b| a.name.cmp(&b.name));
147+
100148
Ok(branches_for_display)
101149
}
102150

@@ -212,10 +260,52 @@ pub fn checkout_branch(
212260
}
213261
Ok(())
214262
} else {
215-
Err(Error::Generic(
216-
format!("Cannot change branch. There are unstaged/staged changes which have not been committed/stashed. There is {:?} changes preventing checking out a different branch.", statuses.len()),
217-
))
263+
Err(Error::UncommittedChanges)
264+
}
265+
}
266+
267+
///
268+
pub fn checkout_remote_branch(
269+
repo_path: &str,
270+
branch: &BranchInfo,
271+
) -> Result<()> {
272+
scope_time!("checkout_remote_branch");
273+
274+
let repo = utils::repo(repo_path)?;
275+
let cur_ref = repo.head()?;
276+
277+
if !repo
278+
.statuses(Some(
279+
git2::StatusOptions::new().include_ignored(false),
280+
))?
281+
.is_empty()
282+
{
283+
return Err(Error::UncommittedChanges);
284+
}
285+
286+
let name = if let Some(pos) = branch.name.rfind('/') {
287+
branch.name[pos..].to_string()
288+
} else {
289+
branch.name.clone()
290+
};
291+
292+
let commit = repo.find_commit(branch.top_commit.into())?;
293+
let mut new_branch = repo.branch(&name, &commit, false)?;
294+
new_branch.set_upstream(Some(&branch.name))?;
295+
296+
repo.set_head(
297+
bytes2string(new_branch.into_reference().name_bytes())?
298+
.as_str(),
299+
)?;
300+
301+
if let Err(e) = repo.checkout_head(Some(
302+
git2::build::CheckoutBuilder::new().force(),
303+
)) {
304+
// This is safe beacuse cur_ref was just found
305+
repo.set_head(bytes2string(cur_ref.name_bytes())?.as_str())?;
306+
return Err(Error::Git(e));
218307
}
308+
Ok(())
219309
}
220310

221311
/// The user must not be on the branch for the branch to be deleted
@@ -341,7 +431,7 @@ mod tests_branches {
341431
let repo_path = root.as_os_str().to_str().unwrap();
342432

343433
assert_eq!(
344-
get_branches_info(repo_path)
434+
get_branches_info(repo_path, true)
345435
.unwrap()
346436
.iter()
347437
.map(|b| b.name.clone())
@@ -359,7 +449,7 @@ mod tests_branches {
359449
create_branch(repo_path, "test").unwrap();
360450

361451
assert_eq!(
362-
get_branches_info(repo_path)
452+
get_branches_info(repo_path, true)
363453
.unwrap()
364454
.iter()
365455
.map(|b| b.name.clone())
@@ -405,7 +495,7 @@ mod tests_branches {
405495
);
406496

407497
//verify we got only master right now
408-
let branches = get_branches_info(repo_path).unwrap();
498+
let branches = get_branches_info(repo_path, true).unwrap();
409499
assert_eq!(branches.len(), 1);
410500
assert_eq!(branches[0].name, String::from("master"));
411501

@@ -423,10 +513,26 @@ mod tests_branches {
423513
"git checkout --track r2/r2branch",
424514
);
425515

426-
let branches = get_branches_info(repo_path).unwrap();
516+
let branches = get_branches_info(repo_path, true).unwrap();
427517
assert_eq!(branches.len(), 3);
428-
assert_eq!(branches[1].remote.as_ref().unwrap(), "r1");
429-
assert_eq!(branches[2].remote.as_ref().unwrap(), "r2");
518+
assert_eq!(
519+
branches[1]
520+
.local_details()
521+
.unwrap()
522+
.remote
523+
.as_ref()
524+
.unwrap(),
525+
"r1"
526+
);
527+
assert_eq!(
528+
branches[2]
529+
.local_details()
530+
.unwrap()
531+
.remote
532+
.as_ref()
533+
.unwrap(),
534+
"r2"
535+
);
430536

431537
assert_eq!(
432538
get_branch_remote(repo_path, "r1branch")
@@ -545,3 +651,95 @@ mod test_delete_branch {
545651
);
546652
}
547653
}
654+
655+
#[cfg(test)]
656+
mod test_remote_branches {
657+
use super::*;
658+
use crate::sync::remotes::push::push;
659+
use crate::sync::tests::{
660+
repo_clone, repo_init_bare, write_commit_file,
661+
};
662+
663+
#[test]
664+
fn test_remote_branches() {
665+
let (r1_dir, _repo) = repo_init_bare().unwrap();
666+
667+
let (clone1_dir, clone1) =
668+
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
669+
670+
let clone1_dir = clone1_dir.path().to_str().unwrap();
671+
672+
// clone1
673+
674+
write_commit_file(&clone1, "test.txt", "test", "commit1");
675+
676+
push(clone1_dir, "origin", "master", false, None, None)
677+
.unwrap();
678+
679+
create_branch(clone1_dir, "foo").unwrap();
680+
681+
write_commit_file(&clone1, "test.txt", "test2", "commit2");
682+
683+
push(clone1_dir, "origin", "foo", false, None, None).unwrap();
684+
685+
// clone2
686+
687+
let (clone2_dir, _clone2) =
688+
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
689+
690+
let clone2_dir = clone2_dir.path().to_str().unwrap();
691+
692+
let local_branches =
693+
get_branches_info(clone2_dir, true).unwrap();
694+
695+
assert_eq!(local_branches.len(), 1);
696+
697+
let branches = get_branches_info(clone2_dir, false).unwrap();
698+
assert_eq!(dbg!(&branches).len(), 3);
699+
assert_eq!(&branches[0].name, "origin/HEAD");
700+
assert_eq!(&branches[1].name, "origin/foo");
701+
assert_eq!(&branches[2].name, "origin/master");
702+
}
703+
704+
#[test]
705+
fn test_checkout_remote_branch() {
706+
let (r1_dir, _repo) = repo_init_bare().unwrap();
707+
708+
let (clone1_dir, clone1) =
709+
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
710+
let clone1_dir = clone1_dir.path().to_str().unwrap();
711+
712+
// clone1
713+
714+
write_commit_file(&clone1, "test.txt", "test", "commit1");
715+
push(clone1_dir, "origin", "master", false, None, None)
716+
.unwrap();
717+
create_branch(clone1_dir, "foo").unwrap();
718+
write_commit_file(&clone1, "test.txt", "test2", "commit2");
719+
push(clone1_dir, "origin", "foo", false, None, None).unwrap();
720+
721+
// clone2
722+
723+
let (clone2_dir, _clone2) =
724+
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
725+
726+
let clone2_dir = clone2_dir.path().to_str().unwrap();
727+
728+
let local_branches =
729+
get_branches_info(clone2_dir, true).unwrap();
730+
731+
assert_eq!(local_branches.len(), 1);
732+
733+
let branches = get_branches_info(clone2_dir, false).unwrap();
734+
735+
// checkout origin/foo
736+
checkout_remote_branch(clone2_dir, &branches[1]).unwrap();
737+
738+
assert_eq!(
739+
get_branches_info(clone2_dir, true).unwrap().len(),
740+
2
741+
);
742+
743+
assert_eq!(&get_branch_name(clone2_dir).unwrap(), "foo");
744+
}
745+
}

asyncgit/src/sync/commits_info.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ impl CommitId {
2121
self.0
2222
}
2323

24-
///
24+
/// 7 chars short hash
2525
pub fn get_short_string(&self) -> String {
2626
self.to_string().chars().take(7).collect()
2727
}

asyncgit/src/sync/remotes/mod.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
pub(crate) mod push;
44
pub(crate) mod tags;
55

6-
use self::push::ProgressNotification;
7-
use super::cred::BasicAuthCredential;
86
use crate::{
97
error::{Error, Result},
10-
sync::utils,
8+
sync::{
9+
cred::BasicAuthCredential,
10+
remotes::push::ProgressNotification, utils,
11+
},
1112
};
1213
use crossbeam_channel::Sender;
1314
use git2::{FetchOptions, Repository};

0 commit comments

Comments
 (0)