38 Commits

Author SHA1 Message Date
d18c49982e Merge branch 'develop' 2022-06-16 00:55:13 +02:00
58db521b5b Release v0.7.3 2022-06-16 00:55:13 +02:00
c21fb5813b just: Remove redunant commands from check target 2022-06-16 00:39:57 +02:00
33a5a1a262 Add short doc snipper about "just check" 2022-06-16 00:39:43 +02:00
df8e69bce2 Enable autoformatting for shell scripts 2022-06-16 00:39:31 +02:00
58fdcfba9f Enable linting for shell scripts 2022-06-16 00:32:16 +02:00
27ef86c1b4 forge: Use "origin" as the default remote name
Close #33
2022-06-15 20:49:15 +02:00
9fc34e6989 just: Add clean target 2022-06-15 20:39:54 +02:00
4b79b6dd1d just: Update targets for static builds 2022-06-15 20:39:54 +02:00
d0cbc2f985 forge: Add option to specify remote name
Close #32
2022-06-15 20:39:54 +02:00
d53e28668b Cargo.lock: Updating http v0.2.7 -> v0.2.8 2022-06-15 20:39:54 +02:00
0b8896d11d Cargo.lock: Updating getrandom v0.2.6 -> v0.2.7 2022-06-15 20:39:54 +02:00
8c0c3ad169 dependencies: Update clap to 3.2.5 2022-06-15 20:39:54 +02:00
aebed5639d Add Max to contributors 2022-06-14 09:37:51 +02:00
4514de9ff5 Add release script 2022-06-14 00:35:03 +02:00
31b9757ef3 Merge branch 'develop' 2022-06-14 00:32:08 +02:00
defb3d1b7d Release v0.7.2 2022-06-14 00:32:08 +02:00
e6b654e990 Cargo.lock: Updating libz-sys v1.1.6 -> v1.1.8 2022-06-14 00:15:15 +02:00
29ddc647e3 dependencies: Update comfy-table to 6.0.0 2022-06-14 00:15:15 +02:00
67c3e40108 just: Update check target to be pre-commit ready 2022-06-14 00:15:15 +02:00
7363ed48b4 Add clippy suggestions 2022-06-14 00:15:15 +02:00
96943c1483 Use new cargo fmt 2022-06-14 00:15:15 +02:00
9f7195282f Enable output in rust unit tests 2022-06-14 00:15:15 +02:00
30480fb568 Update handling of branches on worktree setup 2022-06-14 00:15:15 +02:00
c3aaea3332 Quote branch name on output 2022-06-14 00:15:15 +02:00
fad6f71876 Improve default branch guessing 2022-06-14 00:15:15 +02:00
73158e3d47 Print ok-ish stuff to stdout 2022-06-14 00:15:15 +02:00
6f4ae88260 Add some comments about repo syncing 2022-06-14 00:15:15 +02:00
a8f8803a92 Do not fail on empty clone target 2022-06-14 00:15:15 +02:00
581a513ebd Initialize local branches on clone 2022-06-14 00:15:15 +02:00
f1e212ead9 Add function to get all remote branches 2022-06-14 00:15:15 +02:00
bc3001a4e6 Add function to get basename of branch 2022-06-14 00:15:15 +02:00
c4fd1d0452 Refactor default_branch() for readability 2022-06-14 00:15:15 +02:00
1a65a163a1 Use opaque type for auth token
So we cannot accidentially output it, as it does not implement
`Display`.
2022-06-14 00:15:15 +02:00
4f68a563c6 providers: Use references for field access 2022-06-14 00:15:15 +02:00
e04e8ceeeb Use opaque type for auth token
So we cannot accidentially output it, as it does not implement
`Display`.
2022-06-14 00:15:15 +02:00
Max Volk
b2542b341e Reword some of the documentation and spelling fixes 2022-06-14 00:15:15 +02:00
d402c1f8ce Remove accidentially added file 2022-05-28 22:06:52 +02:00
23 changed files with 591 additions and 210 deletions

View File

