feat(init): add --checkout option to sam init command for specifying git branch, tag, or commit for cookiecutter projects#9045
Conversation
…git branch, tag, or commit for cookiecutter projects
roger-zhangg
left a comment
There was a problem hiding this comment.
Thanks for implementing this long-requested feature (#3555)! The overall approach is clean and follows existing patterns well. A few items to address:
Issues
1. Missing checkout parameter in generate_project function signature (Bug)
The diff adds checkout=None to generate_project() and the if checkout: params["checkout"] = checkout logic, but looking at the current develop branch, the function at samcli/lib/init/__init__.py uses positional arguments when called from init_generator.py (line 24-36 of init_generator.py). The do_generate function in init_generator.py also needs the checkout parameter added, which the diff does include - good.
However, generate_project is called positionally (not with keyword args) in the test assertions and in do_generate. Since checkout is added as the last positional parameter with a default, this is fine for existing callers. No issue here on second look.
2. No integration tests included
The PR checklist marks integration tests as complete, but no integration test files are modified. There are existing integration tests at tests/integration/init/test_init_command.py that test --location with git URLs. An integration test for --checkout with a known git repo + branch/tag would strengthen confidence. At minimum, one test like:
sam init --location https://github.com/aws/aws-sam-cli-app-templates.git --checkout develop --name test-app --no-input
3. --checkout without --location is silently accepted (Minor)
The --checkout option declares incompatible_params but no required_param_lists. This means a user can run sam init --checkout some-branch without --location, enter the interactive flow, pick a managed template, and the checkout value gets passed to cookiecutter which will silently ignore it (since managed templates resolve to local paths).
Consider either:
- Adding a validation that
--checkoutrequires--location(viarequired_param_lists=[["location"]]on the click option), OR - Documenting in the help text that
--checkoutonly takes effect with--location
4. Edge case: empty string handling
if checkout: in generate_project treats both None and "" the same (no checkout passed). This is fine since click won't produce an empty string for a --checkout option without a value (it would error). Just noting this is intentional and correct.
Positives
- Clean wiring through all layers (CLI -> do_cli -> do_interactive/do_generate -> generate_project -> cookiecutter)
- Correct use of
ClickMutexwith sensible incompatible params - Schema updated properly
- Good unit test coverage for the
generate_projectfunction directly - Follows existing patterns (same parameter threading style as
extra_context,tracing, etc.)
Overall this is close to merge-ready. The main asks are: (1) add at least one integration test, and (2) consider adding validation or documentation that --checkout requires --location to be meaningful.
- Error when --checkout is used without --location, using the existing ClickMutex required_param_lists mechanism - Add integration test verifying --checkout without --location fails - Add integration test for --checkout with --location (git URL) - Add integration test for --checkout with incompatible params
| params["extra_context"] = extra_context | ||
|
|
||
| if checkout: | ||
| params["checkout"] = checkout |
There was a problem hiding this comment.
[BUG] The --checkout flag is silently ignored when the target repository does not contain a cookiecutter.json. Cookiecutter clones the repo and applies the checkout, but then raises RepositoryNotFound, which is caught below and dispatched to generate_non_cookiecutter_project():
except RepositoryNotFound:
...
project_output_dir = str(Path(output_dir, name)) if name else output_dir
generate_non_cookiecutter_project(location=params["template"], output_dir=project_output_dir)generate_non_cookiecutter_project() in samcli/lib/init/arbitrary_project.py does not accept a checkout parameter and calls repository.clone(repo_url=location, no_input=no_input) without one. As a result, a user running sam init --location <git-repo-without-cookiecutter.json> --checkout my-branch will get code from the repository's default branch instead of my-branch, with no error or warning.
Propagate checkout to the fallback path so the ref is honored consistently (cookiecutter's repository.clone accepts a checkout kwarg):
except RepositoryNotFound:
...
project_output_dir = str(Path(output_dir, name)) if name else output_dir
generate_non_cookiecutter_project(
location=params["template"],
output_dir=project_output_dir,
checkout=checkout,
)…and update generate_non_cookiecutter_project to accept checkout and forward it to repository.clone. It would also be worth adding a unit test that mocks cookiecutter to raise RepositoryNotFound and asserts the checkout is propagated to the fallback.
roger-zhangg
left a comment
There was a problem hiding this comment.
LGTM — all feedback addressed:
--checkoutnow requires--location(via ClickMutexrequired_param_lists)- Integration tests added covering success, missing --location, and incompatible params
- CI integ failures are pre-existing (setup/permissions, not related to this PR)
Thanks @MustafizSaadi for addressing the long-standing request (#3555)!
| params["extra_context"] = extra_context | ||
|
|
||
| if checkout: | ||
| params["checkout"] = checkout |
There was a problem hiding this comment.
[BUG] The --checkout value is added to the cookiecutter params here, but it is silently dropped on the fallback path used when the template repository does not contain a cookiecutter.json.
Flow:
cookiecutter(**params)is invoked withcheckout. Cookiecutter clones the repo, checks out the requested ref, then looks forcookiecutter.json. If it is missing, cookiecutter raisesRepositoryNotFound.- The
except RepositoryNotFoundhandler a few lines below calls:
project_output_dir = str(Path(output_dir, name)) if name else output_dir
generate_non_cookiecutter_project(location=params["template"], output_dir=project_output_dir)generate_non_cookiecutter_project(insamcli/lib/init/arbitrary_project.py) re-clones the repo without any ref:
elif repository.is_repo_url(location):
LOG.debug("%s location is a source control repository", location)
download_fn = functools.partial(repository.clone, repo_url=location, no_input=no_input)Result: a user running sam init --location <git-url> --checkout <ref> against a repository that isn't a cookiecutter template gets the default branch back, with no error or warning — the --checkout flag is effectively ignored on this path.
This was raised in the earlier automated review and has not been explicitly dismissed by the author. Please propagate checkout through to generate_non_cookiecutter_project and down to the underlying repository.clone call (which accepts a checkout argument) so the requested ref is honored for non-cookiecutter repositories as well. New unit/integration coverage for a non-cookiecutter repo with --checkout would also prevent regressions here.
Which issue(s) does this change fix?
#3555
Why is this change necessary?
Currently,
sam initcannot clone a remote template and check out a specific branch/tag/commit in one step. The template have to be cloned first and then do a local checkout. Only then the specific branch/tag/commit could be utilized to initialize sam project which is not intuitive.How does it address the issue?
The change adds a checkout flag with the
sam initcommand. With the help of that the developers can checkout to their remote url custom branch, tag, or commit utilizing thesam initcommand. Also, added unit tests for checkout and updated the existing unit tests for the new checkout flag.What side effects does this change have?
No side effects.
Mandatory Checklist
PRs will only be reviewed after checklist is complete
make prpassesmake update-reproducible-reqsif dependencies were changedBy submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.