Compare commits
78 Commits
v0.7.11
...
241bf473a7
| Author | SHA1 | Date | |
|---|---|---|---|
| 241bf473a7 | |||
| 8fd663462e | |||
| 4beacbf65d | |||
| 102f5561a8 | |||
| e04f065d42 | |||
| 941dd50868 | |||
| d20dabc91e | |||
| 0e63a1c6bf | |||
| 9792c09850 | |||
| a1519a6bc5 | |||
| 36535dcaec | |||
| 32f94b1ef5 | |||
| 913df16f28 | |||
| f66a512a83 | |||
| de15e799ac | |||
| a8736ed37f | |||
| 1a45887fb6 | |||
| 9403156edf | |||
| 21e3a9b9bb | |||
| ca0c9c28fd | |||
| 1edc61d6e6 | |||
| b20bba529a | |||
| fb0948787a | |||
| 625457e474 | |||
| d4b7cabcf2 | |||
| b2727c7a96 | |||
| ff3cbfbdba | |||
| 44602e7bc2 | |||
| 1706df7236 | |||
| 80fc28c44a | |||
| 7335c0fc62 | |||
| a536e688c9 | |||
| 0d22b43ed0 | |||
| 9d7f566209 | |||
| 1e6f965f7a | |||
| 6183a58204 | |||
| 2a4934b01a | |||
| fc4261b7ac | |||
| 7d248c5ea3 | |||
| 8d4af73364 | |||
|
|
4c738d027a | ||
|
|
f2fa3411d8 | ||
|
|
19443bc4ca | ||
| 60a777276f | |||
|
|
1262ec5a33 | ||
|
|
4c6b69e125 | ||
|
|
28881a23a9 | ||
|
|
e796362e6b | ||
|
|
37094a3295 | ||
|
|
100bac8f87 | ||
|
|
fdafa3aa81 | ||
|
|
d267564bca | ||
|
|
2cc477e551 | ||
|
|
8cbdd9f408 | ||
|
|
21be3e40dd | ||
|
|
a3824c2671 | ||
|
|
8eeb010c3a | ||
|
|
956b172426 | ||
| 9b4ed2837e | |||
| 701e64df6f | |||
| 23fc942db7 | |||
| 38bba1472e | |||
| 7d131bbacf | |||
| 6e79dd318a | |||
| 5fc1d2148f | |||
| de184de5a0 | |||
| 8a3b2ae1c5 | |||
| 93e38b0572 | |||
| 43bbb8e143 | |||
| 0fd9ce68b8 | |||
| 68f2b81e3f | |||
| d7a39fa4e4 | |||
| 1f646fd5f8 | |||
| 96cbf8c568 | |||
| bf199c1b17 | |||
| 0f7a70c895 | |||
| 3151b97bc0 | |||
| 8ce5cfecd4 |
@@ -1,2 +1,3 @@
|
||||
nonnominandus
|
||||
Maximilian Volk
|
||||
Baptiste (@BapRx)
|
||||
|
||||
831
Cargo.lock
generated
831
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
26
Cargo.toml
26
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "git-repo-manager"
|
||||
version = "0.7.11"
|
||||
version = "0.7.14"
|
||||
edition = "2021"
|
||||
|
||||
authors = [
|
||||
@@ -23,7 +23,7 @@ repository = "https://github.com/hakoerber/git-repo-manager"
|
||||
readme = "README.md"
|
||||
|
||||
# Required for `std::path::Path::is_symlink()`. Will be released with 1.57.
|
||||
rust-version = "1.57"
|
||||
rust-version = "1.58"
|
||||
|
||||
license = "GPL-3.0-only"
|
||||
|
||||
@@ -41,36 +41,36 @@ path = "src/grm/main.rs"
|
||||
[dependencies]
|
||||
|
||||
[dependencies.toml]
|
||||
version = "=0.5.9"
|
||||
version = "=0.7.6"
|
||||
|
||||
[dependencies.serde]
|
||||
version = "=1.0.145"
|
||||
version = "=1.0.183"
|
||||
features = ["derive"]
|
||||
|
||||
[dependencies.git2]
|
||||
version = "=0.15.0"
|
||||
version = "=0.17.2"
|
||||
|
||||
[dependencies.shellexpand]
|
||||
version = "=2.1.2"
|
||||
version = "=3.1.0"
|
||||
|
||||
[dependencies.clap]
|
||||
version = "=4.0.11"
|
||||
version = "=4.3.21"
|
||||
features = ["derive", "cargo"]
|
||||
|
||||
[dependencies.console]
|
||||
version = "=0.15.2"
|
||||
version = "=0.15.7"
|
||||
|
||||
[dependencies.regex]
|
||||
version = "=1.6.0"
|
||||
version = "=1.9.3"
|
||||
|
||||
[dependencies.comfy-table]
|
||||
version = "=6.1.0"
|
||||
version = "=7.0.1"
|
||||
|
||||
[dependencies.serde_yaml]
|
||||
version = "=0.9.13"
|
||||
version = "=0.9.25"
|
||||
|
||||
[dependencies.serde_json]
|
||||
version = "=1.0.86"
|
||||
version = "=1.0.104"
|
||||
|
||||
[dependencies.isahc]
|
||||
version = "=1.7.2"
|
||||
@@ -78,7 +78,7 @@ default-features = false
|
||||
features = ["json", "http2", "text-decoding"]
|
||||
|
||||
[dependencies.parse_link_header]
|
||||
version = "=0.3.2"
|
||||
version = "=0.3.3"
|
||||
|
||||
[dependencies.url-escape]
|
||||
version = "=0.1.1"
|
||||
|
||||
42
Justfile
42
Justfile
@@ -4,35 +4,39 @@ set shell := ["/bin/bash", "-c"]
|
||||
|
||||
static_target := "x86_64-unknown-linux-musl"
|
||||
|
||||
cargo := "cargo"
|
||||
|
||||
check: fmt-check lint test
|
||||
cargo check
|
||||
{{cargo}} check
|
||||
|
||||
clean:
|
||||
cargo clean
|
||||
{{cargo}} clean
|
||||
git clean -f -d -X
|
||||
|
||||
fmt:
|
||||
cargo fmt
|
||||
{{cargo}} fmt
|
||||
git ls-files | grep '\.py$' | xargs isort
|
||||
git ls-files | grep '\.py$' | xargs black
|
||||
git ls-files | grep '\.sh$' | xargs -L 1 shfmt --indent 4 --write
|
||||
|
||||
fmt-check:
|
||||
cargo fmt --check
|
||||
{{cargo}} fmt --check
|
||||
git ls-files | grep '\.py$' | xargs black --check
|
||||
git ls-files | grep '\.sh$' | xargs -L 1 shfmt --indent 4 --diff
|
||||
|
||||
lint:
|
||||
cargo clippy --no-deps -- -Dwarnings
|
||||
{{cargo}} clippy --no-deps -- -Dwarnings
|
||||
git ls-files | grep '\.py$' | xargs ruff --ignore E501
|
||||
git ls-files | grep '\.sh$' | xargs -L 1 shellcheck --norc
|
||||
|
||||
lint-fix:
|
||||
cargo clippy --no-deps --fix
|
||||
{{cargo}} clippy --no-deps --fix
|
||||
|
||||
build-release:
|
||||
cargo build --release
|
||||
{{cargo}} build --release
|
||||
|
||||
build-release-static:
|
||||
cargo build --release --target {{static_target}} --features=static-build
|
||||
{{cargo}} build --release --target {{static_target}} --features=static-build
|
||||
|
||||
pushall:
|
||||
for r in $(git remote) ; do \
|
||||
@@ -48,38 +52,38 @@ test-binary:
|
||||
env \
|
||||
GITHUB_API_BASEURL=http://rest:5000/github \
|
||||
GITLAB_API_BASEURL=http://rest:5000/gitlab \
|
||||
cargo build --profile e2e-tests --target {{static_target}} --features=static-build
|
||||
{{cargo}} build --profile e2e-tests --target {{static_target}} --features=static-build
|
||||
|
||||
install:
|
||||
cargo install --path .
|
||||
{{cargo}} install --path .
|
||||
|
||||
install-static:
|
||||
cargo install --target {{static_target}} --features=static-build --path .
|
||||
{{cargo}} install --target {{static_target}} --features=static-build --path .
|
||||
|
||||
build:
|
||||
cargo build
|
||||
{{cargo}} build
|
||||
|
||||
build-static:
|
||||
cargo build --target {{static_target}} --features=static-build
|
||||
{{cargo}} build --target {{static_target}} --features=static-build
|
||||
|
||||
test: test-unit test-integration test-e2e
|
||||
|
||||
test-unit +tests="":
|
||||
cargo test --lib --bins -- --show-output {{tests}}
|
||||
{{cargo}} test --lib --bins -- --show-output {{tests}}
|
||||
|
||||
test-integration:
|
||||
cargo test --test "*"
|
||||
{{cargo}} test --test "*"
|
||||
|
||||
test-e2e +tests=".": test-binary
|
||||
cd ./e2e_tests \
|
||||
&& docker-compose rm --stop -f \
|
||||
&& docker-compose build \
|
||||
&& docker-compose run \
|
||||
&& docker compose rm --stop -f \
|
||||
&& docker compose build \
|
||||
&& docker compose run \
|
||||
--rm \
|
||||
-v $PWD/../target/x86_64-unknown-linux-musl/e2e-tests/grm:/grm \
|
||||
pytest \
|
||||
"GRM_BINARY=/grm ALTERNATE_DOMAIN=alternate-rest python3 -m pytest --exitfirst -p no:cacheprovider --color=yes "$@"" \
|
||||
&& docker-compose rm --stop -f
|
||||
&& docker compose rm --stop -f
|
||||
|
||||
update-dependencies: update-cargo-dependencies
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import subprocess
|
||||
import os
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import semver
|
||||
import tomlkit
|
||||
@@ -94,15 +93,7 @@ for tier in ["dependencies", "dev-dependencies"]:
|
||||
|
||||
try:
|
||||
cmd = subprocess.run(
|
||||
[
|
||||
"cargo",
|
||||
"update",
|
||||
"-Z",
|
||||
"no-index-update",
|
||||
"--aggressive",
|
||||
"--package",
|
||||
name,
|
||||
],
|
||||
["cargo", "update", "--offline", "--aggressive", "--package", name],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -136,15 +127,7 @@ while True:
|
||||
spec = f"{package['name']}:{package['version']}"
|
||||
try:
|
||||
cmd = subprocess.run(
|
||||
[
|
||||
"cargo",
|
||||
"update",
|
||||
"-Z",
|
||||
"no-index-update",
|
||||
"--aggressive",
|
||||
"--package",
|
||||
spec,
|
||||
],
|
||||
["cargo", "update", "--offline", "--aggressive", "--package", spec],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
|
||||
@@ -28,14 +28,14 @@ Feature branches are not required, there are also changes happening directly on
|
||||
|
||||
You will need the following tools:
|
||||
|
||||
* Rust (obviously) (easiest via `rustup`), with the nightly toolchain
|
||||
* Rust (obviously) (easiest via `rustup`)
|
||||
* Python3
|
||||
* [`just`](https://github.com/casey/just), a command runner like `make`. See
|
||||
[here](https://github.com/casey/just#installation) for installation
|
||||
instructions (it's most likely just a simple `cargo install just`).
|
||||
* Docker & docker-compose for the e2e tests
|
||||
* `black` and `shfmt` for formatting.
|
||||
* `shellcheck` for shell script linting
|
||||
* `isort`, `black` and `shfmt` for formatting.
|
||||
* `ruff` and `shellcheck` for linting.
|
||||
* `mdbook` for the documentation
|
||||
|
||||
Here are the tools:
|
||||
@@ -52,18 +52,3 @@ mvdan.cc/sh/v3/cmd/shfmt@latest`, depending on your go build environment.
|
||||
|
||||
For details about rustup and the toolchains, see [the installation
|
||||
section](./installation.md).
|
||||
|
||||
## FAQ
|
||||
|
||||
### Why nightly?
|
||||
|
||||
For now, GRM requires the nightly toolchain for two reasons:
|
||||
|
||||
* [`io_error_more`](https://github.com/rust-lang/rust/issues/86442) to get
|
||||
better error messages on IO errors
|
||||
* [`const_option_ext`](https://github.com/rust-lang/rust/issues/91930) to have
|
||||
static variables read from the environment that fall back to hard coded
|
||||
defaults
|
||||
|
||||
Honestly, both of those are not really necessary or can be handled without
|
||||
nightly. It's just that I'm using nightly anyway.
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
## Installation
|
||||
|
||||
Building GRM currently requires the nightly Rust toolchain. The easiest way is
|
||||
Building GRM requires the Rust toolchain to be installed. The easiest way is
|
||||
using [`rustup`](https://rustup.rs/). Make sure that rustup is properly
|
||||
installed.
|
||||
|
||||
Make sure that the nightly toolchain is installed:
|
||||
Make sure that the stable toolchain is installed:
|
||||
|
||||
```
|
||||
$ rustup toolchain install nightly
|
||||
$ rustup toolchain install stable
|
||||
```
|
||||
|
||||
Then, install the build dependencies:
|
||||
@@ -22,13 +22,13 @@ Then, install the build dependencies:
|
||||
Then, it's a simple command to install the latest stable version:
|
||||
|
||||
```bash
|
||||
$ cargo +nightly install git-repo-manager
|
||||
$ cargo 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
|
||||
$ cargo install --git https://github.com/hakoerber/git-repo-manager.git --branch develop
|
||||
```
|
||||
|
||||
## Static build
|
||||
@@ -47,11 +47,11 @@ need `musl` and a few other build dependencies installed installed:
|
||||
The, add the musl target via `rustup`:
|
||||
|
||||
```
|
||||
$ rustup +nightly target add x86_64-unknown-linux-musl
|
||||
$ rustup 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
|
||||
$ cargo install git-repo-manager --target x86_64-unknown-linux-musl --features=static-build
|
||||
```
|
||||
|
||||
@@ -11,7 +11,7 @@ Let's try it out:
|
||||
## Get the example configuration
|
||||
|
||||
```bash
|
||||
$ curl --proto '=https' --tlsv1.2 -sSfO https://raw.githubusercontent.com/hakoerber/git-repo-manager/master/example.config.toml
|
||||
curl --proto '=https' --tlsv1.2 -sSfO https://raw.githubusercontent.com/hakoerber/git-repo-manager/master/example.config.toml
|
||||
```
|
||||
|
||||
Then, you're ready to run the first sync. This will clone all configured
|
||||
@@ -30,7 +30,7 @@ $ grm repos sync config --config example.config.toml
|
||||
|
||||
If you run it again, it will report no changes:
|
||||
|
||||
```
|
||||
```bash
|
||||
$ grm repos sync config -c example.config.toml
|
||||
[✔] git-repo-manager: OK
|
||||
[✔] dotfiles: OK
|
||||
@@ -43,11 +43,18 @@ write a configuration from scratch. Luckily, GRM has a way to generate a
|
||||
configuration from an existing file tree:
|
||||
|
||||
```bash
|
||||
$ grm repos find local ~/your/project/root > config.toml
|
||||
grm repos find local ~/your/project/root > config.toml
|
||||
```
|
||||
|
||||
This will detect all repositories and remotes and write them to `config.toml`.
|
||||
|
||||
You can exclude repositories from the generated configuration by providing
|
||||
a regex that will be test against the path of each discovered repository:
|
||||
|
||||
```bash
|
||||
grm repos find local ~/your/project/root --exclude "^.*/subdir/match-(foo|bar)/.*$" > config.toml
|
||||
```
|
||||
|
||||
### Show the state of your projects
|
||||
|
||||
```bash
|
||||
@@ -65,7 +72,7 @@ $ grm repos status --config example.config.toml
|
||||
You can also use `status` without `--config` to check the repository you're
|
||||
currently in:
|
||||
|
||||
```
|
||||
```bash
|
||||
$ cd ~/example-projects/dotfiles
|
||||
$ grm repos status
|
||||
╭──────────┬──────────┬────────┬──────────┬───────┬─────────╮
|
||||
|
||||
@@ -49,7 +49,7 @@ Note: You will most likely not need to read this.
|
||||
Each test parameter will exponentially increase the number of tests that will be
|
||||
run. As a general rule, comprehensiveness is more important than test suite
|
||||
runtime (so if in doubt, better to add another parameter to catch every edge
|
||||
case). But try to keep the total runtime sane. Currently, the whole `just e2e`
|
||||
case). But try to keep the total runtime sane. Currently, the whole `just test-e2e`
|
||||
target runs ~8'000 tests and takes around 5 minutes on my machine, exlucding
|
||||
binary and docker build time. I'd say that keeping it under 10 minutes is a good
|
||||
idea.
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import os
|
||||
|
||||
from helpers import *
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
os.environ["GIT_AUTHOR_NAME"] = "Example user"
|
||||
|
||||
@@ -3,5 +3,5 @@ from flask import Flask
|
||||
app = Flask(__name__)
|
||||
app.url_map.strict_slashes = False
|
||||
|
||||
import github
|
||||
import gitlab
|
||||
import github # noqa: E402,F401
|
||||
import gitlab # noqa: E402,F401
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import os.path
|
||||
|
||||
from app import app
|
||||
|
||||
from flask import Flask, request, abort, jsonify, make_response
|
||||
|
||||
import jinja2
|
||||
from app import app
|
||||
from flask import abort, jsonify, make_response, request
|
||||
|
||||
|
||||
def check_headers():
|
||||
@@ -48,7 +46,7 @@ def add_pagination(response, page, last_page):
|
||||
|
||||
def read_project_files(namespaces=[]):
|
||||
last_page = 4
|
||||
page = username = int(request.args.get("page", "1"))
|
||||
page = int(request.args.get("page", "1"))
|
||||
response_file = f"./github_api_page_{page}.json.j2"
|
||||
if not os.path.exists(response_file):
|
||||
return jsonify([])
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import os.path
|
||||
|
||||
from app import app
|
||||
|
||||
from flask import Flask, request, abort, jsonify, make_response
|
||||
|
||||
import jinja2
|
||||
from app import app
|
||||
from flask import abort, jsonify, make_response, request
|
||||
|
||||
|
||||
def check_headers():
|
||||
@@ -48,7 +46,7 @@ def add_pagination(response, page, last_page):
|
||||
|
||||
def read_project_files(namespaces=[]):
|
||||
last_page = 4
|
||||
page = username = int(request.args.get("page", "1"))
|
||||
page = int(request.args.get("page", "1"))
|
||||
response_file = f"./gitlab_api_page_{page}.json"
|
||||
if not os.path.exists(response_file):
|
||||
return jsonify([])
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import hashlib
|
||||
import inspect
|
||||
import os
|
||||
import os.path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import hashlib
|
||||
import shutil
|
||||
import inspect
|
||||
|
||||
import git
|
||||
|
||||
@@ -176,7 +176,6 @@ class TempGitRemote:
|
||||
newobj.remoteid = remoteid
|
||||
return newobj, remoteid
|
||||
else:
|
||||
refresh = False
|
||||
if cachekey not in cls.obj:
|
||||
tmpdir = get_temporary_directory()
|
||||
shell(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from helpers import *
|
||||
from helpers import grm
|
||||
|
||||
|
||||
def test_invalid_command():
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
import toml
|
||||
import pytest
|
||||
import toml
|
||||
import yaml
|
||||
|
||||
from helpers import *
|
||||
from helpers import NonExistentPath, TempGitRepository, grm, shell
|
||||
|
||||
|
||||
def test_repos_find_nonexistent():
|
||||
@@ -40,7 +41,7 @@ def test_repos_find_invalid_format():
|
||||
)
|
||||
assert cmd.returncode != 0
|
||||
assert len(cmd.stdout) == 0
|
||||
assert "isn't a valid value" in cmd.stderr
|
||||
assert "invalid value 'invalidformat'" in cmd.stderr
|
||||
|
||||
|
||||
def test_repos_find_non_git_repos():
|
||||
@@ -63,9 +64,10 @@ def test_repos_find_non_git_repos():
|
||||
assert len(cmd.stderr) != 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("default", [True, False])
|
||||
@pytest.mark.parametrize("default_format", [True, False])
|
||||
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
|
||||
def test_repos_find(configtype, default):
|
||||
@pytest.mark.parametrize("exclude", [None, "^.*/repo2$", "^not_matching$"])
|
||||
def test_repos_find(configtype, exclude, default_format):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
shell(
|
||||
f"""
|
||||
@@ -99,13 +101,19 @@ def test_repos_find(configtype, default):
|
||||
)
|
||||
|
||||
args = ["repos", "find", "local", tmpdir]
|
||||
if not default:
|
||||
if not default_format:
|
||||
args += ["--format", configtype]
|
||||
if exclude:
|
||||
args += ["--exclude", exclude]
|
||||
cmd = grm(args)
|
||||
assert cmd.returncode == 0
|
||||
if exclude == "^.*/repo2$":
|
||||
assert re.match(r"^.*\[skipped\] .*\/repo2$", cmd.stderr.lower())
|
||||
assert "repo2" in cmd.stderr.lower()
|
||||
else:
|
||||
assert len(cmd.stderr) == 0
|
||||
|
||||
if default or configtype == "toml":
|
||||
if default_format or configtype == "toml":
|
||||
output = toml.loads(cmd.stdout)
|
||||
elif configtype == "yaml":
|
||||
output = yaml.safe_load(cmd.stdout)
|
||||
@@ -120,7 +128,7 @@ def test_repos_find(configtype, default):
|
||||
assert set(tree.keys()) == {"root", "repos"}
|
||||
assert tree["root"] == tmpdir
|
||||
assert isinstance(tree["repos"], list)
|
||||
assert len(tree["repos"]) == 2
|
||||
assert len(tree["repos"]) == (1 if exclude == "^.*/repo2$" else 2)
|
||||
|
||||
repo1 = [r for r in tree["repos"] if r["name"] == "repo1"][0]
|
||||
assert repo1["worktree_setup"] is False
|
||||
@@ -137,6 +145,9 @@ def test_repos_find(configtype, default):
|
||||
assert someremote["type"] == "ssh"
|
||||
assert someremote["url"] == "ssh://example.com/repo2.git"
|
||||
|
||||
if exclude == "^.*/repo2$":
|
||||
assert [r for r in tree["repos"] if r["name"] == "repo2"] == []
|
||||
else:
|
||||
repo2 = [r for r in tree["repos"] if r["name"] == "repo2"][0]
|
||||
assert repo2["worktree_setup"] is False
|
||||
assert isinstance(repo1["remotes"], list)
|
||||
@@ -148,19 +159,18 @@ def test_repos_find(configtype, default):
|
||||
assert origin["url"] == "https://example.com/repo2.git"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("default", [True, False])
|
||||
@pytest.mark.parametrize("default_format", [True, False])
|
||||
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
|
||||
def test_repos_find_in_root(configtype, default):
|
||||
def test_repos_find_in_root(configtype, default_format):
|
||||
with TempGitRepository() as repo_dir:
|
||||
|
||||
args = ["repos", "find", "local", repo_dir]
|
||||
if not default:
|
||||
if not default_format:
|
||||
args += ["--format", configtype]
|
||||
cmd = grm(args)
|
||||
assert cmd.returncode == 0
|
||||
assert len(cmd.stderr) == 0
|
||||
|
||||
if default or configtype == "toml":
|
||||
if default_format or configtype == "toml":
|
||||
output = toml.loads(cmd.stdout)
|
||||
elif configtype == "yaml":
|
||||
output = yaml.safe_load(cmd.stdout)
|
||||
@@ -194,8 +204,8 @@ def test_repos_find_in_root(configtype, default):
|
||||
|
||||
|
||||
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
|
||||
@pytest.mark.parametrize("default", [True, False])
|
||||
def test_repos_find_with_invalid_repo(configtype, default):
|
||||
@pytest.mark.parametrize("default_format", [True, False])
|
||||
def test_repos_find_with_invalid_repo(configtype, default_format):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
shell(
|
||||
f"""
|
||||
@@ -229,13 +239,13 @@ def test_repos_find_with_invalid_repo(configtype, default):
|
||||
)
|
||||
|
||||
args = ["repos", "find", "local", tmpdir]
|
||||
if not default:
|
||||
if not default_format:
|
||||
args += ["--format", configtype]
|
||||
cmd = grm(args)
|
||||
assert cmd.returncode == 0
|
||||
assert "broken" in cmd.stderr
|
||||
|
||||
if default or configtype == "toml":
|
||||
if default_format or configtype == "toml":
|
||||
output = toml.loads(cmd.stdout)
|
||||
elif configtype == "yaml":
|
||||
output = yaml.safe_load(cmd.stdout)
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import re
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
import toml
|
||||
import pytest
|
||||
import toml
|
||||
import yaml
|
||||
|
||||
from helpers import *
|
||||
|
||||
from helpers import grm
|
||||
|
||||
ALTERNATE_DOMAIN = os.environ["ALTERNATE_DOMAIN"]
|
||||
PROVIDERS = ["github", "gitlab"]
|
||||
@@ -44,7 +43,9 @@ def test_repos_find_remote_invalid_provider(use_config):
|
||||
assert cmd.returncode != 0
|
||||
assert len(cmd.stdout) == 0
|
||||
if not use_config:
|
||||
assert re.match(".*isn't a valid value for.*provider", cmd.stderr)
|
||||
assert re.match(
|
||||
".*invalid value 'thisproviderdoesnotexist' for.*provider", cmd.stderr
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("provider", PROVIDERS)
|
||||
@@ -67,7 +68,7 @@ def test_repos_find_remote_invalid_format(provider):
|
||||
)
|
||||
assert cmd.returncode != 0
|
||||
assert len(cmd.stdout) == 0
|
||||
assert "isn't a valid value" in cmd.stderr
|
||||
assert "invalid value 'invalidformat'" in cmd.stderr
|
||||
|
||||
|
||||
@pytest.mark.parametrize("provider", PROVIDERS)
|
||||
@@ -166,7 +167,7 @@ def test_repos_find_remote_no_filter(provider, configtype, default, use_config):
|
||||
cmd = grm(args)
|
||||
|
||||
assert cmd.returncode == 0
|
||||
assert len(cmd.stderr) == 0
|
||||
assert "did not specify any filters" in cmd.stderr.lower()
|
||||
|
||||
if default or configtype == "toml":
|
||||
output = toml.loads(cmd.stdout)
|
||||
@@ -275,9 +276,9 @@ def test_repos_find_remote_user(
|
||||
if not worktree_default:
|
||||
cfg += f"worktree = {str(worktree).lower()}\n"
|
||||
if force_ssh:
|
||||
cfg += f"force_ssh = true\n"
|
||||
cfg += "force_ssh = true\n"
|
||||
if override_remote_name:
|
||||
cfg += f'remote_name = "otherremote"\n'
|
||||
cfg += 'remote_name = "otherremote"\n'
|
||||
if use_owner:
|
||||
cfg += """
|
||||
[filters]
|
||||
@@ -475,7 +476,7 @@ def test_repos_find_remote_group(
|
||||
if not worktree_default:
|
||||
cfg += f"worktree = {str(worktree).lower()}\n"
|
||||
if force_ssh:
|
||||
cfg += f"force_ssh = true\n"
|
||||
cfg += "force_ssh = true\n"
|
||||
if use_alternate_endpoint:
|
||||
cfg += f'api_url = "http://{ALTERNATE_DOMAIN}:5000/{provider}"\n'
|
||||
cfg += """
|
||||
@@ -591,7 +592,7 @@ def test_repos_find_remote_user_and_group(
|
||||
if not worktree_default:
|
||||
cfg += f"worktree = {str(worktree).lower()}\n"
|
||||
if force_ssh:
|
||||
cfg += f"force_ssh = true\n"
|
||||
cfg += "force_ssh = true\n"
|
||||
if use_alternate_endpoint:
|
||||
cfg += f'api_url = "http://{ALTERNATE_DOMAIN}:5000/{provider}"\n'
|
||||
cfg += """
|
||||
@@ -742,7 +743,7 @@ def test_repos_find_remote_owner(
|
||||
if not worktree_default:
|
||||
cfg += f"worktree = {str(worktree).lower()}\n"
|
||||
if force_ssh:
|
||||
cfg += f"force_ssh = true\n"
|
||||
cfg += "force_ssh = true\n"
|
||||
if use_alternate_endpoint:
|
||||
cfg += f'api_url = "http://{ALTERNATE_DOMAIN}:5000/{provider}"\n'
|
||||
cfg += """
|
||||
@@ -873,13 +874,11 @@ def test_repos_find_remote_owner(
|
||||
assert repo["remotes"][0]["name"] == "origin"
|
||||
if force_ssh:
|
||||
assert (
|
||||
repo["remotes"][0]["url"] == f"ssh://git@example.com/myuser2/myproject3.git"
|
||||
repo["remotes"][0]["url"] == "ssh://git@example.com/myuser2/myproject3.git"
|
||||
)
|
||||
assert repo["remotes"][0]["type"] == "ssh"
|
||||
else:
|
||||
assert (
|
||||
repo["remotes"][0]["url"] == f"https://example.com/myuser2/myproject3.git"
|
||||
)
|
||||
assert repo["remotes"][0]["url"] == "https://example.com/myuser2/myproject3.git"
|
||||
assert repo["remotes"][0]["type"] == "https"
|
||||
|
||||
group_namespace_1 = [t for t in output["trees"] if t["root"] == "/myroot/mygroup1"][
|
||||
@@ -923,13 +922,13 @@ def test_repos_find_remote_owner(
|
||||
if force_ssh:
|
||||
assert (
|
||||
repo["remotes"][0]["url"]
|
||||
== f"ssh://git@example.com/mygroup1/myproject4.git"
|
||||
== "ssh://git@example.com/mygroup1/myproject4.git"
|
||||
)
|
||||
assert repo["remotes"][0]["type"] == "ssh"
|
||||
else:
|
||||
assert (
|
||||
repo["remotes"][0]["url"]
|
||||
== f"https://example.com/mygroup1/myproject4.git"
|
||||
== "https://example.com/mygroup1/myproject4.git"
|
||||
)
|
||||
assert repo["remotes"][0]["type"] == "https"
|
||||
|
||||
@@ -948,12 +947,11 @@ def test_repos_find_remote_owner(
|
||||
assert repo["remotes"][0]["name"] == "origin"
|
||||
if force_ssh:
|
||||
assert (
|
||||
repo["remotes"][0]["url"]
|
||||
== f"ssh://git@example.com/mygroup2/myproject5.git"
|
||||
repo["remotes"][0]["url"] == "ssh://git@example.com/mygroup2/myproject5.git"
|
||||
)
|
||||
assert repo["remotes"][0]["type"] == "ssh"
|
||||
else:
|
||||
assert (
|
||||
repo["remotes"][0]["url"] == f"https://example.com/mygroup2/myproject5.git"
|
||||
repo["remotes"][0]["url"] == "https://example.com/mygroup2/myproject5.git"
|
||||
)
|
||||
assert repo["remotes"][0]["type"] == "https"
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import tempfile
|
||||
|
||||
from helpers import *
|
||||
from helpers import RepoTree, grm
|
||||
|
||||
|
||||
def test_repos_sync_worktree_clone():
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import tempfile
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
import textwrap
|
||||
|
||||
import pytest
|
||||
import toml
|
||||
import git
|
||||
|
||||
from helpers import *
|
||||
import pytest
|
||||
from helpers import (
|
||||
NonExistentPath,
|
||||
TempGitFileRemote,
|
||||
TempGitRepository,
|
||||
checksum_directory,
|
||||
grm,
|
||||
shell,
|
||||
)
|
||||
|
||||
templates = {
|
||||
"repo_simple": {
|
||||
@@ -291,7 +298,9 @@ def test_repos_sync_unmanaged_repos(configtype):
|
||||
# this removes the prefix (root) from the path (unmanaged_repo)
|
||||
unmanaged_repo_name = os.path.relpath(unmanaged_repo, root)
|
||||
regex = f".*unmanaged.*{unmanaged_repo_name}"
|
||||
assert any([re.match(regex, l) for l in cmd.stderr.lower().split("\n")])
|
||||
assert any(
|
||||
[re.match(regex, line) for line in cmd.stderr.lower().split("\n")]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
|
||||
@@ -374,7 +383,7 @@ def test_repos_sync_repo_in_subdirectory(configtype):
|
||||
assert urls[0] == f"file://{remote}"
|
||||
|
||||
cmd = grm(["repos", "sync", "config", "--config", config.name])
|
||||
assert not "found unmanaged repository" in cmd.stderr.lower()
|
||||
assert "found unmanaged repository" not in cmd.stderr.lower()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
|
||||
@@ -419,7 +428,7 @@ def test_repos_sync_nested_clone(configtype):
|
||||
cmd = grm(["repos", "sync", "config", "--config", config.name])
|
||||
print(cmd.stdout)
|
||||
print(cmd.stderr)
|
||||
assert not "found unmanaged repository" in cmd.stderr.lower()
|
||||
assert "found unmanaged repository" not in cmd.stderr.lower()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
|
||||
@@ -720,14 +729,14 @@ def test_repos_sync_invalid_syntax(configtype):
|
||||
with open(config.name, "w") as f:
|
||||
if configtype == "toml":
|
||||
f.write(
|
||||
f"""
|
||||
"""
|
||||
[[trees]]
|
||||
root = invalid as there are no quotes ;)
|
||||
"""
|
||||
)
|
||||
elif configtype == "yaml":
|
||||
f.write(
|
||||
f"""
|
||||
"""
|
||||
trees:
|
||||
wrong:
|
||||
indentation:
|
||||
@@ -779,8 +788,6 @@ def test_repos_sync_normal_change_to_worktree(configtype):
|
||||
cmd = grm(["repos", "sync", "config", "--config", config.name])
|
||||
assert cmd.returncode == 0
|
||||
|
||||
git_dir = os.path.join(target, "test")
|
||||
|
||||
with open(config.name, "w") as f:
|
||||
f.write(
|
||||
templates["worktree_repo_with_remote"][configtype].format(
|
||||
@@ -810,8 +817,6 @@ def test_repos_sync_worktree_change_to_normal(configtype):
|
||||
cmd = grm(["repos", "sync", "config", "--config", config.name])
|
||||
assert cmd.returncode == 0
|
||||
|
||||
git_dir = os.path.join(target, "test")
|
||||
|
||||
with open(config.name, "w") as f:
|
||||
f.write(
|
||||
templates["repo_with_remote"][configtype].format(
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import pytest
|
||||
import os
|
||||
|
||||
from helpers import *
|
||||
import pytest
|
||||
from helpers import (
|
||||
NonGitDir,
|
||||
TempGitRepository,
|
||||
TempGitRepositoryWorktree,
|
||||
checksum_directory,
|
||||
funcname,
|
||||
grm,
|
||||
shell,
|
||||
)
|
||||
|
||||
|
||||
def test_worktree_clean():
|
||||
@@ -153,13 +162,13 @@ def test_worktree_clean_configured_default_branch(
|
||||
with open(os.path.join(base_dir, "grm.toml"), "w") as f:
|
||||
if branch_list_empty:
|
||||
f.write(
|
||||
f"""
|
||||
"""
|
||||
persistent_branches = []
|
||||
"""
|
||||
)
|
||||
else:
|
||||
f.write(
|
||||
f"""
|
||||
"""
|
||||
persistent_branches = [
|
||||
"mybranch"
|
||||
]
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
import os.path
|
||||
|
||||
from helpers import *
|
||||
import git
|
||||
from helpers import TempGitRepositoryWorktree, checksum_directory, funcname, grm, shell
|
||||
|
||||
|
||||
def test_worktree_never_clean_persistent_branches():
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
from helpers import *
|
||||
from helpers import (
|
||||
EmptyDir,
|
||||
NonGitDir,
|
||||
TempGitRepository,
|
||||
TempGitRepositoryWorktree,
|
||||
checksum_directory,
|
||||
funcname,
|
||||
grm,
|
||||
)
|
||||
|
||||
|
||||
def test_convert():
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from helpers import *
|
||||
|
||||
import re
|
||||
|
||||
import pytest
|
||||
import git
|
||||
import pytest
|
||||
from helpers import (
|
||||
EmptyDir,
|
||||
TempGitFileRemote,
|
||||
TempGitRepositoryWorktree,
|
||||
funcname,
|
||||
grm,
|
||||
shell,
|
||||
)
|
||||
|
||||
|
||||
def test_worktree_fetch():
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from helpers import *
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
import pytest
|
||||
import git
|
||||
import pytest
|
||||
from helpers import TempGitRepositoryWorktree, funcname, grm, shell
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pull", [True, False])
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from helpers import *
|
||||
|
||||
import pytest
|
||||
from helpers import (
|
||||
NonGitDir,
|
||||
TempGitRepository,
|
||||
TempGitRepositoryWorktree,
|
||||
funcname,
|
||||
grm,
|
||||
shell,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("has_config", [True, False])
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from helpers import *
|
||||
import datetime
|
||||
import os.path
|
||||
|
||||
import git
|
||||
import pytest
|
||||
import datetime
|
||||
|
||||
import os.path
|
||||
from helpers import (
|
||||
TempGitRepositoryWorktree,
|
||||
checksum_directory,
|
||||
funcname,
|
||||
grm,
|
||||
shell,
|
||||
tempfile,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -151,7 +157,8 @@ def test_worktree_add(
|
||||
]
|
||||
)
|
||||
|
||||
cachefn = lambda nr: "_".join(
|
||||
def cachefn(nr):
|
||||
return "_".join(
|
||||
[
|
||||
str(nr),
|
||||
str(default_remote),
|
||||
@@ -163,6 +170,7 @@ def test_worktree_add(
|
||||
str(worktree_name),
|
||||
]
|
||||
)
|
||||
|
||||
remote1_cache_key = cachefn(1)
|
||||
remote2_cache_key = cachefn(2)
|
||||
|
||||
@@ -281,7 +289,7 @@ def test_worktree_add(
|
||||
and remotes_differ
|
||||
):
|
||||
assert (
|
||||
f"branch exists on multiple remotes, but they deviate"
|
||||
"branch exists on multiple remotes, but they deviate"
|
||||
in cmd.stderr.lower()
|
||||
)
|
||||
assert len(cmd.stderr.strip().split("\n")) == base + 1
|
||||
|
||||
3
rust-toolchain.toml
Normal file
3
rust-toolchain.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[toolchain]
|
||||
channel = "stable"
|
||||
targets = ["x86_64-unknown-linux-musl"]
|
||||
@@ -33,6 +33,7 @@ pub struct ConfigTrees {
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ConfigProviderFilter {
|
||||
pub access: Option<bool>,
|
||||
pub owner: Option<bool>,
|
||||
@@ -41,6 +42,7 @@ pub struct ConfigProviderFilter {
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ConfigProvider {
|
||||
pub provider: RemoteProvider,
|
||||
pub token_command: String,
|
||||
@@ -181,6 +183,12 @@ impl Config {
|
||||
filters.access.unwrap_or(false),
|
||||
);
|
||||
|
||||
if filter.empty() {
|
||||
print_warning(
|
||||
"The configuration does not contain any filters, so no repos will match",
|
||||
);
|
||||
}
|
||||
|
||||
let repos = match config.provider {
|
||||
RemoteProvider::Github => {
|
||||
match provider::Github::new(filter, token, config.api_url) {
|
||||
@@ -240,7 +248,7 @@ impl Config {
|
||||
|
||||
pub fn normalize(&mut self) {
|
||||
if let Config::ConfigTrees(config) = self {
|
||||
let home = path::env_home().display().to_string();
|
||||
let home = path::env_home();
|
||||
for tree in &mut config.trees_mut().iter_mut() {
|
||||
if tree.root.starts_with(&home) {
|
||||
// The tilde is not handled differently, it's just a normal path component for `Path`.
|
||||
@@ -298,7 +306,7 @@ pub fn read_config<'a, T>(path: &str) -> Result<T, String>
|
||||
where
|
||||
T: for<'de> serde::Deserialize<'de>,
|
||||
{
|
||||
let content = match std::fs::read_to_string(&path) {
|
||||
let content = match std::fs::read_to_string(path) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return Err(format!(
|
||||
|
||||
@@ -63,6 +63,14 @@ pub struct FindLocalArgs {
|
||||
#[clap(help = "The path to search through")]
|
||||
pub path: String,
|
||||
|
||||
#[clap(
|
||||
short,
|
||||
long,
|
||||
help = "Exclude repositories that match the given regex",
|
||||
name = "REGEX"
|
||||
)]
|
||||
pub exclude: Option<String>,
|
||||
|
||||
#[clap(
|
||||
value_enum,
|
||||
short,
|
||||
|
||||
@@ -38,7 +38,7 @@ fn main() {
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
print_error(&format!("Error syncing trees: {}", error));
|
||||
print_error(&format!("Sync error: {}", error));
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,10 @@ fn main() {
|
||||
let filter =
|
||||
provider::Filter::new(args.users, args.groups, args.owner, args.access);
|
||||
|
||||
if filter.empty() {
|
||||
print_warning("You did not specify any filters, so no repos will match");
|
||||
}
|
||||
|
||||
let worktree = args.worktree == "true";
|
||||
|
||||
let repos = match args.provider {
|
||||
@@ -62,7 +66,7 @@ fn main() {
|
||||
match provider::Github::new(filter, token, args.api_url) {
|
||||
Ok(provider) => provider,
|
||||
Err(error) => {
|
||||
print_error(&format!("Error: {}", error));
|
||||
print_error(&format!("Sync error: {}", error));
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
@@ -76,7 +80,7 @@ fn main() {
|
||||
match provider::Gitlab::new(filter, token, args.api_url) {
|
||||
Ok(provider) => provider,
|
||||
Err(error) => {
|
||||
print_error(&format!("Error: {}", error));
|
||||
print_error(&format!("Sync error: {}", error));
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
@@ -112,13 +116,13 @@ fn main() {
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
print_error(&format!("Error syncing trees: {}", error));
|
||||
print_error(&format!("Sync error: {}", error));
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
print_error(&format!("Error: {}", error));
|
||||
print_error(&format!("Sync error: {}", error));
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
@@ -195,7 +199,8 @@ fn main() {
|
||||
}
|
||||
};
|
||||
|
||||
let (found_repos, warnings) = match find_in_tree(&path) {
|
||||
let (found_repos, warnings) = match find_in_tree(&path, args.exclude.as_deref())
|
||||
{
|
||||
Ok((repos, warnings)) => (repos, warnings),
|
||||
Err(error) => {
|
||||
print_error(&error);
|
||||
@@ -278,6 +283,10 @@ fn main() {
|
||||
filters.access.unwrap_or(false),
|
||||
);
|
||||
|
||||
if filter.empty() {
|
||||
print_warning("You did not specify any filters, so no repos will match");
|
||||
}
|
||||
|
||||
let repos = match config.provider {
|
||||
provider::RemoteProvider::Github => {
|
||||
match match provider::Github::new(filter, token, config.api_url) {
|
||||
@@ -383,6 +392,10 @@ fn main() {
|
||||
let filter =
|
||||
provider::Filter::new(args.users, args.groups, args.owner, args.access);
|
||||
|
||||
if filter.empty() {
|
||||
print_warning("You did not specify any filters, so no repos will match");
|
||||
}
|
||||
|
||||
let worktree = args.worktree == "true";
|
||||
|
||||
let repos = match args.provider {
|
||||
|
||||
33
src/lib.rs
33
src/lib.rs
@@ -1,5 +1,3 @@
|
||||
#![feature(io_error_more)]
|
||||
#![feature(const_option_ext)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use std::path::Path;
|
||||
@@ -19,12 +17,22 @@ pub mod worktree;
|
||||
/// The bool in the return value specifies whether there is a repository
|
||||
/// in root itself.
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn find_repos(root: &Path) -> Result<Option<(Vec<repo::Repo>, Vec<String>, bool)>, String> {
|
||||
fn find_repos(
|
||||
root: &Path,
|
||||
exclusion_pattern: Option<&str>,
|
||||
) -> Result<Option<(Vec<repo::Repo>, Vec<String>, bool)>, String> {
|
||||
let mut repos: Vec<repo::Repo> = Vec::new();
|
||||
let mut repo_in_root = false;
|
||||
let mut warnings = Vec::new();
|
||||
|
||||
let exlusion_regex: regex::Regex = regex::Regex::new(exclusion_pattern.unwrap_or(r"^$"))
|
||||
.map_err(|e| format!("invalid regex: {e}"))?;
|
||||
for path in tree::find_repo_paths(root)? {
|
||||
if exclusion_pattern.is_some() && exlusion_regex.is_match(&path::path_as_string(&path)) {
|
||||
warnings.push(format!("[skipped] {}", &path::path_as_string(&path)));
|
||||
continue;
|
||||
}
|
||||
|
||||
let is_worktree = repo::RepoHandle::detect_worktree(&path);
|
||||
if path == root {
|
||||
repo_in_root = true;
|
||||
@@ -63,12 +71,13 @@ fn find_repos(root: &Path) -> Result<Option<(Vec<repo::Repo>, Vec<String>, bool)
|
||||
let name = remote.name();
|
||||
let url = remote.url();
|
||||
let remote_type = match repo::detect_remote_type(&url) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
warnings.push(format!(
|
||||
"{}: Could not detect remote type of \"{}\"",
|
||||
"{}: Could not handle URL {}. Reason: {}",
|
||||
&path::path_as_string(&path),
|
||||
&url
|
||||
&url,
|
||||
e
|
||||
));
|
||||
continue;
|
||||
}
|
||||
@@ -106,7 +115,7 @@ fn find_repos(root: &Path) -> Result<Option<(Vec<repo::Repo>, Vec<String>, bool)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
let name = path.strip_prefix(&root).unwrap();
|
||||
let name = path.strip_prefix(root).unwrap();
|
||||
let namespace = name.parent().unwrap();
|
||||
(
|
||||
if namespace != Path::new("") {
|
||||
@@ -130,10 +139,14 @@ fn find_repos(root: &Path) -> Result<Option<(Vec<repo::Repo>, Vec<String>, bool)
|
||||
Ok(Some((repos, warnings, repo_in_root)))
|
||||
}
|
||||
|
||||
pub fn find_in_tree(path: &Path) -> Result<(tree::Tree, Vec<String>), String> {
|
||||
pub fn find_in_tree(
|
||||
path: &Path,
|
||||
exclusion_pattern: Option<&str>,
|
||||
) -> Result<(tree::Tree, Vec<String>), String> {
|
||||
let mut warnings = Vec::new();
|
||||
|
||||
let (repos, repo_in_root): (Vec<repo::Repo>, bool) = match find_repos(path)? {
|
||||
let (repos, repo_in_root): (Vec<repo::Repo>, bool) = match find_repos(path, exclusion_pattern)?
|
||||
{
|
||||
Some((vec, mut repo_warnings, repo_in_root)) => {
|
||||
warnings.append(&mut repo_warnings);
|
||||
(vec, repo_in_root)
|
||||
|
||||
12
src/path.rs
12
src/path.rs
@@ -47,9 +47,9 @@ pub fn path_as_string(path: &Path) -> String {
|
||||
path.to_path_buf().into_os_string().into_string().unwrap()
|
||||
}
|
||||
|
||||
pub fn env_home() -> PathBuf {
|
||||
pub fn env_home() -> String {
|
||||
match std::env::var("HOME") {
|
||||
Ok(path) => Path::new(&path).to_path_buf(),
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
print_error(&format!("Unable to read HOME: {}", e));
|
||||
process::exit(1);
|
||||
@@ -58,16 +58,12 @@ pub fn env_home() -> PathBuf {
|
||||
}
|
||||
|
||||
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,
|
||||
|| Some(env_home()),
|
||||
|name| -> Result<Option<String>, &'static str> {
|
||||
match name {
|
||||
"HOME" => Ok(Some(path_as_string(home_dir().unwrap().as_path()))),
|
||||
"HOME" => Ok(Some(env_home())),
|
||||
_ => Ok(None),
|
||||
}
|
||||
},
|
||||
|
||||
@@ -9,8 +9,10 @@ use super::Project;
|
||||
use super::Provider;
|
||||
|
||||
const ACCEPT_HEADER_JSON: &str = "application/vnd.github.v3+json";
|
||||
const GITHUB_API_BASEURL: &str =
|
||||
option_env!("GITHUB_API_BASEURL").unwrap_or("https://api.github.com");
|
||||
const GITHUB_API_BASEURL: &str = match option_env!("GITHUB_API_BASEURL") {
|
||||
Some(url) => url,
|
||||
None => "https://api.github.com",
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct GithubProject {
|
||||
|
||||
@@ -9,7 +9,10 @@ use super::Project;
|
||||
use super::Provider;
|
||||
|
||||
const ACCEPT_HEADER_JSON: &str = "application/json";
|
||||
const GITLAB_API_BASEURL: &str = option_env!("GITLAB_API_BASEURL").unwrap_or("https://gitlab.com");
|
||||
const GITLAB_API_BASEURL: &str = match option_env!("GITLAB_API_BASEURL") {
|
||||
Some(url) => url,
|
||||
None => "https://gitlab.com",
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
|
||||
@@ -89,6 +89,10 @@ impl Filter {
|
||||
access,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty(&self) -> bool {
|
||||
self.users.is_empty() && self.groups.is_empty() && !self.owner && !self.access
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ApiErrorResponse<T>
|
||||
@@ -270,11 +274,15 @@ pub trait Provider {
|
||||
}
|
||||
|
||||
for group in &self.filter().groups {
|
||||
let group_projects = self
|
||||
.get_group_projects(group)
|
||||
.map_err(|error| match error {
|
||||
let group_projects = self.get_group_projects(group).map_err(|error| {
|
||||
format!(
|
||||
"group \"{}\": {}",
|
||||
group,
|
||||
match error {
|
||||
ApiErrorResponse::Json(x) => x.to_string(),
|
||||
ApiErrorResponse::String(s) => s,
|
||||
}
|
||||
)
|
||||
})?;
|
||||
for group_project in group_projects {
|
||||
let mut already_present = false;
|
||||
|
||||
92
src/repo.rs
92
src/repo.rs
@@ -406,50 +406,78 @@ mod tests {
|
||||
fn check_ssh_remote() {
|
||||
assert_eq!(
|
||||
detect_remote_type("ssh://git@example.com"),
|
||||
Some(RemoteType::Ssh)
|
||||
Ok(RemoteType::Ssh)
|
||||
);
|
||||
assert_eq!(detect_remote_type("git@example.git"), Some(RemoteType::Ssh));
|
||||
assert_eq!(detect_remote_type("git@example.git"), Ok(RemoteType::Ssh));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_https_remote() {
|
||||
assert_eq!(
|
||||
detect_remote_type("https://example.com"),
|
||||
Some(RemoteType::Https)
|
||||
Ok(RemoteType::Https)
|
||||
);
|
||||
assert_eq!(
|
||||
detect_remote_type("https://example.com/test.git"),
|
||||
Some(RemoteType::Https)
|
||||
Ok(RemoteType::Https)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_file_remote() {
|
||||
assert_eq!(
|
||||
detect_remote_type("file:///somedir"),
|
||||
Some(RemoteType::File)
|
||||
);
|
||||
assert_eq!(detect_remote_type("file:///somedir"), Ok(RemoteType::File));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_invalid_remotes() {
|
||||
assert_eq!(detect_remote_type("https//example.com"), None);
|
||||
assert_eq!(detect_remote_type("https:example.com"), None);
|
||||
assert_eq!(detect_remote_type("ssh//example.com"), None);
|
||||
assert_eq!(detect_remote_type("ssh:example.com"), None);
|
||||
assert_eq!(detect_remote_type("git@example.com"), None);
|
||||
assert_eq!(
|
||||
detect_remote_type("https//example.com"),
|
||||
Err(String::from(
|
||||
"The remote URL starts with an unimplemented protocol"
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
detect_remote_type("https:example.com"),
|
||||
Err(String::from(
|
||||
"The remote URL starts with an unimplemented protocol",
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
detect_remote_type("ssh//example.com"),
|
||||
Err(String::from(
|
||||
"The remote URL starts with an unimplemented protocol",
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
detect_remote_type("ssh:example.com"),
|
||||
Err(String::from(
|
||||
"The remote URL starts with an unimplemented protocol",
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
detect_remote_type("git@example.com"),
|
||||
Err(String::from(
|
||||
"The remote URL starts with an unimplemented protocol",
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn check_unsupported_protocol_http() {
|
||||
detect_remote_type("http://example.com");
|
||||
assert_eq!(
|
||||
detect_remote_type("http://example.com"),
|
||||
Err(String::from(
|
||||
"Remotes using HTTP protocol are not supported",
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn check_unsupported_protocol_git() {
|
||||
detect_remote_type("git://example.com");
|
||||
assert_eq!(
|
||||
detect_remote_type("git://example.com"),
|
||||
Err(String::from("Remotes using git protocol are not supported"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -473,27 +501,31 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn detect_remote_type(remote_url: &str) -> Option<RemoteType> {
|
||||
pub fn detect_remote_type(remote_url: &str) -> Result<RemoteType, String> {
|
||||
let git_regex = regex::Regex::new(r"^[a-zA-Z]+@.*$").unwrap();
|
||||
if remote_url.starts_with("ssh://") {
|
||||
return Some(RemoteType::Ssh);
|
||||
return Ok(RemoteType::Ssh);
|
||||
}
|
||||
if git_regex.is_match(remote_url) && remote_url.ends_with(".git") {
|
||||
return Some(RemoteType::Ssh);
|
||||
return Ok(RemoteType::Ssh);
|
||||
}
|
||||
if remote_url.starts_with("https://") {
|
||||
return Some(RemoteType::Https);
|
||||
return Ok(RemoteType::Https);
|
||||
}
|
||||
if remote_url.starts_with("file://") {
|
||||
return Some(RemoteType::File);
|
||||
return Ok(RemoteType::File);
|
||||
}
|
||||
if remote_url.starts_with("http://") {
|
||||
unimplemented!("Remotes using HTTP protocol are not supported");
|
||||
return Err(String::from(
|
||||
"Remotes using HTTP protocol are not supported",
|
||||
));
|
||||
}
|
||||
if remote_url.starts_with("git://") {
|
||||
unimplemented!("Remotes using git protocol are not supported");
|
||||
return Err(String::from("Remotes using git protocol are not supported"));
|
||||
}
|
||||
None
|
||||
Err(String::from(
|
||||
"The remote URL starts with an unimplemented protocol",
|
||||
))
|
||||
}
|
||||
|
||||
pub struct RepoHandle(git2::Repository);
|
||||
@@ -785,7 +817,7 @@ impl RepoHandle {
|
||||
))
|
||||
})?;
|
||||
|
||||
for entry in match std::fs::read_dir(&root_dir) {
|
||||
for entry in match std::fs::read_dir(root_dir) {
|
||||
Ok(iterator) => iterator,
|
||||
Err(error) => {
|
||||
return Err(WorktreeConversionFailureReason::Error(format!(
|
||||
@@ -1343,7 +1375,7 @@ impl RepoHandle {
|
||||
},
|
||||
})
|
||||
{
|
||||
let repo_dir = &directory.join(&worktree.name());
|
||||
let repo_dir = &directory.join(worktree.name());
|
||||
if repo_dir.exists() {
|
||||
match self.remove_worktree(
|
||||
directory,
|
||||
@@ -1387,12 +1419,12 @@ impl RepoHandle {
|
||||
.map_err(|error| format!("Getting worktrees failed: {}", error))?;
|
||||
|
||||
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 = path::path_as_string(
|
||||
entry
|
||||
.map_err(|error| error.to_string())?
|
||||
.path()
|
||||
.strip_prefix(&directory)
|
||||
.strip_prefix(directory)
|
||||
// that unwrap() is safe as each entry is
|
||||
// guaranteed to be a subentry of &directory
|
||||
.unwrap(),
|
||||
@@ -1588,7 +1620,7 @@ impl RemoteHandle<'_> {
|
||||
|
||||
pub fn is_pushable(&self) -> Result<bool, String> {
|
||||
let remote_type = detect_remote_type(self.0.url().expect("Remote name is not valid utf-8"))
|
||||
.ok_or_else(|| String::from("Could not detect remote type"))?;
|
||||
.expect("Could not detect remote type");
|
||||
Ok(matches!(remote_type, RemoteType::Ssh | RemoteType::File))
|
||||
}
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ pub fn get_worktree_status_table(
|
||||
|
||||
add_worktree_table_header(&mut table);
|
||||
for worktree in &worktrees {
|
||||
let worktree_dir = &directory.join(&worktree.name());
|
||||
let worktree_dir = &directory.join(worktree.name());
|
||||
if worktree_dir.exists() {
|
||||
let repo = match repo::RepoHandle::open(worktree_dir, false) {
|
||||
Ok(repo) => repo,
|
||||
|
||||
@@ -128,8 +128,6 @@ pub fn find_repo_paths(path: &Path) -> Result<Vec<PathBuf>, String> {
|
||||
"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()),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user