89 Commits

Author SHA1 Message Date
241bf473a7 Merge branch 'develop' 2023-08-09 00:32:33 +02:00
8fd663462e Release v0.7.14 2023-08-09 00:32:33 +02:00
4beacbf65d Reformat with new black version 2023-08-09 00:30:57 +02:00
102f5561a8 Use new compose call 2023-08-09 00:30:57 +02:00
e04f065d42 Drop nightly requirement 2023-08-09 00:30:57 +02:00
941dd50868 Cargo.lock: Updating pin-project v1.0.12 -> v1.1.3 2023-08-09 00:30:57 +02:00
d20dabc91e Cargo.lock: Updating pin-project-lite v0.2.9 -> v0.2.11 2023-08-09 00:30:57 +02:00
0e63a1c6bf Cargo.lock: Updating curl-sys v0.4.61+curl-8.0.1 -> v0.4.65+curl-8.2.1 2023-08-09 00:30:57 +02:00
9792c09850 Cargo.lock: Updating async-channel v1.8.0 -> v1.9.0 2023-08-09 00:30:57 +02:00
a1519a6bc5 dependencies: Update serde_json to 1.0.104 2023-08-09 00:30:57 +02:00
36535dcaec dependencies: Update serde_yaml to 0.9.25 2023-08-09 00:30:57 +02:00
32f94b1ef5 dependencies: Update comfy-table to 7.0.1 2023-08-09 00:30:57 +02:00
913df16f28 dependencies: Update regex to 1.9.3 2023-08-09 00:30:57 +02:00
f66a512a83 dependencies: Update console to 0.15.7 2023-08-09 00:30:57 +02:00
de15e799ac dependencies: Update clap to 4.3.21 2023-08-09 00:30:57 +02:00
a8736ed37f dependencies: Update git2 to 0.17.2 2023-08-09 00:30:57 +02:00
1a45887fb6 dependencies: Update serde to 1.0.183 2023-08-09 00:30:57 +02:00
9403156edf dependencies: Update toml to 0.7.6 2023-08-09 00:30:57 +02:00
21e3a9b9bb Merge branch 'develop' 2023-05-06 19:15:46 +02:00
ca0c9c28fd Release v0.7.13 2023-05-06 19:15:46 +02:00
1edc61d6e6 Fix tests related to clap changes 2023-05-06 19:13:53 +02:00
b20bba529a Cargo.lock: Updating http v0.2.8 -> v0.2.9 2023-05-06 17:16:58 +02:00
fb0948787a Cargo.lock: Updating futures-lite v1.12.0 -> v1.13.0 2023-05-06 17:16:58 +02:00
625457e474 Cargo.lock: Updating futures-io v0.3.25 -> v0.3.28 2023-05-06 17:16:58 +02:00
d4b7cabcf2 Cargo.lock: Updating fastrand v1.8.0 -> v1.9.0 2023-05-06 17:16:58 +02:00
b2727c7a96 Cargo.lock: Updating encoding_rs v0.8.31 -> v0.8.32 2023-05-06 17:16:58 +02:00
ff3cbfbdba Cargo.lock: Updating curl-sys v0.4.59+curl-7.86.0 -> v0.4.61+curl-8.0.1 2023-05-06 17:16:58 +02:00
44602e7bc2 Cargo.lock: Updating bytes v1.3.0 -> v1.4.0 2023-05-06 17:16:58 +02:00
1706df7236 Cargo.lock: Updating concurrent-queue v2.0.0 -> v2.2.0 2023-05-06 17:16:58 +02:00
80fc28c44a dependencies: Update serde_json to 1.0.96 2023-05-06 17:16:58 +02:00
7335c0fc62 dependencies: Update serde_yaml to 0.9.21 2023-05-06 17:16:58 +02:00
a536e688c9 dependencies: Update comfy-table to 6.1.4 2023-05-06 17:16:58 +02:00
0d22b43ed0 dependencies: Update regex to 1.8.1 2023-05-06 17:16:58 +02:00
9d7f566209 dependencies: Update console to 0.15.5 2023-05-06 17:16:58 +02:00
1e6f965f7a dependencies: Update clap to 4.2.7 2023-05-06 17:16:58 +02:00
6183a58204 dependencies: Update shellexpand to 3.1.0 2023-05-06 17:16:58 +02:00
2a4934b01a dependencies: Update git2 to 0.17.1 2023-05-06 17:16:58 +02:00
fc4261b7ac dependencies: Update serde to 1.0.162 2023-05-06 17:16:58 +02:00
7d248c5ea3 dependencies: Update toml to 0.7.3 2023-05-06 17:16:58 +02:00
8d4af73364 Merge pull request #54 from BapRx/feat/add-verbosity-repo-detection
feat: Return an error if the remote type cannot be detected
2023-05-04 14:36:12 +02:00
Hannes Körber
4c738d027a Always use cargo +nightly in Justfile 2023-05-04 13:45:57 +02:00
Hannes Körber
f2fa3411d8 Fix const Option::unwrap_or()
Fixes #57
2023-05-04 11:27:56 +02:00
Baptiste Roux
19443bc4ca chore: Update warning message 2023-02-10 18:02:13 +01:00
60a777276f Merge pull request #51 from BapRx/feat/exclude-paths-based-on-regex
chore(repo/find): Exlude paths based on regex
2023-02-07 22:50:02 +01:00
Baptiste Roux
1262ec5a33 chore: code format 2023-02-07 16:56:17 +01:00
Baptiste Roux
4c6b69e125 chore: Add linting exclusion 2023-02-07 16:55:23 +01:00
Baptiste Roux
28881a23a9 chore: Remove condition between default and exlude arguments 2023-02-07 16:50:45 +01:00
Baptiste Roux
e796362e6b chore: Avoid passing unnecessary reference 2023-02-07 16:49:21 +01:00
Baptiste Roux
37094a3295 feat: Return an error if the remote type cannot be detected 2023-02-02 23:11:04 +01:00
Baptiste Roux
100bac8f87 chore: Return error if the regex is invalid 2023-02-01 18:24:39 +01:00
Baptiste Roux
fdafa3aa81 chore: Pass regex pattern as slice instead of string 2023-02-01 18:14:56 +01:00
Baptiste Roux
d267564bca docs: Document the --exclude flag 2023-02-01 17:56:43 +01:00
Baptiste Roux
2cc477e551 test: Add e2e test for the path regex exclusion 2023-02-01 17:56:22 +01:00
Baptiste Roux
8cbdd9f408 chore: Add fmt in justfile; Update doc 2023-02-01 03:50:54 +01:00
Baptiste Roux
21be3e40dd fix: Rollback change that broke test 2023-02-01 03:48:59 +01:00
Baptiste Roux
a3824c2671 chore: Specify channel and target used in the project 2023-02-01 03:35:26 +01:00
Baptiste Roux
8eeb010c3a chore(e2e_tests): make the linter happier 2023-01-26 16:51:46 +01:00
Baptiste Roux
956b172426 chore(repo/find): Exlude paths based on regex 2023-01-14 14:40:11 +01:00
9b4ed2837e Merge branch 'develop' 2022-12-12 17:41:41 +01:00
701e64df6f Release v0.7.12 2022-12-12 17:41:41 +01:00
23fc942db7 Warn on empty filters
Closes #29
2022-12-12 15:43:27 +01:00
38bba1472e Improve error messages during sync errors
Closes #46
2022-12-12 15:21:42 +01:00
7d131bbacf 'Enable deny_unknown_fields for all config structs 2022-12-12 15:10:00 +01:00
6e79dd318a Make clippy happy 2022-12-12 14:46:08 +01:00
5fc1d2148f Cargo.lock: Updating polling v2.3.0 -> v2.5.1 2022-12-12 14:41:07 +01:00
de184de5a0 Cargo.lock: Updating futures-io v0.3.24 -> v0.3.25 2022-12-12 14:41:07 +01:00
8a3b2ae1c5 Cargo.lock: Updating curl-sys v0.4.56+curl-7.83.1 -> v0.4.59+curl-7.86.0 2022-12-12 14:41:07 +01:00
93e38b0572 Cargo.lock: Updating cc v1.0.73 -> v1.0.77 2022-12-12 14:41:07 +01:00
43bbb8e143 Cargo.lock: Updating crossbeam-utils v0.8.12 -> v0.8.14 2022-12-12 14:41:07 +01:00
0fd9ce68b8 Cargo.lock: Updating async-channel v1.7.1 -> v1.8.0 2022-12-12 14:41:07 +01:00
68f2b81e3f dependencies: Update parse_link_header to 0.3.3 2022-12-12 14:41:07 +01:00
d7a39fa4e4 dependencies: Update serde_json to 1.0.89 2022-12-12 14:41:07 +01:00
1f646fd5f8 dependencies: Update serde_yaml to 0.9.14 2022-12-12 14:41:07 +01:00
96cbf8c568 dependencies: Update comfy-table to 6.1.3 2022-12-12 14:41:07 +01:00
bf199c1b17 dependencies: Update regex to 1.7.0 2022-12-12 14:41:07 +01:00
0f7a70c895 dependencies: Update clap to 4.0.29 2022-12-12 14:41:07 +01:00
3151b97bc0 dependencies: Update shellexpand to 3.0.0 2022-12-12 14:41:07 +01:00
8ce5cfecd4 dependencies: Update serde to 1.0.150 2022-12-12 13:55:05 +01:00
6da27c6444 Merge branch 'develop' 2022-12-12 13:53:02 +01:00
3026b3e6de Release v0.7.11 2022-12-12 13:53:02 +01:00
725414cc71 release-script: Fix missing newline 2022-12-12 13:43:34 +01:00
defb8fafca Merge branch 'develop' 2022-10-10 18:50:40 +02:00
f747c085c9 Release v0.7.10 2022-10-10 18:50:40 +02:00
85dd794b53 Cargo.lock: Updating tracing v0.1.36 -> v0.1.37 2022-10-10 18:06:27 +02:00
be8d85cb66 dependencies: Update serde_json to 1.0.86 2022-10-10 18:06:25 +02:00
0b7527fc7d dependencies: Update clap to 4.0.11 2022-10-10 18:06:25 +02:00
3a568a774a Remove init_worktree from sync config
It was currently unused and only confuses. The initialization of
worktrees can currently only be controlled via the `--init-worktree`
command line switch. This is unfortunate, but it's the only was to
handle it right now. Changing it would mean a restructure of the code,
mainly the `tree::sync_trees` function.
2022-10-06 12:59:56 +02:00
a6ecb66547 Merge branch 'develop' 2022-10-06 12:38:32 +02:00
8a04db8130 Release v0.7.9 2022-10-06 12:38:32 +02:00
39 changed files with 906 additions and 581 deletions

