Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ $ pipx install --suffix=@next 'vcspull' --pip-args '\--pre' --force
- Refreshed logo
- `vcspull sync`:

- Terms with no match in config will show a notice (#394)

> No repo found in config(s) for "non_existent_repo"

- Syncing will now skip to the next repos if an error is encountered

- Learned `--exit-on-error` / `-x`
Expand Down
28 changes: 27 additions & 1 deletion docs/cli/sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,35 @@

## Error handling

### Repos not found in config

As of 1.13.x, if you enter a repo term (or terms) that aren't found throughout
your configurations, it will show a warning:

```console
$ vcspull sync non_existent_repo
No repo found in config(s) for "non_existent_repo"
```

```console
$ vcspull sync non_existent_repo existing_repo
No repo found in config(s) for "non_existent_repo"
```

```console
$ vcspull sync non_existent_repo existing_repo another_repo_not_in_config
No repo found in config(s) for "non_existent_repo"
No repo found in config(s) for "another_repo_not_in_config"
```

Since syncing terms are treated as a filter rather than a lookup, the message is
considered a warning, so will not exit even if `--exit-on-error` flag is used.

### Syncing

As of 1.13.x, vcspull will continue to the next repo if an error is encountered when syncing multiple repos.

To imitate the old behavior, use `--exit-on-error` / `-x`:
To imitate the old behavior, the `--exit-on-error` / `-x` flag:

```console
$ vcspull sync --exit-on-error grako django
Expand Down
4 changes: 4 additions & 0 deletions src/vcspull/cli/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def clamp(n, _min, _max):


EXIT_ON_ERROR_MSG = "Exiting via error (--exit-on-error passed)"
NO_REPOS_FOR_TERM_MSG = 'No repo found in config(s) for "{name}"'


@click.command(name="sync")
Expand Down Expand Up @@ -100,6 +101,9 @@ def sync(repo_terms, config, exit_on_error: bool) -> None:
name = repo_term

# collect the repos from the config files
found = filter_repos(configs, dir=dir, vcs_url=vcs_url, name=name)
if len(found) == 0:
click.echo(NO_REPOS_FOR_TERM_MSG.format(name=name))
found_repos.extend(
filter_repos(configs, dir=dir, vcs_url=vcs_url, name=name)
)
Expand Down
109 changes: 91 additions & 18 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,94 @@

from libvcs.sync.git import GitSync
from vcspull.cli import cli
from vcspull.cli.sync import EXIT_ON_ERROR_MSG
from vcspull.cli.sync import EXIT_ON_ERROR_MSG, NO_REPOS_FOR_TERM_MSG

if t.TYPE_CHECKING:
from typing_extensions import TypeAlias

ExpectedOutput: TypeAlias = t.Optional[t.Union[str, t.List[str]]]


class SyncCLINonExistentRepo(t.NamedTuple):
test_id: str
sync_args: list[str]
expected_exit_code: int
expected_in_output: "ExpectedOutput" = None
expected_not_in_output: "ExpectedOutput" = None


SYNC_CLI_EXISTENT_REPO_FIXTURES = [
SyncCLINonExistentRepo(
test_id="exists",
sync_args=["my_git_project"],
expected_exit_code=0,
expected_in_output="Already on 'master'",
expected_not_in_output=NO_REPOS_FOR_TERM_MSG.format(name="my_git_repo"),
),
SyncCLINonExistentRepo(
test_id="non-existent-only",
sync_args=["this_isnt_in_the_config"],
expected_exit_code=0,
expected_in_output=NO_REPOS_FOR_TERM_MSG.format(name="this_isnt_in_the_config"),
),
SyncCLINonExistentRepo(
test_id="non-existent-mixed",
sync_args=["this_isnt_in_the_config", "my_git_project", "another"],
expected_exit_code=0,
expected_in_output=[
NO_REPOS_FOR_TERM_MSG.format(name="this_isnt_in_the_config"),
NO_REPOS_FOR_TERM_MSG.format(name="another"),
],
expected_not_in_output=NO_REPOS_FOR_TERM_MSG.format(name="my_git_repo"),
),
]


@pytest.mark.parametrize(
list(SyncCLINonExistentRepo._fields),
SYNC_CLI_EXISTENT_REPO_FIXTURES,
ids=[test.test_id for test in SYNC_CLI_EXISTENT_REPO_FIXTURES],
)
def test_sync_cli_repo_term_non_existent(
user_path: pathlib.Path,
config_path: pathlib.Path,
tmp_path: pathlib.Path,
git_repo: GitSync,
test_id: str,
sync_args: list[str],
expected_exit_code: int,
expected_in_output: "ExpectedOutput",
expected_not_in_output: "ExpectedOutput",
) -> None:
config = {
"~/github_projects/": {
"my_git_project": {
"url": f"git+file://{git_repo.dir}",
"remotes": {"test_remote": f"git+file://{git_repo.dir}"},
},
}
}
yaml_config = config_path / ".vcspull.yaml"
yaml_config_data = yaml.dump(config, default_flow_style=False)
yaml_config.write_text(yaml_config_data, encoding="utf-8")

def test_sync_cli_non_existent(tmp_path: pathlib.Path) -> None:
runner = CliRunner()
with runner.isolated_filesystem(temp_dir=tmp_path):
result = runner.invoke(cli, ["sync", "hi"])
assert result.exit_code == 0
assert "" in result.output
result = runner.invoke(cli, ["sync", *sync_args])
assert result.exit_code == expected_exit_code
output = "".join(list(result.output))

if expected_in_output is not None:
if isinstance(expected_in_output, str):
expected_in_output = [expected_in_output]
for needle in expected_in_output:
assert needle in output

if expected_not_in_output is not None:
if isinstance(expected_not_in_output, str):
expected_not_in_output = [expected_not_in_output]
for needle in expected_not_in_output:
assert needle not in output


def test_sync(
Expand Down Expand Up @@ -51,12 +130,6 @@ def test_sync(
assert "my_git_repo" in output


if t.TYPE_CHECKING:
from typing_extensions import TypeAlias

ExpectedOutput: TypeAlias = t.Optional[t.Union[str, t.List[str]]]


class SyncBrokenFixture(t.NamedTuple):
test_id: str
sync_args: list[str]
Expand Down Expand Up @@ -86,25 +159,25 @@ class SyncBrokenFixture(t.NamedTuple):
),
SyncBrokenFixture(
test_id="normal-first-broken",
sync_args=["non_existent_repo", "my_git_repo"],
sync_args=["my_git_repo_not_found", "my_git_repo"],
expected_exit_code=0,
expected_not_in_output=EXIT_ON_ERROR_MSG,
),
SyncBrokenFixture(
test_id="normal-last-broken",
sync_args=["my_git_repo", "non_existent_repo"],
sync_args=["my_git_repo", "my_git_repo_not_found"],
expected_exit_code=0,
expected_not_in_output=EXIT_ON_ERROR_MSG,
),
SyncBrokenFixture(
test_id="exit-on-error--exit-on-error-first-broken",
sync_args=["non_existent_repo", "my_git_repo", "--exit-on-error"],
sync_args=["my_git_repo_not_found", "my_git_repo", "--exit-on-error"],
expected_exit_code=1,
expected_in_output=EXIT_ON_ERROR_MSG,
),
SyncBrokenFixture(
test_id="exit-on-error--x-first-broken",
sync_args=["non_existent_repo", "my_git_repo", "-x"],
sync_args=["my_git_repo_not_found", "my_git_repo", "-x"],
expected_exit_code=1,
expected_in_output=EXIT_ON_ERROR_MSG,
expected_not_in_output="master",
Expand All @@ -114,13 +187,13 @@ class SyncBrokenFixture(t.NamedTuple):
#
SyncBrokenFixture(
test_id="exit-on-error--exit-on-error-last-broken",
sync_args=["my_git_repo", "non_existent_repo", "-x"],
sync_args=["my_git_repo", "my_git_repo_not_found", "-x"],
expected_exit_code=1,
expected_in_output=[EXIT_ON_ERROR_MSG, "Already on 'master'"],
),
SyncBrokenFixture(
test_id="exit-on-error--x-last-item",
sync_args=["my_git_repo", "non_existent_repo", "--exit-on-error"],
sync_args=["my_git_repo", "my_git_repo_not_found", "--exit-on-error"],
expected_exit_code=1,
expected_in_output=[EXIT_ON_ERROR_MSG, "Already on 'master'"],
),
Expand Down Expand Up @@ -157,7 +230,7 @@ def test_sync_broken(
"url": f"git+file://{git_repo.dir}",
"remotes": {"test_remote": f"git+file://{git_repo.dir}"},
},
"non_existent_repo": {
"my_git_repo_not_found": {
"url": "git+file:///dev/null",
},
}
Expand Down