Merge pull request #51 from BapRx/feat/exclude-paths-based-on-regex

chore(repo/find): Exlude paths based on regex
This commit is contained in:
2023-02-07 22:50:02 +01:00
committed by GitHub
26 changed files with 203 additions and 127 deletions

View File

@@ -13,6 +13,7 @@ clean:
fmt: fmt:
cargo fmt cargo fmt
git ls-files | grep '\.py$' | xargs isort
git ls-files | grep '\.py$' | xargs black git ls-files | grep '\.py$' | xargs black
git ls-files | grep '\.sh$' | xargs -L 1 shfmt --indent 4 --write git ls-files | grep '\.sh$' | xargs -L 1 shfmt --indent 4 --write
@@ -23,6 +24,7 @@ fmt-check:
lint: 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 git ls-files | grep '\.sh$' | xargs -L 1 shellcheck --norc
lint-fix: lint-fix:

View File

@@ -1,9 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import subprocess
import os
import json import json
import sys import os
import subprocess
import semver import semver
import tomlkit import tomlkit

View File

@@ -34,8 +34,8 @@ You will need the following tools:
[here](https://github.com/casey/just#installation) for installation [here](https://github.com/casey/just#installation) for installation
instructions (it's most likely just a simple `cargo install just`). instructions (it's most likely just a simple `cargo install just`).
* Docker & docker-compose for the e2e tests * Docker & docker-compose for the e2e tests
* `black` and `shfmt` for formatting. * `isort`, `black` and `shfmt` for formatting.
* `shellcheck` for shell script linting * `ruff` and `shellcheck` for linting.
* `mdbook` for the documentation * `mdbook` for the documentation
Here are the tools: Here are the tools:

View File

@@ -11,7 +11,7 @@ Let's try it out:
## Get the example configuration ## Get the example configuration
```bash ```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 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: If you run it again, it will report no changes:
``` ```bash
$ grm repos sync config -c example.config.toml $ grm repos sync config -c example.config.toml
[] git-repo-manager: OK [] git-repo-manager: OK
[] dotfiles: 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: configuration from an existing file tree:
```bash ```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`. 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 ### Show the state of your projects
```bash ```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 You can also use `status` without `--config` to check the repository you're
currently in: currently in:
``` ```bash
$ cd ~/example-projects/dotfiles $ cd ~/example-projects/dotfiles
$ grm repos status $ 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 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 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. 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 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 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 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 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 binary and docker build time. I'd say that keeping it under 10 minutes is a good
idea. idea.

View File

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

View File

@@ -3,5 +3,5 @@ from flask import Flask
app = Flask(__name__) app = Flask(__name__)
app.url_map.strict_slashes = False app.url_map.strict_slashes = False
import github import github # noqa: E402,F401
import gitlab import gitlab # noqa: E402,F401

View File

@@ -1,10 +1,8 @@
import os.path import os.path
from app import app
from flask import Flask, request, abort, jsonify, make_response
import jinja2 import jinja2
from app import app
from flask import abort, jsonify, make_response, request
def check_headers(): def check_headers():
@@ -48,7 +46,7 @@ def add_pagination(response, page, last_page):
def read_project_files(namespaces=[]): def read_project_files(namespaces=[]):
last_page = 4 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" response_file = f"./github_api_page_{page}.json.j2"
if not os.path.exists(response_file): if not os.path.exists(response_file):
return jsonify([]) return jsonify([])

View File

@@ -1,10 +1,8 @@
import os.path import os.path
from app import app
from flask import Flask, request, abort, jsonify, make_response
import jinja2 import jinja2
from app import app
from flask import abort, jsonify, make_response, request
def check_headers(): def check_headers():
@@ -48,7 +46,7 @@ def add_pagination(response, page, last_page):
def read_project_files(namespaces=[]): def read_project_files(namespaces=[]):
last_page = 4 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" response_file = f"./gitlab_api_page_{page}.json"
if not os.path.exists(response_file): if not os.path.exists(response_file):
return jsonify([]) return jsonify([])

View File

@@ -1,12 +1,12 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import hashlib
import inspect
import os import os
import os.path import os.path
import shutil
import subprocess import subprocess
import tempfile import tempfile
import hashlib
import shutil
import inspect
import git import git
@@ -176,7 +176,6 @@ class TempGitRemote:
newobj.remoteid = remoteid newobj.remoteid = remoteid
return newobj, remoteid return newobj, remoteid
else: else:
refresh = False
if cachekey not in cls.obj: if cachekey not in cls.obj:
tmpdir = get_temporary_directory() tmpdir = get_temporary_directory()
shell( shell(

View File

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

View File

@@ -1,12 +1,13 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os
import re
import tempfile import tempfile
import toml
import pytest import pytest
import toml
import yaml import yaml
from helpers import NonExistentPath, TempGitRepository, grm, shell
from helpers import *
def test_repos_find_nonexistent(): def test_repos_find_nonexistent():
@@ -63,9 +64,10 @@ def test_repos_find_non_git_repos():
assert len(cmd.stderr) != 0 assert len(cmd.stderr) != 0
@pytest.mark.parametrize("default", [True, False]) @pytest.mark.parametrize("default_format", [True, False])
@pytest.mark.parametrize("configtype", ["toml", "yaml"]) @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: with tempfile.TemporaryDirectory() as tmpdir:
shell( shell(
f""" f"""
@@ -99,13 +101,19 @@ def test_repos_find(configtype, default):
) )
args = ["repos", "find", "local", tmpdir] args = ["repos", "find", "local", tmpdir]
if not default: if not default_format:
args += ["--format", configtype] args += ["--format", configtype]
if exclude:
args += ["--exclude", exclude]
cmd = grm(args) cmd = grm(args)
assert cmd.returncode == 0 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) output = toml.loads(cmd.stdout)
elif configtype == "yaml": elif configtype == "yaml":
output = yaml.safe_load(cmd.stdout) output = yaml.safe_load(cmd.stdout)
@@ -120,7 +128,7 @@ def test_repos_find(configtype, default):
assert set(tree.keys()) == {"root", "repos"} assert set(tree.keys()) == {"root", "repos"}
assert tree["root"] == tmpdir assert tree["root"] == tmpdir
assert isinstance(tree["repos"], list) 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] repo1 = [r for r in tree["repos"] if r["name"] == "repo1"][0]
assert repo1["worktree_setup"] is False assert repo1["worktree_setup"] is False
@@ -137,30 +145,33 @@ def test_repos_find(configtype, default):
assert someremote["type"] == "ssh" assert someremote["type"] == "ssh"
assert someremote["url"] == "ssh://example.com/repo2.git" assert someremote["url"] == "ssh://example.com/repo2.git"
repo2 = [r for r in tree["repos"] if r["name"] == "repo2"][0] if exclude == "^.*/repo2$":
assert repo2["worktree_setup"] is False assert [r for r in tree["repos"] if r["name"] == "repo2"] == []
assert isinstance(repo1["remotes"], list) else:
assert len(repo2["remotes"]) == 1 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] origin = [r for r in repo2["remotes"] if r["name"] == "origin"][0]
assert set(origin.keys()) == {"name", "type", "url"} assert set(origin.keys()) == {"name", "type", "url"}
assert origin["type"] == "https" assert origin["type"] == "https"
assert origin["url"] == "https://example.com/repo2.git" 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"]) @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: with TempGitRepository() as repo_dir:
args = ["repos", "find", "local", repo_dir] args = ["repos", "find", "local", repo_dir]
if not default: if not default_format:
args += ["--format", configtype] args += ["--format", configtype]
cmd = grm(args) cmd = grm(args)
assert cmd.returncode == 0 assert cmd.returncode == 0
assert len(cmd.stderr) == 0 assert len(cmd.stderr) == 0
if default or configtype == "toml": if default_format or configtype == "toml":
output = toml.loads(cmd.stdout) output = toml.loads(cmd.stdout)
elif configtype == "yaml": elif configtype == "yaml":
output = yaml.safe_load(cmd.stdout) output = yaml.safe_load(cmd.stdout)
@@ -194,8 +205,8 @@ def test_repos_find_in_root(configtype, default):
@pytest.mark.parametrize("configtype", ["toml", "yaml"]) @pytest.mark.parametrize("configtype", ["toml", "yaml"])
@pytest.mark.parametrize("default", [True, False]) @pytest.mark.parametrize("default_format", [True, False])
def test_repos_find_with_invalid_repo(configtype, default): def test_repos_find_with_invalid_repo(configtype, default_format):
with tempfile.TemporaryDirectory() as tmpdir: with tempfile.TemporaryDirectory() as tmpdir:
shell( shell(
f""" f"""
@@ -229,13 +240,13 @@ def test_repos_find_with_invalid_repo(configtype, default):
) )
args = ["repos", "find", "local", tmpdir] args = ["repos", "find", "local", tmpdir]
if not default: if not default_format:
args += ["--format", configtype] args += ["--format", configtype]
cmd = grm(args) cmd = grm(args)
assert cmd.returncode == 0 assert cmd.returncode == 0
assert "broken" in cmd.stderr assert "broken" in cmd.stderr
if default or configtype == "toml": if default_format or configtype == "toml":
output = toml.loads(cmd.stdout) output = toml.loads(cmd.stdout)
elif configtype == "yaml": elif configtype == "yaml":
output = yaml.safe_load(cmd.stdout) output = yaml.safe_load(cmd.stdout)

View File

@@ -1,14 +1,13 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import re
import os import os
import re
import tempfile
import toml
import pytest import pytest
import toml
import yaml import yaml
from helpers import grm
from helpers import *
ALTERNATE_DOMAIN = os.environ["ALTERNATE_DOMAIN"] ALTERNATE_DOMAIN = os.environ["ALTERNATE_DOMAIN"]
PROVIDERS = ["github", "gitlab"] PROVIDERS = ["github", "gitlab"]
@@ -275,9 +274,9 @@ def test_repos_find_remote_user(
if not worktree_default: if not worktree_default:
cfg += f"worktree = {str(worktree).lower()}\n" cfg += f"worktree = {str(worktree).lower()}\n"
if force_ssh: if force_ssh:
cfg += f"force_ssh = true\n" cfg += "force_ssh = true\n"
if override_remote_name: if override_remote_name:
cfg += f'remote_name = "otherremote"\n' cfg += 'remote_name = "otherremote"\n'
if use_owner: if use_owner:
cfg += """ cfg += """
[filters] [filters]
@@ -475,7 +474,7 @@ def test_repos_find_remote_group(
if not worktree_default: if not worktree_default:
cfg += f"worktree = {str(worktree).lower()}\n" cfg += f"worktree = {str(worktree).lower()}\n"
if force_ssh: if force_ssh:
cfg += f"force_ssh = true\n" cfg += "force_ssh = true\n"
if use_alternate_endpoint: if use_alternate_endpoint:
cfg += f'api_url = "http://{ALTERNATE_DOMAIN}:5000/{provider}"\n' cfg += f'api_url = "http://{ALTERNATE_DOMAIN}:5000/{provider}"\n'
cfg += """ cfg += """
@@ -591,7 +590,7 @@ def test_repos_find_remote_user_and_group(
if not worktree_default: if not worktree_default:
cfg += f"worktree = {str(worktree).lower()}\n" cfg += f"worktree = {str(worktree).lower()}\n"
if force_ssh: if force_ssh:
cfg += f"force_ssh = true\n" cfg += "force_ssh = true\n"
if use_alternate_endpoint: if use_alternate_endpoint:
cfg += f'api_url = "http://{ALTERNATE_DOMAIN}:5000/{provider}"\n' cfg += f'api_url = "http://{ALTERNATE_DOMAIN}:5000/{provider}"\n'
cfg += """ cfg += """
@@ -742,7 +741,7 @@ def test_repos_find_remote_owner(
if not worktree_default: if not worktree_default:
cfg += f"worktree = {str(worktree).lower()}\n" cfg += f"worktree = {str(worktree).lower()}\n"
if force_ssh: if force_ssh:
cfg += f"force_ssh = true\n" cfg += "force_ssh = true\n"
if use_alternate_endpoint: if use_alternate_endpoint:
cfg += f'api_url = "http://{ALTERNATE_DOMAIN}:5000/{provider}"\n' cfg += f'api_url = "http://{ALTERNATE_DOMAIN}:5000/{provider}"\n'
cfg += """ cfg += """
@@ -873,13 +872,11 @@ def test_repos_find_remote_owner(
assert repo["remotes"][0]["name"] == "origin" assert repo["remotes"][0]["name"] == "origin"
if force_ssh: if force_ssh:
assert ( assert (
repo["remotes"][0]["url"] == f"ssh://git@example.com/myuser2/myproject3.git" repo["remotes"][0]["url"] == "ssh://git@example.com/myuser2/myproject3.git"
) )
assert repo["remotes"][0]["type"] == "ssh" assert repo["remotes"][0]["type"] == "ssh"
else: else:
assert ( assert repo["remotes"][0]["url"] == "https://example.com/myuser2/myproject3.git"
repo["remotes"][0]["url"] == f"https://example.com/myuser2/myproject3.git"
)
assert repo["remotes"][0]["type"] == "https" assert repo["remotes"][0]["type"] == "https"
group_namespace_1 = [t for t in output["trees"] if t["root"] == "/myroot/mygroup1"][ group_namespace_1 = [t for t in output["trees"] if t["root"] == "/myroot/mygroup1"][
@@ -923,13 +920,13 @@ def test_repos_find_remote_owner(
if force_ssh: if force_ssh:
assert ( assert (
repo["remotes"][0]["url"] 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" assert repo["remotes"][0]["type"] == "ssh"
else: else:
assert ( assert (
repo["remotes"][0]["url"] repo["remotes"][0]["url"]
== f"https://example.com/mygroup1/myproject4.git" == "https://example.com/mygroup1/myproject4.git"
) )
assert repo["remotes"][0]["type"] == "https" assert repo["remotes"][0]["type"] == "https"
@@ -948,12 +945,11 @@ def test_repos_find_remote_owner(
assert repo["remotes"][0]["name"] == "origin" assert repo["remotes"][0]["name"] == "origin"
if force_ssh: if force_ssh:
assert ( assert (
repo["remotes"][0]["url"] repo["remotes"][0]["url"] == "ssh://git@example.com/mygroup2/myproject5.git"
== f"ssh://git@example.com/mygroup2/myproject5.git"
) )
assert repo["remotes"][0]["type"] == "ssh" assert repo["remotes"][0]["type"] == "ssh"
else: else:
assert ( 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" assert repo["remotes"][0]["type"] == "https"

View File

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

View File

@@ -1,14 +1,21 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import tempfile import os
import re import re
import subprocess
import tempfile
import textwrap import textwrap
import pytest
import toml
import git import git
import pytest
from helpers import * from helpers import (
NonExistentPath,
TempGitFileRemote,
TempGitRepository,
checksum_directory,
grm,
shell,
)
templates = { templates = {
"repo_simple": { "repo_simple": {
@@ -291,7 +298,9 @@ def test_repos_sync_unmanaged_repos(configtype):
# this removes the prefix (root) from the path (unmanaged_repo) # this removes the prefix (root) from the path (unmanaged_repo)
unmanaged_repo_name = os.path.relpath(unmanaged_repo, root) unmanaged_repo_name = os.path.relpath(unmanaged_repo, root)
regex = f".*unmanaged.*{unmanaged_repo_name}" 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"]) @pytest.mark.parametrize("configtype", ["toml", "yaml"])
@@ -374,7 +383,7 @@ def test_repos_sync_repo_in_subdirectory(configtype):
assert urls[0] == f"file://{remote}" assert urls[0] == f"file://{remote}"
cmd = grm(["repos", "sync", "config", "--config", config.name]) 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"]) @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]) cmd = grm(["repos", "sync", "config", "--config", config.name])
print(cmd.stdout) print(cmd.stdout)
print(cmd.stderr) 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"]) @pytest.mark.parametrize("configtype", ["toml", "yaml"])
@@ -720,14 +729,14 @@ def test_repos_sync_invalid_syntax(configtype):
with open(config.name, "w") as f: with open(config.name, "w") as f:
if configtype == "toml": if configtype == "toml":
f.write( f.write(
f""" """
[[trees]] [[trees]]
root = invalid as there are no quotes ;) root = invalid as there are no quotes ;)
""" """
) )
elif configtype == "yaml": elif configtype == "yaml":
f.write( f.write(
f""" """
trees: trees:
wrong: wrong:
indentation: indentation:
@@ -779,8 +788,6 @@ def test_repos_sync_normal_change_to_worktree(configtype):
cmd = grm(["repos", "sync", "config", "--config", config.name]) cmd = grm(["repos", "sync", "config", "--config", config.name])
assert cmd.returncode == 0 assert cmd.returncode == 0
git_dir = os.path.join(target, "test")
with open(config.name, "w") as f: with open(config.name, "w") as f:
f.write( f.write(
templates["worktree_repo_with_remote"][configtype].format( 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]) cmd = grm(["repos", "sync", "config", "--config", config.name])
assert cmd.returncode == 0 assert cmd.returncode == 0
git_dir = os.path.join(target, "test")
with open(config.name, "w") as f: with open(config.name, "w") as f:
f.write( f.write(
templates["repo_with_remote"][configtype].format( templates["repo_with_remote"][configtype].format(

View File

@@ -1,8 +1,17 @@
#!/usr/bin/env python3 #!/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(): 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: with open(os.path.join(base_dir, "grm.toml"), "w") as f:
if branch_list_empty: if branch_list_empty:
f.write( f.write(
f""" """
persistent_branches = [] persistent_branches = []
""" """
) )
else: else:
f.write( f.write(
f""" """
persistent_branches = [ persistent_branches = [
"mybranch" "mybranch"
] ]

View File

@@ -2,7 +2,8 @@
import os.path import os.path
from helpers import * import git
from helpers import TempGitRepositoryWorktree, checksum_directory, funcname, grm, shell
def test_worktree_never_clean_persistent_branches(): def test_worktree_never_clean_persistent_branches():

View File

@@ -1,8 +1,16 @@
#!/usr/bin/env python3 #!/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(): def test_convert():

View File

@@ -1,11 +1,17 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from helpers import *
import re import re
import pytest
import git import git
import pytest
from helpers import (
EmptyDir,
TempGitFileRemote,
TempGitRepositoryWorktree,
funcname,
grm,
shell,
)
def test_worktree_fetch(): def test_worktree_fetch():

View File

@@ -1,11 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from helpers import * import os
import re import re
import pytest
import git import git
import pytest
from helpers import TempGitRepositoryWorktree, funcname, grm, shell
@pytest.mark.parametrize("pull", [True, False]) @pytest.mark.parametrize("pull", [True, False])

View File

@@ -1,10 +1,17 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os
import re import re
from helpers import *
import pytest import pytest
from helpers import (
NonGitDir,
TempGitRepository,
TempGitRepositoryWorktree,
funcname,
grm,
shell,
)
@pytest.mark.parametrize("has_config", [True, False]) @pytest.mark.parametrize("has_config", [True, False])

View File

@@ -1,12 +1,18 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from helpers import * import datetime
import os.path
import git import git
import pytest import pytest
import datetime from helpers import (
TempGitRepositoryWorktree,
import os.path checksum_directory,
funcname,
grm,
shell,
tempfile,
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -151,18 +157,20 @@ def test_worktree_add(
] ]
) )
cachefn = lambda nr: "_".join( def cachefn(nr):
[ return "_".join(
str(nr), [
str(default_remote), str(nr),
str(local_branch_exists), str(default_remote),
str(remote_branch_already_exists), str(local_branch_exists),
str(remote_branch_with_prefix_already_exists), str(remote_branch_already_exists),
str(remote_count), str(remote_branch_with_prefix_already_exists),
str(remotes_differ), str(remote_count),
str(worktree_name), str(remotes_differ),
] str(worktree_name),
) ]
)
remote1_cache_key = cachefn(1) remote1_cache_key = cachefn(1)
remote2_cache_key = cachefn(2) remote2_cache_key = cachefn(2)
@@ -281,7 +289,7 @@ def test_worktree_add(
and remotes_differ and remotes_differ
): ):
assert ( assert (
f"branch exists on multiple remotes, but they deviate" "branch exists on multiple remotes, but they deviate"
in cmd.stderr.lower() in cmd.stderr.lower()
) )
assert len(cmd.stderr.strip().split("\n")) == base + 1 assert len(cmd.stderr.strip().split("\n")) == base + 1

3
rust-toolchain.toml Normal file
View File

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

View File

@@ -63,6 +63,14 @@ pub struct FindLocalArgs {
#[clap(help = "The path to search through")] #[clap(help = "The path to search through")]
pub path: String, pub path: String,
#[clap(
short,
long,
help = "Exclude repositories that match the given regex",
name = "REGEX"
)]
pub exclude: Option<String>,
#[clap( #[clap(
value_enum, value_enum,
short, short,

View File

@@ -199,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), Ok((repos, warnings)) => (repos, warnings),
Err(error) => { Err(error) => {
print_error(&error); print_error(&error);

View File

@@ -19,12 +19,22 @@ pub mod worktree;
/// The bool in the return value specifies whether there is a repository /// The bool in the return value specifies whether there is a repository
/// in root itself. /// in root itself.
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
fn find_repos(root: &Path) -> Result<Option<(Vec<repo::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 repos: Vec<repo::Repo> = Vec::new();
let mut repo_in_root = false; let mut repo_in_root = false;
let mut warnings = Vec::new(); let mut warnings = Vec::new();
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)? { 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); let is_worktree = repo::RepoHandle::detect_worktree(&path);
if path == root { if path == root {
repo_in_root = true; repo_in_root = true;
@@ -130,10 +140,14 @@ fn find_repos(root: &Path) -> Result<Option<(Vec<repo::Repo>, Vec<String>, bool)
Ok(Some((repos, warnings, repo_in_root))) 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 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)) => { Some((vec, mut repo_warnings, repo_in_root)) => {
warnings.append(&mut repo_warnings); warnings.append(&mut repo_warnings);
(vec, repo_in_root) (vec, repo_in_root)