From d402c1f8ce57a08b330b3a08a85bc569279d376f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Sat, 28 May 2022 22:06:52 +0200 Subject: [PATCH 01/22] Remove accidentially added file --- BRANCH_NAMESPACE_SEPARATOR | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 BRANCH_NAMESPACE_SEPARATOR diff --git a/BRANCH_NAMESPACE_SEPARATOR b/BRANCH_NAMESPACE_SEPARATOR deleted file mode 100644 index e69de29..0000000 From b2542b341e551202c79e03b9186b0f3827ec7aee Mon Sep 17 00:00:00 2001 From: Max Volk <49154333+Kindskopf32@users.noreply.github.com> Date: Sat, 28 May 2022 13:33:57 +0200 Subject: [PATCH 02/22] Reword some of the documentation and spelling fixes --- CONTRIBUTING.md | 8 ++++---- docs/src/forge_integration.md | 4 ++-- docs/src/repos.md | 6 +++--- docs/src/worktrees.md | 16 ++++++++-------- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c358044..7464058 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,13 +37,13 @@ a separate e2e test suite in python (`just test-e2e`). To run all tests, run `just test`. -When contributing, consider whether it makes sense to add tests that to prevent -regressions in the future. When fixing bugs, it makes sense to add tests that -expose the wrong behaviour beforehand. +When contributing, consider whether it makes sense to add tests which could +prevent regressions in the future. When fixing bugs, it makes sense to add +tests that expose the wrong behaviour beforehand. ## Documentation The documentation lives in `docs` and uses -[mdBook](https://github.com/rust-lang/mdBook). Please document new user-facing +[mdBook](https://github.com/rust-lang/mdBook). Please document new user-facing features here! diff --git a/docs/src/forge_integration.md b/docs/src/forge_integration.md index 22b105b..ec33984 100644 --- a/docs/src/forge_integration.md +++ b/docs/src/forge_integration.md @@ -17,7 +17,7 @@ You will end up with your projects cloned into `~/projects/{your_github_username ## Authentication -The only currently supported authentication option is using personal access +The only currently supported authentication option is using a personal access token. ### GitHub @@ -27,7 +27,7 @@ See the GitHub documentation for personal access tokens: The only required permission is the "repo" scope. -### GitHub +### GitLab See the GitLab documentation for personal access tokens: [Link](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html). diff --git a/docs/src/repos.md b/docs/src/repos.md index cd8fe10..88cd7c4 100644 --- a/docs/src/repos.md +++ b/docs/src/repos.md @@ -77,6 +77,6 @@ $ grm repos status ## YAML By default, the repo configuration uses TOML. If you prefer YAML, just give it -a YAML file instead (file ending does not matter, `grm` will figure out the format -itself). For generating a configuration, pass `--format yaml` to `grm repo find` -to generate YAML instead of TOML. +a YAML file instead (file ending does not matter, `grm` will figure out the format). +For generating a configuration, pass `--format yaml` to `grm repo find` +which generates a YAML config instead of a TOML configuration. diff --git a/docs/src/worktrees.md b/docs/src/worktrees.md index 81cae45..d10fa73 100644 --- a/docs/src/worktrees.md +++ b/docs/src/worktrees.md @@ -5,11 +5,11 @@ The default workflow when using git is having your repository in a single directory. Then, you can check out a certain reference (usually a branch), which will update the files in the directory to match the state of that reference. Most of the time, -this is exactly what you need and works perfectly. But especially when you're using +this is exactly what you need and works perfectly. But especially when you're working with branches a lot, you may notice that there is a lot of work required to make -everything run smootly. +everything run smoothly. -Maybe you experienced the following: You're working on a feature branch. Then, +Maybe you have experienced the following: You're working on a feature branch. Then, for some reason, you have to change branches (maybe to investigate some issue). But you get the following: @@ -20,7 +20,7 @@ error: Your local changes to the following files would be overwritten by checkou Now you can create a temporary commit or stash your changes. In any case, you have some mental overhead before you can work on something else. Especially with stashes, you'll have to remember to do a `git stash pop` before resuming your work (I -cannot count the number of times where is "rediscovered" some code hidden in some +cannot count the number of times where I "rediscovered" some code hidden in some old stash I forgot about. And even worse: If you're currently in the process of resolving merge conflicts or an @@ -40,7 +40,7 @@ In any case, Git Worktrees are here for the rescue: independent checkouts of your repository on different directories. You can have multiple directories that correspond to different references in your repository. Each worktree has it's independent working tree (duh) and index, so there is no -to run into conflicts. Changing to a different branch is just a `cd` away (if +way to run into conflicts. Changing to a different branch is just a `cd` away (if the worktree is already set up). ## Worktrees in GRM @@ -210,7 +210,7 @@ your changes to. I'd rather not delete work that you cannot recover." Note that `grm` is very cautious here. As your repository will not be deleted, you could still recover the commits via [`git-reflog`](https://git-scm.com/docs/git-reflog). -But better safe then sorry! Note that you'd get a similar error message if your +But better safe than sorry! Note that you'd get a similar error message if your worktree had any uncommitted files, for the same reason. Now you can either commit & push your changes, or your tell `grm` that you know what you're doing: @@ -241,7 +241,7 @@ calls them "persistent branches" and treats them a bit differently: `grm wt delete`, which will not require a `--force` flag. Note that of course, actual changes in the worktree will still block an automatic cleanup! * As soon as you enable persistent branches, non-persistent branches will only - ever cleaned up when merged into a persistent branch. + ever be cleaned up when merged into a persistent branch. To elaborate: This is mostly relevant for a feature-branch workflow. Whenever a feature branch is merged, it can usually be thrown away. As merging is usually @@ -340,7 +340,7 @@ $ grm wt rebase --pull --rebase hell is there a `--rebase` flag in the `rebase` command?" Yes, it's kind of weird. Remember that `pull` only ever updates each worktree -to their remote branch, if possible. `rebase` rabases onto the **default** branch +to their remote branch, if possible. `rebase` rebases onto the **default** branch instead. The switches to `rebase` are just convenience, so you do not have to run two commands. From e04e8ceeeb5d81d20d1662e563b49765bd825dca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Sat, 28 May 2022 22:20:53 +0200 Subject: [PATCH 03/22] Use opaque type for auth token So we cannot accidentially output it, as it does not implement `Display`. --- src/auth.rs | 14 ++++++++++++-- src/provider/github.rs | 10 +++++----- src/provider/gitlab.rs | 10 +++++----- src/provider/mod.rs | 13 ++++++------- 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index d4a79a7..7c1490b 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,6 +1,16 @@ use std::process; -pub fn get_token_from_command(command: &str) -> Result { +#[derive(Clone)] +pub struct AuthToken(String); + +impl AuthToken { + pub fn access(&self) -> &str { + &self.0 + } + +} + +pub fn get_token_from_command(command: &str) -> Result { let output = process::Command::new("/usr/bin/env") .arg("sh") .arg("-c") @@ -32,5 +42,5 @@ pub fn get_token_from_command(command: &str) -> Result { .next() .ok_or_else(|| String::from("Output did not contain any newline"))?; - Ok(token.to_string()) + Ok(AuthToken(token.to_string())) } diff --git a/src/provider/github.rs b/src/provider/github.rs index e721ca3..8c05c53 100644 --- a/src/provider/github.rs +++ b/src/provider/github.rs @@ -6,7 +6,7 @@ use super::Filter; use super::JsonError; use super::Project; use super::Provider; -use super::SecretToken; +use super::auth; const PROVIDER_NAME: &str = "github"; const ACCEPT_HEADER_JSON: &str = "application/vnd.github.v3+json"; @@ -67,7 +67,7 @@ impl JsonError for GithubApiErrorResponse { pub struct Github { filter: Filter, - secret_token: SecretToken, + secret_token: auth::AuthToken, } impl Provider for Github { @@ -76,7 +76,7 @@ impl Provider for Github { fn new( filter: Filter, - secret_token: SecretToken, + secret_token: auth::AuthToken, api_url_override: Option, ) -> Result { if api_url_override.is_some() { @@ -96,8 +96,8 @@ impl Provider for Github { self.filter.clone() } - fn secret_token(&self) -> SecretToken { - self.secret_token.clone() + fn secret_token(&self) -> &auth::AuthToken { + &self.secret_token } fn auth_header_key() -> String { diff --git a/src/provider/gitlab.rs b/src/provider/gitlab.rs index 4f9853e..bbbfa99 100644 --- a/src/provider/gitlab.rs +++ b/src/provider/gitlab.rs @@ -6,7 +6,7 @@ use super::Filter; use super::JsonError; use super::Project; use super::Provider; -use super::SecretToken; +use super::auth; const PROVIDER_NAME: &str = "gitlab"; const ACCEPT_HEADER_JSON: &str = "application/json"; @@ -75,7 +75,7 @@ impl JsonError for GitlabApiErrorResponse { pub struct Gitlab { filter: Filter, - secret_token: SecretToken, + secret_token: auth::AuthToken, api_url_override: Option, } @@ -95,7 +95,7 @@ impl Provider for Gitlab { fn new( filter: Filter, - secret_token: SecretToken, + secret_token: auth::AuthToken, api_url_override: Option, ) -> Result { Ok(Self { @@ -113,8 +113,8 @@ impl Provider for Gitlab { self.filter.clone() } - fn secret_token(&self) -> SecretToken { - self.secret_token.clone() + fn secret_token(&self) -> &auth::AuthToken { + &self.secret_token } fn auth_header_key() -> String { diff --git a/src/provider/mod.rs b/src/provider/mod.rs index edfc74d..27c1e29 100644 --- a/src/provider/mod.rs +++ b/src/provider/mod.rs @@ -10,6 +10,7 @@ pub use github::Github; pub use gitlab::Gitlab; use super::repo; +use super::auth; use std::collections::HashMap; @@ -69,8 +70,6 @@ pub trait Project { fn private(&self) -> bool; } -type SecretToken = String; - #[derive(Clone)] pub struct Filter { users: Vec, @@ -117,7 +116,7 @@ pub trait Provider { fn new( filter: Filter, - secret_token: SecretToken, + secret_token: auth::AuthToken, api_url_override: Option, ) -> Result where @@ -125,7 +124,7 @@ pub trait Provider { fn name(&self) -> String; fn filter(&self) -> Filter; - fn secret_token(&self) -> SecretToken; + fn secret_token(&self) -> &auth::AuthToken; fn auth_header_key() -> String; fn get_user_projects( @@ -167,7 +166,7 @@ pub trait Provider { .header("accept", accept_header.unwrap_or("application/json")) .header( "authorization", - format!("{} {}", Self::auth_header_key(), &self.secret_token()), + format!("{} {}", Self::auth_header_key(), &self.secret_token().access()), ) .body(()) .map_err(|error| error.to_string())?; @@ -308,7 +307,7 @@ pub trait Provider { fn call( uri: &str, auth_header_key: &str, - secret_token: &str, + secret_token: &auth::AuthToken, accept_header: Option<&str>, ) -> Result> where @@ -322,7 +321,7 @@ where .header("accept", accept_header.unwrap_or("application/json")) .header( "authorization", - format!("{} {}", &auth_header_key, &secret_token), + format!("{} {}", &auth_header_key, &secret_token.access()), ) .body(()) .map_err(|error| ApiErrorResponse::String(error.to_string()))?; From 4f68a563c635bd8fc7637d66485a341e35ed306a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Sat, 28 May 2022 22:24:23 +0200 Subject: [PATCH 04/22] providers: Use references for field access --- src/provider/github.rs | 12 ++++++------ src/provider/gitlab.rs | 12 ++++++------ src/provider/mod.rs | 6 +++--- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/provider/github.rs b/src/provider/github.rs index 8c05c53..42a75cf 100644 --- a/src/provider/github.rs +++ b/src/provider/github.rs @@ -88,20 +88,20 @@ impl Provider for Github { }) } - fn name(&self) -> String { - String::from(PROVIDER_NAME) + fn name(&self) -> &str { + PROVIDER_NAME } - fn filter(&self) -> Filter { - self.filter.clone() + fn filter(&self) -> &Filter { + &self.filter } fn secret_token(&self) -> &auth::AuthToken { &self.secret_token } - fn auth_header_key() -> String { - "token".to_string() + fn auth_header_key() -> &'static str { + "token" } fn get_user_projects( diff --git a/src/provider/gitlab.rs b/src/provider/gitlab.rs index bbbfa99..2dc2132 100644 --- a/src/provider/gitlab.rs +++ b/src/provider/gitlab.rs @@ -105,20 +105,20 @@ impl Provider for Gitlab { }) } - fn name(&self) -> String { - String::from(PROVIDER_NAME) + fn name(&self) -> &str { + PROVIDER_NAME } - fn filter(&self) -> Filter { - self.filter.clone() + fn filter(&self) -> &Filter { + &self.filter } fn secret_token(&self) -> &auth::AuthToken { &self.secret_token } - fn auth_header_key() -> String { - "bearer".to_string() + fn auth_header_key() -> &'static str { + "bearer" } fn get_user_projects( diff --git a/src/provider/mod.rs b/src/provider/mod.rs index 27c1e29..a1c9a9c 100644 --- a/src/provider/mod.rs +++ b/src/provider/mod.rs @@ -122,10 +122,10 @@ pub trait Provider { where Self: Sized; - fn name(&self) -> String; - fn filter(&self) -> Filter; + fn name(&self) -> &str; + fn filter(&self) -> &Filter; fn secret_token(&self) -> &auth::AuthToken; - fn auth_header_key() -> String; + fn auth_header_key() -> &'static str; fn get_user_projects( &self, From 1a65a163a145d8a370a3c84990d6360843989b83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Mon, 13 Jun 2022 22:47:49 +0200 Subject: [PATCH 05/22] Use opaque type for auth token So we cannot accidentially output it, as it does not implement `Display`. --- src/auth.rs | 1 - src/provider/github.rs | 2 +- src/provider/gitlab.rs | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index 7c1490b..5ab5119 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -7,7 +7,6 @@ impl AuthToken { pub fn access(&self) -> &str { &self.0 } - } pub fn get_token_from_command(command: &str) -> Result { diff --git a/src/provider/github.rs b/src/provider/github.rs index 42a75cf..094b860 100644 --- a/src/provider/github.rs +++ b/src/provider/github.rs @@ -1,12 +1,12 @@ use serde::Deserialize; +use super::auth; use super::escape; use super::ApiErrorResponse; use super::Filter; use super::JsonError; use super::Project; use super::Provider; -use super::auth; const PROVIDER_NAME: &str = "github"; const ACCEPT_HEADER_JSON: &str = "application/vnd.github.v3+json"; diff --git a/src/provider/gitlab.rs b/src/provider/gitlab.rs index 2dc2132..fc4b216 100644 --- a/src/provider/gitlab.rs +++ b/src/provider/gitlab.rs @@ -1,12 +1,12 @@ use serde::Deserialize; +use super::auth; use super::escape; use super::ApiErrorResponse; use super::Filter; use super::JsonError; use super::Project; use super::Provider; -use super::auth; const PROVIDER_NAME: &str = "gitlab"; const ACCEPT_HEADER_JSON: &str = "application/json"; From c4fd1d0452d187c82d6a484d3aa8a328199bdcbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Mon, 13 Jun 2022 22:47:49 +0200 Subject: [PATCH 06/22] Refactor default_branch() for readability --- src/repo.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/repo.rs b/src/repo.rs index 48b1a65..70df251 100644 --- a/src/repo.rs +++ b/src/repo.rs @@ -1035,13 +1035,15 @@ impl RepoHandle { } pub fn default_branch(&self) -> Result { - match self.0.find_branch("main", git2::BranchType::Local) { - Ok(branch) => Ok(Branch(branch)), - Err(_) => match self.0.find_branch("master", git2::BranchType::Local) { - Ok(branch) => Ok(Branch(branch)), - Err(_) => Err(String::from("Could not determine default branch")), - }, + let branch_names = vec!["main", "master"]; + + for branch_name in &branch_names { + if let Ok(branch) = self.0.find_branch(branch_name, git2::BranchType::Local) { + return Ok(Branch(branch)) + } } + + Err(String::from("Could not determine default branch")) } // Looks like there is no distinguishing between the error cases From bc3001a4e684278a59b6c56f237c06e747f98577 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Mon, 13 Jun 2022 22:47:49 +0200 Subject: [PATCH 07/22] Add function to get basename of branch --- src/repo.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/repo.rs b/src/repo.rs index 70df251..b2bff1d 100644 --- a/src/repo.rs +++ b/src/repo.rs @@ -1396,6 +1396,15 @@ impl Branch<'_> { self.0.delete().map_err(convert_libgit2_error) } + pub fn basename(&self) -> Result { + let name = self.name()?; + if let Some((_prefix, basename)) = name.split_once('/') { + Ok(basename.to_string()) + } else { + Ok(name) + } + } + // only used internally in this module, exposes libgit2 details fn as_reference(&self) -> &git2::Reference { self.0.get() From f1e212ead9d311071cb16c2dcac94f9ac3075140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Mon, 13 Jun 2022 22:47:49 +0200 Subject: [PATCH 08/22] Add function to get all remote branches --- src/repo.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/repo.rs b/src/repo.rs index b2bff1d..afda6ab 100644 --- a/src/repo.rs +++ b/src/repo.rs @@ -659,6 +659,14 @@ impl RepoHandle { .collect::, String>>() } + pub fn remote_branches(&self) -> Result, String> { + self.0 + .branches(Some(git2::BranchType::Remote)) + .map_err(convert_libgit2_error)? + .map(|branch| Ok(Branch(branch.map_err(convert_libgit2_error)?.0))) + .collect::, String>>() + } + pub fn fetch(&self, remote_name: &str) -> Result<(), String> { let mut remote = self .0 From 581a513ebd2cbdc894592405209a477334702a5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Mon, 13 Jun 2022 22:47:49 +0200 Subject: [PATCH 09/22] Initialize local branches on clone --- src/repo.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/repo.rs b/src/repo.rs index afda6ab..7b99179 100644 --- a/src/repo.rs +++ b/src/repo.rs @@ -1548,6 +1548,24 @@ pub fn clone_repo( repo.rename_remote(&origin, &remote.name)?; } + // Initialize local branches. For all remote branches, we set up local + // tracking branches with the same name (just without the remote prefix). + for remote_branch in repo.remote_branches()? { + let local_branch_name = remote_branch.basename()?; + + if repo.find_local_branch(&local_branch_name).is_ok() { + continue; + } + + // Ignore /HEAD, as this is not something we can check out + if local_branch_name == "HEAD" { + continue; + } + + let mut local_branch = repo.create_branch(&local_branch_name, &remote_branch.commit()?)?; + local_branch.set_upstream(&remote.name, &local_branch_name)?; + } + // If there is no head_branch, we most likely cloned an empty repository and // there is no point in setting any upstreams. if let Ok(mut active_branch) = repo.head_branch() { From a8f8803a92e73f71dde38050dea2b694e58ffb23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Mon, 13 Jun 2022 22:47:49 +0200 Subject: [PATCH 10/22] Do not fail on empty clone target --- src/tree.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/tree.rs b/src/tree.rs index bf35179..1a1eb02 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -143,7 +143,13 @@ fn sync_repo(root_path: &Path, repo: &repo::Repo, init_worktree: bool) -> Result let mut newly_created = false; - if repo_path.exists() { + if repo_path.exists() + && repo_path + .read_dir() + .map_err(|error| error.to_string())? + .next() + .is_some() + { if repo.worktree_setup && !actual_git_directory.exists() { return Err(String::from( "Repo already exists, but is not using a worktree setup", From 6f4ae88260d2ca60d80bd7132f506b96fdf85586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Mon, 13 Jun 2022 22:47:49 +0200 Subject: [PATCH 11/22] Add some comments about repo syncing --- src/tree.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/tree.rs b/src/tree.rs index 1a1eb02..d765298 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -143,6 +143,28 @@ fn sync_repo(root_path: &Path, repo: &repo::Repo, init_worktree: bool) -> Result let mut newly_created = false; + // Syncing a repository can have a few different flows, depending on the repository + // that is to be cloned and the local directory: + // + // * If the local directory already exists, we have to make sure that it matches the + // worktree configuration, as there is no way to convert. If the sync is supposed + // to be worktree-aware, but the local directory is not, we abort. Note that we could + // also automatically convert here. In any case, the other direction (converting a + // worktree repository to non-worktree) cannot work, as we'd have to throw away the + // worktrees. + // + // * If the local directory does not yet exist, we have to actually do something ;). If + // no remote is specified, we just initialize a new repository (git init) and are done. + // + // If there are (potentially multiple) remotes configured, we have to clone. We assume + // that the first remote is the canonical one that we do the first clone from. After + // cloning, we just add the other remotes as usual (as if they were added to the config + // afterwards) + // + // Branch handling: + // + // Handling the branches on checkout is a bit magic. For minimum surprises, we just set + // up local tracking branches for all remote branches. if repo_path.exists() && repo_path .read_dir() From 73158e3d47afbd287c7f073caeec5b5b67a44cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Mon, 13 Jun 2022 22:47:49 +0200 Subject: [PATCH 12/22] Print ok-ish stuff to stdout --- e2e_tests/test_repos_sync.py | 1 - src/output.rs | 12 ++++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/e2e_tests/test_repos_sync.py b/e2e_tests/test_repos_sync.py index 8302532..bba5b7c 100644 --- a/e2e_tests/test_repos_sync.py +++ b/e2e_tests/test_repos_sync.py @@ -303,7 +303,6 @@ def test_repos_sync_root_is_file(configtype): cmd = grm(["repos", "sync", "config", "--config", config.name]) assert cmd.returncode != 0 - assert len(cmd.stdout) == 0 assert "not a directory" in cmd.stderr.lower() diff --git a/src/output.rs b/src/output.rs index b86aca6..3952249 100644 --- a/src/output.rs +++ b/src/output.rs @@ -20,12 +20,12 @@ pub fn print_repo_action(repo: &str, message: &str) { } pub fn print_action(message: &str) { - let stderr = Term::stderr(); + let stdout = Term::stdout(); let mut style = Style::new().yellow(); - if stderr.is_term() { + if stdout.is_term() { style = style.force_styling(true); } - stderr + stdout .write_line(&format!("[{}] {}", style.apply_to('\u{2699}'), &message)) .unwrap(); } @@ -46,13 +46,13 @@ pub fn print_repo_success(repo: &str, message: &str) { } pub fn print_success(message: &str) { - let stderr = Term::stderr(); + let stdout = Term::stdout(); let mut style = Style::new().green(); - if stderr.is_term() { + if stdout.is_term() { style = style.force_styling(true); } - stderr + stdout .write_line(&format!("[{}] {}", style.apply_to('\u{2714}'), &message)) .unwrap(); } From fad6f71876b14f0ebeeedc4a90c90d11facf4f20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Mon, 13 Jun 2022 22:47:49 +0200 Subject: [PATCH 13/22] Improve default branch guessing --- src/repo.rs | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 82 insertions(+), 4 deletions(-) diff --git a/src/repo.rs b/src/repo.rs index 7b99179..763705b 100644 --- a/src/repo.rs +++ b/src/repo.rs @@ -1042,12 +1042,76 @@ impl RepoHandle { }) } - pub fn default_branch(&self) -> Result { - let branch_names = vec!["main", "master"]; + pub fn get_remote_default_branch(&self, remote_name: &str) -> Result, String> { + // libgit2's `git_remote_default_branch()` and `Remote::default_branch()` + // need an actual connection to the remote, so they may fail. + if let Some(mut remote) = self.find_remote(remote_name)? { + if remote.connected() { + let remote = remote; // unmut + if let Ok(remote_default_branch) = remote.default_branch() { + return Ok(Some(self.find_local_branch(&remote_default_branch)?)); + }; + } + } - for branch_name in &branch_names { + // Note that /HEAD only exists after a normal clone, there is no way to get the + // remote HEAD afterwards. So this is a "best effort" approach. + if let Ok(remote_head) = self.find_remote_branch(remote_name, "HEAD") { + if let Some(pointer_name) = remote_head.as_reference().symbolic_target() { + if let Some(local_branch_name) = + pointer_name.strip_prefix(&format!("refs/remotes/{}/", remote_name)) + { + return Ok(Some(self.find_local_branch(local_branch_name)?)); + } else { + eprintln!("Remote HEAD ({}) pointer is invalid", pointer_name); + } + } else { + eprintln!("Remote HEAD does not point to a symbolic target"); + } + } + Ok(None) + } + + pub fn default_branch(&self) -> Result { + // This is a bit of a guessing game. + // + // In the best case, there is only one remote. Then, we can check /HEAD to get the + // default remote branch. + // + // If there are multiple remotes, we first check whether they all have the same + // /HEAD branch. If yes, good! If not, we use whatever "origin" uses, if that + // exists. If it does not, there is no way to reliably get a remote default branch. + // + // In this case, we just try to guess a local branch from a list. If even that does not + // work, well, bad luck. + let remotes = self.remotes()?; + + if remotes.len() == 1 { + let remote_name = &remotes[0]; + if let Some(default_branch) = self.get_remote_default_branch(remote_name)? { + return Ok(default_branch); + } + } else { + let mut default_branches: Vec = vec![]; + for remote_name in remotes { + if let Some(default_branch) = self.get_remote_default_branch(&remote_name)? { + default_branches.push(default_branch) + } + } + + if !default_branches.is_empty() + && (default_branches.len() == 1 + || default_branches + .windows(2) + .all(|w| w[0].name() == w[1].name())) + { + return Ok(default_branches.remove(0)); + } + } + + for branch_name in &vec!["main", "master"] { if let Ok(branch) = self.0.find_branch(branch_name, git2::BranchType::Local) { - return Ok(Branch(branch)) + return Ok(Branch(branch)); } } @@ -1458,6 +1522,20 @@ impl RemoteHandle<'_> { .to_string() } + pub fn connected(&mut self) -> bool { + self.0.connected() + } + + pub fn default_branch(&self) -> Result { + Ok(self + .0 + .default_branch() + .map_err(convert_libgit2_error)? + .as_str() + .expect("Remote branch name is not valid utf-8") + .to_string()) + } + pub fn is_pushable(&self) -> Result { let remote_type = detect_remote_type(self.0.url().expect("Remote name is not valid utf-8")) .ok_or_else(|| String::from("Could not detect remote type"))?; From c3aaea3332769c54620b55512bc958b0156e1564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Mon, 13 Jun 2022 22:47:49 +0200 Subject: [PATCH 14/22] Quote branch name on output --- src/repo.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/repo.rs b/src/repo.rs index 763705b..f6b92d5 100644 --- a/src/repo.rs +++ b/src/repo.rs @@ -1180,7 +1180,7 @@ impl RepoHandle { && !branch_name.ends_with(&format!("{}{}", super::BRANCH_NAMESPACE_SEPARATOR, name)) { return Err(WorktreeRemoveFailureReason::Error(format!( - "Branch {} is checked out in worktree, this does not look correct", + "Branch \"{}\" is checked out in worktree, this does not look correct", &branch_name ))); } From 30480fb56810110cfd0a5ba3817256b4a247e859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Mon, 13 Jun 2022 22:47:49 +0200 Subject: [PATCH 15/22] Update handling of branches on worktree setup --- src/worktree.rs | 118 +++++++++++++++++++++++++++++++----------------- 1 file changed, 77 insertions(+), 41 deletions(-) diff --git a/src/worktree.rs b/src/worktree.rs index 1f85499..35db51f 100644 --- a/src/worktree.rs +++ b/src/worktree.rs @@ -1,9 +1,23 @@ use std::path::Path; +use super::output::*; use super::repo; pub const GIT_MAIN_WORKTREE_DIRECTORY: &str = ".git-main-working-tree"; +// The logic about the base branch and the tracking branch is as follows: +// +// * If a branch with the same name does not exist and no track is given, use the default +// branch +// +// * If a branch with the same name exists and no track is given, use that +// +// * If a branch with the same name does not exist and track is given, use the +// local branch that tracks that branch +// +// * If a branch with the same name exists and track is given, use the locally +// existing branch. If the locally existing branch is not the local branch to +// the remote tracking branch, issue a warning pub fn add_worktree( directory: &Path, name: &str, @@ -31,55 +45,77 @@ pub fn add_worktree( let mut remote_branch_exists = false; - let default_checkout = || repo.default_branch()?.to_commit(); - - let checkout_commit; - if no_track { - checkout_commit = default_checkout()?; - } else { - match track { - Some((remote_name, remote_branch_name)) => { - let remote_branch = repo.find_remote_branch(remote_name, remote_branch_name); - match remote_branch { - Ok(branch) => { + let mut target_branch = match repo.find_local_branch(name) { + Ok(branchref) => { + if !no_track { + if let Some((remote_name, remote_branch_name)) = track { + let remote_branch = repo.find_remote_branch(remote_name, remote_branch_name); + if let Ok(remote_branch) = remote_branch { remote_branch_exists = true; - checkout_commit = branch.to_commit()?; - } - Err(_) => { - remote_branch_exists = false; - checkout_commit = default_checkout()?; + if let Ok(local_upstream_branch) = branchref.upstream() { + if remote_branch.name()? != local_upstream_branch.name()? { + print_warning(&format!( + "You specified a tracking branch ({}/{}) for an existing branch ({}), but \ + it differs from the current upstream ({}). Will keep current upstream" + , remote_name, remote_branch_name, branchref.name()?, local_upstream_branch.name()?)) + } + } } } } - None => match &config { - None => checkout_commit = default_checkout()?, - Some(config) => match &config.track { - None => checkout_commit = default_checkout()?, - Some(track_config) => { - if track_config.default { - let remote_branch = - repo.find_remote_branch(&track_config.default_remote, name); - match remote_branch { - Ok(branch) => { - remote_branch_exists = true; - checkout_commit = branch.to_commit()?; - } - Err(_) => { + branchref + } + Err(_) => { + let default_checkout = || repo.default_branch()?.to_commit(); + + let checkout_commit; + + if no_track { + checkout_commit = default_checkout()?; + } else { + match track { + Some((remote_name, remote_branch_name)) => { + let remote_branch = + repo.find_remote_branch(remote_name, remote_branch_name); + match remote_branch { + Ok(branch) => { + remote_branch_exists = true; + checkout_commit = branch.to_commit()?; + } + Err(_) => { + remote_branch_exists = false; + checkout_commit = default_checkout()?; + } + } + } + None => match &config { + None => checkout_commit = default_checkout()?, + Some(config) => match &config.track { + None => checkout_commit = default_checkout()?, + Some(track_config) => { + if track_config.default { + let remote_branch = + repo.find_remote_branch(&track_config.default_remote, name); + match remote_branch { + Ok(branch) => { + remote_branch_exists = true; + checkout_commit = branch.to_commit()?; + } + Err(_) => { + checkout_commit = default_checkout()?; + } + } + } else { checkout_commit = default_checkout()?; } } - } else { - checkout_commit = default_checkout()?; - } - } - }, - }, - }; - } + }, + }, + }; + } - let mut target_branch = match repo.find_local_branch(name) { - Ok(branchref) => branchref, - Err(_) => repo.create_branch(name, &checkout_commit)?, + repo.create_branch(name, &checkout_commit)? + } }; fn push( From 9f7195282f0120e7e70ae2377e49f1910c5c5599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Mon, 13 Jun 2022 22:47:49 +0200 Subject: [PATCH 16/22] Enable output in rust unit tests --- Justfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Justfile b/Justfile index e4c22c3..c9e2bf0 100644 --- a/Justfile +++ b/Justfile @@ -40,8 +40,8 @@ build-static: test: test-unit test-integration test-e2e -test-unit: - cargo test --lib --bins +test-unit +tests="": + cargo test --lib --bins -- --show-output {{tests}} test-integration: cargo test --test "*" From 96943c148394616a22d910c0046a5ad683f33ca6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Mon, 13 Jun 2022 22:47:49 +0200 Subject: [PATCH 17/22] Use new cargo fmt --- src/provider/mod.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/provider/mod.rs b/src/provider/mod.rs index a1c9a9c..be0535b 100644 --- a/src/provider/mod.rs +++ b/src/provider/mod.rs @@ -9,8 +9,8 @@ pub mod gitlab; pub use github::Github; pub use gitlab::Gitlab; -use super::repo; use super::auth; +use super::repo; use std::collections::HashMap; @@ -166,7 +166,11 @@ pub trait Provider { .header("accept", accept_header.unwrap_or("application/json")) .header( "authorization", - format!("{} {}", Self::auth_header_key(), &self.secret_token().access()), + format!( + "{} {}", + Self::auth_header_key(), + &self.secret_token().access() + ), ) .body(()) .map_err(|error| error.to_string())?; From 7363ed48b40773c120298ec86d9a9fcf50af8cb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Mon, 13 Jun 2022 22:47:49 +0200 Subject: [PATCH 18/22] Add clippy suggestions --- src/provider/github.rs | 4 ++-- src/provider/gitlab.rs | 4 ++-- src/provider/mod.rs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/provider/github.rs b/src/provider/github.rs index 094b860..76b6b53 100644 --- a/src/provider/github.rs +++ b/src/provider/github.rs @@ -136,8 +136,8 @@ impl Provider for Github { fn get_current_user(&self) -> Result> { Ok(super::call::( &format!("{GITHUB_API_BASEURL}/user"), - &Self::auth_header_key(), - &self.secret_token(), + Self::auth_header_key(), + self.secret_token(), Some(ACCEPT_HEADER_JSON), )? .username) diff --git a/src/provider/gitlab.rs b/src/provider/gitlab.rs index fc4b216..6acff35 100644 --- a/src/provider/gitlab.rs +++ b/src/provider/gitlab.rs @@ -157,8 +157,8 @@ impl Provider for Gitlab { fn get_current_user(&self) -> Result> { Ok(super::call::( &format!("{}/api/v4/user", self.api_url()), - &Self::auth_header_key(), - &self.secret_token(), + Self::auth_header_key(), + self.secret_token(), Some(ACCEPT_HEADER_JSON), )? .username) diff --git a/src/provider/mod.rs b/src/provider/mod.rs index be0535b..da72500 100644 --- a/src/provider/mod.rs +++ b/src/provider/mod.rs @@ -295,7 +295,7 @@ pub trait Provider { for repo in repos { let namespace = repo.namespace(); - let mut repo = repo.into_repo_config(&self.name(), worktree_setup, force_ssh); + let mut repo = repo.into_repo_config(self.name(), worktree_setup, force_ssh); // Namespace is already part of the hashmap key. I'm not too happy // about the data exchange format here. From 67c3e40108b9bff57e04266831fb22e7b7d666d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Mon, 13 Jun 2022 22:54:19 +0200 Subject: [PATCH 19/22] just: Update check target to be pre-commit ready --- Justfile | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Justfile b/Justfile index c9e2bf0..670a285 100644 --- a/Justfile +++ b/Justfile @@ -2,7 +2,7 @@ set positional-arguments target := "x86_64-unknown-linux-musl" -check: test +check: fmt-check lint test cargo check cargo fmt --check cargo clippy --no-deps -- -Dwarnings @@ -11,8 +11,12 @@ fmt: cargo fmt git ls-files | grep '\.py$' | xargs black +fmt-check: + cargo fmt --check + git ls-files | grep '\.py$' | xargs black --check + lint: - cargo clippy --no-deps + cargo clippy --no-deps -- -Dwarnings lint-fix: cargo clippy --no-deps --fix From 29ddc647e3daa5db28fd6cfae3542545ba47a932 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Mon, 13 Jun 2022 23:15:11 +0200 Subject: [PATCH 20/22] dependencies: Update comfy-table to 6.0.0 --- Cargo.lock | 35 ++++++++++------------------------- Cargo.toml | 2 +- 2 files changed, 11 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 581034f..828bdee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,7 +101,7 @@ version = "3.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c" dependencies = [ - "heck 0.4.0", + "heck", "proc-macro-error", "proc-macro2", "quote", @@ -119,9 +119,9 @@ dependencies = [ [[package]] name = "comfy-table" -version = "5.0.1" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b103d85ca6e209388771bfb7aa6b68a7aeec4afbf6f0a0264bfbf50360e5212e" +checksum = "121d8a5b0346092c18a4b2fd6f620d7a06f0eb7ac0a45860939a0884bc579c56" dependencies = [ "crossterm", "strum", @@ -371,15 +371,6 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" -[[package]] -name = "heck" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "heck" version = "0.4.0" @@ -646,9 +637,9 @@ checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" [[package]] name = "parking_lot" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", "parking_lot_core", @@ -1005,17 +996,17 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strum" -version = "0.23.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cae14b91c7d11c9a851d3fbc80a963198998c2a64eec840477fa92d8ce9b70bb" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" [[package]] name = "strum_macros" -version = "0.23.1" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bb0dc7ee9c15cea6199cde9a127fa16a4c5819af85395457ad72d68edc85a38" +checksum = "9550962e7cf70d9980392878dfaf1dcc3ece024f4cf3bf3c46b978d0bad61d6c" dependencies = [ - "heck 0.3.3", + "heck", "proc-macro2", "quote", "rustversion", @@ -1176,12 +1167,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-segmentation" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" - [[package]] name = "unicode-width" version = "0.1.9" diff --git a/Cargo.toml b/Cargo.toml index 2374de6..cc42ff1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,7 +64,7 @@ version = "=0.15.0" version = "=1.5.6" [dependencies.comfy-table] -version = "=5.0.1" +version = "=6.0.0" [dependencies.serde_yaml] version = "=0.8.24" From e6b654e9901a2cae8c7303ee0b669341d5473610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Mon, 13 Jun 2022 23:15:14 +0200 Subject: [PATCH 21/22] Cargo.lock: Updating libz-sys v1.1.6 -> v1.1.8 --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 828bdee..5ef4ae3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -523,9 +523,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.6" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e7e15d7610cce1d9752e137625f14e61a28cd45929b6e12e47b50fe154ee2e" +checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" dependencies = [ "cc", "libc", @@ -611,9 +611,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.73" +version = "0.9.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5fd19fb3e0a8191c1e34935718976a3e70c112ab9a24af6d7cadccd9d90bc0" +checksum = "835363342df5fba8354c5b453325b110ffd54044e588c539cf2f20a8014e4cb1" dependencies = [ "autocfg", "cc", From defb3d1b7d84facafe373bfe561d70e287288aa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Tue, 14 Jun 2022 00:32:08 +0200 Subject: [PATCH 22/22] Release v0.7.2 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5ef4ae3..d916426 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -332,7 +332,7 @@ dependencies = [ [[package]] name = "git-repo-manager" -version = "0.7.1" +version = "0.7.2" dependencies = [ "clap", "comfy-table", diff --git a/Cargo.toml b/Cargo.toml index cc42ff1..b97339c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "git-repo-manager" -version = "0.7.1" +version = "0.7.2" edition = "2021" authors = [