Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e75aead3a8 | |||
| dca2b3c9b4 | |||
| a71711978e | |||
| 90d188e01e | |||
| 2e6166e807 | |||
| 8aaaa55d45 | |||
| df39bb3076 | |||
| bc3d4e1c49 | |||
| 32eb4676ee |
0
BRANCH_NAMESPACE_SEPARATOR
Normal file
0
BRANCH_NAMESPACE_SEPARATOR
Normal file
22
Cargo.lock
generated
22
Cargo.lock
generated
@@ -332,7 +332,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "git-repo-manager"
|
name = "git-repo-manager"
|
||||||
version = "0.7.0"
|
version = "0.7.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"comfy-table",
|
"comfy-table",
|
||||||
@@ -347,6 +347,7 @@ dependencies = [
|
|||||||
"shellexpand",
|
"shellexpand",
|
||||||
"tempdir",
|
"tempdir",
|
||||||
"toml",
|
"toml",
|
||||||
|
"url-escape",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -608,6 +609,15 @@ version = "0.1.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-src"
|
||||||
|
version = "111.20.0+1.1.1o"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92892c4f87d56e376e469ace79f1128fdaded07646ddf73aa0be4706ff712dec"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-sys"
|
name = "openssl-sys"
|
||||||
version = "0.9.73"
|
version = "0.9.73"
|
||||||
@@ -617,6 +627,7 @@ dependencies = [
|
|||||||
"autocfg",
|
"autocfg",
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
|
"openssl-src",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
@@ -1189,6 +1200,15 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "url-escape"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "44e0ce4d1246d075ca5abec4b41d33e87a6054d08e2366b63205665e950db218"
|
||||||
|
dependencies = [
|
||||||
|
"percent-encoding",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vcpkg"
|
name = "vcpkg"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
|
|||||||
17
Cargo.toml
17
Cargo.toml
@@ -1,7 +1,8 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "git-repo-manager"
|
name = "git-repo-manager"
|
||||||
version = "0.7.0"
|
version = "0.7.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
authors = [
|
authors = [
|
||||||
"Hannes Körber <hannes@hkoerber.de>",
|
"Hannes Körber <hannes@hkoerber.de>",
|
||||||
]
|
]
|
||||||
@@ -73,10 +74,22 @@ version = "=1.0.81"
|
|||||||
|
|
||||||
[dependencies.isahc]
|
[dependencies.isahc]
|
||||||
version = "=1.7.2"
|
version = "=1.7.2"
|
||||||
features = ["json"]
|
default-features = false
|
||||||
|
features = ["json", "http2", "text-decoding"]
|
||||||
|
|
||||||
[dependencies.parse_link_header]
|
[dependencies.parse_link_header]
|
||||||
version = "=0.3.2"
|
version = "=0.3.2"
|
||||||
|
|
||||||
|
[dependencies.url-escape]
|
||||||
|
version = "=0.1.1"
|
||||||
|
|
||||||
[dev-dependencies.tempdir]
|
[dev-dependencies.tempdir]
|
||||||
version = "=0.3.7"
|
version = "=0.3.7"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
static-build = [
|
||||||
|
"git2/vendored-openssl",
|
||||||
|
"git2/vendored-libgit2",
|
||||||
|
"isahc/static-curl",
|
||||||
|
"isahc/static-ssl",
|
||||||
|
]
|
||||||
|
|||||||
39
Justfile
39
Justfile
@@ -1,5 +1,7 @@
|
|||||||
set positional-arguments
|
set positional-arguments
|
||||||
|
|
||||||
|
target := "x86_64-unknown-linux-musl"
|
||||||
|
|
||||||
check: test
|
check: test
|
||||||
cargo check
|
cargo check
|
||||||
cargo fmt --check
|
cargo fmt --check
|
||||||
@@ -16,23 +18,26 @@ lint-fix:
|
|||||||
cargo clippy --no-deps --fix
|
cargo clippy --no-deps --fix
|
||||||
|
|
||||||
release:
|
release:
|
||||||
cargo build --release
|
cargo build --release --target {{target}}
|
||||||
|
|
||||||
test-binary-docker:
|
|
||||||
env \
|
|
||||||
GITHUB_API_BASEURL=http://rest:5000/github \
|
|
||||||
GITLAB_API_BASEURL=http://rest:5000/gitlab \
|
|
||||||
cargo build --profile e2e-tests
|
|
||||||
|
|
||||||
test-binary:
|
test-binary:
|
||||||
env \
|
env \
|
||||||
GITHUB_API_BASEURL=http://localhost:5000/github \
|
GITHUB_API_BASEURL=http://rest:5000/github \
|
||||||
GITLAB_API_BASEURL=http://localhost:5000/gitlab \
|
GITLAB_API_BASEURL=http://rest:5000/gitlab \
|
||||||
cargo build --profile e2e-tests
|
cargo build --target {{target}} --profile e2e-tests --features=static-build
|
||||||
|
|
||||||
install:
|
install:
|
||||||
cargo install --path .
|
cargo install --path .
|
||||||
|
|
||||||
|
install-static:
|
||||||
|
cargo install --target {{target}} --features=static-build --path .
|
||||||
|
|
||||||
|
build:
|
||||||
|
cargo build
|
||||||
|
|
||||||
|
build-static:
|
||||||
|
cargo build --target {{target}} --features=static-build
|
||||||
|
|
||||||
test: test-unit test-integration test-e2e
|
test: test-unit test-integration test-e2e
|
||||||
|
|
||||||
test-unit:
|
test-unit:
|
||||||
@@ -41,23 +46,15 @@ test-unit:
|
|||||||
test-integration:
|
test-integration:
|
||||||
cargo test --test "*"
|
cargo test --test "*"
|
||||||
|
|
||||||
test-e2e-docker +tests=".": test-binary-docker
|
test-e2e +tests=".": test-binary
|
||||||
cd ./e2e_tests \
|
cd ./e2e_tests \
|
||||||
&& docker-compose rm --stop -f \
|
&& docker-compose rm --stop -f \
|
||||||
&& docker-compose build \
|
&& docker-compose build \
|
||||||
&& docker-compose run \
|
&& docker-compose run \
|
||||||
--rm \
|
--rm \
|
||||||
-v $PWD/../target/e2e-tests/grm:/grm \
|
-v $PWD/../target/{{target}}/e2e-tests/grm:/grm \
|
||||||
pytest \
|
pytest \
|
||||||
"GRM_BINARY=/grm python3 ALTERNATE_DOMAIN=alternate-rest -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
|
|
||||||
|
|
||||||
test-e2e +tests=".": test-binary
|
|
||||||
cd ./e2e_tests \
|
|
||||||
&& docker-compose rm --stop -f \
|
|
||||||
&& docker-compose build \
|
|
||||||
&& docker-compose up -d rest \
|
|
||||||
&& GRM_BINARY={{justfile_directory()}}/target/e2e-tests/grm ALTERNATE_DOMAIN=127.0.0.1 python3 -m pytest -p no:cacheprovider --color=yes {{tests}} \
|
|
||||||
&& docker-compose rm --stop -f
|
&& docker-compose rm --stop -f
|
||||||
|
|
||||||
update-dependencies: update-cargo-dependencies
|
update-dependencies: update-cargo-dependencies
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Summary
|
# Summary
|
||||||
|
|
||||||
- [Overview](./overview.md)
|
- [Overview](./overview.md)
|
||||||
- [Getting started](./getting_started.md)
|
- [Installation](./installation.md)
|
||||||
- [Repository trees](./repos.md)
|
- [Repository trees](./repos.md)
|
||||||
- [Git Worktrees](./worktrees.md)
|
- [Git Worktrees](./worktrees.md)
|
||||||
- [Forge Integrations](./forge_integration.md)
|
- [Forge Integrations](./forge_integration.md)
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
# Quickstart
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
Building GRM currently requires the nightly Rust toolchain. The easiest way
|
|
||||||
is using [`rustup`](https://rustup.rs/). Make sure that rustup is properly installed.
|
|
||||||
|
|
||||||
Make sure that the nightly toolchain is installed:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ rustup toolchain install nightly
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ cargo +nightly install --git https://github.com/hakoerber/git-repo-manager.git --branch master
|
|
||||||
```
|
|
||||||
|
|
||||||
If you're brave, you can also run the development build:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ cargo +nightly install --git https://github.com/hakoerber/git-repo-manager.git --branch develop
|
|
||||||
```
|
|
||||||
56
docs/src/installation.md
Normal file
56
docs/src/installation.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# Installation
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Building GRM currently requires the nightly Rust toolchain. The easiest way
|
||||||
|
is using [`rustup`](https://rustup.rs/). Make sure that rustup is properly installed.
|
||||||
|
|
||||||
|
Make sure that the nightly toolchain is installed:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ rustup toolchain install nightly
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, install the build dependencies:
|
||||||
|
|
||||||
|
| Distribution | Command |
|
||||||
|
| ------------- | ------------------------------------------------------------------------------ |
|
||||||
|
| Archlinux | `pacman -S --needed gcc openssl pkg-config` |
|
||||||
|
| Ubuntu/Debian | `apt-get install --no-install-recommends pkg-config gcc libssl-dev zlib1g-dev` |
|
||||||
|
|
||||||
|
Then, it's a simple command to install the latest stable version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ cargo +nightly install git-repo-manager
|
||||||
|
```
|
||||||
|
|
||||||
|
If you're brave, you can also run the development build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ cargo +nightly install --git https://github.com/hakoerber/git-repo-manager.git --branch develop
|
||||||
|
```
|
||||||
|
|
||||||
|
## Static build
|
||||||
|
|
||||||
|
Note that by default, you will get a dynamically linked executable.
|
||||||
|
Alternatively, you can also build a statically linked binary. For this, you
|
||||||
|
will need `musl` and a few other build dependencies installed installed:
|
||||||
|
|
||||||
|
| Distribution | Command |
|
||||||
|
| ------------- | --------------------------------------------------------------------------- |
|
||||||
|
| Archlinux | `pacman -S --needed gcc musl perl make` |
|
||||||
|
| Ubuntu/Debian | `apt-get install --no-install-recommends gcc musl-tools libc-dev perl make` |
|
||||||
|
|
||||||
|
(`perl` and `make` are required for the OpenSSL build script)
|
||||||
|
|
||||||
|
The, add the musl target via `rustup`:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ rustup +nightly target add x86_64-unknown-linux-musl
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, use a modified build command to get a statically linked binary:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ cargo +nightly install git-repo-manager --target x86_64-unknown-linux-musl --features=static-build
|
||||||
|
```
|
||||||
@@ -23,8 +23,6 @@ services:
|
|||||||
build: ./docker-rest/
|
build: ./docker-rest/
|
||||||
expose:
|
expose:
|
||||||
- "5000"
|
- "5000"
|
||||||
ports:
|
|
||||||
- "5000:5000"
|
|
||||||
networks:
|
networks:
|
||||||
main:
|
main:
|
||||||
aliases:
|
aliases:
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ def check_headers():
|
|||||||
app.logger.error("Invalid accept header")
|
app.logger.error("Invalid accept header")
|
||||||
abort(500)
|
abort(500)
|
||||||
auth_header = request.headers.get("authorization")
|
auth_header = request.headers.get("authorization")
|
||||||
if auth_header != "token authtoken":
|
if auth_header != "token secret-token:myauthtoken":
|
||||||
app.logger.error("Invalid authorization header: %s", auth_header)
|
app.logger.error("Invalid authorization header: %s", auth_header)
|
||||||
abort(
|
abort(
|
||||||
make_response(
|
make_response(
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ def check_headers():
|
|||||||
app.logger.error("Invalid accept header")
|
app.logger.error("Invalid accept header")
|
||||||
abort(500)
|
abort(500)
|
||||||
auth_header = request.headers.get("authorization")
|
auth_header = request.headers.get("authorization")
|
||||||
if auth_header != "bearer authtoken":
|
if auth_header != "bearer secret-token:myauthtoken":
|
||||||
app.logger.error("Invalid authorization header: %s", auth_header)
|
app.logger.error("Invalid authorization header: %s", auth_header)
|
||||||
abort(
|
abort(
|
||||||
make_response(
|
make_response(
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ def grm(args, cwd=None, is_invalid=False):
|
|||||||
print(f"grmcmd: {args}")
|
print(f"grmcmd: {args}")
|
||||||
print(f"stdout:\n{cmd.stdout}")
|
print(f"stdout:\n{cmd.stdout}")
|
||||||
print(f"stderr:\n{cmd.stderr}")
|
print(f"stderr:\n{cmd.stderr}")
|
||||||
|
assert "secret-token:" not in cmd.stdout
|
||||||
|
assert "secret-token:" not in cmd.stderr
|
||||||
assert "panicked" not in cmd.stderr
|
assert "panicked" not in cmd.stderr
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ def test_repos_find_remote_no_filter(provider, configtype, default, use_config):
|
|||||||
f.write(
|
f.write(
|
||||||
f"""
|
f"""
|
||||||
provider = "{provider}"
|
provider = "{provider}"
|
||||||
token_command = "echo authtoken"
|
token_command = "echo secret-token:myauthtoken"
|
||||||
root = "/myroot"
|
root = "/myroot"
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
@@ -157,7 +157,7 @@ def test_repos_find_remote_no_filter(provider, configtype, default, use_config):
|
|||||||
"--provider",
|
"--provider",
|
||||||
provider,
|
provider,
|
||||||
"--token-command",
|
"--token-command",
|
||||||
"echo authtoken",
|
"echo secret-token:myauthtoken",
|
||||||
"--root",
|
"--root",
|
||||||
"/myroot",
|
"/myroot",
|
||||||
]
|
]
|
||||||
@@ -193,7 +193,7 @@ def test_repos_find_remote_user_empty(
|
|||||||
with open(config.name, "w") as f:
|
with open(config.name, "w") as f:
|
||||||
cfg = f"""
|
cfg = f"""
|
||||||
provider = "{provider}"
|
provider = "{provider}"
|
||||||
token_command = "echo authtoken"
|
token_command = "echo secret-token:myauthtoken"
|
||||||
root = "/myroot"
|
root = "/myroot"
|
||||||
|
|
||||||
[filters]
|
[filters]
|
||||||
@@ -213,7 +213,7 @@ def test_repos_find_remote_user_empty(
|
|||||||
"--provider",
|
"--provider",
|
||||||
provider,
|
provider,
|
||||||
"--token-command",
|
"--token-command",
|
||||||
"echo authtoken",
|
"echo secret-token:myauthtoken",
|
||||||
"--root",
|
"--root",
|
||||||
"/myroot",
|
"/myroot",
|
||||||
"--user",
|
"--user",
|
||||||
@@ -264,7 +264,7 @@ def test_repos_find_remote_user(
|
|||||||
with open(config.name, "w") as f:
|
with open(config.name, "w") as f:
|
||||||
cfg = f"""
|
cfg = f"""
|
||||||
provider = "{provider}"
|
provider = "{provider}"
|
||||||
token_command = "echo authtoken"
|
token_command = "echo secret-token:myauthtoken"
|
||||||
root = "/myroot"
|
root = "/myroot"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -300,7 +300,7 @@ def test_repos_find_remote_user(
|
|||||||
"--provider",
|
"--provider",
|
||||||
provider,
|
provider,
|
||||||
"--token-command",
|
"--token-command",
|
||||||
"echo authtoken",
|
"echo secret-token:myauthtoken",
|
||||||
"--root",
|
"--root",
|
||||||
"/myroot",
|
"/myroot",
|
||||||
]
|
]
|
||||||
@@ -378,7 +378,7 @@ def test_repos_find_remote_group_empty(
|
|||||||
with open(config.name, "w") as f:
|
with open(config.name, "w") as f:
|
||||||
cfg = f"""
|
cfg = f"""
|
||||||
provider = "{provider}"
|
provider = "{provider}"
|
||||||
token_command = "echo authtoken"
|
token_command = "echo secret-token:myauthtoken"
|
||||||
root = "/myroot"
|
root = "/myroot"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -403,7 +403,7 @@ def test_repos_find_remote_group_empty(
|
|||||||
"--provider",
|
"--provider",
|
||||||
provider,
|
provider,
|
||||||
"--token-command",
|
"--token-command",
|
||||||
"echo authtoken",
|
"echo secret-token:myauthtoken",
|
||||||
"--root",
|
"--root",
|
||||||
"/myroot",
|
"/myroot",
|
||||||
"--group",
|
"--group",
|
||||||
@@ -459,7 +459,7 @@ def test_repos_find_remote_group(
|
|||||||
with open(config.name, "w") as f:
|
with open(config.name, "w") as f:
|
||||||
cfg = f"""
|
cfg = f"""
|
||||||
provider = "{provider}"
|
provider = "{provider}"
|
||||||
token_command = "echo authtoken"
|
token_command = "echo secret-token:myauthtoken"
|
||||||
root = "/myroot"
|
root = "/myroot"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -488,7 +488,7 @@ def test_repos_find_remote_group(
|
|||||||
"--provider",
|
"--provider",
|
||||||
provider,
|
provider,
|
||||||
"--token-command",
|
"--token-command",
|
||||||
"echo authtoken",
|
"echo secret-token:myauthtoken",
|
||||||
"--root",
|
"--root",
|
||||||
"/myroot",
|
"/myroot",
|
||||||
"--group",
|
"--group",
|
||||||
@@ -575,7 +575,7 @@ def test_repos_find_remote_user_and_group(
|
|||||||
with open(config.name, "w") as f:
|
with open(config.name, "w") as f:
|
||||||
cfg = f"""
|
cfg = f"""
|
||||||
provider = "{provider}"
|
provider = "{provider}"
|
||||||
token_command = "echo authtoken"
|
token_command = "echo secret-token:myauthtoken"
|
||||||
root = "/myroot"
|
root = "/myroot"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -609,7 +609,7 @@ def test_repos_find_remote_user_and_group(
|
|||||||
"--provider",
|
"--provider",
|
||||||
provider,
|
provider,
|
||||||
"--token-command",
|
"--token-command",
|
||||||
"echo authtoken",
|
"echo secret-token:myauthtoken",
|
||||||
"--root",
|
"--root",
|
||||||
"/myroot",
|
"/myroot",
|
||||||
"--group",
|
"--group",
|
||||||
@@ -726,7 +726,7 @@ def test_repos_find_remote_owner(
|
|||||||
with open(config.name, "w") as f:
|
with open(config.name, "w") as f:
|
||||||
cfg = f"""
|
cfg = f"""
|
||||||
provider = "{provider}"
|
provider = "{provider}"
|
||||||
token_command = "echo authtoken"
|
token_command = "echo secret-token:myauthtoken"
|
||||||
root = "/myroot"
|
root = "/myroot"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -761,7 +761,7 @@ def test_repos_find_remote_owner(
|
|||||||
"--provider",
|
"--provider",
|
||||||
provider,
|
provider,
|
||||||
"--token-command",
|
"--token-command",
|
||||||
"echo authtoken",
|
"echo secret-token:myauthtoken",
|
||||||
"--root",
|
"--root",
|
||||||
"/myroot",
|
"/myroot",
|
||||||
"--access",
|
"--access",
|
||||||
|
|||||||
36
src/auth.rs
Normal file
36
src/auth.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
use std::process;
|
||||||
|
|
||||||
|
pub fn get_token_from_command(command: &str) -> Result<String, String> {
|
||||||
|
let output = process::Command::new("/usr/bin/env")
|
||||||
|
.arg("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(command)
|
||||||
|
.output()
|
||||||
|
.map_err(|error| format!("Failed to run token-command: {}", error))?;
|
||||||
|
|
||||||
|
let stderr = String::from_utf8(output.stderr).map_err(|error| error.to_string())?;
|
||||||
|
let stdout = String::from_utf8(output.stdout).map_err(|error| error.to_string())?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
if !stderr.is_empty() {
|
||||||
|
return Err(format!("Token command failed: {}", stderr));
|
||||||
|
} else {
|
||||||
|
return Err(String::from("Token command failed."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !stderr.is_empty() {
|
||||||
|
return Err(format!("Token command produced stderr: {}", stderr));
|
||||||
|
}
|
||||||
|
|
||||||
|
if stdout.is_empty() {
|
||||||
|
return Err(String::from("Token command did not produce output"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = stdout
|
||||||
|
.split('\n')
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| String::from("Output did not contain any newline"))?;
|
||||||
|
|
||||||
|
Ok(token.to_string())
|
||||||
|
}
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::process;
|
use std::process;
|
||||||
|
|
||||||
use crate::output::*;
|
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::{get_token_from_command, path_as_string, Remote, Repo, Tree};
|
use super::auth;
|
||||||
|
use super::output::*;
|
||||||
|
use super::path;
|
||||||
|
use super::provider;
|
||||||
|
use super::provider::Filter;
|
||||||
|
use super::provider::Provider;
|
||||||
|
use super::repo;
|
||||||
|
use super::tree;
|
||||||
|
|
||||||
use crate::provider;
|
pub type RemoteProvider = provider::RemoteProvider;
|
||||||
use crate::provider::Filter;
|
pub type RemoteType = repo::RemoteType;
|
||||||
use crate::provider::Provider;
|
|
||||||
|
|
||||||
pub type RemoteProvider = crate::provider::RemoteProvider;
|
|
||||||
pub type RemoteType = crate::repo::RemoteType;
|
|
||||||
|
|
||||||
fn worktree_setup_default() -> bool {
|
fn worktree_setup_default() -> bool {
|
||||||
false
|
false
|
||||||
@@ -64,7 +65,7 @@ pub struct RemoteConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RemoteConfig {
|
impl RemoteConfig {
|
||||||
pub fn from_remote(remote: Remote) -> Self {
|
pub fn from_remote(remote: repo::Remote) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: remote.name,
|
name: remote.name,
|
||||||
url: remote.url,
|
url: remote.url,
|
||||||
@@ -72,8 +73,8 @@ impl RemoteConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_remote(self) -> Remote {
|
pub fn into_remote(self) -> repo::Remote {
|
||||||
Remote {
|
repo::Remote {
|
||||||
name: self.name,
|
name: self.name,
|
||||||
url: self.url,
|
url: self.url,
|
||||||
remote_type: self.remote_type,
|
remote_type: self.remote_type,
|
||||||
@@ -93,7 +94,7 @@ pub struct RepoConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RepoConfig {
|
impl RepoConfig {
|
||||||
pub fn from_repo(repo: Repo) -> Self {
|
pub fn from_repo(repo: repo::Repo) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: repo.name,
|
name: repo.name,
|
||||||
worktree_setup: repo.worktree_setup,
|
worktree_setup: repo.worktree_setup,
|
||||||
@@ -103,14 +104,14 @@ impl RepoConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_repo(self) -> Repo {
|
pub fn into_repo(self) -> repo::Repo {
|
||||||
let (namespace, name) = if let Some((namespace, name)) = self.name.rsplit_once('/') {
|
let (namespace, name) = if let Some((namespace, name)) = self.name.rsplit_once('/') {
|
||||||
(Some(namespace.to_string()), name.to_string())
|
(Some(namespace.to_string()), name.to_string())
|
||||||
} else {
|
} else {
|
||||||
(None, self.name)
|
(None, self.name)
|
||||||
};
|
};
|
||||||
|
|
||||||
Repo {
|
repo::Repo {
|
||||||
name,
|
name,
|
||||||
namespace,
|
namespace,
|
||||||
worktree_setup: self.worktree_setup,
|
worktree_setup: self.worktree_setup,
|
||||||
@@ -133,7 +134,7 @@ impl ConfigTrees {
|
|||||||
ConfigTrees { trees: vec }
|
ConfigTrees { trees: vec }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_trees(vec: Vec<Tree>) -> Self {
|
pub fn from_trees(vec: Vec<tree::Tree>) -> Self {
|
||||||
ConfigTrees {
|
ConfigTrees {
|
||||||
trees: vec.into_iter().map(ConfigTree::from_tree).collect(),
|
trees: vec.into_iter().map(ConfigTree::from_tree).collect(),
|
||||||
}
|
}
|
||||||
@@ -157,7 +158,7 @@ impl Config {
|
|||||||
match self {
|
match self {
|
||||||
Config::ConfigTrees(config) => Ok(config.trees),
|
Config::ConfigTrees(config) => Ok(config.trees),
|
||||||
Config::ConfigProvider(config) => {
|
Config::ConfigProvider(config) => {
|
||||||
let token = match get_token_from_command(&config.token_command) {
|
let token = match auth::get_token_from_command(&config.token_command) {
|
||||||
Ok(token) => token,
|
Ok(token) => token,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
print_error(&format!("Getting token from command failed: {}", error));
|
print_error(&format!("Getting token from command failed: {}", error));
|
||||||
@@ -217,9 +218,9 @@ impl Config {
|
|||||||
.collect();
|
.collect();
|
||||||
let tree = ConfigTree {
|
let tree = ConfigTree {
|
||||||
root: if let Some(namespace) = namespace {
|
root: if let Some(namespace) = namespace {
|
||||||
path_as_string(&Path::new(&config.root).join(namespace))
|
path::path_as_string(&Path::new(&config.root).join(namespace))
|
||||||
} else {
|
} else {
|
||||||
path_as_string(Path::new(&config.root))
|
path::path_as_string(Path::new(&config.root))
|
||||||
},
|
},
|
||||||
repos: Some(repos),
|
repos: Some(repos),
|
||||||
};
|
};
|
||||||
@@ -236,7 +237,7 @@ impl Config {
|
|||||||
|
|
||||||
pub fn normalize(&mut self) {
|
pub fn normalize(&mut self) {
|
||||||
if let Config::ConfigTrees(config) = self {
|
if let Config::ConfigTrees(config) = self {
|
||||||
let home = super::env_home().display().to_string();
|
let home = path::env_home().display().to_string();
|
||||||
for tree in &mut config.trees_mut().iter_mut() {
|
for tree in &mut config.trees_mut().iter_mut() {
|
||||||
if tree.root.starts_with(&home) {
|
if tree.root.starts_with(&home) {
|
||||||
// The tilde is not handled differently, it's just a normal path component for `Path`.
|
// The tilde is not handled differently, it's just a normal path component for `Path`.
|
||||||
@@ -275,14 +276,14 @@ pub struct ConfigTree {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigTree {
|
impl ConfigTree {
|
||||||
pub fn from_repos(root: String, repos: Vec<Repo>) -> Self {
|
pub fn from_repos(root: String, repos: Vec<repo::Repo>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
root,
|
root,
|
||||||
repos: Some(repos.into_iter().map(RepoConfig::from_repo).collect()),
|
repos: Some(repos.into_iter().map(RepoConfig::from_repo).collect()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_tree(tree: Tree) -> Self {
|
pub fn from_tree(tree: tree::Tree) -> Self {
|
||||||
Self {
|
Self {
|
||||||
root: tree.root,
|
root: tree.root,
|
||||||
repos: Some(tree.repos.into_iter().map(RepoConfig::from_repo).collect()),
|
repos: Some(tree.repos.into_iter().map(RepoConfig::from_repo).collect()),
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ pub struct Config {
|
|||||||
pub init_worktree: String,
|
pub init_worktree: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type RemoteProvider = grm::provider::RemoteProvider;
|
pub type RemoteProvider = super::provider::RemoteProvider;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[clap()]
|
#[clap()]
|
||||||
|
|||||||
118
src/grm/main.rs
118
src/grm/main.rs
@@ -3,12 +3,17 @@ use std::process;
|
|||||||
|
|
||||||
mod cmd;
|
mod cmd;
|
||||||
|
|
||||||
|
use grm::auth;
|
||||||
use grm::config;
|
use grm::config;
|
||||||
|
use grm::find_in_tree;
|
||||||
use grm::output::*;
|
use grm::output::*;
|
||||||
use grm::path_as_string;
|
use grm::path;
|
||||||
use grm::provider;
|
use grm::provider;
|
||||||
use grm::provider::Provider;
|
use grm::provider::Provider;
|
||||||
use grm::repo;
|
use grm::repo;
|
||||||
|
use grm::table;
|
||||||
|
use grm::tree;
|
||||||
|
use grm::worktree;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let opts = cmd::parse();
|
let opts = cmd::parse();
|
||||||
@@ -24,7 +29,7 @@ fn main() {
|
|||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
match grm::sync_trees(config, args.init_worktree == "true") {
|
match tree::sync_trees(config, args.init_worktree == "true") {
|
||||||
Ok(success) => {
|
Ok(success) => {
|
||||||
if !success {
|
if !success {
|
||||||
process::exit(1)
|
process::exit(1)
|
||||||
@@ -37,7 +42,7 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
cmd::SyncAction::Remote(args) => {
|
cmd::SyncAction::Remote(args) => {
|
||||||
let token = match grm::get_token_from_command(&args.token_command) {
|
let token = match auth::get_token_from_command(&args.token_command) {
|
||||||
Ok(token) => token,
|
Ok(token) => token,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
print_error(&format!("Getting token from command failed: {}", error));
|
print_error(&format!("Getting token from command failed: {}", error));
|
||||||
@@ -45,18 +50,14 @@ fn main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let filter = grm::provider::Filter::new(
|
let filter =
|
||||||
args.users,
|
provider::Filter::new(args.users, args.groups, args.owner, args.access);
|
||||||
args.groups,
|
|
||||||
args.owner,
|
|
||||||
args.access,
|
|
||||||
);
|
|
||||||
|
|
||||||
let worktree = args.worktree == "true";
|
let worktree = args.worktree == "true";
|
||||||
|
|
||||||
let repos = match args.provider {
|
let repos = match args.provider {
|
||||||
cmd::RemoteProvider::Github => {
|
cmd::RemoteProvider::Github => {
|
||||||
match grm::provider::Github::new(filter, token, args.api_url) {
|
match provider::Github::new(filter, token, args.api_url) {
|
||||||
Ok(provider) => provider,
|
Ok(provider) => provider,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
print_error(&format!("Error: {}", error));
|
print_error(&format!("Error: {}", error));
|
||||||
@@ -66,7 +67,7 @@ fn main() {
|
|||||||
.get_repos(worktree, args.force_ssh)
|
.get_repos(worktree, args.force_ssh)
|
||||||
}
|
}
|
||||||
cmd::RemoteProvider::Gitlab => {
|
cmd::RemoteProvider::Gitlab => {
|
||||||
match grm::provider::Gitlab::new(filter, token, args.api_url) {
|
match provider::Gitlab::new(filter, token, args.api_url) {
|
||||||
Ok(provider) => provider,
|
Ok(provider) => provider,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
print_error(&format!("Error: {}", error));
|
print_error(&format!("Error: {}", error));
|
||||||
@@ -83,9 +84,9 @@ fn main() {
|
|||||||
|
|
||||||
for (namespace, repolist) in repos {
|
for (namespace, repolist) in repos {
|
||||||
let root = if let Some(namespace) = namespace {
|
let root = if let Some(namespace) = namespace {
|
||||||
path_as_string(&Path::new(&args.root).join(namespace))
|
path::path_as_string(&Path::new(&args.root).join(namespace))
|
||||||
} else {
|
} else {
|
||||||
path_as_string(Path::new(&args.root))
|
path::path_as_string(Path::new(&args.root))
|
||||||
};
|
};
|
||||||
|
|
||||||
let tree = config::ConfigTree::from_repos(root, repolist);
|
let tree = config::ConfigTree::from_repos(root, repolist);
|
||||||
@@ -94,7 +95,7 @@ fn main() {
|
|||||||
|
|
||||||
let config = config::Config::from_trees(trees);
|
let config = config::Config::from_trees(trees);
|
||||||
|
|
||||||
match grm::sync_trees(config, args.init_worktree == "true") {
|
match tree::sync_trees(config, args.init_worktree == "true") {
|
||||||
Ok(success) => {
|
Ok(success) => {
|
||||||
if !success {
|
if !success {
|
||||||
process::exit(1)
|
process::exit(1)
|
||||||
@@ -122,7 +123,7 @@ fn main() {
|
|||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
match grm::table::get_status_table(config) {
|
match table::get_status_table(config) {
|
||||||
Ok((tables, errors)) => {
|
Ok((tables, errors)) => {
|
||||||
for table in tables {
|
for table in tables {
|
||||||
println!("{}", table);
|
println!("{}", table);
|
||||||
@@ -146,7 +147,7 @@ fn main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match grm::table::show_single_repo_status(&dir) {
|
match table::show_single_repo_status(&dir) {
|
||||||
Ok((table, warnings)) => {
|
Ok((table, warnings)) => {
|
||||||
println!("{}", table);
|
println!("{}", table);
|
||||||
for warning in warnings {
|
for warning in warnings {
|
||||||
@@ -184,7 +185,7 @@ fn main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let (found_repos, warnings) = match grm::find_in_tree(&path) {
|
let (found_repos, warnings) = match find_in_tree(&path) {
|
||||||
Ok((repos, warnings)) => (repos, warnings),
|
Ok((repos, warnings)) => (repos, warnings),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
print_error(&error);
|
print_error(&error);
|
||||||
@@ -192,7 +193,7 @@ fn main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let trees = grm::config::ConfigTrees::from_trees(vec![found_repos]);
|
let trees = config::ConfigTrees::from_trees(vec![found_repos]);
|
||||||
if trees.trees_ref().iter().all(|t| match &t.repos {
|
if trees.trees_ref().iter().all(|t| match &t.repos {
|
||||||
None => false,
|
None => false,
|
||||||
Some(r) => r.is_empty(),
|
Some(r) => r.is_empty(),
|
||||||
@@ -237,8 +238,7 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
cmd::FindAction::Config(args) => {
|
cmd::FindAction::Config(args) => {
|
||||||
let config: crate::config::ConfigProvider =
|
let config: config::ConfigProvider = match config::read_config(&args.config) {
|
||||||
match config::read_config(&args.config) {
|
|
||||||
Ok(config) => config,
|
Ok(config) => config,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
print_error(&error);
|
print_error(&error);
|
||||||
@@ -246,7 +246,7 @@ fn main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let token = match grm::get_token_from_command(&config.token_command) {
|
let token = match auth::get_token_from_command(&config.token_command) {
|
||||||
Ok(token) => token,
|
Ok(token) => token,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
print_error(&format!("Getting token from command failed: {}", error));
|
print_error(&format!("Getting token from command failed: {}", error));
|
||||||
@@ -254,7 +254,7 @@ fn main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let filters = config.filters.unwrap_or(grm::config::ConfigProviderFilter {
|
let filters = config.filters.unwrap_or(config::ConfigProviderFilter {
|
||||||
access: Some(false),
|
access: Some(false),
|
||||||
owner: Some(false),
|
owner: Some(false),
|
||||||
users: Some(vec![]),
|
users: Some(vec![]),
|
||||||
@@ -314,14 +314,14 @@ fn main() {
|
|||||||
for (namespace, namespace_repos) in repos {
|
for (namespace, namespace_repos) in repos {
|
||||||
let tree = config::ConfigTree {
|
let tree = config::ConfigTree {
|
||||||
root: if let Some(namespace) = namespace {
|
root: if let Some(namespace) = namespace {
|
||||||
path_as_string(&Path::new(&config.root).join(namespace))
|
path::path_as_string(&Path::new(&config.root).join(namespace))
|
||||||
} else {
|
} else {
|
||||||
path_as_string(Path::new(&config.root))
|
path::path_as_string(Path::new(&config.root))
|
||||||
},
|
},
|
||||||
repos: Some(
|
repos: Some(
|
||||||
namespace_repos
|
namespace_repos
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(grm::config::RepoConfig::from_repo)
|
.map(config::RepoConfig::from_repo)
|
||||||
.collect(),
|
.collect(),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
@@ -360,7 +360,7 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
cmd::FindAction::Remote(args) => {
|
cmd::FindAction::Remote(args) => {
|
||||||
let token = match grm::get_token_from_command(&args.token_command) {
|
let token = match auth::get_token_from_command(&args.token_command) {
|
||||||
Ok(token) => token,
|
Ok(token) => token,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
print_error(&format!("Getting token from command failed: {}", error));
|
print_error(&format!("Getting token from command failed: {}", error));
|
||||||
@@ -368,18 +368,14 @@ fn main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let filter = grm::provider::Filter::new(
|
let filter =
|
||||||
args.users,
|
provider::Filter::new(args.users, args.groups, args.owner, args.access);
|
||||||
args.groups,
|
|
||||||
args.owner,
|
|
||||||
args.access,
|
|
||||||
);
|
|
||||||
|
|
||||||
let worktree = args.worktree == "true";
|
let worktree = args.worktree == "true";
|
||||||
|
|
||||||
let repos = match args.provider {
|
let repos = match args.provider {
|
||||||
cmd::RemoteProvider::Github => {
|
cmd::RemoteProvider::Github => {
|
||||||
match grm::provider::Github::new(filter, token, args.api_url) {
|
match provider::Github::new(filter, token, args.api_url) {
|
||||||
Ok(provider) => provider,
|
Ok(provider) => provider,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
print_error(&format!("Error: {}", error));
|
print_error(&format!("Error: {}", error));
|
||||||
@@ -389,7 +385,7 @@ fn main() {
|
|||||||
.get_repos(worktree, args.force_ssh)
|
.get_repos(worktree, args.force_ssh)
|
||||||
}
|
}
|
||||||
cmd::RemoteProvider::Gitlab => {
|
cmd::RemoteProvider::Gitlab => {
|
||||||
match grm::provider::Gitlab::new(filter, token, args.api_url) {
|
match provider::Gitlab::new(filter, token, args.api_url) {
|
||||||
Ok(provider) => provider,
|
Ok(provider) => provider,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
print_error(&format!("Error: {}", error));
|
print_error(&format!("Error: {}", error));
|
||||||
@@ -410,14 +406,14 @@ fn main() {
|
|||||||
for (namespace, repolist) in repos {
|
for (namespace, repolist) in repos {
|
||||||
let tree = config::ConfigTree {
|
let tree = config::ConfigTree {
|
||||||
root: if let Some(namespace) = namespace {
|
root: if let Some(namespace) = namespace {
|
||||||
path_as_string(&Path::new(&args.root).join(namespace))
|
path::path_as_string(&Path::new(&args.root).join(namespace))
|
||||||
} else {
|
} else {
|
||||||
path_as_string(Path::new(&args.root))
|
path::path_as_string(Path::new(&args.root))
|
||||||
},
|
},
|
||||||
repos: Some(
|
repos: Some(
|
||||||
repolist
|
repolist
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(grm::config::RepoConfig::from_repo)
|
.map(config::RepoConfig::from_repo)
|
||||||
.collect(),
|
.collect(),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
@@ -503,7 +499,13 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match grm::add_worktree(&cwd, name, subdirectory, track, action_args.no_track) {
|
match worktree::add_worktree(
|
||||||
|
&cwd,
|
||||||
|
name,
|
||||||
|
subdirectory,
|
||||||
|
track,
|
||||||
|
action_args.no_track,
|
||||||
|
) {
|
||||||
Ok(_) => print_success(&format!("Worktree {} created", &action_args.name)),
|
Ok(_) => print_success(&format!("Worktree {} created", &action_args.name)),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
print_error(&format!("Error creating worktree: {}", error));
|
print_error(&format!("Error creating worktree: {}", error));
|
||||||
@@ -525,7 +527,7 @@ fn main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let repo = grm::RepoHandle::open(&cwd, true).unwrap_or_else(|error| {
|
let repo = repo::RepoHandle::open(&cwd, true).unwrap_or_else(|error| {
|
||||||
print_error(&format!("Error opening repository: {}", error));
|
print_error(&format!("Error opening repository: {}", error));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
});
|
});
|
||||||
@@ -539,17 +541,17 @@ fn main() {
|
|||||||
Ok(_) => print_success(&format!("Worktree {} deleted", &action_args.name)),
|
Ok(_) => print_success(&format!("Worktree {} deleted", &action_args.name)),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
match error {
|
match error {
|
||||||
grm::WorktreeRemoveFailureReason::Error(msg) => {
|
repo::WorktreeRemoveFailureReason::Error(msg) => {
|
||||||
print_error(&msg);
|
print_error(&msg);
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
grm::WorktreeRemoveFailureReason::Changes(changes) => {
|
repo::WorktreeRemoveFailureReason::Changes(changes) => {
|
||||||
print_warning(&format!(
|
print_warning(&format!(
|
||||||
"Changes in worktree: {}. Refusing to delete",
|
"Changes in worktree: {}. Refusing to delete",
|
||||||
changes
|
changes
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
grm::WorktreeRemoveFailureReason::NotMerged(message) => {
|
repo::WorktreeRemoveFailureReason::NotMerged(message) => {
|
||||||
print_warning(&message);
|
print_warning(&message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -558,12 +560,12 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
cmd::WorktreeAction::Status(_args) => {
|
cmd::WorktreeAction::Status(_args) => {
|
||||||
let repo = grm::RepoHandle::open(&cwd, true).unwrap_or_else(|error| {
|
let repo = repo::RepoHandle::open(&cwd, true).unwrap_or_else(|error| {
|
||||||
print_error(&format!("Error opening repository: {}", error));
|
print_error(&format!("Error opening repository: {}", error));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
match grm::table::get_worktree_status_table(&repo, &cwd) {
|
match table::get_worktree_status_table(&repo, &cwd) {
|
||||||
Ok((table, errors)) => {
|
Ok((table, errors)) => {
|
||||||
println!("{}", table);
|
println!("{}", table);
|
||||||
for error in errors {
|
for error in errors {
|
||||||
@@ -583,8 +585,8 @@ fn main() {
|
|||||||
// * Remove all files
|
// * Remove all files
|
||||||
// * Set `core.bare` to `true`
|
// * Set `core.bare` to `true`
|
||||||
|
|
||||||
let repo = grm::RepoHandle::open(&cwd, false).unwrap_or_else(|error| {
|
let repo = repo::RepoHandle::open(&cwd, false).unwrap_or_else(|error| {
|
||||||
if error.kind == grm::RepoErrorKind::NotFound {
|
if error.kind == repo::RepoErrorKind::NotFound {
|
||||||
print_error("Directory does not contain a git repository");
|
print_error("Directory does not contain a git repository");
|
||||||
} else {
|
} else {
|
||||||
print_error(&format!("Opening repository failed: {}", error));
|
print_error(&format!("Opening repository failed: {}", error));
|
||||||
@@ -611,8 +613,8 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
cmd::WorktreeAction::Clean(_args) => {
|
cmd::WorktreeAction::Clean(_args) => {
|
||||||
let repo = grm::RepoHandle::open(&cwd, true).unwrap_or_else(|error| {
|
let repo = repo::RepoHandle::open(&cwd, true).unwrap_or_else(|error| {
|
||||||
if error.kind == grm::RepoErrorKind::NotFound {
|
if error.kind == repo::RepoErrorKind::NotFound {
|
||||||
print_error("Directory does not contain a git repository");
|
print_error("Directory does not contain a git repository");
|
||||||
} else {
|
} else {
|
||||||
print_error(&format!("Opening repository failed: {}", error));
|
print_error(&format!("Opening repository failed: {}", error));
|
||||||
@@ -645,8 +647,8 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
cmd::WorktreeAction::Fetch(_args) => {
|
cmd::WorktreeAction::Fetch(_args) => {
|
||||||
let repo = grm::RepoHandle::open(&cwd, true).unwrap_or_else(|error| {
|
let repo = repo::RepoHandle::open(&cwd, true).unwrap_or_else(|error| {
|
||||||
if error.kind == grm::RepoErrorKind::NotFound {
|
if error.kind == repo::RepoErrorKind::NotFound {
|
||||||
print_error("Directory does not contain a git repository");
|
print_error("Directory does not contain a git repository");
|
||||||
} else {
|
} else {
|
||||||
print_error(&format!("Opening repository failed: {}", error));
|
print_error(&format!("Opening repository failed: {}", error));
|
||||||
@@ -661,8 +663,8 @@ fn main() {
|
|||||||
print_success("Fetched from all remotes");
|
print_success("Fetched from all remotes");
|
||||||
}
|
}
|
||||||
cmd::WorktreeAction::Pull(args) => {
|
cmd::WorktreeAction::Pull(args) => {
|
||||||
let repo = grm::RepoHandle::open(&cwd, true).unwrap_or_else(|error| {
|
let repo = repo::RepoHandle::open(&cwd, true).unwrap_or_else(|error| {
|
||||||
if error.kind == grm::RepoErrorKind::NotFound {
|
if error.kind == repo::RepoErrorKind::NotFound {
|
||||||
print_error("Directory does not contain a git repository");
|
print_error("Directory does not contain a git repository");
|
||||||
} else {
|
} else {
|
||||||
print_error(&format!("Opening repository failed: {}", error));
|
print_error(&format!("Opening repository failed: {}", error));
|
||||||
@@ -702,8 +704,8 @@ fn main() {
|
|||||||
print_error("There is no point in using --rebase without --pull");
|
print_error("There is no point in using --rebase without --pull");
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
let repo = grm::RepoHandle::open(&cwd, true).unwrap_or_else(|error| {
|
let repo = repo::RepoHandle::open(&cwd, true).unwrap_or_else(|error| {
|
||||||
if error.kind == grm::RepoErrorKind::NotFound {
|
if error.kind == repo::RepoErrorKind::NotFound {
|
||||||
print_error("Directory does not contain a git repository");
|
print_error("Directory does not contain a git repository");
|
||||||
} else {
|
} else {
|
||||||
print_error(&format!("Opening repository failed: {}", error));
|
print_error(&format!("Opening repository failed: {}", error));
|
||||||
@@ -718,12 +720,8 @@ fn main() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let config =
|
let config = repo::read_worktree_root_config(&cwd).unwrap_or_else(|error| {
|
||||||
grm::repo::read_worktree_root_config(&cwd).unwrap_or_else(|error| {
|
print_error(&format!("Failed to read worktree configuration: {}", error));
|
||||||
print_error(&format!(
|
|
||||||
"Failed to read worktree configuration: {}",
|
|
||||||
error
|
|
||||||
));
|
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
595
src/lib.rs
595
src/lib.rs
@@ -1,423 +1,37 @@
|
|||||||
#![feature(io_error_more)]
|
#![feature(io_error_more)]
|
||||||
#![feature(const_option_ext)]
|
#![feature(const_option_ext)]
|
||||||
|
|
||||||
use std::fs;
|
use std::path::Path;
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::process;
|
|
||||||
|
|
||||||
|
pub mod auth;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod output;
|
pub mod output;
|
||||||
|
pub mod path;
|
||||||
pub mod provider;
|
pub mod provider;
|
||||||
pub mod repo;
|
pub mod repo;
|
||||||
pub mod table;
|
pub mod table;
|
||||||
|
pub mod tree;
|
||||||
|
pub mod worktree;
|
||||||
|
|
||||||
use config::Config;
|
|
||||||
use output::*;
|
|
||||||
|
|
||||||
use repo::{clone_repo, detect_remote_type, Remote, RemoteType};
|
|
||||||
|
|
||||||
pub use repo::{
|
|
||||||
RemoteTrackingStatus, Repo, RepoErrorKind, RepoHandle, WorktreeRemoveFailureReason,
|
|
||||||
};
|
|
||||||
|
|
||||||
const GIT_MAIN_WORKTREE_DIRECTORY: &str = ".git-main-working-tree";
|
|
||||||
const BRANCH_NAMESPACE_SEPARATOR: &str = "/";
|
const BRANCH_NAMESPACE_SEPARATOR: &str = "/";
|
||||||
|
|
||||||
const GIT_CONFIG_BARE_KEY: &str = "core.bare";
|
|
||||||
const GIT_CONFIG_PUSH_DEFAULT: &str = "push.default";
|
|
||||||
|
|
||||||
pub struct Tree {
|
|
||||||
root: String,
|
|
||||||
repos: Vec<Repo>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn setup() {
|
|
||||||
std::env::set_var("HOME", "/home/test");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn check_expand_tilde() {
|
|
||||||
setup();
|
|
||||||
assert_eq!(
|
|
||||||
expand_path(Path::new("~/file")),
|
|
||||||
Path::new("/home/test/file")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn check_expand_invalid_tilde() {
|
|
||||||
setup();
|
|
||||||
assert_eq!(
|
|
||||||
expand_path(Path::new("/home/~/file")),
|
|
||||||
Path::new("/home/~/file")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn check_expand_home() {
|
|
||||||
setup();
|
|
||||||
assert_eq!(
|
|
||||||
expand_path(Path::new("$HOME/file")),
|
|
||||||
Path::new("/home/test/file")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
expand_path(Path::new("${HOME}/file")),
|
|
||||||
Path::new("/home/test/file")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn path_as_string(path: &Path) -> String {
|
|
||||||
path.to_path_buf().into_os_string().into_string().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn env_home() -> PathBuf {
|
|
||||||
match std::env::var("HOME") {
|
|
||||||
Ok(path) => Path::new(&path).to_path_buf(),
|
|
||||||
Err(e) => {
|
|
||||||
print_error(&format!("Unable to read HOME: {}", e));
|
|
||||||
process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn expand_path(path: &Path) -> PathBuf {
|
|
||||||
fn home_dir() -> Option<PathBuf> {
|
|
||||||
Some(env_home())
|
|
||||||
}
|
|
||||||
|
|
||||||
let expanded_path = match shellexpand::full_with_context(
|
|
||||||
&path_as_string(path),
|
|
||||||
home_dir,
|
|
||||||
|name| -> Result<Option<String>, &'static str> {
|
|
||||||
match name {
|
|
||||||
"HOME" => Ok(Some(path_as_string(home_dir().unwrap().as_path()))),
|
|
||||||
_ => Ok(None),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Ok(std::borrow::Cow::Borrowed(path)) => path.to_owned(),
|
|
||||||
Ok(std::borrow::Cow::Owned(path)) => path,
|
|
||||||
Err(e) => {
|
|
||||||
print_error(&format!("Unable to expand root: {}", e));
|
|
||||||
process::exit(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Path::new(&expanded_path).to_path_buf()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_token_from_command(command: &str) -> Result<String, String> {
|
|
||||||
let output = std::process::Command::new("/usr/bin/env")
|
|
||||||
.arg("sh")
|
|
||||||
.arg("-c")
|
|
||||||
.arg(command)
|
|
||||||
.output()
|
|
||||||
.map_err(|error| format!("Failed to run token-command: {}", error))?;
|
|
||||||
|
|
||||||
let stderr = String::from_utf8(output.stderr).map_err(|error| error.to_string())?;
|
|
||||||
let stdout = String::from_utf8(output.stdout).map_err(|error| error.to_string())?;
|
|
||||||
|
|
||||||
if !output.status.success() {
|
|
||||||
if !stderr.is_empty() {
|
|
||||||
return Err(format!("Token command failed: {}", stderr));
|
|
||||||
} else {
|
|
||||||
return Err(String::from("Token command failed."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !stderr.is_empty() {
|
|
||||||
return Err(format!("Token command produced stderr: {}", stderr));
|
|
||||||
}
|
|
||||||
|
|
||||||
if stdout.is_empty() {
|
|
||||||
return Err(String::from("Token command did not produce output"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let token = stdout
|
|
||||||
.split('\n')
|
|
||||||
.next()
|
|
||||||
.ok_or_else(|| String::from("Output did not contain any newline"))?;
|
|
||||||
|
|
||||||
Ok(token.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sync_repo(root_path: &Path, repo: &Repo, init_worktree: bool) -> Result<(), String> {
|
|
||||||
let repo_path = root_path.join(&repo.fullname());
|
|
||||||
let actual_git_directory = get_actual_git_directory(&repo_path, repo.worktree_setup);
|
|
||||||
|
|
||||||
let mut newly_created = false;
|
|
||||||
|
|
||||||
if repo_path.exists() {
|
|
||||||
if repo.worktree_setup && !actual_git_directory.exists() {
|
|
||||||
return Err(String::from(
|
|
||||||
"Repo already exists, but is not using a worktree setup",
|
|
||||||
));
|
|
||||||
};
|
|
||||||
} else if matches!(&repo.remotes, None) || repo.remotes.as_ref().unwrap().is_empty() {
|
|
||||||
print_repo_action(
|
|
||||||
&repo.name,
|
|
||||||
"Repository does not have remotes configured, initializing new",
|
|
||||||
);
|
|
||||||
match RepoHandle::init(&repo_path, repo.worktree_setup) {
|
|
||||||
Ok(r) => {
|
|
||||||
print_repo_success(&repo.name, "Repository created");
|
|
||||||
Some(r)
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
return Err(format!("Repository failed during init: {}", e));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
let first = repo.remotes.as_ref().unwrap().first().unwrap();
|
|
||||||
|
|
||||||
match clone_repo(first, &repo_path, repo.worktree_setup) {
|
|
||||||
Ok(_) => {
|
|
||||||
print_repo_success(&repo.name, "Repository successfully cloned");
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
return Err(format!("Repository failed during clone: {}", e));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
newly_created = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let repo_handle = match RepoHandle::open(&repo_path, repo.worktree_setup) {
|
|
||||||
Ok(repo) => repo,
|
|
||||||
Err(error) => {
|
|
||||||
if !repo.worktree_setup && RepoHandle::open(&repo_path, true).is_ok() {
|
|
||||||
return Err(String::from(
|
|
||||||
"Repo already exists, but is using a worktree setup",
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
return Err(format!("Opening repository failed: {}", error));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if newly_created && repo.worktree_setup && init_worktree {
|
|
||||||
match repo_handle.default_branch() {
|
|
||||||
Ok(branch) => {
|
|
||||||
add_worktree(&repo_path, &branch.name()?, None, None, false)?;
|
|
||||||
}
|
|
||||||
Err(_error) => print_repo_error(
|
|
||||||
&repo.name,
|
|
||||||
"Could not determine default branch, skipping worktree initializtion",
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(remotes) = &repo.remotes {
|
|
||||||
let current_remotes: Vec<String> = repo_handle
|
|
||||||
.remotes()
|
|
||||||
.map_err(|error| format!("Repository failed during getting the remotes: {}", error))?;
|
|
||||||
|
|
||||||
for remote in remotes {
|
|
||||||
let current_remote = repo_handle.find_remote(&remote.name)?;
|
|
||||||
|
|
||||||
match current_remote {
|
|
||||||
Some(current_remote) => {
|
|
||||||
let current_url = current_remote.url();
|
|
||||||
|
|
||||||
if remote.url != current_url {
|
|
||||||
print_repo_action(
|
|
||||||
&repo.name,
|
|
||||||
&format!("Updating remote {} to \"{}\"", &remote.name, &remote.url),
|
|
||||||
);
|
|
||||||
if let Err(e) = repo_handle.remote_set_url(&remote.name, &remote.url) {
|
|
||||||
return Err(format!("Repository failed during setting of the remote URL for remote \"{}\": {}", &remote.name, e));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
print_repo_action(
|
|
||||||
&repo.name,
|
|
||||||
&format!(
|
|
||||||
"Setting up new remote \"{}\" to \"{}\"",
|
|
||||||
&remote.name, &remote.url
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if let Err(e) = repo_handle.new_remote(&remote.name, &remote.url) {
|
|
||||||
return Err(format!(
|
|
||||||
"Repository failed during setting the remotes: {}",
|
|
||||||
e
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for current_remote in ¤t_remotes {
|
|
||||||
if !remotes.iter().any(|r| &r.name == current_remote) {
|
|
||||||
print_repo_action(
|
|
||||||
&repo.name,
|
|
||||||
&format!("Deleting remote \"{}\"", ¤t_remote,),
|
|
||||||
);
|
|
||||||
if let Err(e) = repo_handle.remote_delete(current_remote) {
|
|
||||||
return Err(format!(
|
|
||||||
"Repository failed during deleting remote \"{}\": {}",
|
|
||||||
¤t_remote, e
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn find_unmanaged_repos(
|
|
||||||
root_path: &Path,
|
|
||||||
managed_repos: &[Repo],
|
|
||||||
) -> Result<Vec<PathBuf>, String> {
|
|
||||||
let mut unmanaged_repos = Vec::new();
|
|
||||||
|
|
||||||
for repo_path in find_repo_paths(root_path)? {
|
|
||||||
if !managed_repos
|
|
||||||
.iter()
|
|
||||||
.any(|r| Path::new(root_path).join(r.fullname()) == repo_path)
|
|
||||||
{
|
|
||||||
unmanaged_repos.push(repo_path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(unmanaged_repos)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sync_trees(config: Config, init_worktree: bool) -> Result<bool, String> {
|
|
||||||
let mut failures = false;
|
|
||||||
|
|
||||||
let mut unmanaged_repos_absolute_paths = vec![];
|
|
||||||
let mut managed_repos_absolute_paths = vec![];
|
|
||||||
|
|
||||||
let trees = config.trees()?;
|
|
||||||
|
|
||||||
for tree in trees {
|
|
||||||
let repos: Vec<Repo> = tree
|
|
||||||
.repos
|
|
||||||
.unwrap_or_default()
|
|
||||||
.into_iter()
|
|
||||||
.map(|repo| repo.into_repo())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let root_path = expand_path(Path::new(&tree.root));
|
|
||||||
|
|
||||||
for repo in &repos {
|
|
||||||
managed_repos_absolute_paths.push(root_path.join(repo.fullname()));
|
|
||||||
match sync_repo(&root_path, repo, init_worktree) {
|
|
||||||
Ok(_) => print_repo_success(&repo.name, "OK"),
|
|
||||||
Err(error) => {
|
|
||||||
print_repo_error(&repo.name, &error);
|
|
||||||
failures = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match find_unmanaged_repos(&root_path, &repos) {
|
|
||||||
Ok(repos) => {
|
|
||||||
unmanaged_repos_absolute_paths.extend(repos);
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
print_error(&format!("Error getting unmanaged repos: {}", error));
|
|
||||||
failures = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for unmanaged_repo_absolute_path in &unmanaged_repos_absolute_paths {
|
|
||||||
if managed_repos_absolute_paths
|
|
||||||
.iter()
|
|
||||||
.any(|managed_repo_absolute_path| {
|
|
||||||
managed_repo_absolute_path == unmanaged_repo_absolute_path
|
|
||||||
})
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
print_warning(&format!(
|
|
||||||
"Found unmanaged repository: \"{}\"",
|
|
||||||
path_as_string(unmanaged_repo_absolute_path)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(!failures)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Finds repositories recursively, returning their path
|
|
||||||
fn find_repo_paths(path: &Path) -> Result<Vec<PathBuf>, String> {
|
|
||||||
let mut repos = Vec::new();
|
|
||||||
|
|
||||||
let git_dir = path.join(".git");
|
|
||||||
let git_worktree = path.join(GIT_MAIN_WORKTREE_DIRECTORY);
|
|
||||||
|
|
||||||
if git_dir.exists() || git_worktree.exists() {
|
|
||||||
repos.push(path.to_path_buf());
|
|
||||||
} else {
|
|
||||||
match fs::read_dir(path) {
|
|
||||||
Ok(contents) => {
|
|
||||||
for content in contents {
|
|
||||||
match content {
|
|
||||||
Ok(entry) => {
|
|
||||||
let path = entry.path();
|
|
||||||
if path.is_symlink() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if path.is_dir() {
|
|
||||||
match find_repo_paths(&path) {
|
|
||||||
Ok(ref mut r) => repos.append(r),
|
|
||||||
Err(error) => return Err(error),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
return Err(format!("Error accessing directory: {}", e));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
return Err(format!(
|
|
||||||
"Failed to open \"{}\": {}",
|
|
||||||
&path.display(),
|
|
||||||
match e.kind() {
|
|
||||||
std::io::ErrorKind::NotADirectory =>
|
|
||||||
String::from("directory expected, but path is not a directory"),
|
|
||||||
std::io::ErrorKind::NotFound => String::from("not found"),
|
|
||||||
_ => format!("{:?}", e.kind()),
|
|
||||||
}
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(repos)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_actual_git_directory(path: &Path, is_worktree: bool) -> PathBuf {
|
|
||||||
match is_worktree {
|
|
||||||
false => path.to_path_buf(),
|
|
||||||
true => path.join(GIT_MAIN_WORKTREE_DIRECTORY),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find all git repositories under root, recursively
|
/// Find all git repositories under root, recursively
|
||||||
///
|
///
|
||||||
/// The bool in the return value specifies whether there is a repository
|
/// The bool in the return value specifies whether there is a repository
|
||||||
/// in root itself.
|
/// in root itself.
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
fn find_repos(root: &Path) -> Result<Option<(Vec<Repo>, Vec<String>, bool)>, String> {
|
fn find_repos(root: &Path) -> Result<Option<(Vec<repo::Repo>, Vec<String>, bool)>, String> {
|
||||||
let mut repos: Vec<Repo> = Vec::new();
|
let mut repos: Vec<repo::Repo> = Vec::new();
|
||||||
let mut repo_in_root = false;
|
let mut repo_in_root = false;
|
||||||
let mut warnings = Vec::new();
|
let mut warnings = Vec::new();
|
||||||
|
|
||||||
for path in find_repo_paths(root)? {
|
for path in tree::find_repo_paths(root)? {
|
||||||
let is_worktree = RepoHandle::detect_worktree(&path);
|
let is_worktree = repo::RepoHandle::detect_worktree(&path);
|
||||||
if path == root {
|
if path == root {
|
||||||
repo_in_root = true;
|
repo_in_root = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
match RepoHandle::open(&path, is_worktree) {
|
match repo::RepoHandle::open(&path, is_worktree) {
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
warnings.push(format!(
|
warnings.push(format!(
|
||||||
"Error opening repo {}{}: {}",
|
"Error opening repo {}{}: {}",
|
||||||
@@ -436,32 +50,32 @@ fn find_repos(root: &Path) -> Result<Option<(Vec<Repo>, Vec<String>, bool)>, Str
|
|||||||
Err(error) => {
|
Err(error) => {
|
||||||
warnings.push(format!(
|
warnings.push(format!(
|
||||||
"{}: Error getting remotes: {}",
|
"{}: Error getting remotes: {}",
|
||||||
&path_as_string(&path),
|
&path::path_as_string(&path),
|
||||||
error
|
error
|
||||||
));
|
));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut results: Vec<Remote> = Vec::new();
|
let mut results: Vec<repo::Remote> = Vec::new();
|
||||||
for remote_name in remotes.iter() {
|
for remote_name in remotes.iter() {
|
||||||
match repo.find_remote(remote_name)? {
|
match repo.find_remote(remote_name)? {
|
||||||
Some(remote) => {
|
Some(remote) => {
|
||||||
let name = remote.name();
|
let name = remote.name();
|
||||||
let url = remote.url();
|
let url = remote.url();
|
||||||
let remote_type = match detect_remote_type(&url) {
|
let remote_type = match repo::detect_remote_type(&url) {
|
||||||
Some(t) => t,
|
Some(t) => t,
|
||||||
None => {
|
None => {
|
||||||
warnings.push(format!(
|
warnings.push(format!(
|
||||||
"{}: Could not detect remote type of \"{}\"",
|
"{}: Could not detect remote type of \"{}\"",
|
||||||
&path_as_string(&path),
|
&path::path_as_string(&path),
|
||||||
&url
|
&url
|
||||||
));
|
));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
results.push(Remote {
|
results.push(repo::Remote {
|
||||||
name,
|
name,
|
||||||
url,
|
url,
|
||||||
remote_type,
|
remote_type,
|
||||||
@@ -470,7 +84,7 @@ fn find_repos(root: &Path) -> Result<Option<(Vec<Repo>, Vec<String>, bool)>, Str
|
|||||||
None => {
|
None => {
|
||||||
warnings.push(format!(
|
warnings.push(format!(
|
||||||
"{}: Remote {} not found",
|
"{}: Remote {} not found",
|
||||||
&path_as_string(&path),
|
&path::path_as_string(&path),
|
||||||
remote_name
|
remote_name
|
||||||
));
|
));
|
||||||
continue;
|
continue;
|
||||||
@@ -483,7 +97,9 @@ fn find_repos(root: &Path) -> Result<Option<(Vec<Repo>, Vec<String>, bool)>, Str
|
|||||||
(
|
(
|
||||||
None,
|
None,
|
||||||
match &root.parent() {
|
match &root.parent() {
|
||||||
Some(parent) => path_as_string(path.strip_prefix(parent).unwrap()),
|
Some(parent) => {
|
||||||
|
path::path_as_string(path.strip_prefix(parent).unwrap())
|
||||||
|
}
|
||||||
None => {
|
None => {
|
||||||
warnings.push(String::from("Getting name of the search root failed. Do you have a git repository in \"/\"?"));
|
warnings.push(String::from("Getting name of the search root failed. Do you have a git repository in \"/\"?"));
|
||||||
continue;
|
continue;
|
||||||
@@ -495,15 +111,15 @@ fn find_repos(root: &Path) -> Result<Option<(Vec<Repo>, Vec<String>, bool)>, Str
|
|||||||
let namespace = name.parent().unwrap();
|
let namespace = name.parent().unwrap();
|
||||||
(
|
(
|
||||||
if namespace != Path::new("") {
|
if namespace != Path::new("") {
|
||||||
Some(path_as_string(namespace).to_string())
|
Some(path::path_as_string(namespace).to_string())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
path_as_string(name),
|
path::path_as_string(name),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
repos.push(Repo {
|
repos.push(repo::Repo {
|
||||||
name,
|
name,
|
||||||
namespace,
|
namespace,
|
||||||
remotes: Some(remotes),
|
remotes: Some(remotes),
|
||||||
@@ -515,10 +131,10 @@ fn find_repos(root: &Path) -> Result<Option<(Vec<Repo>, Vec<String>, bool)>, Str
|
|||||||
Ok(Some((repos, warnings, repo_in_root)))
|
Ok(Some((repos, warnings, repo_in_root)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_in_tree(path: &Path) -> Result<(Tree, Vec<String>), String> {
|
pub fn find_in_tree(path: &Path) -> Result<(tree::Tree, Vec<String>), String> {
|
||||||
let mut warnings = Vec::new();
|
let mut warnings = Vec::new();
|
||||||
|
|
||||||
let (repos, repo_in_root): (Vec<Repo>, bool) = match find_repos(path)? {
|
let (repos, repo_in_root): (Vec<repo::Repo>, bool) = match find_repos(path)? {
|
||||||
Some((vec, mut repo_warnings, repo_in_root)) => {
|
Some((vec, mut repo_warnings, repo_in_root)) => {
|
||||||
warnings.append(&mut repo_warnings);
|
warnings.append(&mut repo_warnings);
|
||||||
(vec, repo_in_root)
|
(vec, repo_in_root)
|
||||||
@@ -539,171 +155,10 @@ pub fn find_in_tree(path: &Path) -> Result<(Tree, Vec<String>), String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
Tree {
|
tree::Tree {
|
||||||
root: root.into_os_string().into_string().unwrap(),
|
root: root.into_os_string().into_string().unwrap(),
|
||||||
repos,
|
repos,
|
||||||
},
|
},
|
||||||
warnings,
|
warnings,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_worktree(
|
|
||||||
directory: &Path,
|
|
||||||
name: &str,
|
|
||||||
subdirectory: Option<&Path>,
|
|
||||||
track: Option<(&str, &str)>,
|
|
||||||
no_track: bool,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let repo = RepoHandle::open(directory, true).map_err(|error| match error.kind {
|
|
||||||
RepoErrorKind::NotFound => {
|
|
||||||
String::from("Current directory does not contain a worktree setup")
|
|
||||||
}
|
|
||||||
_ => format!("Error opening repo: {}", error),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let config = repo::read_worktree_root_config(directory)?;
|
|
||||||
|
|
||||||
if repo.find_worktree(name).is_ok() {
|
|
||||||
return Err(format!("Worktree {} already exists", &name));
|
|
||||||
}
|
|
||||||
|
|
||||||
let path = match subdirectory {
|
|
||||||
Some(dir) => directory.join(dir).join(name),
|
|
||||||
None => directory.join(Path::new(name)),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut remote_branch_exists = false;
|
|
||||||
|
|
||||||
let default_checkout = || repo.default_branch()?.to_commit();
|
|
||||||
|
|
||||||
let checkout_commit;
|
|
||||||
if no_track {
|
|
||||||
checkout_commit = default_checkout()?;
|
|
||||||
} else {
|
|
||||||
match track {
|
|
||||||
Some((remote_name, remote_branch_name)) => {
|
|
||||||
let remote_branch = repo.find_remote_branch(remote_name, remote_branch_name);
|
|
||||||
match remote_branch {
|
|
||||||
Ok(branch) => {
|
|
||||||
remote_branch_exists = true;
|
|
||||||
checkout_commit = branch.to_commit()?;
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
remote_branch_exists = false;
|
|
||||||
checkout_commit = default_checkout()?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => match &config {
|
|
||||||
None => checkout_commit = default_checkout()?,
|
|
||||||
Some(config) => match &config.track {
|
|
||||||
None => checkout_commit = default_checkout()?,
|
|
||||||
Some(track_config) => {
|
|
||||||
if track_config.default {
|
|
||||||
let remote_branch =
|
|
||||||
repo.find_remote_branch(&track_config.default_remote, name);
|
|
||||||
match remote_branch {
|
|
||||||
Ok(branch) => {
|
|
||||||
remote_branch_exists = true;
|
|
||||||
checkout_commit = branch.to_commit()?;
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
checkout_commit = default_checkout()?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
checkout_commit = default_checkout()?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut target_branch = match repo.find_local_branch(name) {
|
|
||||||
Ok(branchref) => branchref,
|
|
||||||
Err(_) => repo.create_branch(name, &checkout_commit)?,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn push(
|
|
||||||
remote: &mut repo::RemoteHandle,
|
|
||||||
branch_name: &str,
|
|
||||||
remote_branch_name: &str,
|
|
||||||
repo: &repo::RepoHandle,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
if !remote.is_pushable()? {
|
|
||||||
return Err(format!(
|
|
||||||
"Cannot push to non-pushable remote {}",
|
|
||||||
remote.url()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
remote.push(branch_name, remote_branch_name, repo)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !no_track {
|
|
||||||
if let Some((remote_name, remote_branch_name)) = track {
|
|
||||||
if remote_branch_exists {
|
|
||||||
target_branch.set_upstream(remote_name, remote_branch_name)?;
|
|
||||||
} else {
|
|
||||||
let mut remote = repo
|
|
||||||
.find_remote(remote_name)
|
|
||||||
.map_err(|error| format!("Error getting remote {}: {}", remote_name, error))?
|
|
||||||
.ok_or_else(|| format!("Remote {} not found", remote_name))?;
|
|
||||||
|
|
||||||
push(
|
|
||||||
&mut remote,
|
|
||||||
&target_branch.name()?,
|
|
||||||
remote_branch_name,
|
|
||||||
&repo,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
target_branch.set_upstream(remote_name, remote_branch_name)?;
|
|
||||||
}
|
|
||||||
} else if let Some(config) = config {
|
|
||||||
if let Some(track_config) = config.track {
|
|
||||||
if track_config.default {
|
|
||||||
let remote_name = track_config.default_remote;
|
|
||||||
if remote_branch_exists {
|
|
||||||
target_branch.set_upstream(&remote_name, name)?;
|
|
||||||
} else {
|
|
||||||
let remote_branch_name = match track_config.default_remote_prefix {
|
|
||||||
Some(prefix) => {
|
|
||||||
format!("{}{}{}", &prefix, BRANCH_NAMESPACE_SEPARATOR, &name)
|
|
||||||
}
|
|
||||||
None => name.to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut remote = repo
|
|
||||||
.find_remote(&remote_name)
|
|
||||||
.map_err(|error| {
|
|
||||||
format!("Error getting remote {}: {}", remote_name, error)
|
|
||||||
})?
|
|
||||||
.ok_or_else(|| format!("Remote {} not found", remote_name))?;
|
|
||||||
|
|
||||||
if !remote.is_pushable()? {
|
|
||||||
return Err(format!(
|
|
||||||
"Cannot push to non-pushable remote {}",
|
|
||||||
remote.url()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
push(
|
|
||||||
&mut remote,
|
|
||||||
&target_branch.name()?,
|
|
||||||
&remote_branch_name,
|
|
||||||
&repo,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
target_branch.set_upstream(&remote_name, &remote_branch_name)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(subdirectory) = subdirectory {
|
|
||||||
std::fs::create_dir_all(subdirectory).map_err(|error| error.to_string())?;
|
|
||||||
}
|
|
||||||
repo.new_worktree(name, &path, &target_branch)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|||||||
84
src/path.rs
Normal file
84
src/path.rs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process;
|
||||||
|
|
||||||
|
use super::output::*;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn setup() {
|
||||||
|
std::env::set_var("HOME", "/home/test");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_expand_tilde() {
|
||||||
|
setup();
|
||||||
|
assert_eq!(
|
||||||
|
expand_path(Path::new("~/file")),
|
||||||
|
Path::new("/home/test/file")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_expand_invalid_tilde() {
|
||||||
|
setup();
|
||||||
|
assert_eq!(
|
||||||
|
expand_path(Path::new("/home/~/file")),
|
||||||
|
Path::new("/home/~/file")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_expand_home() {
|
||||||
|
setup();
|
||||||
|
assert_eq!(
|
||||||
|
expand_path(Path::new("$HOME/file")),
|
||||||
|
Path::new("/home/test/file")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
expand_path(Path::new("${HOME}/file")),
|
||||||
|
Path::new("/home/test/file")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path_as_string(path: &Path) -> String {
|
||||||
|
path.to_path_buf().into_os_string().into_string().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn env_home() -> PathBuf {
|
||||||
|
match std::env::var("HOME") {
|
||||||
|
Ok(path) => Path::new(&path).to_path_buf(),
|
||||||
|
Err(e) => {
|
||||||
|
print_error(&format!("Unable to read HOME: {}", e));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expand_path(path: &Path) -> PathBuf {
|
||||||
|
fn home_dir() -> Option<PathBuf> {
|
||||||
|
Some(env_home())
|
||||||
|
}
|
||||||
|
|
||||||
|
let expanded_path = match shellexpand::full_with_context(
|
||||||
|
&path_as_string(path),
|
||||||
|
home_dir,
|
||||||
|
|name| -> Result<Option<String>, &'static str> {
|
||||||
|
match name {
|
||||||
|
"HOME" => Ok(Some(path_as_string(home_dir().unwrap().as_path()))),
|
||||||
|
_ => Ok(None),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Ok(std::borrow::Cow::Borrowed(path)) => path.to_owned(),
|
||||||
|
Ok(std::borrow::Cow::Owned(path)) => path,
|
||||||
|
Err(e) => {
|
||||||
|
print_error(&format!("Unable to expand root: {}", e));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Path::new(&expanded_path).to_path_buf()
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use super::escape;
|
||||||
use super::ApiErrorResponse;
|
use super::ApiErrorResponse;
|
||||||
use super::Filter;
|
use super::Filter;
|
||||||
use super::JsonError;
|
use super::JsonError;
|
||||||
@@ -108,7 +109,7 @@ impl Provider for Github {
|
|||||||
user: &str,
|
user: &str,
|
||||||
) -> Result<Vec<GithubProject>, ApiErrorResponse<GithubApiErrorResponse>> {
|
) -> Result<Vec<GithubProject>, ApiErrorResponse<GithubApiErrorResponse>> {
|
||||||
self.call_list(
|
self.call_list(
|
||||||
&format!("{GITHUB_API_BASEURL}/users/{user}/repos"),
|
&format!("{GITHUB_API_BASEURL}/users/{}/repos", escape(user)),
|
||||||
Some(ACCEPT_HEADER_JSON),
|
Some(ACCEPT_HEADER_JSON),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -118,7 +119,7 @@ impl Provider for Github {
|
|||||||
group: &str,
|
group: &str,
|
||||||
) -> Result<Vec<GithubProject>, ApiErrorResponse<GithubApiErrorResponse>> {
|
) -> Result<Vec<GithubProject>, ApiErrorResponse<GithubApiErrorResponse>> {
|
||||||
self.call_list(
|
self.call_list(
|
||||||
&format!("{GITHUB_API_BASEURL}/orgs/{group}/repos?type=all"),
|
&format!("{GITHUB_API_BASEURL}/orgs/{}/repos?type=all", escape(group)),
|
||||||
Some(ACCEPT_HEADER_JSON),
|
Some(ACCEPT_HEADER_JSON),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use super::escape;
|
||||||
use super::ApiErrorResponse;
|
use super::ApiErrorResponse;
|
||||||
use super::Filter;
|
use super::Filter;
|
||||||
use super::JsonError;
|
use super::JsonError;
|
||||||
@@ -56,13 +57,13 @@ impl Project for GitlabProject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn private(&self) -> bool {
|
fn private(&self) -> bool {
|
||||||
matches!(self.visibility, GitlabVisibility::Private)
|
!matches!(self.visibility, GitlabVisibility::Public)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct GitlabApiErrorResponse {
|
pub struct GitlabApiErrorResponse {
|
||||||
#[serde(alias = "error_description")]
|
#[serde(alias = "error_description", alias = "error")]
|
||||||
pub message: String,
|
pub message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +126,7 @@ impl Provider for Gitlab {
|
|||||||
user: &str,
|
user: &str,
|
||||||
) -> Result<Vec<GitlabProject>, ApiErrorResponse<GitlabApiErrorResponse>> {
|
) -> Result<Vec<GitlabProject>, ApiErrorResponse<GitlabApiErrorResponse>> {
|
||||||
self.call_list(
|
self.call_list(
|
||||||
&format!("{}/api/v4/users/{}/projects", self.api_url(), user),
|
&format!("{}/api/v4/users/{}/projects", self.api_url(), escape(user)),
|
||||||
Some(ACCEPT_HEADER_JSON),
|
Some(ACCEPT_HEADER_JSON),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -138,7 +139,7 @@ impl Provider for Gitlab {
|
|||||||
&format!(
|
&format!(
|
||||||
"{}/api/v4/groups/{}/projects?include_subgroups=true&archived=false",
|
"{}/api/v4/groups/{}/projects?include_subgroups=true&archived=false",
|
||||||
self.api_url(),
|
self.api_url(),
|
||||||
group
|
escape(group),
|
||||||
),
|
),
|
||||||
Some(ACCEPT_HEADER_JSON),
|
Some(ACCEPT_HEADER_JSON),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ pub mod gitlab;
|
|||||||
pub use github::Github;
|
pub use github::Github;
|
||||||
pub use gitlab::Gitlab;
|
pub use gitlab::Gitlab;
|
||||||
|
|
||||||
use crate::{Remote, RemoteType, Repo};
|
use super::repo;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
@@ -28,16 +28,25 @@ enum ProjectResponse<T, U> {
|
|||||||
Failure(U),
|
Failure(U),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn escape(s: &str) -> String {
|
||||||
|
url_escape::encode_component(s).to_string()
|
||||||
|
}
|
||||||
|
|
||||||
pub trait Project {
|
pub trait Project {
|
||||||
fn into_repo_config(self, provider_name: &str, worktree_setup: bool, force_ssh: bool) -> Repo
|
fn into_repo_config(
|
||||||
|
self,
|
||||||
|
provider_name: &str,
|
||||||
|
worktree_setup: bool,
|
||||||
|
force_ssh: bool,
|
||||||
|
) -> repo::Repo
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
{
|
{
|
||||||
Repo {
|
repo::Repo {
|
||||||
name: self.name(),
|
name: self.name(),
|
||||||
namespace: self.namespace(),
|
namespace: self.namespace(),
|
||||||
worktree_setup,
|
worktree_setup,
|
||||||
remotes: Some(vec![Remote {
|
remotes: Some(vec![repo::Remote {
|
||||||
name: String::from(provider_name),
|
name: String::from(provider_name),
|
||||||
url: if force_ssh || self.private() {
|
url: if force_ssh || self.private() {
|
||||||
self.ssh_url()
|
self.ssh_url()
|
||||||
@@ -45,9 +54,9 @@ pub trait Project {
|
|||||||
self.http_url()
|
self.http_url()
|
||||||
},
|
},
|
||||||
remote_type: if force_ssh || self.private() {
|
remote_type: if force_ssh || self.private() {
|
||||||
RemoteType::Ssh
|
repo::RemoteType::Ssh
|
||||||
} else {
|
} else {
|
||||||
RemoteType::Https
|
repo::RemoteType::Https
|
||||||
},
|
},
|
||||||
}]),
|
}]),
|
||||||
}
|
}
|
||||||
@@ -201,7 +210,7 @@ pub trait Provider {
|
|||||||
&self,
|
&self,
|
||||||
worktree_setup: bool,
|
worktree_setup: bool,
|
||||||
force_ssh: bool,
|
force_ssh: bool,
|
||||||
) -> Result<HashMap<Option<String>, Vec<Repo>>, String> {
|
) -> Result<HashMap<Option<String>, Vec<repo::Repo>>, String> {
|
||||||
let mut repos = vec![];
|
let mut repos = vec![];
|
||||||
|
|
||||||
if self.filter().owner {
|
if self.filter().owner {
|
||||||
@@ -278,7 +287,7 @@ pub trait Provider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut ret: HashMap<Option<String>, Vec<Repo>> = HashMap::new();
|
let mut ret: HashMap<Option<String>, Vec<repo::Repo>> = HashMap::new();
|
||||||
|
|
||||||
for repo in repos {
|
for repo in repos {
|
||||||
let namespace = repo.namespace();
|
let namespace = repo.namespace();
|
||||||
|
|||||||
38
src/repo.rs
38
src/repo.rs
@@ -3,9 +3,13 @@ use std::path::Path;
|
|||||||
|
|
||||||
use git2::Repository;
|
use git2::Repository;
|
||||||
|
|
||||||
use crate::output::*;
|
use super::output::*;
|
||||||
|
use super::path;
|
||||||
|
use super::worktree;
|
||||||
|
|
||||||
const WORKTREE_CONFIG_FILE_NAME: &str = "grm.toml";
|
const WORKTREE_CONFIG_FILE_NAME: &str = "grm.toml";
|
||||||
|
const GIT_CONFIG_BARE_KEY: &str = "core.bare";
|
||||||
|
const GIT_CONFIG_PUSH_DEFAULT: &str = "push.default";
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
@@ -506,7 +510,7 @@ impl RepoHandle {
|
|||||||
false => Repository::open,
|
false => Repository::open,
|
||||||
};
|
};
|
||||||
let path = match is_worktree {
|
let path = match is_worktree {
|
||||||
true => path.join(crate::GIT_MAIN_WORKTREE_DIRECTORY),
|
true => path.join(worktree::GIT_MAIN_WORKTREE_DIRECTORY),
|
||||||
false => path.to_path_buf(),
|
false => path.to_path_buf(),
|
||||||
};
|
};
|
||||||
match open_func(path) {
|
match open_func(path) {
|
||||||
@@ -679,7 +683,7 @@ impl RepoHandle {
|
|||||||
pub fn init(path: &Path, is_worktree: bool) -> Result<Self, String> {
|
pub fn init(path: &Path, is_worktree: bool) -> Result<Self, String> {
|
||||||
let repo = match is_worktree {
|
let repo = match is_worktree {
|
||||||
false => Repository::init(path).map_err(convert_libgit2_error)?,
|
false => Repository::init(path).map_err(convert_libgit2_error)?,
|
||||||
true => Repository::init_bare(path.join(crate::GIT_MAIN_WORKTREE_DIRECTORY))
|
true => Repository::init_bare(path.join(worktree::GIT_MAIN_WORKTREE_DIRECTORY))
|
||||||
.map_err(convert_libgit2_error)?,
|
.map_err(convert_libgit2_error)?,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -742,8 +746,8 @@ impl RepoHandle {
|
|||||||
let mut config = self.config()?;
|
let mut config = self.config()?;
|
||||||
|
|
||||||
config
|
config
|
||||||
.set_bool(crate::GIT_CONFIG_BARE_KEY, value)
|
.set_bool(GIT_CONFIG_BARE_KEY, value)
|
||||||
.map_err(|error| format!("Could not set {}: {}", crate::GIT_CONFIG_BARE_KEY, error))
|
.map_err(|error| format!("Could not set {}: {}", GIT_CONFIG_BARE_KEY, error))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn convert_to_worktree(
|
pub fn convert_to_worktree(
|
||||||
@@ -766,7 +770,7 @@ impl RepoHandle {
|
|||||||
return Err(WorktreeConversionFailureReason::Ignored);
|
return Err(WorktreeConversionFailureReason::Ignored);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::fs::rename(".git", crate::GIT_MAIN_WORKTREE_DIRECTORY).map_err(|error| {
|
std::fs::rename(".git", worktree::GIT_MAIN_WORKTREE_DIRECTORY).map_err(|error| {
|
||||||
WorktreeConversionFailureReason::Error(format!(
|
WorktreeConversionFailureReason::Error(format!(
|
||||||
"Error moving .git directory: {}",
|
"Error moving .git directory: {}",
|
||||||
error
|
error
|
||||||
@@ -786,7 +790,7 @@ impl RepoHandle {
|
|||||||
Ok(entry) => {
|
Ok(entry) => {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
// unwrap is safe here, the path will ALWAYS have a file component
|
// unwrap is safe here, the path will ALWAYS have a file component
|
||||||
if path.file_name().unwrap() == crate::GIT_MAIN_WORKTREE_DIRECTORY {
|
if path.file_name().unwrap() == worktree::GIT_MAIN_WORKTREE_DIRECTORY {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if path.is_file() || path.is_symlink() {
|
if path.is_file() || path.is_symlink() {
|
||||||
@@ -835,18 +839,12 @@ impl RepoHandle {
|
|||||||
|
|
||||||
config
|
config
|
||||||
.set_str(
|
.set_str(
|
||||||
crate::GIT_CONFIG_PUSH_DEFAULT,
|
GIT_CONFIG_PUSH_DEFAULT,
|
||||||
match value {
|
match value {
|
||||||
GitPushDefaultSetting::Upstream => "upstream",
|
GitPushDefaultSetting::Upstream => "upstream",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.map_err(|error| {
|
.map_err(|error| format!("Could not set {}: {}", GIT_CONFIG_PUSH_DEFAULT, error))
|
||||||
format!(
|
|
||||||
"Could not set {}: {}",
|
|
||||||
crate::GIT_CONFIG_PUSH_DEFAULT,
|
|
||||||
error
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_untracked_files(&self, is_worktree: bool) -> Result<bool, String> {
|
pub fn has_untracked_files(&self, is_worktree: bool) -> Result<bool, String> {
|
||||||
@@ -1105,7 +1103,7 @@ impl RepoHandle {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
if branch_name != name
|
if branch_name != name
|
||||||
&& !branch_name.ends_with(&format!("{}{}", crate::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",
|
||||||
@@ -1275,7 +1273,7 @@ impl RepoHandle {
|
|||||||
|
|
||||||
let mut unmanaged_worktrees = Vec::new();
|
let mut unmanaged_worktrees = Vec::new();
|
||||||
for entry in std::fs::read_dir(&directory).map_err(|error| error.to_string())? {
|
for entry in std::fs::read_dir(&directory).map_err(|error| error.to_string())? {
|
||||||
let dirname = crate::path_as_string(
|
let dirname = path::path_as_string(
|
||||||
entry
|
entry
|
||||||
.map_err(|error| error.to_string())?
|
.map_err(|error| error.to_string())?
|
||||||
.path()
|
.path()
|
||||||
@@ -1308,7 +1306,7 @@ impl RepoHandle {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if dirname == crate::GIT_MAIN_WORKTREE_DIRECTORY {
|
if dirname == worktree::GIT_MAIN_WORKTREE_DIRECTORY {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if dirname == WORKTREE_CONFIG_FILE_NAME {
|
if dirname == WORKTREE_CONFIG_FILE_NAME {
|
||||||
@@ -1327,7 +1325,7 @@ impl RepoHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn detect_worktree(path: &Path) -> bool {
|
pub fn detect_worktree(path: &Path) -> bool {
|
||||||
path.join(crate::GIT_MAIN_WORKTREE_DIRECTORY).exists()
|
path.join(worktree::GIT_MAIN_WORKTREE_DIRECTORY).exists()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1486,7 +1484,7 @@ pub fn clone_repo(
|
|||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let clone_target = match is_worktree {
|
let clone_target = match is_worktree {
|
||||||
false => path.to_path_buf(),
|
false => path.to_path_buf(),
|
||||||
true => path.join(crate::GIT_MAIN_WORKTREE_DIRECTORY),
|
true => path.join(worktree::GIT_MAIN_WORKTREE_DIRECTORY),
|
||||||
};
|
};
|
||||||
|
|
||||||
print_action(&format!(
|
print_action(&format!(
|
||||||
|
|||||||
38
src/table.rs
38
src/table.rs
@@ -1,4 +1,6 @@
|
|||||||
use crate::RepoHandle;
|
use super::config;
|
||||||
|
use super::path;
|
||||||
|
use super::repo;
|
||||||
|
|
||||||
use comfy_table::{Cell, Table};
|
use comfy_table::{Cell, Table};
|
||||||
|
|
||||||
@@ -21,7 +23,7 @@ fn add_table_header(table: &mut Table) {
|
|||||||
fn add_repo_status(
|
fn add_repo_status(
|
||||||
table: &mut Table,
|
table: &mut Table,
|
||||||
repo_name: &str,
|
repo_name: &str,
|
||||||
repo_handle: &crate::RepoHandle,
|
repo_handle: &repo::RepoHandle,
|
||||||
is_worktree: bool,
|
is_worktree: bool,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let repo_status = repo_handle.status(is_worktree)?;
|
let repo_status = repo_handle.status(is_worktree)?;
|
||||||
@@ -65,11 +67,11 @@ fn add_repo_status(
|
|||||||
" <{}>{}",
|
" <{}>{}",
|
||||||
remote_branch_name,
|
remote_branch_name,
|
||||||
&match remote_tracking_status {
|
&match remote_tracking_status {
|
||||||
crate::RemoteTrackingStatus::UpToDate =>
|
repo::RemoteTrackingStatus::UpToDate =>
|
||||||
String::from(" \u{2714}"),
|
String::from(" \u{2714}"),
|
||||||
crate::RemoteTrackingStatus::Ahead(d) => format!(" [+{}]", &d),
|
repo::RemoteTrackingStatus::Ahead(d) => format!(" [+{}]", &d),
|
||||||
crate::RemoteTrackingStatus::Behind(d) => format!(" [-{}]", &d),
|
repo::RemoteTrackingStatus::Behind(d) => format!(" [-{}]", &d),
|
||||||
crate::RemoteTrackingStatus::Diverged(d1, d2) =>
|
repo::RemoteTrackingStatus::Diverged(d1, d2) =>
|
||||||
format!(" [+{}/-{}]", &d1, &d2),
|
format!(" [+{}/-{}]", &d1, &d2),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -99,7 +101,7 @@ fn add_repo_status(
|
|||||||
|
|
||||||
// Don't return table, return a type that implements Display(?)
|
// Don't return table, return a type that implements Display(?)
|
||||||
pub fn get_worktree_status_table(
|
pub fn get_worktree_status_table(
|
||||||
repo: &crate::RepoHandle,
|
repo: &repo::RepoHandle,
|
||||||
directory: &Path,
|
directory: &Path,
|
||||||
) -> Result<(impl std::fmt::Display, Vec<String>), String> {
|
) -> Result<(impl std::fmt::Display, Vec<String>), String> {
|
||||||
let worktrees = repo.get_worktrees()?;
|
let worktrees = repo.get_worktrees()?;
|
||||||
@@ -111,7 +113,7 @@ pub fn get_worktree_status_table(
|
|||||||
for worktree in &worktrees {
|
for worktree in &worktrees {
|
||||||
let worktree_dir = &directory.join(&worktree.name());
|
let worktree_dir = &directory.join(&worktree.name());
|
||||||
if worktree_dir.exists() {
|
if worktree_dir.exists() {
|
||||||
let repo = match crate::RepoHandle::open(worktree_dir, false) {
|
let repo = match repo::RepoHandle::open(worktree_dir, false) {
|
||||||
Ok(repo) => repo,
|
Ok(repo) => repo,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
errors.push(format!(
|
errors.push(format!(
|
||||||
@@ -132,7 +134,7 @@ pub fn get_worktree_status_table(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for worktree in RepoHandle::find_unmanaged_worktrees(repo, directory)? {
|
for worktree in repo::RepoHandle::find_unmanaged_worktrees(repo, directory)? {
|
||||||
errors.push(format!(
|
errors.push(format!(
|
||||||
"Found {}, which is not a valid worktree directory!",
|
"Found {}, which is not a valid worktree directory!",
|
||||||
&worktree
|
&worktree
|
||||||
@@ -141,13 +143,13 @@ pub fn get_worktree_status_table(
|
|||||||
Ok((table, errors))
|
Ok((table, errors))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_status_table(config: crate::Config) -> Result<(Vec<Table>, Vec<String>), String> {
|
pub fn get_status_table(config: config::Config) -> Result<(Vec<Table>, Vec<String>), String> {
|
||||||
let mut errors = Vec::new();
|
let mut errors = Vec::new();
|
||||||
let mut tables = Vec::new();
|
let mut tables = Vec::new();
|
||||||
for tree in config.trees()? {
|
for tree in config.trees()? {
|
||||||
let repos = tree.repos.unwrap_or_default();
|
let repos = tree.repos.unwrap_or_default();
|
||||||
|
|
||||||
let root_path = crate::expand_path(Path::new(&tree.root));
|
let root_path = path::expand_path(Path::new(&tree.root));
|
||||||
|
|
||||||
let mut table = Table::new();
|
let mut table = Table::new();
|
||||||
add_table_header(&mut table);
|
add_table_header(&mut table);
|
||||||
@@ -163,12 +165,12 @@ pub fn get_status_table(config: crate::Config) -> Result<(Vec<Table>, Vec<String
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let repo_handle = crate::RepoHandle::open(&repo_path, repo.worktree_setup);
|
let repo_handle = repo::RepoHandle::open(&repo_path, repo.worktree_setup);
|
||||||
|
|
||||||
let repo_handle = match repo_handle {
|
let repo_handle = match repo_handle {
|
||||||
Ok(repo) => repo,
|
Ok(repo) => repo,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
if error.kind == crate::RepoErrorKind::NotFound {
|
if error.kind == repo::RepoErrorKind::NotFound {
|
||||||
errors.push(format!(
|
errors.push(format!(
|
||||||
"{}: No git repository found. Run sync?",
|
"{}: No git repository found. Run sync?",
|
||||||
&repo.name
|
&repo.name
|
||||||
@@ -206,8 +208,8 @@ fn add_worktree_table_header(table: &mut Table) {
|
|||||||
|
|
||||||
fn add_worktree_status(
|
fn add_worktree_status(
|
||||||
table: &mut Table,
|
table: &mut Table,
|
||||||
worktree: &crate::repo::Worktree,
|
worktree: &repo::Worktree,
|
||||||
repo: &crate::RepoHandle,
|
repo: &repo::RepoHandle,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let repo_status = repo.status(false)?;
|
let repo_status = repo.status(false)?;
|
||||||
|
|
||||||
@@ -272,13 +274,13 @@ pub fn show_single_repo_status(
|
|||||||
let mut table = Table::new();
|
let mut table = Table::new();
|
||||||
let mut warnings = Vec::new();
|
let mut warnings = Vec::new();
|
||||||
|
|
||||||
let is_worktree = crate::RepoHandle::detect_worktree(path);
|
let is_worktree = repo::RepoHandle::detect_worktree(path);
|
||||||
add_table_header(&mut table);
|
add_table_header(&mut table);
|
||||||
|
|
||||||
let repo_handle = crate::RepoHandle::open(path, is_worktree);
|
let repo_handle = repo::RepoHandle::open(path, is_worktree);
|
||||||
|
|
||||||
if let Err(error) = repo_handle {
|
if let Err(error) = repo_handle {
|
||||||
if error.kind == crate::RepoErrorKind::NotFound {
|
if error.kind == repo::RepoErrorKind::NotFound {
|
||||||
return Err(String::from("Directory is not a git directory"));
|
return Err(String::from("Directory is not a git directory"));
|
||||||
} else {
|
} else {
|
||||||
return Err(format!("Opening repository failed: {}", error));
|
return Err(format!("Opening repository failed: {}", error));
|
||||||
|
|||||||
268
src/tree.rs
Normal file
268
src/tree.rs
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use super::config;
|
||||||
|
use super::output::*;
|
||||||
|
use super::path;
|
||||||
|
use super::repo;
|
||||||
|
use super::worktree;
|
||||||
|
|
||||||
|
pub struct Tree {
|
||||||
|
pub root: String,
|
||||||
|
pub repos: Vec<repo::Repo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_unmanaged_repos(
|
||||||
|
root_path: &Path,
|
||||||
|
managed_repos: &[repo::Repo],
|
||||||
|
) -> Result<Vec<PathBuf>, String> {
|
||||||
|
let mut unmanaged_repos = Vec::new();
|
||||||
|
|
||||||
|
for repo_path in find_repo_paths(root_path)? {
|
||||||
|
if !managed_repos
|
||||||
|
.iter()
|
||||||
|
.any(|r| Path::new(root_path).join(r.fullname()) == repo_path)
|
||||||
|
{
|
||||||
|
unmanaged_repos.push(repo_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(unmanaged_repos)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sync_trees(config: config::Config, init_worktree: bool) -> Result<bool, String> {
|
||||||
|
let mut failures = false;
|
||||||
|
|
||||||
|
let mut unmanaged_repos_absolute_paths = vec![];
|
||||||
|
let mut managed_repos_absolute_paths = vec![];
|
||||||
|
|
||||||
|
let trees = config.trees()?;
|
||||||
|
|
||||||
|
for tree in trees {
|
||||||
|
let repos: Vec<repo::Repo> = tree
|
||||||
|
.repos
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.map(|repo| repo.into_repo())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let root_path = path::expand_path(Path::new(&tree.root));
|
||||||
|
|
||||||
|
for repo in &repos {
|
||||||
|
managed_repos_absolute_paths.push(root_path.join(repo.fullname()));
|
||||||
|
match sync_repo(&root_path, repo, init_worktree) {
|
||||||
|
Ok(_) => print_repo_success(&repo.name, "OK"),
|
||||||
|
Err(error) => {
|
||||||
|
print_repo_error(&repo.name, &error);
|
||||||
|
failures = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match find_unmanaged_repos(&root_path, &repos) {
|
||||||
|
Ok(repos) => {
|
||||||
|
unmanaged_repos_absolute_paths.extend(repos);
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
print_error(&format!("Error getting unmanaged repos: {}", error));
|
||||||
|
failures = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for unmanaged_repo_absolute_path in &unmanaged_repos_absolute_paths {
|
||||||
|
if managed_repos_absolute_paths
|
||||||
|
.iter()
|
||||||
|
.any(|managed_repo_absolute_path| {
|
||||||
|
managed_repo_absolute_path == unmanaged_repo_absolute_path
|
||||||
|
})
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
print_warning(&format!(
|
||||||
|
"Found unmanaged repository: \"{}\"",
|
||||||
|
path::path_as_string(unmanaged_repo_absolute_path)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(!failures)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds repositories recursively, returning their path
|
||||||
|
pub fn find_repo_paths(path: &Path) -> Result<Vec<PathBuf>, String> {
|
||||||
|
let mut repos = Vec::new();
|
||||||
|
|
||||||
|
let git_dir = path.join(".git");
|
||||||
|
let git_worktree = path.join(worktree::GIT_MAIN_WORKTREE_DIRECTORY);
|
||||||
|
|
||||||
|
if git_dir.exists() || git_worktree.exists() {
|
||||||
|
repos.push(path.to_path_buf());
|
||||||
|
} else {
|
||||||
|
match fs::read_dir(path) {
|
||||||
|
Ok(contents) => {
|
||||||
|
for content in contents {
|
||||||
|
match content {
|
||||||
|
Ok(entry) => {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_symlink() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if path.is_dir() {
|
||||||
|
match find_repo_paths(&path) {
|
||||||
|
Ok(ref mut r) => repos.append(r),
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(format!("Error accessing directory: {}", e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(format!(
|
||||||
|
"Failed to open \"{}\": {}",
|
||||||
|
&path.display(),
|
||||||
|
match e.kind() {
|
||||||
|
std::io::ErrorKind::NotADirectory =>
|
||||||
|
String::from("directory expected, but path is not a directory"),
|
||||||
|
std::io::ErrorKind::NotFound => String::from("not found"),
|
||||||
|
_ => format!("{:?}", e.kind()),
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(repos)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sync_repo(root_path: &Path, repo: &repo::Repo, init_worktree: bool) -> Result<(), String> {
|
||||||
|
let repo_path = root_path.join(&repo.fullname());
|
||||||
|
let actual_git_directory = get_actual_git_directory(&repo_path, repo.worktree_setup);
|
||||||
|
|
||||||
|
let mut newly_created = false;
|
||||||
|
|
||||||
|
if repo_path.exists() {
|
||||||
|
if repo.worktree_setup && !actual_git_directory.exists() {
|
||||||
|
return Err(String::from(
|
||||||
|
"Repo already exists, but is not using a worktree setup",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
} else if matches!(&repo.remotes, None) || repo.remotes.as_ref().unwrap().is_empty() {
|
||||||
|
print_repo_action(
|
||||||
|
&repo.name,
|
||||||
|
"Repository does not have remotes configured, initializing new",
|
||||||
|
);
|
||||||
|
match repo::RepoHandle::init(&repo_path, repo.worktree_setup) {
|
||||||
|
Ok(r) => {
|
||||||
|
print_repo_success(&repo.name, "Repository created");
|
||||||
|
Some(r)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(format!("Repository failed during init: {}", e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
let first = repo.remotes.as_ref().unwrap().first().unwrap();
|
||||||
|
|
||||||
|
match repo::clone_repo(first, &repo_path, repo.worktree_setup) {
|
||||||
|
Ok(_) => {
|
||||||
|
print_repo_success(&repo.name, "Repository successfully cloned");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(format!("Repository failed during clone: {}", e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
newly_created = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let repo_handle = match repo::RepoHandle::open(&repo_path, repo.worktree_setup) {
|
||||||
|
Ok(repo) => repo,
|
||||||
|
Err(error) => {
|
||||||
|
if !repo.worktree_setup && repo::RepoHandle::open(&repo_path, true).is_ok() {
|
||||||
|
return Err(String::from(
|
||||||
|
"Repo already exists, but is using a worktree setup",
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
return Err(format!("Opening repository failed: {}", error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if newly_created && repo.worktree_setup && init_worktree {
|
||||||
|
match repo_handle.default_branch() {
|
||||||
|
Ok(branch) => {
|
||||||
|
worktree::add_worktree(&repo_path, &branch.name()?, None, None, false)?;
|
||||||
|
}
|
||||||
|
Err(_error) => print_repo_error(
|
||||||
|
&repo.name,
|
||||||
|
"Could not determine default branch, skipping worktree initializtion",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(remotes) = &repo.remotes {
|
||||||
|
let current_remotes: Vec<String> = repo_handle
|
||||||
|
.remotes()
|
||||||
|
.map_err(|error| format!("Repository failed during getting the remotes: {}", error))?;
|
||||||
|
|
||||||
|
for remote in remotes {
|
||||||
|
let current_remote = repo_handle.find_remote(&remote.name)?;
|
||||||
|
|
||||||
|
match current_remote {
|
||||||
|
Some(current_remote) => {
|
||||||
|
let current_url = current_remote.url();
|
||||||
|
|
||||||
|
if remote.url != current_url {
|
||||||
|
print_repo_action(
|
||||||
|
&repo.name,
|
||||||
|
&format!("Updating remote {} to \"{}\"", &remote.name, &remote.url),
|
||||||
|
);
|
||||||
|
if let Err(e) = repo_handle.remote_set_url(&remote.name, &remote.url) {
|
||||||
|
return Err(format!("Repository failed during setting of the remote URL for remote \"{}\": {}", &remote.name, e));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
print_repo_action(
|
||||||
|
&repo.name,
|
||||||
|
&format!(
|
||||||
|
"Setting up new remote \"{}\" to \"{}\"",
|
||||||
|
&remote.name, &remote.url
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if let Err(e) = repo_handle.new_remote(&remote.name, &remote.url) {
|
||||||
|
return Err(format!(
|
||||||
|
"Repository failed during setting the remotes: {}",
|
||||||
|
e
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for current_remote in ¤t_remotes {
|
||||||
|
if !remotes.iter().any(|r| &r.name == current_remote) {
|
||||||
|
print_repo_action(
|
||||||
|
&repo.name,
|
||||||
|
&format!("Deleting remote \"{}\"", ¤t_remote,),
|
||||||
|
);
|
||||||
|
if let Err(e) = repo_handle.remote_delete(current_remote) {
|
||||||
|
return Err(format!(
|
||||||
|
"Repository failed during deleting remote \"{}\": {}",
|
||||||
|
¤t_remote, e
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_actual_git_directory(path: &Path, is_worktree: bool) -> PathBuf {
|
||||||
|
match is_worktree {
|
||||||
|
false => path.to_path_buf(),
|
||||||
|
true => path.join(worktree::GIT_MAIN_WORKTREE_DIRECTORY),
|
||||||
|
}
|
||||||
|
}
|
||||||
166
src/worktree.rs
Normal file
166
src/worktree.rs
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use super::repo;
|
||||||
|
|
||||||
|
pub const GIT_MAIN_WORKTREE_DIRECTORY: &str = ".git-main-working-tree";
|
||||||
|
|
||||||
|
pub fn add_worktree(
|
||||||
|
directory: &Path,
|
||||||
|
name: &str,
|
||||||
|
subdirectory: Option<&Path>,
|
||||||
|
track: Option<(&str, &str)>,
|
||||||
|
no_track: bool,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let repo = repo::RepoHandle::open(directory, true).map_err(|error| match error.kind {
|
||||||
|
repo::RepoErrorKind::NotFound => {
|
||||||
|
String::from("Current directory does not contain a worktree setup")
|
||||||
|
}
|
||||||
|
_ => format!("Error opening repo: {}", error),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let config = repo::read_worktree_root_config(directory)?;
|
||||||
|
|
||||||
|
if repo.find_worktree(name).is_ok() {
|
||||||
|
return Err(format!("Worktree {} already exists", &name));
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = match subdirectory {
|
||||||
|
Some(dir) => directory.join(dir).join(name),
|
||||||
|
None => directory.join(Path::new(name)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut remote_branch_exists = false;
|
||||||
|
|
||||||
|
let default_checkout = || repo.default_branch()?.to_commit();
|
||||||
|
|
||||||
|
let checkout_commit;
|
||||||
|
if no_track {
|
||||||
|
checkout_commit = default_checkout()?;
|
||||||
|
} else {
|
||||||
|
match track {
|
||||||
|
Some((remote_name, remote_branch_name)) => {
|
||||||
|
let remote_branch = repo.find_remote_branch(remote_name, remote_branch_name);
|
||||||
|
match remote_branch {
|
||||||
|
Ok(branch) => {
|
||||||
|
remote_branch_exists = true;
|
||||||
|
checkout_commit = branch.to_commit()?;
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
remote_branch_exists = false;
|
||||||
|
checkout_commit = default_checkout()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => match &config {
|
||||||
|
None => checkout_commit = default_checkout()?,
|
||||||
|
Some(config) => match &config.track {
|
||||||
|
None => checkout_commit = default_checkout()?,
|
||||||
|
Some(track_config) => {
|
||||||
|
if track_config.default {
|
||||||
|
let remote_branch =
|
||||||
|
repo.find_remote_branch(&track_config.default_remote, name);
|
||||||
|
match remote_branch {
|
||||||
|
Ok(branch) => {
|
||||||
|
remote_branch_exists = true;
|
||||||
|
checkout_commit = branch.to_commit()?;
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
checkout_commit = default_checkout()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
checkout_commit = default_checkout()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut target_branch = match repo.find_local_branch(name) {
|
||||||
|
Ok(branchref) => branchref,
|
||||||
|
Err(_) => repo.create_branch(name, &checkout_commit)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn push(
|
||||||
|
remote: &mut repo::RemoteHandle,
|
||||||
|
branch_name: &str,
|
||||||
|
remote_branch_name: &str,
|
||||||
|
repo: &repo::RepoHandle,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if !remote.is_pushable()? {
|
||||||
|
return Err(format!(
|
||||||
|
"Cannot push to non-pushable remote {}",
|
||||||
|
remote.url()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
remote.push(branch_name, remote_branch_name, repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !no_track {
|
||||||
|
if let Some((remote_name, remote_branch_name)) = track {
|
||||||
|
if remote_branch_exists {
|
||||||
|
target_branch.set_upstream(remote_name, remote_branch_name)?;
|
||||||
|
} else {
|
||||||
|
let mut remote = repo
|
||||||
|
.find_remote(remote_name)
|
||||||
|
.map_err(|error| format!("Error getting remote {}: {}", remote_name, error))?
|
||||||
|
.ok_or_else(|| format!("Remote {} not found", remote_name))?;
|
||||||
|
|
||||||
|
push(
|
||||||
|
&mut remote,
|
||||||
|
&target_branch.name()?,
|
||||||
|
remote_branch_name,
|
||||||
|
&repo,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
target_branch.set_upstream(remote_name, remote_branch_name)?;
|
||||||
|
}
|
||||||
|
} else if let Some(config) = config {
|
||||||
|
if let Some(track_config) = config.track {
|
||||||
|
if track_config.default {
|
||||||
|
let remote_name = track_config.default_remote;
|
||||||
|
if remote_branch_exists {
|
||||||
|
target_branch.set_upstream(&remote_name, name)?;
|
||||||
|
} else {
|
||||||
|
let remote_branch_name = match track_config.default_remote_prefix {
|
||||||
|
Some(prefix) => {
|
||||||
|
format!("{}{}{}", &prefix, super::BRANCH_NAMESPACE_SEPARATOR, &name)
|
||||||
|
}
|
||||||
|
None => name.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut remote = repo
|
||||||
|
.find_remote(&remote_name)
|
||||||
|
.map_err(|error| {
|
||||||
|
format!("Error getting remote {}: {}", remote_name, error)
|
||||||
|
})?
|
||||||
|
.ok_or_else(|| format!("Remote {} not found", remote_name))?;
|
||||||
|
|
||||||
|
if !remote.is_pushable()? {
|
||||||
|
return Err(format!(
|
||||||
|
"Cannot push to non-pushable remote {}",
|
||||||
|
remote.url()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
push(
|
||||||
|
&mut remote,
|
||||||
|
&target_branch.name()?,
|
||||||
|
&remote_branch_name,
|
||||||
|
&repo,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
target_branch.set_upstream(&remote_name, &remote_branch_name)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(subdirectory) = subdirectory {
|
||||||
|
std::fs::create_dir_all(subdirectory).map_err(|error| error.to_string())?;
|
||||||
|
}
|
||||||
|
repo.new_worktree(name, &path, &target_branch)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user