Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 31b9757ef3 | |||
| defb3d1b7d | |||
| e6b654e990 | |||
| 29ddc647e3 | |||
| 67c3e40108 | |||
| 7363ed48b4 | |||
| 96943c1483 | |||
| 9f7195282f | |||
| 30480fb568 | |||
| c3aaea3332 | |||
| fad6f71876 | |||
| 73158e3d47 | |||
| 6f4ae88260 | |||
| a8f8803a92 | |||
| 581a513ebd | |||
| f1e212ead9 | |||
| bc3001a4e6 | |||
| c4fd1d0452 | |||
| 1a65a163a1 | |||
| 4f68a563c6 | |||
| e04e8ceeeb | |||
|
|
b2542b341e | ||
| d402c1f8ce |
@@ -37,9 +37,9 @@ 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
|
||||
|
||||
|
||||
45
Cargo.lock
generated
45
Cargo.lock
generated
@@ -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",
|
||||
@@ -332,7 +332,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "git-repo-manager"
|
||||
version = "0.7.1"
|
||||
version = "0.7.2"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"comfy-table",
|
||||
@@ -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"
|
||||
@@ -532,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",
|
||||
@@ -620,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",
|
||||
@@ -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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "git-repo-manager"
|
||||
version = "0.7.1"
|
||||
version = "0.7.2"
|
||||
edition = "2021"
|
||||
|
||||
authors = [
|
||||
@@ -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"
|
||||
|
||||
12
Justfile
12
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
|
||||
@@ -40,8 +44,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 "*"
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
13
src/auth.rs
13
src/auth.rs
@@ -1,6 +1,15 @@
|
||||
use std::process;
|
||||
|
||||
pub fn get_token_from_command(command: &str) -> Result<String, String> {
|
||||
#[derive(Clone)]
|
||||
pub struct AuthToken(String);
|
||||
|
||||
impl AuthToken {
|
||||
pub fn access(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_token_from_command(command: &str) -> Result<AuthToken, String> {
|
||||
let output = process::Command::new("/usr/bin/env")
|
||||
.arg("sh")
|
||||
.arg("-c")
|
||||
@@ -32,5 +41,5 @@ pub fn get_token_from_command(command: &str) -> Result<String, String> {
|
||||
.next()
|
||||
.ok_or_else(|| String::from("Output did not contain any newline"))?;
|
||||
|
||||
Ok(token.to_string())
|
||||
Ok(AuthToken(token.to_string()))
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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::SecretToken;
|
||||
|
||||
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<String>,
|
||||
) -> Result<Self, String> {
|
||||
if api_url_override.is_some() {
|
||||
@@ -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) -> SecretToken {
|
||||
self.secret_token.clone()
|
||||
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(
|
||||
@@ -136,8 +136,8 @@ impl Provider for Github {
|
||||
fn get_current_user(&self) -> Result<String, ApiErrorResponse<GithubApiErrorResponse>> {
|
||||
Ok(super::call::<GithubUser, GithubApiErrorResponse>(
|
||||
&format!("{GITHUB_API_BASEURL}/user"),
|
||||
&Self::auth_header_key(),
|
||||
&self.secret_token(),
|
||||
Self::auth_header_key(),
|
||||
self.secret_token(),
|
||||
Some(ACCEPT_HEADER_JSON),
|
||||
)?
|
||||
.username)
|
||||
|
||||
@@ -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::SecretToken;
|
||||
|
||||
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<String>,
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ impl Provider for Gitlab {
|
||||
|
||||
fn new(
|
||||
filter: Filter,
|
||||
secret_token: SecretToken,
|
||||
secret_token: auth::AuthToken,
|
||||
api_url_override: Option<String>,
|
||||
) -> Result<Self, String> {
|
||||
Ok(Self {
|
||||
@@ -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) -> SecretToken {
|
||||
self.secret_token.clone()
|
||||
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(
|
||||
@@ -157,8 +157,8 @@ impl Provider for Gitlab {
|
||||
fn get_current_user(&self) -> Result<String, ApiErrorResponse<GitlabApiErrorResponse>> {
|
||||
Ok(super::call::<GitlabUser, GitlabApiErrorResponse>(
|
||||
&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)
|
||||
|
||||
@@ -9,6 +9,7 @@ pub mod gitlab;
|
||||
pub use github::Github;
|
||||
pub use gitlab::Gitlab;
|
||||
|
||||
use super::auth;
|
||||
use super::repo;
|
||||
|
||||
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<String>,
|
||||
@@ -117,16 +116,16 @@ pub trait Provider {
|
||||
|
||||
fn new(
|
||||
filter: Filter,
|
||||
secret_token: SecretToken,
|
||||
secret_token: auth::AuthToken,
|
||||
api_url_override: Option<String>,
|
||||
) -> Result<Self, String>
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
fn name(&self) -> String;
|
||||
fn filter(&self) -> Filter;
|
||||
fn secret_token(&self) -> SecretToken;
|
||||
fn auth_header_key() -> String;
|
||||
fn name(&self) -> &str;
|
||||
fn filter(&self) -> &Filter;
|
||||
fn secret_token(&self) -> &auth::AuthToken;
|
||||
fn auth_header_key() -> &'static str;
|
||||
|
||||
fn get_user_projects(
|
||||
&self,
|
||||
@@ -167,7 +166,11 @@ 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())?;
|
||||
@@ -292,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.
|
||||
@@ -308,7 +311,7 @@ pub trait Provider {
|
||||
fn call<T, U>(
|
||||
uri: &str,
|
||||
auth_header_key: &str,
|
||||
secret_token: &str,
|
||||
secret_token: &auth::AuthToken,
|
||||
accept_header: Option<&str>,
|
||||
) -> Result<T, ApiErrorResponse<U>>
|
||||
where
|
||||
@@ -322,7 +325,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()))?;
|
||||
|
||||
131
src/repo.rs
131
src/repo.rs
@@ -659,6 +659,14 @@ impl RepoHandle {
|
||||
.collect::<Result<Vec<Branch>, String>>()
|
||||
}
|
||||
|
||||
pub fn remote_branches(&self) -> Result<Vec<Branch>, 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::<Result<Vec<Branch>, String>>()
|
||||
}
|
||||
|
||||
pub fn fetch(&self, remote_name: &str) -> Result<(), String> {
|
||||
let mut remote = self
|
||||
.0
|
||||
@@ -1034,16 +1042,82 @@ impl RepoHandle {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn default_branch(&self) -> Result<Branch, String> {
|
||||
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")),
|
||||
},
|
||||
pub fn get_remote_default_branch(&self, remote_name: &str) -> Result<Option<Branch>, 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)?));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Note that <remote>/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<Branch, String> {
|
||||
// This is a bit of a guessing game.
|
||||
//
|
||||
// In the best case, there is only one remote. Then, we can check <remote>/HEAD to get the
|
||||
// default remote branch.
|
||||
//
|
||||
// If there are multiple remotes, we first check whether they all have the same
|
||||
// <remote>/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<Branch> = 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));
|
||||
}
|
||||
}
|
||||
|
||||
Err(String::from("Could not determine default branch"))
|
||||
}
|
||||
|
||||
// Looks like there is no distinguishing between the error cases
|
||||
// "no such remote" and "failed to get remote for some reason".
|
||||
// May be a good idea to handle this explicitly, by returning a
|
||||
@@ -1106,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
|
||||
)));
|
||||
}
|
||||
@@ -1394,6 +1468,15 @@ impl Branch<'_> {
|
||||
self.0.delete().map_err(convert_libgit2_error)
|
||||
}
|
||||
|
||||
pub fn basename(&self) -> Result<String, String> {
|
||||
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()
|
||||
@@ -1439,6 +1522,20 @@ impl RemoteHandle<'_> {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub fn connected(&mut self) -> bool {
|
||||
self.0.connected()
|
||||
}
|
||||
|
||||
pub fn default_branch(&self) -> Result<String, String> {
|
||||
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<bool, String> {
|
||||
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"))?;
|
||||
@@ -1529,6 +1626,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 <remote>/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() {
|
||||
|
||||
30
src/tree.rs
30
src/tree.rs
@@ -143,7 +143,35 @@ fn sync_repo(root_path: &Path, repo: &repo::Repo, init_worktree: bool) -> Result
|
||||
|
||||
let mut newly_created = false;
|
||||
|
||||
if repo_path.exists() {
|
||||
// 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()
|
||||
.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",
|
||||
|
||||
@@ -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,15 +45,38 @@ pub fn add_worktree(
|
||||
|
||||
let mut remote_branch_exists = false;
|
||||
|
||||
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;
|
||||
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()?))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
let remote_branch =
|
||||
repo.find_remote_branch(remote_name, remote_branch_name);
|
||||
match remote_branch {
|
||||
Ok(branch) => {
|
||||
remote_branch_exists = true;
|
||||
@@ -77,9 +114,8 @@ pub fn add_worktree(
|
||||
};
|
||||
}
|
||||
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user