From 6e39d1572a74edb7199f359c737cfe6826282922 Mon Sep 17 00:00:00 2001 From: jovnc <95868357+jovnc@users.noreply.github.com> Date: Mon, 1 Dec 2025 16:13:54 +0800 Subject: [PATCH 1/4] feat(checkout-step): Add support for start-point in checkout step --- src/repo_smith/steps/checkout_step.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/repo_smith/steps/checkout_step.py b/src/repo_smith/steps/checkout_step.py index a90ba4f..79c5f85 100644 --- a/src/repo_smith/steps/checkout_step.py +++ b/src/repo_smith/steps/checkout_step.py @@ -10,12 +10,17 @@ class CheckoutStep(Step): branch_name: Optional[str] commit_hash: Optional[str] + start_point: Optional[str] step_type: StepType = field(init=False, default=StepType.CHECKOUT) def execute(self, repo: Repo) -> None: if self.branch_name is not None: - if self.branch_name not in repo.heads: + if self.start_point is not None: + if self.branch_name in repo.heads: + raise ValueError('"start-point" cannot be provided when "branch-name" already exists in checkout step.') + repo.git.checkout("-b", self.branch_name, self.start_point) + elif self.branch_name not in repo.heads: raise ValueError("Invalid branch name") else: repo.heads[self.branch_name].checkout() @@ -44,6 +49,11 @@ def parse( raise ValueError( 'Provide either "branch-name" or "commit-hash", not both, in checkout step.' ) + + if step.get("start-point") is not None and step.get("branch-name") is None: + raise ValueError( + '"start-point" requires "branch-name" to be provided in checkout step.' + ) if step.get("branch-name") is not None and step["branch-name"].strip() == "": raise ValueError('Empty "branch-name" field in checkout step.') @@ -51,10 +61,14 @@ def parse( if step.get("commit-hash") is not None and step["commit-hash"].strip() == "": raise ValueError('Empty "commit-hash" field in checkout step.') + if step.get("start-point") is not None and step["start-point"].strip() == "": + raise ValueError('Empty "start-point" field in checkout step.') + return cls( name=name, description=description, id=id, branch_name=step.get("branch-name"), commit_hash=step.get("commit-hash"), + start_point=step.get("start-point"), ) From a5c11a98481117ffb6efa922741094052352f8c9 Mon Sep 17 00:00:00 2001 From: jovnc <95868357+jovnc@users.noreply.github.com> Date: Mon, 1 Dec 2025 16:14:06 +0800 Subject: [PATCH 2/4] test(checkout-step): Add unit test for start-point in checkout step --- tests/unit/steps/test_checkout_step.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/unit/steps/test_checkout_step.py b/tests/unit/steps/test_checkout_step.py index 5a21d2c..269bb7b 100644 --- a/tests/unit/steps/test_checkout_step.py +++ b/tests/unit/steps/test_checkout_step.py @@ -21,6 +21,14 @@ def test_checkout_step_parse_both_branch_name_and_commit_hash(): ) +def test_checkout_step_parse_start_point_with_commit_hash(): + with pytest.raises( + ValueError, + match='"start-point" requires "branch-name" to be provided in checkout step.', + ): + CheckoutStep.parse("n", "d", "id", {"start-point": "HEAD~1", "commit-hash": "abc123"}) + + def test_checkout_step_parse_empty_branch_name(): with pytest.raises(ValueError, match='Empty "branch-name" field in checkout step.'): CheckoutStep.parse("n", "d", "id", {"branch-name": ""}) @@ -31,6 +39,11 @@ def test_checkout_step_parse_empty_commit_hash(): CheckoutStep.parse("n", "d", "id", {"commit-hash": ""}) +def test_checkout_step_parse_empty_start_point(): + with pytest.raises(ValueError, match='Empty "start-point" field in checkout step.'): + CheckoutStep.parse("n", "d", "id", {"start-point": "", "branch-name": "test"}) + + def test_checkout_step_parse_with_branch_name(): step = CheckoutStep.parse("n", "d", "id", {"branch-name": "test"}) assert isinstance(step, CheckoutStep) @@ -39,6 +52,7 @@ def test_checkout_step_parse_with_branch_name(): assert step.id == "id" assert step.branch_name == "test" assert step.commit_hash is None + assert step.start_point is None def test_checkout_step_parse_with_commit_hash(): @@ -49,3 +63,15 @@ def test_checkout_step_parse_with_commit_hash(): assert step.id == "id" assert step.branch_name is None assert step.commit_hash == "abc123" + assert step.start_point is None + + +def test_checkout_step_parse_with_start_point(): + step = CheckoutStep.parse("n", "d", "id", {"branch-name": "test", "start-point": "HEAD~1"}) + assert isinstance(step, CheckoutStep) + assert step.name == "n" + assert step.description == "d" + assert step.id == "id" + assert step.branch_name == "test" + assert step.commit_hash is None + assert step.start_point == "HEAD~1" From 7791f8244df095f4681d9a97f6f027fba0ac4a4f Mon Sep 17 00:00:00 2001 From: jovnc <95868357+jovnc@users.noreply.github.com> Date: Mon, 1 Dec 2025 16:14:15 +0800 Subject: [PATCH 3/4] test(checkout-step): Add integration test for start-point in checkout step --- tests/integration/steps/test_checkout_step.py | 37 +++++++++++++++++++ ...heckout_step_start_point_branch_exists.yml | 14 +++++++ ...kout_step_start_point_with_commit_hash.yml | 10 +++++ ...eckout_step_start_point_without_branch.yml | 9 +++++ .../checkout_step_with_start_point.yml | 14 +++++++ 5 files changed, 84 insertions(+) create mode 100644 tests/specs/checkout_step/checkout_step_start_point_branch_exists.yml create mode 100644 tests/specs/checkout_step/checkout_step_start_point_with_commit_hash.yml create mode 100644 tests/specs/checkout_step/checkout_step_start_point_without_branch.yml create mode 100644 tests/specs/checkout_step/checkout_step_with_start_point.yml diff --git a/tests/integration/steps/test_checkout_step.py b/tests/integration/steps/test_checkout_step.py index bb03849..09f30cf 100644 --- a/tests/integration/steps/test_checkout_step.py +++ b/tests/integration/steps/test_checkout_step.py @@ -25,6 +25,29 @@ def test_checkout_step_missing_branch(): pass +def test_checkout_step_start_point_without_branch(): + with pytest.raises(Exception): + initialize_repo( + "tests/specs/checkout_step/checkout_step_start_point_without_branch.yml" + ) + + +def test_checkout_step_start_point_with_commit_hash(): + with pytest.raises(Exception): + initialize_repo( + "tests/specs/checkout_step/checkout_step_start_point_with_commit_hash.yml" + ) + + +def test_checkout_step_start_point_branch_exists(): + with pytest.raises(Exception): + repo_initializer = initialize_repo( + "tests/specs/checkout_step/checkout_step_start_point_branch_exists.yml" + ) + with repo_initializer.initialize(): + pass + + def test_checkout_step(): def first_hook(r: Repo) -> None: assert r.active_branch.name == "main" @@ -38,3 +61,17 @@ def second_hook(r: Repo) -> None: with repo_initializer.initialize() as r: assert len(r.branches) == 2 assert "test" in r.heads + + +def test_checkout_step_with_start_point(): + def checkout_hook(r: Repo) -> None: + assert r.active_branch.name == "new-branch" + + repo_initializer = initialize_repo( + "tests/specs/checkout_step/checkout_step_with_start_point.yml" + ) + repo_initializer.add_post_hook("checkout-with-start-point", checkout_hook) + with repo_initializer.initialize() as r: + assert len(r.branches) == 2 + assert "new-branch" in r.heads + assert r.heads["new-branch"].commit.message.strip() == "first commit" diff --git a/tests/specs/checkout_step/checkout_step_start_point_branch_exists.yml b/tests/specs/checkout_step/checkout_step_start_point_branch_exists.yml new file mode 100644 index 0000000..a79606e --- /dev/null +++ b/tests/specs/checkout_step/checkout_step_start_point_branch_exists.yml @@ -0,0 +1,14 @@ +name: Checkout step start-point branch exists +description: Checkout step start-point branch exists raises error +initialization: + steps: + - type: commit + message: first commit + empty: true + - type: branch + branch-name: existing-branch + - type: checkout + branch-name: main + - type: checkout + branch-name: existing-branch + start-point: HEAD~1 diff --git a/tests/specs/checkout_step/checkout_step_start_point_with_commit_hash.yml b/tests/specs/checkout_step/checkout_step_start_point_with_commit_hash.yml new file mode 100644 index 0000000..9bdb6fe --- /dev/null +++ b/tests/specs/checkout_step/checkout_step_start_point_with_commit_hash.yml @@ -0,0 +1,10 @@ +name: Checkout step start-point with commit hash +description: Checkout step start-point with commit hash raises error +initialization: + steps: + - type: commit + message: test commit + empty: true + - type: checkout + start-point: HEAD~1 + commit-hash: abc123 diff --git a/tests/specs/checkout_step/checkout_step_start_point_without_branch.yml b/tests/specs/checkout_step/checkout_step_start_point_without_branch.yml new file mode 100644 index 0000000..9f5b2f5 --- /dev/null +++ b/tests/specs/checkout_step/checkout_step_start_point_without_branch.yml @@ -0,0 +1,9 @@ +name: Checkout step start-point without branch +description: Checkout step start-point without branch raises error +initialization: + steps: + - type: commit + message: test commit + empty: true + - type: checkout + start-point: HEAD~1 diff --git a/tests/specs/checkout_step/checkout_step_with_start_point.yml b/tests/specs/checkout_step/checkout_step_with_start_point.yml new file mode 100644 index 0000000..921623e --- /dev/null +++ b/tests/specs/checkout_step/checkout_step_with_start_point.yml @@ -0,0 +1,14 @@ +name: Checkout step with start-point +description: Checkout creating a new branch at a specific commit +initialization: + steps: + - type: commit + message: first commit + empty: true + - type: commit + message: second commit + empty: true + - type: checkout + branch-name: new-branch + start-point: HEAD~1 + id: checkout-with-start-point From 46a7c5796d5188210fc31b35f7ab825b4ecd2fd9 Mon Sep 17 00:00:00 2001 From: jovnc <95868357+jovnc@users.noreply.github.com> Date: Mon, 1 Dec 2025 16:30:35 +0800 Subject: [PATCH 4/4] docs(specification): Document start-point for branch creation in checkout step --- specification.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/specification.md b/specification.md index 0313142..41e9a73 100644 --- a/specification.md +++ b/specification.md @@ -207,6 +207,20 @@ Commit hash. Only read if `initialization.steps[*].type` is `checkout`. Type: `string` +#### `initialization.steps[*].start-point` + +Starting point for creating a new branch. Only read if +`initialization.steps[*].type` is `checkout`. + +When provided, `branch-name` must also be specified and the branch must not +already exist. This creates a new branch at the specified commit reference +(equivalent to `git checkout -b `). + +Accepts any valid git revision: commit SHAs, relative references (e.g., +`HEAD~1`), branch names, or tags. + +Type: `string` + #### `initialization.steps[*].remote-name` Remote name. Only read if `initialization.steps[*].type` is `remote` or `fetch`.