Skip to content

Commit

Permalink
Merge pull request #1072 from projectsyn/feat/template-version-update…
Browse files Browse the repository at this point in the history
…-sync

Add support for custom template versions for dependency update and sync
  • Loading branch information
simu authored Dec 31, 2024
2 parents 0d03e44 + 26563d4 commit cf35c02
Show file tree
Hide file tree
Showing 10 changed files with 248 additions and 64 deletions.
14 changes: 8 additions & 6 deletions commodore/cli/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,12 +291,7 @@ def component_group(config: Config, verbose):
show_default=True,
help="The URL of the component cookiecutter template.",
)
@click.option(
"--template-version",
default="main",
show_default=True,
help="The component template version (Git tree-ish) to use.",
)
@options.template_version("main")
@new_update_options(new_cmd=True)
@options.verbosity
@options.pass_config
Expand Down Expand Up @@ -430,13 +425,15 @@ def component_new(
default=True,
help="Whether to commit the rendered template changes.",
)
@options.template_version(None)
@options.verbosity
@options.pass_config
def component_update(
config: Config,
verbose: int,
component_path: str,
copyright_holder: str,
template_version: Optional[str],
golden_tests: Optional[bool],
matrix_tests: Optional[bool],
lib: Optional[bool],
Expand Down Expand Up @@ -492,6 +489,8 @@ def component_update(
t.automerge_patch_v0 = automerge_patch_v0
if autorelease is not None:
t.autorelease = autorelease
if template_version is not None:
t.template_version = template_version

test_cases = t.test_cases
test_cases.extend(additional_test_case)
Expand Down Expand Up @@ -625,6 +624,7 @@ def component_compile(
@options.pr_batch_size
@options.github_pause
@options.dependency_filter
@options.template_version(None)
def component_sync(
config: Config,
verbose: int,
Expand All @@ -636,6 +636,7 @@ def component_sync(
pr_batch_size: int,
github_pause: int,
filter: str,
template_version: Optional[str],
):
"""This command processes all components listed in the provided `COMPONENT_LIST`
YAML file.
Expand Down Expand Up @@ -672,4 +673,5 @@ def component_sync(
pr_batch_size,
timedelta(seconds=github_pause),
filter,
template_version,
)
18 changes: 18 additions & 0 deletions commodore/cli/options.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Click options which are reused for multiple commands"""

from typing import Optional

import click

from commodore.config import Config
Expand Down Expand Up @@ -125,6 +127,22 @@
)


def template_version(default: Optional[str]):
help_str = "The component template version (Git tree-ish) to use."
if default is None:
help_str = (
help_str
+ " If not provided, the currently active template version will be used."
)

return click.option(
"--template-version",
default=default,
show_default=default is not None,
help=help_str,
)


def local(help: str):
return click.option(
"--local",
Expand Down
15 changes: 9 additions & 6 deletions commodore/cli/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,7 @@ def package_group(config: Config, verbose: int):
show_default=True,
help="The URL of the package cookiecutter template.",
)
@click.option(
"--template-version",
default="main",
show_default=True,
help="The package template version (Git tree-ish) to use.",
)
@options.template_version("main")
@click.option(
"--output-dir",
default="",
Expand Down Expand Up @@ -167,6 +162,7 @@ def package_new(
default=True,
help="Whether to commit the rendered template changes.",
)
@options.template_version(None)
@options.verbosity
@options.pass_config
# pylint: disable=too-many-arguments
Expand All @@ -180,6 +176,7 @@ def package_update(
additional_test_case: Iterable[str],
remove_test_case: Iterable[str],
commit: bool,
template_version: Optional[str],
):
"""This command updates the package at PACKAGE_PATH to the latest version of the
template which was originally used to create it, if the template version is given as
Expand All @@ -201,6 +198,9 @@ def package_update(
t.golden_tests = golden_tests
if update_copyright_year:
t.copyright_year = None
if template_version is not None:
t.template_version = template_version

test_cases = t.test_cases
test_cases.extend(additional_test_case)
t.test_cases = [tc for tc in test_cases if tc not in remove_test_case]
Expand Down Expand Up @@ -278,6 +278,7 @@ def package_compile(
@options.pr_batch_size
@options.github_pause
@options.dependency_filter
@options.template_version(None)
def package_sync(
config: Config,
verbose: int,
Expand All @@ -289,6 +290,7 @@ def package_sync(
pr_batch_size: int,
github_pause: int,
filter: str,
template_version: Optional[str],
):
"""This command processes all packages listed in the provided `PACKAGE_LIST` YAML file.
Expand Down Expand Up @@ -324,4 +326,5 @@ def package_sync(
pr_batch_size,
timedelta(seconds=github_pause),
filter,
template_version,
)
101 changes: 68 additions & 33 deletions commodore/dependency_syncer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from collections.abc import Iterable
from datetime import timedelta
from pathlib import Path
from typing import Union, Type
from typing import Optional, Union, Type

import click
import git
Expand Down Expand Up @@ -36,11 +36,17 @@ def sync_dependencies(
pr_batch_size: int = 10,
pause: timedelta = timedelta(seconds=120),
depfilter: str = "",
template_version: Optional[str] = None,
) -> None:
if not config.github_token:
raise click.ClickException("Can't continue, missing GitHub API token.")

deptype_str = deptype.__name__.lower()
if template_version is not None and not dry_run:
click.secho(
" > Custom template version provided for sync, but dry run not active. Forcing dry run",
fg="yellow",
)
dry_run = True

deps = read_dependency_list(dependency_list, depfilter)
dep_count = len(deps)
Expand All @@ -49,38 +55,17 @@ def sync_dependencies(
# Keep track of how many PRs we've created to better avoid running into rate limits
update_count = 0
for i, dn in enumerate(deps, start=1):
click.secho(f"Synchronizing {dn}", bold=True)
_, dreponame = dn.split("/")
dname = dreponame.replace(f"{deptype_str}-", "", 1)

# Clone dependency
try:
gr = gh.get_repo(dn)
except github.UnknownObjectException:
click.secho(f" > Repository {dn} doesn't exist, skipping...", fg="yellow")
continue

if gr.archived:
click.secho(f" > Repository {dn} is archived, skipping...", fg="yellow")
continue

d = deptype.clone(config, gr.clone_url, dname, version=gr.default_branch)

if not (d.target_dir / ".cruft.json").is_file():
click.echo(f" > Skipping repo {dn} which doesn't have `.cruft.json`")
continue

# Update the dependency
t = templater.from_existing(config, d.target_dir)
changed = t.update(
print_completion_message=False,
commit=not dry_run,
ignore_template_commit=True,
changed = sync_dependency(
config,
gh,
dn,
deptype,
templater,
template_version,
dry_run,
pr_branch,
pr_label,
)

# Create or update PR if there were updates
comment = render_pr_comment(d)
create_or_update_pr(d, dn, gr, changed, pr_branch, pr_label, dry_run, comment)
if changed:
update_count += 1
if not dry_run and i < dep_count:
Expand All @@ -90,6 +75,56 @@ def sync_dependencies(
_maybe_pause(update_count, pr_batch_size, pause)


def sync_dependency(
config: Config,
gh: github.Github,
depname: str,
deptype: Type[Union[Component, Package]],
templater,
template_version: Optional[str],
dry_run: bool,
pr_branch: str,
pr_label: Iterable[str],
):
deptype_str = deptype.__name__.lower()

click.secho(f"Synchronizing {depname}", bold=True)
_, dreponame = depname.split("/")
dname = dreponame.replace(f"{deptype_str}-", "", 1)

# Clone dependency
try:
gr = gh.get_repo(depname)
except github.UnknownObjectException:
click.secho(f" > Repository {depname} doesn't exist, skipping...", fg="yellow")
return

if gr.archived:
click.secho(f" > Repository {depname} is archived, skipping...", fg="yellow")
return

d = deptype.clone(config, gr.clone_url, dname, version=gr.default_branch)

if not (d.target_dir / ".cruft.json").is_file():
click.echo(f" > Skipping repo {depname} which doesn't have `.cruft.json`")
return

# Update the dependency
t = templater.from_existing(config, d.target_dir)
if template_version is not None:
t.template_version = template_version
changed = t.update(
print_completion_message=False,
commit=not dry_run,
ignore_template_commit=True,
)

# Create or update PR if there were updates
comment = render_pr_comment(d)
create_or_update_pr(d, depname, gr, changed, pr_branch, pr_label, dry_run, comment)
return changed


def read_dependency_list(dependency_list: Path, depfilter: str) -> list[str]:
try:
deps = yaml_load(dependency_list)
Expand Down
12 changes: 10 additions & 2 deletions commodore/dependency_templater.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,20 @@ def _ignore_cruft_json_commit_id(
n=0,
)
)[3:]
# If the context-less diff has exactly 2 lines and both of them start with
# '[-+] "commit":', we omit the diff
if (
# If the context-less diff has exactly 2 lines and both of them start with
# '[-+] "commit":', we omit the diff
len(minimal_diff) == 2
and minimal_diff[0].startswith('- "commit":')
and minimal_diff[1].startswith('+ "commit":')
) or (
# If the context-less diff has exactly 4 lines and the two pairs start with
# '[-+] "commit":' and '[-+] "checkout":', we omit the diff
len(minimal_diff) == 4
and minimal_diff[0].startswith('- "commit":')
and minimal_diff[1].startswith('- "checkout":')
and minimal_diff[2].startswith('+ "commit":')
and minimal_diff[3].startswith('+ "checkout":')
):
omit = True
# never suppress diffs in default difffunc
Expand Down
16 changes: 16 additions & 0 deletions docs/modules/ROOT/pages/reference/cli.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,10 @@ NOTE: If autorelease is enabled, new releases will be generated for automerged d
*--commit / --no-commit*::
Whether to commit the rendered template changes.

*--template-version* TEXT::
The component template version (Git tree-ish) to use.
If not provided, the currently active template version will be used.

*--automerge-patch / --no-automerge-patch*::
Enable automerging of patch-level dependency PRs.

Expand Down Expand Up @@ -440,6 +444,10 @@ However, labels added by previous runs can't be removed since we've got no easy
Regex to select which dependencies to sync.
If the option isn't given, all dependencies listed in the provided YAML are synced.

*--template-version* TEXT::
The component template version (Git tree-ish) to use.
If not provided, the currently active template version will be used.

== Inventory Components / Packages / Show

*-f, --values*::
Expand Down Expand Up @@ -549,6 +557,10 @@ However, labels added by previous runs can't be removed since we've got no easy
*--commit / --no-commit*::
Whether to commit the rendered template changes.

*--template-version* TEXT::
The component template version (Git tree-ish) to use.
If not provided, the currently active template version will be used.

== Package Compile

*-f, --values* FILE::
Expand Down Expand Up @@ -638,3 +650,7 @@ However, labels added by previous runs can't be removed since we've got no easy
*--filter* REGEX::
Regex to select which dependencies to sync.
If the option isn't given, all dependencies listed in the provided YAML are synced.

*--template-version* TEXT::
The component template version (Git tree-ish) to use.
If not provided, the currently active template version will be used.
21 changes: 21 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,24 @@ def bare_repo(self) -> GitRepo:
@pytest.fixture
def mockdep(tmp_path):
return MockMultiDependency(Repo.init(tmp_path / "repo.git"))


class MockTemplater:
def __init__(self):
self.template_version = None
self.test_cases = []

def update(self, *args, **kwargs):
pass


def make_mock_templater(mock_templater, expected_path):
mt = MockTemplater()

def mock_from_existing(_config: Config, path: Path):
assert path == expected_path
return mt

mock_templater.from_existing = mock_from_existing

return mt
Loading

0 comments on commit cf35c02

Please sign in to comment.