View File

@@ -1,2 +1,3 @@
nonnominandus
Maximilian Volk
Baptiste (@BapRx)

837
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "git-repo-manager"
version = "0.7.8"
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.10"
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.85"
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"

View File

@@ -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

View File

@@ -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,

View File

@@ -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.

View File

@@ -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
```

View File

@@ -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
╭──────────┬──────────┬────────┬──────────┬───────┬─────────╮
@@ -79,5 +86,5 @@ $ grm repos status
By default, the repo configuration uses TOML. If you prefer YAML, just give it a
YAML file instead (file ending does not matter, `grm` will figure out the
format). For generating a configuration, pass `--format yaml` to `grm repo
format). For generating a configuration, pass `--format yaml` to `grm repo
find` which generates a YAML configuration instead of a TOML configuration.

View File

@@ -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.

View File

@@ -1,7 +1,5 @@
import os
from helpers import *
def pytest_configure(config):
os.environ["GIT_AUTHOR_NAME"] = "Example user"

View File

@@ -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

View File

@@ -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([])

View File

@@ -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([])

View File

@@ -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(

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
from helpers import *
from helpers import grm
def test_invalid_command():

View File

@@ -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
assert len(cmd.stderr) == 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,30 +145,32 @@ def test_repos_find(configtype, default):
assert someremote["type"] == "ssh"
assert someremote["url"] == "ssh://example.com/repo2.git"
repo2 = [r for r in tree["repos"] if r["name"] == "repo2"][0]
assert repo2["worktree_setup"] is False
assert isinstance(repo1["remotes"], list)
assert len(repo2["remotes"]) == 1
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)
assert len(repo2["remotes"]) == 1
origin = [r for r in repo2["remotes"] if r["name"] == "origin"][0]
assert set(origin.keys()) == {"name", "type", "url"}
assert origin["type"] == "https"
assert origin["url"] == "https://example.com/repo2.git"
origin = [r for r in repo2["remotes"] if r["name"] == "origin"][0]
assert set(origin.keys()) == {"name", "type", "url"}
assert origin["type"] == "https"
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)