@@ -21,7 +21,8 @@ If you want, add yourself to the `CONTRIBUTORS` file in your pull request.
For Rust, just use `cargo fmt`. For Python, use For Rust, just use `cargo fmt`. For Python, use
[black](https://github.com/psf/black). I'd rather not spend any effort in [black](https://github.com/psf/black). I'd rather not spend any effort in
configuring the formatters (not possible for black anyway). configuring the formatters (not possible for black anyway). For shell scripts,
use [`shfmt`](https://github.com/mvdan/sh).
## Tooling ## Tooling
@@ -37,9 +38,12 @@ a separate e2e test suite in python (`just test-e2e`).
To run all tests, run `just test`. To run all tests, run `just test`.
When contributing, consider whether it makes sense to add tests that to prevent When contributing, consider whether it makes sense to add tests which could
regressions in the future. When fixing bugs, it makes sense to add tests that prevent regressions in the future. When fixing bugs, it makes sense to add
expose the wrong behaviour beforehand. tests that expose the wrong behaviour beforehand.
To also ensure proper formatting and that the linter is happy, use `just check`.
If that succeeds, your code is most likely fine to push!
## Documentation ## Documentation

View File

@@ -1 +1,2 @@
nonnominandus nonnominandus
Maximilian Volk

99
Cargo.lock generated
View File

@@ -80,16 +80,16 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "clap" name = "clap"
version = "3.1.18" version = "3.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b" checksum = "d53da17d37dba964b9b3ecb5c5a1f193a2762c700e6829201e645b9381c99dc7"
dependencies = [ dependencies = [
"atty", "atty",
"bitflags", "bitflags",
"clap_derive", "clap_derive",
"clap_lex", "clap_lex",
"indexmap", "indexmap",
"lazy_static", "once_cell",
"strsim", "strsim",
"termcolor", "termcolor",
"textwrap", "textwrap",
@@ -97,11 +97,11 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "3.1.18" version = "3.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c" checksum = "c11d40217d16aee8508cc8e5fde8b4ff24639758608e5374e731b53f85749fb9"
dependencies = [ dependencies = [
"heck 0.4.0", "heck",
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -110,18 +110,18 @@ dependencies = [
[[package]] [[package]]
name = "clap_lex" name = "clap_lex"
version = "0.2.0" version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" checksum = "5538cd660450ebeb4234cfecf8f2284b844ffc4c50531e66d584ad5b91293613"
dependencies = [ dependencies = [
"os_str_bytes", "os_str_bytes",
] ]
[[package]] [[package]]
name = "comfy-table" name = "comfy-table"
version = "5.0.1" version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b103d85ca6e209388771bfb7aa6b68a7aeec4afbf6f0a0264bfbf50360e5212e" checksum = "121d8a5b0346092c18a4b2fd6f620d7a06f0eb7ac0a45860939a0884bc579c56"
dependencies = [ dependencies = [
"crossterm", "crossterm",
"strum", "strum",
@@ -321,18 +321,18 @@ dependencies = [
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.6" version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"wasi 0.10.2+wasi-snapshot-preview1", "wasi",
] ]
[[package]] [[package]]
name = "git-repo-manager" name = "git-repo-manager"
version = "0.7.1" version = "0.7.3"
dependencies = [ dependencies = [
"clap", "clap",
"comfy-table", "comfy-table",
@@ -371,15 +371,6 @@ version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" 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]] [[package]]
name = "heck" name = "heck"
version = "0.4.0" version = "0.4.0"
@@ -397,9 +388,9 @@ dependencies = [
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.7" version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff8670570af52249509a86f5e3e18a08c60b177071826898fde8997cf5f6bfbb" checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
dependencies = [ dependencies = [
"bytes", "bytes",
"fnv", "fnv",
@@ -419,9 +410,9 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.8.1" version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"hashbrown", "hashbrown",
@@ -532,9 +523,9 @@ dependencies = [
[[package]] [[package]]
name = "libz-sys" name = "libz-sys"
version = "1.1.6" version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92e7e15d7610cce1d9752e137625f14e61a28cd45929b6e12e47b50fe154ee2e" checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
@@ -593,7 +584,7 @@ checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799"
dependencies = [ dependencies = [
"libc", "libc",
"log", "log",
"wasi 0.11.0+wasi-snapshot-preview1", "wasi",
"windows-sys", "windows-sys",
] ]
@@ -620,9 +611,9 @@ dependencies = [
[[package]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.73" version = "0.9.74"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d5fd19fb3e0a8191c1e34935718976a3e70c112ab9a24af6d7cadccd9d90bc0" checksum = "835363342df5fba8354c5b453325b110ffd54044e588c539cf2f20a8014e4cb1"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"cc", "cc",
@@ -646,9 +637,9 @@ checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.0" version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
dependencies = [ dependencies = [
"lock_api", "lock_api",
"parking_lot_core", "parking_lot_core",
@@ -1005,17 +996,17 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]] [[package]]
name = "strum" name = "strum"
version = "0.23.0" version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cae14b91c7d11c9a851d3fbc80a963198998c2a64eec840477fa92d8ce9b70bb" checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
[[package]] [[package]]
name = "strum_macros" name = "strum_macros"
version = "0.23.1" version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bb0dc7ee9c15cea6199cde9a127fa16a4c5819af85395457ad72d68edc85a38" checksum = "9550962e7cf70d9980392878dfaf1dcc3ece024f4cf3bf3c46b978d0bad61d6c"
dependencies = [ dependencies = [
"heck 0.3.3", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"rustversion", "rustversion",
@@ -1024,9 +1015,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.95" version = "1.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942" checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1114,9 +1105,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.34" version = "0.1.35"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09" checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"log", "log",
@@ -1138,11 +1129,11 @@ dependencies = [
[[package]] [[package]]
name = "tracing-core" name = "tracing-core"
version = "0.1.26" version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f" checksum = "7709595b8878a4965ce5e87ebf880a7d39c9afc6837721b21a5a816a8117d921"
dependencies = [ dependencies = [
"lazy_static", "once_cell",
] ]
[[package]] [[package]]
@@ -1163,9 +1154,9 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.0" version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c"
[[package]] [[package]]
name = "unicode-normalization" name = "unicode-normalization"
@@ -1176,12 +1167,6 @@ dependencies = [
"tinyvec", "tinyvec",
] ]
[[package]]
name = "unicode-segmentation"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.1.9" version = "0.1.9"
@@ -1227,12 +1212,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "git-repo-manager" name = "git-repo-manager"
version = "0.7.1" version = "0.7.3"
edition = "2021" edition = "2021"
authors = [ authors = [
@@ -54,7 +54,7 @@ version = "=0.14.4"
version = "=2.1.0" version = "=2.1.0"
[dependencies.clap] [dependencies.clap]
version = "=3.1.18" version = "=3.2.5"
features = ["derive", "cargo"] features = ["derive", "cargo"]
[dependencies.console] [dependencies.console]
@@ -64,7 +64,7 @@ version = "=0.15.0"
version = "=1.5.6" version = "=1.5.6"
[dependencies.comfy-table] [dependencies.comfy-table]
version = "=5.0.1" version = "=6.0.0"
[dependencies.serde_yaml] [dependencies.serde_yaml]
version = "=0.8.24" version = "=0.8.24"

View File

@@ -1,47 +1,59 @@
set positional-arguments set positional-arguments
target := "x86_64-unknown-linux-musl" static_target := "x86_64-unknown-linux-musl"
check: test check: fmt-check lint test
cargo check cargo check
cargo fmt --check
cargo clippy --no-deps -- -Dwarnings clean:
cargo clean
git clean -f -d -X
fmt: fmt:
cargo fmt cargo fmt
git ls-files | grep '\.py$' | xargs black git ls-files | grep '\.py$' | xargs black
git ls-files | grep '\.sh$' | xargs -L 1 shfmt --indent 4 --write
fmt-check:
cargo fmt --check
git ls-files | grep '\.py$' | xargs black --check
git ls-files | grep '\.sh$' | xargs -L 1 shfmt --indent 4 --diff
lint: lint:
cargo clippy --no-deps cargo clippy --no-deps -- -Dwarnings
git ls-files | grep '\.sh$' | xargs -L 1 shellcheck --norc
lint-fix: lint-fix:
cargo clippy --no-deps --fix cargo clippy --no-deps --fix
release: release:
cargo build --release --target {{target}} cargo build --release
release-static:
cargo build --release --target {{static_target}} --features=static-build
test-binary: test-binary:
env \ env \
GITHUB_API_BASEURL=http://rest:5000/github \ GITHUB_API_BASEURL=http://rest:5000/github \
GITLAB_API_BASEURL=http://rest:5000/gitlab \ GITLAB_API_BASEURL=http://rest:5000/gitlab \
cargo build --target {{target}} --profile e2e-tests --features=static-build cargo build --target {{static_target}} --profile e2e-tests --features=static-build
install: install:
cargo install --path . cargo install --path .
install-static: install-static:
cargo install --target {{target}} --features=static-build --path . cargo install --target {{static_target}} --features=static-build --path .
build: build:
cargo build cargo build
build-static: build-static:
cargo build --target {{target}} --features=static-build cargo build --target {{static_target}} --features=static-build
test: test-unit test-integration test-e2e test: test-unit test-integration test-e2e
test-unit: test-unit +tests="":
cargo test --lib --bins cargo test --lib --bins -- --show-output {{tests}}
test-integration: test-integration:
cargo test --test "*" cargo test --test "*"
@@ -52,7 +64,7 @@ test-e2e +tests=".": test-binary
&& docker-compose build \ && docker-compose build \
&& docker-compose run \ && docker-compose run \
--rm \ --rm \
-v $PWD/../target/{{target}}/e2e-tests/grm:/grm \ -v $PWD/../target/{{static_target}}/e2e-tests/grm:/grm \
pytest \ pytest \
"GRM_BINARY=/grm ALTERNATE_DOMAIN=alternate-rest python3 -m pytest -p no:cacheprovider --color=yes "$@"" \ "GRM_BINARY=/grm ALTERNATE_DOMAIN=alternate-rest python3 -m pytest -p no:cacheprovider --color=yes "$@"" \
&& docker-compose rm --stop -f && docker-compose rm --stop -f

View File

@@ -17,7 +17,7 @@ You will end up with your projects cloned into `~/projects/{your_github_username
## Authentication ## Authentication
The only currently supported authentication option is using personal access The only currently supported authentication option is using a personal access
token. token.
### GitHub ### GitHub
@@ -27,7 +27,7 @@ See the GitHub documentation for personal access tokens:
The only required permission is the "repo" scope. The only required permission is the "repo" scope.
### GitHub ### GitLab
See the GitLab documentation for personal access tokens: See the GitLab documentation for personal access tokens:
[Link](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html). [Link](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html).

View File

@@ -77,6 +77,6 @@ $ grm repos status
## YAML ## YAML
By default, the repo configuration uses TOML. If you prefer YAML, just give it 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 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` For generating a configuration, pass `--format yaml` to `grm repo find`
to generate YAML instead of TOML. which generates a YAML config instead of a TOML configuration.

View File

@@ -5,11 +5,11 @@
The default workflow when using git is having your repository in a single directory. 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 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, 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 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). for some reason, you have to change branches (maybe to investigate some issue).
But you get the following: 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 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, 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 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. old stash I forgot about.
And even worse: If you're currently in the process of resolving merge conflicts or an 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 independent checkouts of your repository on different directories. You can have
multiple directories that correspond to different references in your repository. 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 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). the worktree is already set up).
## Worktrees in GRM ## 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, 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). 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 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: 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 `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! course, actual changes in the worktree will still block an automatic cleanup!
* As soon as you enable persistent branches, non-persistent branches will only * 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 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 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?" hell is there a `--rebase` flag in the `rebase` command?"
Yes, it's kind of weird. Remember that `pull` only ever updates each worktree 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 instead. The switches to `rebase` are just convenience, so you do not have to
run two commands. run two commands.

View File

@@ -248,6 +248,7 @@ def test_repos_find_remote_user_empty(
@pytest.mark.parametrize("force_ssh", [True, False]) @pytest.mark.parametrize("force_ssh", [True, False])
@pytest.mark.parametrize("use_alternate_endpoint", [True, False]) @pytest.mark.parametrize("use_alternate_endpoint", [True, False])
@pytest.mark.parametrize("use_config", [True, False]) @pytest.mark.parametrize("use_config", [True, False])
@pytest.mark.parametrize("override_remote_name", [True, False])
def test_repos_find_remote_user( def test_repos_find_remote_user(
provider, provider,
configtype, configtype,
@@ -258,6 +259,7 @@ def test_repos_find_remote_user(
force_ssh, force_ssh,
use_alternate_endpoint, use_alternate_endpoint,
use_config, use_config,
override_remote_name,
): ):
if use_config: if use_config:
with tempfile.NamedTemporaryFile() as config: with tempfile.NamedTemporaryFile() as config:
@@ -274,6 +276,8 @@ def test_repos_find_remote_user(
cfg += f"worktree = {str(worktree).lower()}\n" cfg += f"worktree = {str(worktree).lower()}\n"
if force_ssh: if force_ssh:
cfg += f"force_ssh = true\n" cfg += f"force_ssh = true\n"
if override_remote_name:
cfg += f'remote_name = "otherremote"\n'
if use_owner: if use_owner:
cfg += """ cfg += """
[filters] [filters]
@@ -310,6 +314,8 @@ def test_repos_find_remote_user(
args += ["--user", "myuser1"] args += ["--user", "myuser1"]
if force_ssh: if force_ssh:
args += ["--force-ssh"] args += ["--force-ssh"]
if override_remote_name:
args += ["--remote-name", "otherremote"]
if not worktree_default: if not worktree_default:
args += ["--worktree", str(worktree).lower()] args += ["--worktree", str(worktree).lower()]
if use_alternate_endpoint: if use_alternate_endpoint:
@@ -350,7 +356,10 @@ def test_repos_find_remote_user(
assert repo["worktree_setup"] is (not worktree_default and worktree) assert repo["worktree_setup"] is (not worktree_default and worktree)
assert isinstance(repo["remotes"], list) assert isinstance(repo["remotes"], list)
assert len(repo["remotes"]) == 1 assert len(repo["remotes"]) == 1
assert repo["remotes"][0]["name"] == provider if override_remote_name:
assert repo["remotes"][0]["name"] == "otherremote"
else:
assert repo["remotes"][0]["name"] == "origin"
if force_ssh or i == 1: if force_ssh or i == 1:
assert ( assert (
repo["remotes"][0]["url"] repo["remotes"][0]["url"]
@@ -535,14 +544,14 @@ def test_repos_find_remote_group(
assert isinstance(repo["remotes"], list) assert isinstance(repo["remotes"], list)
assert len(repo["remotes"]) == 1 assert len(repo["remotes"]) == 1
if force_ssh or i == 1: if force_ssh or i == 1:
assert repo["remotes"][0]["name"] == provider assert repo["remotes"][0]["name"] == "origin"
assert ( assert (
repo["remotes"][0]["url"] repo["remotes"][0]["url"]
== f"ssh://git@example.com/mygroup1/myproject{i}.git" == f"ssh://git@example.com/mygroup1/myproject{i}.git"
) )
assert repo["remotes"][0]["type"] == "ssh" assert repo["remotes"][0]["type"] == "ssh"
else: else:
assert repo["remotes"][0]["name"] == provider assert repo["remotes"][0]["name"] == "origin"
assert ( assert (
repo["remotes"][0]["url"] repo["remotes"][0]["url"]
== f"https://example.com/mygroup1/myproject{i}.git" == f"https://example.com/mygroup1/myproject{i}.git"
@@ -659,7 +668,7 @@ def test_repos_find_remote_user_and_group(
assert repo["worktree_setup"] is (not worktree_default and worktree) assert repo["worktree_setup"] is (not worktree_default and worktree)
assert isinstance(repo["remotes"], list) assert isinstance(repo["remotes"], list)
assert len(repo["remotes"]) == 1 assert len(repo["remotes"]) == 1
assert repo["remotes"][0]["name"] == provider assert repo["remotes"][0]["name"] == "origin"
if force_ssh or i == 1: if force_ssh or i == 1:
assert ( assert (
repo["remotes"][0]["url"] repo["remotes"][0]["url"]
@@ -684,7 +693,7 @@ def test_repos_find_remote_user_and_group(
assert repo["worktree_setup"] is (not worktree_default and worktree) assert repo["worktree_setup"] is (not worktree_default and worktree)
assert isinstance(repo["remotes"], list) assert isinstance(repo["remotes"], list)
assert len(repo["remotes"]) == 1 assert len(repo["remotes"]) == 1
assert repo["remotes"][0]["name"] == provider assert repo["remotes"][0]["name"] == "origin"
if force_ssh or i == 1: if force_ssh or i == 1:
assert ( assert (
repo["remotes"][0]["url"] repo["remotes"][0]["url"]
@@ -814,7 +823,7 @@ def test_repos_find_remote_owner(
assert repo["worktree_setup"] is (not worktree_default and worktree) assert repo["worktree_setup"] is (not worktree_default and worktree)
assert isinstance(repo["remotes"], list) assert isinstance(repo["remotes"], list)
assert len(repo["remotes"]) == 1 assert len(repo["remotes"]) == 1
assert repo["remotes"][0]["name"] == provider assert repo["remotes"][0]["name"] == "origin"
if force_ssh or i == 1: if force_ssh or i == 1:
assert ( assert (
repo["remotes"][0]["url"] repo["remotes"][0]["url"]
@@ -837,7 +846,7 @@ def test_repos_find_remote_owner(
assert repo["worktree_setup"] is (not worktree_default and worktree) assert repo["worktree_setup"] is (not worktree_default and worktree)
assert isinstance(repo["remotes"], list) assert isinstance(repo["remotes"], list)
assert len(repo["remotes"]) == 1 assert len(repo["remotes"]) == 1
assert repo["remotes"][0]["name"] == provider assert repo["remotes"][0]["name"] == "origin"
if force_ssh or i == 1: if force_ssh or i == 1:
assert ( assert (
repo["remotes"][0]["url"] repo["remotes"][0]["url"]
@@ -861,7 +870,7 @@ def test_repos_find_remote_owner(
assert repo["worktree_setup"] is (not worktree_default and worktree) assert repo["worktree_setup"] is (not worktree_default and worktree)
assert isinstance(repo["remotes"], list) assert isinstance(repo["remotes"], list)
assert len(repo["remotes"]) == 1 assert len(repo["remotes"]) == 1
assert repo["remotes"][0]["name"] == provider assert repo["remotes"][0]["name"] == "origin"
if force_ssh: if force_ssh:
assert ( assert (
repo["remotes"][0]["url"] == f"ssh://git@example.com/myuser2/myproject3.git" repo["remotes"][0]["url"] == f"ssh://git@example.com/myuser2/myproject3.git"
@@ -890,7 +899,7 @@ def test_repos_find_remote_owner(
assert repo["worktree_setup"] is (not worktree_default and worktree) assert repo["worktree_setup"] is (not worktree_default and worktree)
assert isinstance(repo["remotes"], list) assert isinstance(repo["remotes"], list)
assert len(repo["remotes"]) == 1 assert len(repo["remotes"]) == 1
assert repo["remotes"][0]["name"] == provider assert repo["remotes"][0]["name"] == "origin"
if force_ssh or i == 1: if force_ssh or i == 1:
assert ( assert (
repo["remotes"][0]["url"] repo["remotes"][0]["url"]
@@ -910,7 +919,7 @@ def test_repos_find_remote_owner(
assert repo["worktree_setup"] is (not worktree_default and worktree) assert repo["worktree_setup"] is (not worktree_default and worktree)
assert isinstance(repo["remotes"], list) assert isinstance(repo["remotes"], list)
assert len(repo["remotes"]) == 1 assert len(repo["remotes"]) == 1
assert repo["remotes"][0]["name"] == provider assert repo["remotes"][0]["name"] == "origin"
if force_ssh: if force_ssh:
assert ( assert (
repo["remotes"][0]["url"] repo["remotes"][0]["url"]
@@ -936,7 +945,7 @@ def test_repos_find_remote_owner(
assert repo["worktree_setup"] is (not worktree_default and worktree) assert repo["worktree_setup"] is (not worktree_default and worktree)
assert isinstance(repo["remotes"], list) assert isinstance(repo["remotes"], list)
assert len(repo["remotes"]) == 1 assert len(repo["remotes"]) == 1
assert repo["remotes"][0]["name"] == provider assert repo["remotes"][0]["name"] == "origin"
if force_ssh: if force_ssh:
assert ( assert (
repo["remotes"][0]["url"] repo["remotes"][0]["url"]

View File

@@ -303,7 +303,6 @@ def test_repos_sync_root_is_file(configtype):
cmd = grm(["repos", "sync", "config", "--config", config.name]) cmd = grm(["repos", "sync", "config", "--config", config.name])
assert cmd.returncode != 0 assert cmd.returncode != 0
assert len(cmd.stdout) == 0
assert "not a directory" in cmd.stderr.lower() assert "not a directory" in cmd.stderr.lower()

164
release.sh Executable file
View File

@@ -0,0 +1,164 @@
#!/usr/bin/env bash
set -o nounset
set -o errexit
set -o pipefail
usage() {
printf '%s\n' "usage: $0 (master|minor|patch)" >&2
}
if (($# != 1)); then
usage
exit 1
fi
current_version="$(grep '^version \?=' Cargo.toml | head -1 | cut -d '=' -f 2 | tr -d " '"'"')"
major="$(printf '%s' "${current_version}" | grep -oP '^\d+')"
minor="$(printf '%s' "${current_version}" | grep -oP '\.\d+\.' | tr -d '.')"
patch="$(printf '%s' "${current_version}" | grep -oP '\d+$' | tr -d '.')"
case "$1" in
major)
((major++)) || true
minor=0
patch=0
;;
minor)
((minor++)) || true
patch=0
;;
patch)
((patch++)) || true
;;
*)
usage
exit 1
;;
esac
new_version="${major}.${minor}.${patch}"
if ! [[ "${new_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
printf '%s\n' 'Version has to a complete semver' >&2
exit 1
fi
current_branch="$(git rev-parse --abbrev-ref HEAD)"
if [[ "${current_branch}" != "develop" ]]; then
printf '%s\n' 'You need to be on develop' >&2
exit 1
fi
gitstatus="$(git status --porcelain)"
if [[ -n "${gitstatus}" ]]; then
printf '%s\n' 'There are uncommitted changes' >&2
exit 1
fi
if git tag --list "v${new_version}" | grep -q .; then
printf 'Tag %s already exists\n' "v${new_version}" >&2
exit 1
fi
for remote in $(git remote); do
if git ls-remote --tags "${remote}" | grep -q "refs/tags/v${new_version}$"; then
printf 'Tag %s already exists on %s' "v${new_version}" "${remote}" >&2
exit 1
fi
done
git fetch --all
for remote in $(git remote); do
for branch in master develop; do
if ! git diff --quiet "${remote}/${branch}..${branch}"; then
printf 'Remote branch %s/%s not up to date, synchronize first!\n' "${remote}" "${branch}" >&2
exit 1
fi
done
done
if ! git merge-base --is-ancestor master develop; then
printf '%s\n' 'Develop is not a straight descendant of master, rebase!' >&2
exit 1
fi
changes="$(git log --oneline master..develop | wc -l)"
if ((changes == 0)); then
printf '%s\n' 'No changes between master and develop?' >&2
exit 1
fi
just update-dependencies
just check
sed -i "0,/^version/{s/^version.*$/version = \"${new_version}\"/}" Cargo.toml
cargo update --package git-repo-manager --precise "${new_version}"
diff="$(git diff --numstat)"
if (($(printf '%s\n' "${diff}" | wc -l || true) != 2)); then
printf '%s\n' 'Weird changes detected, bailing' >&2
exit 1
fi
if ! printf '%s\n' "${diff}" | grep -Pq '^1\s+1\s+Cargo.lock$'; then
printf '%s\n' 'Weird changes detected, bailing' >&2
exit 1
fi
if ! printf '%s\n' "${diff}" | grep -Pq '^1\s+1\s+Cargo.toml$'; then
printf '%s\n' 'Weird changes detected, bailing' >&2
exit 1
fi
git add Cargo.lock Cargo.toml
git commit -m "Release v${new_version}"
git switch master 2>/dev/null || { [[ -d "../master" ]] && cd "../master"; } || {
printf '%s\n' 'Could not change to master' >&2
exit 1
}
current_branch="$(git rev-parse --abbrev-ref HEAD)"
if [[ "${current_branch}" != "master" ]]; then
printf '%s\n' 'Looks like branch switching to master did not work' >&2
exit 1
fi
git merge --no-ff --no-edit develop
git tag "v${new_version}"
for remote in $(git remote); do
while ! git push "${remote}" "v${new_version}" master; do
:
done
done
git switch develop 2>/dev/null || { [[ -d "../develop" ]] && cd "../develop"; } || {
printf '%s\n' 'Could not change to develop' >&2
exit 1
}
current_branch="$(git rev-parse --abbrev-ref HEAD)"
if [[ "${current_branch}" != "develop" ]]; then
printf '%s\n' 'Looks like branch switching to develop did not work' >&2
exit 1
fi
git merge --ff-only master
for remote in $(git remote); do
while ! git push "${remote}" develop; do
:
done
done
cargo publish
printf 'Published %s successfully\n' "${new_version}"
exit 0

View File

@@ -1,6 +1,15 @@
use std::process; 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") let output = process::Command::new("/usr/bin/env")
.arg("sh") .arg("sh")
.arg("-c") .arg("-c")
@@ -32,5 +41,5 @@ pub fn get_token_from_command(command: &str) -> Result<String, String> {
.next() .next()
.ok_or_else(|| String::from("Output did not contain any newline"))?; .ok_or_else(|| String::from("Output did not contain any newline"))?;
Ok(token.to_string()) Ok(AuthToken(token.to_string()))
} }

View File

@@ -53,6 +53,8 @@ pub struct ConfigProvider {
pub worktree: Option<bool>, pub worktree: Option<bool>,
pub init_worktree: Option<bool>, pub init_worktree: Option<bool>,
pub remote_name: Option<String>,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
@@ -192,6 +194,7 @@ impl Config {
.get_repos( .get_repos(
config.worktree.unwrap_or(false), config.worktree.unwrap_or(false),
config.force_ssh.unwrap_or(false), config.force_ssh.unwrap_or(false),
config.remote_name,
)? )?
} }
RemoteProvider::Gitlab => { RemoteProvider::Gitlab => {
@@ -205,6 +208,7 @@ impl Config {
.get_repos( .get_repos(
config.worktree.unwrap_or(false), config.worktree.unwrap_or(false),
config.force_ssh.unwrap_or(false), config.force_ssh.unwrap_or(false),
config.remote_name,
)? )?
} }
}; };

View File

@@ -103,6 +103,9 @@ pub struct FindRemoteArgs {
#[clap(arg_enum, short, long, help = "Remote provider to use")] #[clap(arg_enum, short, long, help = "Remote provider to use")]
pub provider: RemoteProvider, pub provider: RemoteProvider,
#[clap(short, long, help = "Name of the remote to use")]
pub remote_name: Option<String>,
#[clap( #[clap(
multiple_occurrences = true, multiple_occurrences = true,
name = "user", name = "user",
@@ -189,6 +192,9 @@ pub struct SyncRemoteArgs {
#[clap(arg_enum, short, long, help = "Remote provider to use")] #[clap(arg_enum, short, long, help = "Remote provider to use")]
pub provider: RemoteProvider, pub provider: RemoteProvider,
#[clap(short, long, help = "Name of the remote to use")]
pub remote_name: Option<String>,
#[clap( #[clap(
multiple_occurrences = true, multiple_occurrences = true,
name = "user", name = "user",

View File

@@ -64,7 +64,11 @@ fn main() {
process::exit(1); process::exit(1);
} }
} }
.get_repos(worktree, args.force_ssh) .get_repos(
worktree,
args.force_ssh,
args.remote_name,
)
} }
cmd::RemoteProvider::Gitlab => { cmd::RemoteProvider::Gitlab => {
match provider::Gitlab::new(filter, token, args.api_url) { match provider::Gitlab::new(filter, token, args.api_url) {
@@ -74,7 +78,11 @@ fn main() {
process::exit(1); process::exit(1);
} }
} }
.get_repos(worktree, args.force_ssh) .get_repos(
worktree,
args.force_ssh,
args.remote_name,
)
} }
}; };
@@ -280,6 +288,7 @@ fn main() {
.get_repos( .get_repos(
config.worktree.unwrap_or(false), config.worktree.unwrap_or(false),
config.force_ssh.unwrap_or(false), config.force_ssh.unwrap_or(false),
config.remote_name,
) { ) {
Ok(provider) => provider, Ok(provider) => provider,
Err(error) => { Err(error) => {
@@ -299,6 +308,7 @@ fn main() {
.get_repos( .get_repos(
config.worktree.unwrap_or(false), config.worktree.unwrap_or(false),
config.force_ssh.unwrap_or(false), config.force_ssh.unwrap_or(false),
config.remote_name,
) { ) {
Ok(provider) => provider, Ok(provider) => provider,
Err(error) => { Err(error) => {
@@ -382,7 +392,11 @@ fn main() {
process::exit(1); process::exit(1);
} }
} }
.get_repos(worktree, args.force_ssh) .get_repos(
worktree,
args.force_ssh,
args.remote_name,
)
} }
cmd::RemoteProvider::Gitlab => { cmd::RemoteProvider::Gitlab => {
match provider::Gitlab::new(filter, token, args.api_url) { match provider::Gitlab::new(filter, token, args.api_url) {
@@ -392,7 +406,11 @@ fn main() {
process::exit(1); process::exit(1);
} }
} }
.get_repos(worktree, args.force_ssh) .get_repos(
worktree,
args.force_ssh,
args.remote_name,
)
} }
}; };

View File

@@ -20,12 +20,12 @@ pub fn print_repo_action(repo: &str, message: &str) {
} }
pub fn print_action(message: &str) { pub fn print_action(message: &str) {
let stderr = Term::stderr(); let stdout = Term::stdout();
let mut style = Style::new().yellow(); let mut style = Style::new().yellow();
if stderr.is_term() { if stdout.is_term() {
style = style.force_styling(true); style = style.force_styling(true);
} }
stderr stdout
.write_line(&format!("[{}] {}", style.apply_to('\u{2699}'), &message)) .write_line(&format!("[{}] {}", style.apply_to('\u{2699}'), &message))
.unwrap(); .unwrap();
} }
@@ -46,13 +46,13 @@ pub fn print_repo_success(repo: &str, message: &str) {
} }
pub fn print_success(message: &str) { pub fn print_success(message: &str) {
let stderr = Term::stderr(); let stdout = Term::stdout();
let mut style = Style::new().green(); let mut style = Style::new().green();
if stderr.is_term() { if stdout.is_term() {
style = style.force_styling(true); style = style.force_styling(true);
} }
stderr stdout
.write_line(&format!("[{}] {}", style.apply_to('\u{2714}'), &message)) .write_line(&format!("[{}] {}", style.apply_to('\u{2714}'), &message))
.unwrap(); .unwrap();
} }

View File

@@ -1,14 +1,13 @@
use serde::Deserialize; use serde::Deserialize;
use super::auth;
use super::escape; use super::escape;
use super::ApiErrorResponse; use super::ApiErrorResponse;
use super::Filter; use super::Filter;
use super::JsonError; use super::JsonError;
use super::Project; use super::Project;
use super::Provider; use super::Provider;
use super::SecretToken;
const PROVIDER_NAME: &str = "github";
const ACCEPT_HEADER_JSON: &str = "application/vnd.github.v3+json"; const ACCEPT_HEADER_JSON: &str = "application/vnd.github.v3+json";
const GITHUB_API_BASEURL: &str = const GITHUB_API_BASEURL: &str =
option_env!("GITHUB_API_BASEURL").unwrap_or("https://api.github.com"); option_env!("GITHUB_API_BASEURL").unwrap_or("https://api.github.com");
@@ -67,7 +66,7 @@ impl JsonError for GithubApiErrorResponse {
pub struct Github { pub struct Github {
filter: Filter, filter: Filter,
secret_token: SecretToken, secret_token: auth::AuthToken,
} }
impl Provider for Github { impl Provider for Github {
@@ -76,7 +75,7 @@ impl Provider for Github {
fn new( fn new(
filter: Filter, filter: Filter,
secret_token: SecretToken, secret_token: auth::AuthToken,
api_url_override: Option<String>, api_url_override: Option<String>,
) -> Result<Self, String> { ) -> Result<Self, String> {
if api_url_override.is_some() { if api_url_override.is_some() {
@@ -88,20 +87,16 @@ impl Provider for Github {
}) })
} }
fn name(&self) -> String { fn filter(&self) -> &Filter {
String::from(PROVIDER_NAME) &self.filter
} }
fn filter(&self) -> Filter { fn secret_token(&self) -> &auth::AuthToken {
self.filter.clone() &self.secret_token
} }
fn secret_token(&self) -> SecretToken { fn auth_header_key() -> &'static str {
self.secret_token.clone() "token"
}
fn auth_header_key() -> String {
"token".to_string()
} }
fn get_user_projects( fn get_user_projects(
@@ -136,8 +131,8 @@ impl Provider for Github {
fn get_current_user(&self) -> Result<String, ApiErrorResponse<GithubApiErrorResponse>> { fn get_current_user(&self) -> Result<String, ApiErrorResponse<GithubApiErrorResponse>> {
Ok(super::call::<GithubUser, GithubApiErrorResponse>( Ok(super::call::<GithubUser, GithubApiErrorResponse>(
&format!("{GITHUB_API_BASEURL}/user"), &format!("{GITHUB_API_BASEURL}/user"),
&Self::auth_header_key(), Self::auth_header_key(),
&self.secret_token(), self.secret_token(),
Some(ACCEPT_HEADER_JSON), Some(ACCEPT_HEADER_JSON),
)? )?
.username) .username)

View File

@@ -1,14 +1,13 @@
use serde::Deserialize; use serde::Deserialize;
use super::auth;
use super::escape; use super::escape;
use super::ApiErrorResponse; use super::ApiErrorResponse;
use super::Filter; use super::Filter;
use super::JsonError; use super::JsonError;
use super::Project; use super::Project;
use super::Provider; use super::Provider;
use super::SecretToken;
const PROVIDER_NAME: &str = "gitlab";
const ACCEPT_HEADER_JSON: &str = "application/json"; const ACCEPT_HEADER_JSON: &str = "application/json";
const GITLAB_API_BASEURL: &str = option_env!("GITLAB_API_BASEURL").unwrap_or("https://gitlab.com"); const GITLAB_API_BASEURL: &str = option_env!("GITLAB_API_BASEURL").unwrap_or("https://gitlab.com");
@@ -75,7 +74,7 @@ impl JsonError for GitlabApiErrorResponse {
pub struct Gitlab { pub struct Gitlab {
filter: Filter, filter: Filter,
secret_token: SecretToken, secret_token: auth::AuthToken,
api_url_override: Option<String>, api_url_override: Option<String>,
} }
@@ -95,7 +94,7 @@ impl Provider for Gitlab {
fn new( fn new(
filter: Filter, filter: Filter,
secret_token: SecretToken, secret_token: auth::AuthToken,
api_url_override: Option<String>, api_url_override: Option<String>,
) -> Result<Self, String> { ) -> Result<Self, String> {
Ok(Self { Ok(Self {
@@ -105,20 +104,16 @@ impl Provider for Gitlab {
}) })
} }
fn name(&self) -> String { fn filter(&self) -> &Filter {
String::from(PROVIDER_NAME) &self.filter
} }
fn filter(&self) -> Filter { fn secret_token(&self) -> &auth::AuthToken {
self.filter.clone() &self.secret_token
} }
fn secret_token(&self) -> SecretToken { fn auth_header_key() -> &'static str {
self.secret_token.clone() "bearer"
}
fn auth_header_key() -> String {
"bearer".to_string()
} }
fn get_user_projects( fn get_user_projects(
@@ -157,8 +152,8 @@ impl Provider for Gitlab {
fn get_current_user(&self) -> Result<String, ApiErrorResponse<GitlabApiErrorResponse>> { fn get_current_user(&self) -> Result<String, ApiErrorResponse<GitlabApiErrorResponse>> {
Ok(super::call::<GitlabUser, GitlabApiErrorResponse>( Ok(super::call::<GitlabUser, GitlabApiErrorResponse>(
&format!("{}/api/v4/user", self.api_url()), &format!("{}/api/v4/user", self.api_url()),
&Self::auth_header_key(), Self::auth_header_key(),
&self.secret_token(), self.secret_token(),
Some(ACCEPT_HEADER_JSON), Some(ACCEPT_HEADER_JSON),
)? )?
.username) .username)

View File

@@ -9,10 +9,13 @@ pub mod gitlab;
pub use github::Github; pub use github::Github;
pub use gitlab::Gitlab; pub use gitlab::Gitlab;
use super::auth;
use super::repo; use super::repo;
use std::collections::HashMap; use std::collections::HashMap;
const DEFAULT_REMOTE_NAME: &str = "origin";
#[derive(Debug, Deserialize, Serialize, clap::ArgEnum, Clone)] #[derive(Debug, Deserialize, Serialize, clap::ArgEnum, Clone)]
pub enum RemoteProvider { pub enum RemoteProvider {
#[serde(alias = "github", alias = "GitHub")] #[serde(alias = "github", alias = "GitHub")]
@@ -69,8 +72,6 @@ pub trait Project {
fn private(&self) -> bool; fn private(&self) -> bool;
} }
type SecretToken = String;
#[derive(Clone)] #[derive(Clone)]
pub struct Filter { pub struct Filter {
users: Vec<String>, users: Vec<String>,
@@ -117,16 +118,15 @@ pub trait Provider {
fn new( fn new(
filter: Filter, filter: Filter,
secret_token: SecretToken, secret_token: auth::AuthToken,
api_url_override: Option<String>, api_url_override: Option<String>,
) -> Result<Self, String> ) -> Result<Self, String>
where where
Self: Sized; Self: Sized;
fn name(&self) -> String; fn filter(&self) -> &Filter;
fn filter(&self) -> Filter; fn secret_token(&self) -> &auth::AuthToken;
fn secret_token(&self) -> SecretToken; fn auth_header_key() -> &'static str;
fn auth_header_key() -> String;
fn get_user_projects( fn get_user_projects(
&self, &self,
@@ -167,7 +167,11 @@ pub trait Provider {
.header("accept", accept_header.unwrap_or("application/json")) .header("accept", accept_header.unwrap_or("application/json"))
.header( .header(
"authorization", "authorization",
format!("{} {}", Self::auth_header_key(), &self.secret_token()), format!(
"{} {}",
Self::auth_header_key(),
&self.secret_token().access()
),
) )
.body(()) .body(())
.map_err(|error| error.to_string())?; .map_err(|error| error.to_string())?;
@@ -210,6 +214,7 @@ pub trait Provider {
&self, &self,
worktree_setup: bool, worktree_setup: bool,
force_ssh: bool, force_ssh: bool,
remote_name: Option<String>,
) -> Result<HashMap<Option<String>, Vec<repo::Repo>>, String> { ) -> Result<HashMap<Option<String>, Vec<repo::Repo>>, String> {
let mut repos = vec![]; let mut repos = vec![];
@@ -289,10 +294,12 @@ pub trait Provider {
let mut ret: HashMap<Option<String>, Vec<repo::Repo>> = HashMap::new(); let mut ret: HashMap<Option<String>, Vec<repo::Repo>> = HashMap::new();
let remote_name = remote_name.unwrap_or_else(|| DEFAULT_REMOTE_NAME.to_string());
for repo in repos { for repo in repos {
let namespace = repo.namespace(); let namespace = repo.namespace();
let mut repo = repo.into_repo_config(&self.name(), worktree_setup, force_ssh); let mut repo = repo.into_repo_config(&remote_name, worktree_setup, force_ssh);
// Namespace is already part of the hashmap key. I'm not too happy // Namespace is already part of the hashmap key. I'm not too happy
// about the data exchange format here. // about the data exchange format here.
@@ -308,7 +315,7 @@ pub trait Provider {
fn call<T, U>( fn call<T, U>(
uri: &str, uri: &str,
auth_header_key: &str, auth_header_key: &str,
secret_token: &str, secret_token: &auth::AuthToken,
accept_header: Option<&str>, accept_header: Option<&str>,
) -> Result<T, ApiErrorResponse<U>> ) -> Result<T, ApiErrorResponse<U>>
where where
@@ -322,7 +329,7 @@ where
.header("accept", accept_header.unwrap_or("application/json")) .header("accept", accept_header.unwrap_or("application/json"))
.header( .header(
"authorization", "authorization",
format!("{} {}", &auth_header_key, &secret_token), format!("{} {}", &auth_header_key, &secret_token.access()),
) )
.body(()) .body(())
.map_err(|error| ApiErrorResponse::String(error.to_string()))?; .map_err(|error| ApiErrorResponse::String(error.to_string()))?;

View File

@@ -659,6 +659,14 @@ impl RepoHandle {
.collect::<Result<Vec<Branch>, String>>() .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> { pub fn fetch(&self, remote_name: &str) -> Result<(), String> {
let mut remote = self let mut remote = self
.0 .0
@@ -1034,16 +1042,82 @@ impl RepoHandle {
}) })
} }
pub fn default_branch(&self) -> Result<Branch, String> { pub fn get_remote_default_branch(&self, remote_name: &str) -> Result<Option<Branch>, String> {
match self.0.find_branch("main", git2::BranchType::Local) { // libgit2's `git_remote_default_branch()` and `Remote::default_branch()`
Ok(branch) => Ok(Branch(branch)), // need an actual connection to the remote, so they may fail.
Err(_) => match self.0.find_branch("master", git2::BranchType::Local) { if let Some(mut remote) = self.find_remote(remote_name)? {
Ok(branch) => Ok(Branch(branch)), if remote.connected() {
Err(_) => Err(String::from("Could not determine default branch")), 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 // Looks like there is no distinguishing between the error cases
// "no such remote" and "failed to get remote for some reason". // "no such remote" and "failed to get remote for some reason".
// May be a good idea to handle this explicitly, by returning a // 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)) && !branch_name.ends_with(&format!("{}{}", super::BRANCH_NAMESPACE_SEPARATOR, name))
{ {
return Err(WorktreeRemoveFailureReason::Error(format!( 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 &branch_name
))); )));
} }
@@ -1394,6 +1468,15 @@ impl Branch<'_> {
self.0.delete().map_err(convert_libgit2_error) 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 // only used internally in this module, exposes libgit2 details
fn as_reference(&self) -> &git2::Reference { fn as_reference(&self) -> &git2::Reference {
self.0.get() self.0.get()
@@ -1439,6 +1522,20 @@ impl RemoteHandle<'_> {
.to_string() .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> { 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")) 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"))?; .ok_or_else(|| String::from("Could not detect remote type"))?;
@@ -1529,6 +1626,24 @@ pub fn clone_repo(
repo.rename_remote(&origin, &remote.name)?; 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 // If there is no head_branch, we most likely cloned an empty repository and
// there is no point in setting any upstreams. // there is no point in setting any upstreams.
if let Ok(mut active_branch) = repo.head_branch() { if let Ok(mut active_branch) = repo.head_branch() {

View File

@@ -143,7 +143,35 @@ fn sync_repo(root_path: &Path, repo: &repo::Repo, init_worktree: bool) -> Result
let mut newly_created = false; 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() { if repo.worktree_setup && !actual_git_directory.exists() {
return Err(String::from( return Err(String::from(
"Repo already exists, but is not using a worktree setup", "Repo already exists, but is not using a worktree setup",

View File

@@ -1,9 +1,23 @@
use std::path::Path; use std::path::Path;
use super::output::*;
use super::repo; use super::repo;
pub const GIT_MAIN_WORKTREE_DIRECTORY: &str = ".git-main-working-tree"; 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( pub fn add_worktree(
directory: &Path, directory: &Path,
name: &str, name: &str,
@@ -31,15 +45,38 @@ pub fn add_worktree(
let mut remote_branch_exists = false; 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 default_checkout = || repo.default_branch()?.to_commit();
let checkout_commit; let checkout_commit;
if no_track { if no_track {
checkout_commit = default_checkout()?; checkout_commit = default_checkout()?;
} else { } else {
match track { match track {
Some((remote_name, remote_branch_name)) => { 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 { match remote_branch {
Ok(branch) => { Ok(branch) => {
remote_branch_exists = true; remote_branch_exists = true;
@@ -77,9 +114,8 @@ pub fn add_worktree(
}; };
} }
let mut target_branch = match repo.find_local_branch(name) { repo.create_branch(name, &checkout_commit)?
Ok(branchref) => branchref, }
Err(_) => repo.create_branch(name, &checkout_commit)?,
}; };
fn push( fn push(