View File

@@ -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"

View File

@@ -1,8 +1,6 @@
#!/usr/bin/env python3
import tempfile
from helpers import *
from helpers import RepoTree, grm
def test_repos_sync_worktree_clone():

View File

@@ -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"])
@@ -303,7 +312,7 @@ def test_repos_sync_root_is_file(configtype):
cmd = grm(["repos", "sync", "config", "--config", config.name])
assert cmd.returncode != 0
assert "not a directory" in cmd.stderr.lower()
assert "notadirectory" in cmd.stderr.lower()
@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(

View File

@@ -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"
]

View File

@@ -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():

View File

@@ -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():

View File

@@ -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():

View File

@@ -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])

View File

@@ -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])

View File

@@ -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,18 +157,20 @@ def test_worktree_add(
]
)
cachefn = lambda nr: "_".join(
[
str(nr),
str(default_remote),
str(local_branch_exists),
str(remote_branch_already_exists),
str(remote_branch_with_prefix_already_exists),
str(remote_count),
str(remotes_differ),
str(worktree_name),
]
)
def cachefn(nr):
return "_".join(
[
str(nr),
str(default_remote),
str(local_branch_exists),
str(remote_branch_already_exists),
str(remote_branch_with_prefix_already_exists),
str(remote_count),
str(remotes_differ),
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

View File

@@ -67,7 +67,7 @@ fi
for remote in $(git remote); do
if git ls-remote --tags "${remote}" | grep -q "refs/tags/v${new_version}$"; then
printf 'Tag %s already exists on %s' "v${new_version}" "${remote}" >&2
printf 'Tag %s already exists on %s\n' "v${new_version}" "${remote}" >&2
exit 1
fi
done

3
rust-toolchain.toml Normal file
View File

@@ -0,0 +1,3 @@
[toolchain]
channel = "stable"
targets = ["x86_64-unknown-linux-musl"]

View File

@@ -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,
@@ -52,7 +54,6 @@ pub struct ConfigProvider {
pub api_url: Option<String>,
pub worktree: Option<bool>,
pub init_worktree: Option<bool>,
pub remote_name: Option<String>,
}
@@ -182,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) {
@@ -241,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`.
@@ -299,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!(

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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),
}
},

View File

@@ -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 {

View File

@@ -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")]

View File

@@ -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,12 +274,16 @@ pub trait Provider {
}
for group in &self.filter().groups {
let group_projects = self
.get_group_projects(group)
.map_err(|error| match error {
ApiErrorResponse::Json(x) => x.to_string(),
ApiErrorResponse::String(s) => s,
})?;
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;
for repo in &repos {

View File

@@ -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))
}

View 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,

View File

@@ -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()),
}