rebake 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,35 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - '*'
7
+
8
+ jobs:
9
+ deploy:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ id-token: write
13
+ environment:
14
+ name: pypi
15
+ url: https://pypi.org/p/rebake
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ with:
20
+ fetch-depth: 0
21
+ - name: Set up the latest version of uv
22
+ uses: astral-sh/setup-uv@v7
23
+ with:
24
+ enable-cache: true
25
+ - name: Build
26
+ run: uv build
27
+
28
+ - name: upload dists
29
+ uses: actions/upload-artifact@v4
30
+ with:
31
+ name: release-dists
32
+ path: dist/
33
+
34
+ - name: Publish package distributions to PyPI
35
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,78 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+ branches: [ main ]
8
+
9
+ jobs:
10
+ test:
11
+ name: Test Python ${{ matrix.python-version }}
12
+ runs-on: ubuntu-latest
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ python-version: ["3.12", "3.13"]
17
+
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+
21
+ - name: Set up Python ${{ matrix.python-version }}
22
+ uses: actions/setup-python@v5
23
+ with:
24
+ python-version: ${{ matrix.python-version }}
25
+
26
+ - name: Install uv
27
+ uses: astral-sh/setup-uv@v5
28
+
29
+ - name: Install dependencies
30
+ run: uv sync --all-extras
31
+
32
+ - name: Run tests
33
+ run: uv run pytest
34
+
35
+ lint:
36
+ name: Lint
37
+ runs-on: ubuntu-latest
38
+
39
+ steps:
40
+ - uses: actions/checkout@v4
41
+
42
+ - name: Set up Python
43
+ uses: actions/setup-python@v5
44
+ with:
45
+ python-version: "3.13"
46
+
47
+ - name: Install uv
48
+ uses: astral-sh/setup-uv@v5
49
+
50
+ - name: Install dependencies
51
+ run: uv sync --all-extras
52
+
53
+ - name: Run ruff check
54
+ run: uv run ruff check
55
+
56
+ - name: Run ruff format check
57
+ run: uv run ruff format --check
58
+
59
+ type-check:
60
+ name: Type Check
61
+ runs-on: ubuntu-latest
62
+
63
+ steps:
64
+ - uses: actions/checkout@v4
65
+
66
+ - name: Set up Python
67
+ uses: actions/setup-python@v5
68
+ with:
69
+ python-version: "3.13"
70
+
71
+ - name: Install uv
72
+ uses: astral-sh/setup-uv@v5
73
+
74
+ - name: Install dependencies
75
+ run: uv sync --all-extras
76
+
77
+ - name: Run type check
78
+ run: uv run pyrefly check
@@ -0,0 +1 @@
1
+ __pycache__/
@@ -0,0 +1 @@
1
+ 3.12
rebake-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.4
2
+ Name: rebake
3
+ Version: 0.0.1
4
+ Summary: A spiritual successor to cruft for managing cookiecutter projects
5
+ Author-email: Ryo Kitagawa <kitadrum50@gmail.com>
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: cookiecutter>=2.6.0
8
+ Requires-Dist: gitpython>=3.1.44
9
+ Requires-Dist: rich>=13.9.4
10
+ Requires-Dist: typer>=0.15.1
11
+ Description-Content-Type: text/markdown
12
+
13
+ # rebake
14
+
15
+ A spiritual successor to [cruft](https://github.com/cruft/cruft) for managing [cookiecutter](https://github.com/cookiecutter/cookiecutter) projects.
16
+
17
+ rebake improves on cruft in two key areas:
18
+
19
+ 1. **Better conflict UX** — uses `git apply -3` to produce inline conflict markers instead of `.rej` files
20
+ 2. **New variable detection** — prompts for variables added to the template since the project was last updated
21
+
22
+ ## Requirements
23
+
24
+ - Python 3.12+
25
+ - [uv](https://docs.astral.sh/uv/)
26
+ - Git
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ uv tool install rebake
32
+ ```
33
+
34
+ Or add it to a project:
35
+
36
+ ```bash
37
+ uv add rebake
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ### `rebake check`
43
+
44
+ Check whether the project is up-to-date with its template.
45
+
46
+ ```bash
47
+ rebake check [PROJECT_DIR]
48
+ ```
49
+
50
+ Exit codes:
51
+ - `0` — up-to-date
52
+ - `1` — outdated
53
+ - `2` — error (e.g. `.cruft.json` not found)
54
+
55
+ ### `rebake update`
56
+
57
+ Apply the latest template changes to the project.
58
+
59
+ ```bash
60
+ rebake update [PROJECT_DIR]
61
+ ```
62
+
63
+ rebake will:
64
+ 1. Abort if there are uncommitted changes (commit or stash first)
65
+ 2. Detect new variables added to the template and prompt for their values
66
+ 3. Generate a diff between the old and new rendered templates
67
+ 4. Apply the diff with `git apply -3` — conflicts appear as inline markers
68
+ 5. Update `.cruft.json` with the new commit hash and any newly added variables
69
+
70
+ ## Migrating from cruft
71
+
72
+ rebake reads `.cruft.json` as-is. No migration needed — just replace `cruft` with `rebake` in your commands.
73
+
74
+ ```bash
75
+ # before
76
+ cruft check
77
+ cruft update
78
+
79
+ # after
80
+ rebake check
81
+ rebake update
82
+ ```
83
+
84
+ ## `.cruft.json` format
85
+
86
+ ```json
87
+ {
88
+ "template": "https://github.com/owner/template",
89
+ "commit": "abc123...",
90
+ "checkout": "main",
91
+ "context": {
92
+ "cookiecutter": {
93
+ "project_name": "my-project",
94
+ "author": "Jane Doe"
95
+ }
96
+ },
97
+ "skip": ["go.sum", "*.lock"]
98
+ }
99
+ ```
100
+
101
+ ## Development
102
+
103
+ ```bash
104
+ git clone https://github.com/kitagry/rebake
105
+ cd rebake
106
+ uv sync
107
+ uv run pytest
108
+ ```
rebake-0.0.1/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # rebake
2
+
3
+ A spiritual successor to [cruft](https://github.com/cruft/cruft) for managing [cookiecutter](https://github.com/cookiecutter/cookiecutter) projects.
4
+
5
+ rebake improves on cruft in two key areas:
6
+
7
+ 1. **Better conflict UX** — uses `git apply -3` to produce inline conflict markers instead of `.rej` files
8
+ 2. **New variable detection** — prompts for variables added to the template since the project was last updated
9
+
10
+ ## Requirements
11
+
12
+ - Python 3.12+
13
+ - [uv](https://docs.astral.sh/uv/)
14
+ - Git
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ uv tool install rebake
20
+ ```
21
+
22
+ Or add it to a project:
23
+
24
+ ```bash
25
+ uv add rebake
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ### `rebake check`
31
+
32
+ Check whether the project is up-to-date with its template.
33
+
34
+ ```bash
35
+ rebake check [PROJECT_DIR]
36
+ ```
37
+
38
+ Exit codes:
39
+ - `0` — up-to-date
40
+ - `1` — outdated
41
+ - `2` — error (e.g. `.cruft.json` not found)
42
+
43
+ ### `rebake update`
44
+
45
+ Apply the latest template changes to the project.
46
+
47
+ ```bash
48
+ rebake update [PROJECT_DIR]
49
+ ```
50
+
51
+ rebake will:
52
+ 1. Abort if there are uncommitted changes (commit or stash first)
53
+ 2. Detect new variables added to the template and prompt for their values
54
+ 3. Generate a diff between the old and new rendered templates
55
+ 4. Apply the diff with `git apply -3` — conflicts appear as inline markers
56
+ 5. Update `.cruft.json` with the new commit hash and any newly added variables
57
+
58
+ ## Migrating from cruft
59
+
60
+ rebake reads `.cruft.json` as-is. No migration needed — just replace `cruft` with `rebake` in your commands.
61
+
62
+ ```bash
63
+ # before
64
+ cruft check
65
+ cruft update
66
+
67
+ # after
68
+ rebake check
69
+ rebake update
70
+ ```
71
+
72
+ ## `.cruft.json` format
73
+
74
+ ```json
75
+ {
76
+ "template": "https://github.com/owner/template",
77
+ "commit": "abc123...",
78
+ "checkout": "main",
79
+ "context": {
80
+ "cookiecutter": {
81
+ "project_name": "my-project",
82
+ "author": "Jane Doe"
83
+ }
84
+ },
85
+ "skip": ["go.sum", "*.lock"]
86
+ }
87
+ ```
88
+
89
+ ## Development
90
+
91
+ ```bash
92
+ git clone https://github.com/kitagry/rebake
93
+ cd rebake
94
+ uv sync
95
+ uv run pytest
96
+ ```
@@ -0,0 +1,49 @@
1
+ [project]
2
+ name = "rebake"
3
+ description = "A spiritual successor to cruft for managing cookiecutter projects"
4
+ readme = "README.md"
5
+ authors = [
6
+ { name = "Ryo Kitagawa", email = "kitadrum50@gmail.com" }
7
+ ]
8
+ requires-python = ">=3.12"
9
+ dynamic = ["version"]
10
+ dependencies = [
11
+ "cookiecutter>=2.6.0",
12
+ "typer>=0.15.1",
13
+ "gitpython>=3.1.44",
14
+ "rich>=13.9.4",
15
+ ]
16
+
17
+ [project.scripts]
18
+ rebake = "rebake.cli:app"
19
+
20
+ [dependency-groups]
21
+ dev = [
22
+ "pytest>=8.3.5",
23
+ "pytest-mock>=3.14.0",
24
+ "ruff>=0.9.0",
25
+ "pyrefly>=0.17.0",
26
+ ]
27
+
28
+ [tool.pytest.ini_options]
29
+ markers = ["e2e: end-to-end tests using real git and cookiecutter"]
30
+
31
+ [tool.ruff]
32
+ target-version = "py312"
33
+ line-length = 120
34
+
35
+ [tool.ruff.lint]
36
+ select = ["E", "F", "I"]
37
+
38
+ [tool.hatch.version]
39
+ source = "uv-dynamic-versioning"
40
+
41
+ [tool.uv-dynamic-versioning]
42
+ enable = true
43
+
44
+ [tool.hatch.build.targets.wheel]
45
+ packages = ["src/rebake"]
46
+
47
+ [build-system]
48
+ requires = ["hatchling", "uv-dynamic-versioning"]
49
+ build-backend = "hatchling.build"
File without changes
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+ from pathlib import Path
5
+
6
+ from rebake.config import CruftConfig
7
+ from rebake.utils.git import get_template_head_commit
8
+
9
+
10
+ class CheckResult(Enum):
11
+ UP_TO_DATE = "up-to-date"
12
+ OUTDATED = "outdated"
13
+
14
+
15
+ def is_up_to_date(project_dir: Path = Path(".")) -> CheckResult:
16
+ """Check whether the project is up-to-date with its template."""
17
+ config = CruftConfig.load(project_dir)
18
+ head_commit = get_template_head_commit(config.template, checkout=config.checkout)
19
+ if config.commit == head_commit:
20
+ return CheckResult.UP_TO_DATE
21
+ return CheckResult.OUTDATED
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import typer
6
+ from rich.console import Console
7
+
8
+ from rebake.check import CheckResult, is_up_to_date
9
+
10
+ app = typer.Typer(help="A spiritual successor to cruft for managing cookiecutter projects.")
11
+ console = Console()
12
+ err_console = Console(stderr=True)
13
+
14
+
15
+ @app.command()
16
+ def check(
17
+ project_dir: Path = typer.Argument(Path("."), help="Path to the project directory"),
18
+ ) -> None:
19
+ """Check if the project is up-to-date with its template."""
20
+ try:
21
+ result = is_up_to_date(project_dir)
22
+ except FileNotFoundError as e:
23
+ err_console.print(f"[red]Error:[/red] {e}")
24
+ raise typer.Exit(code=2)
25
+
26
+ if result == CheckResult.UP_TO_DATE:
27
+ console.print("[green]✓[/green] Project is up-to-date.")
28
+ raise typer.Exit(code=0)
29
+ else:
30
+ console.print("[yellow]![/yellow] Project is outdated.")
31
+ raise typer.Exit(code=1)
32
+
33
+
34
+ @app.command()
35
+ def update(
36
+ project_dir: Path = typer.Argument(Path("."), help="Path to the project directory"),
37
+ ) -> None:
38
+ """Apply the latest template changes to the project."""
39
+ from rebake.update import run_update
40
+
41
+ try:
42
+ run_update(project_dir)
43
+ except Exception as e:
44
+ err_console.print(f"[red]Error:[/red] {e}")
45
+ raise typer.Exit(code=1)
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ CRUFT_FILE = ".cruft.json"
9
+
10
+
11
+ @dataclass
12
+ class CruftConfig:
13
+ template: str
14
+ commit: str
15
+ context: dict[str, Any]
16
+ checkout: str | None = None
17
+ skip: list[str] = field(default_factory=list)
18
+
19
+ @classmethod
20
+ def load(cls, project_dir: Path = Path(".")) -> "CruftConfig":
21
+ cruft_file = project_dir / CRUFT_FILE
22
+ if not cruft_file.exists():
23
+ raise FileNotFoundError(f"{CRUFT_FILE} not found in {project_dir}")
24
+ data = json.loads(cruft_file.read_text())
25
+ return cls(
26
+ template=data["template"],
27
+ commit=data["commit"],
28
+ context=data.get("context", {}),
29
+ checkout=data.get("checkout"),
30
+ skip=data.get("skip", []),
31
+ )
32
+
33
+ def save(self, project_dir: Path = Path(".")) -> None:
34
+ cruft_file = project_dir / CRUFT_FILE
35
+ data: dict[str, Any] = {
36
+ "template": self.template,
37
+ "commit": self.commit,
38
+ "context": self.context,
39
+ }
40
+ if self.checkout is not None:
41
+ data["checkout"] = self.checkout
42
+ if self.skip:
43
+ data["skip"] = self.skip
44
+ cruft_file.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n")
File without changes
@@ -0,0 +1,90 @@
1
+ from __future__ import annotations
2
+
3
+ import tempfile
4
+ from pathlib import Path
5
+
6
+ from rich.console import Console
7
+
8
+ from rebake.config import CruftConfig
9
+ from rebake.utils.git import (
10
+ apply_patch,
11
+ clone_at_commit,
12
+ generate_diff,
13
+ get_template_head_commit,
14
+ is_working_tree_clean,
15
+ )
16
+ from rebake.utils.template import render_template
17
+ from rebake.utils.variables import detect_new_variables, prompt_new_variables
18
+
19
+ console = Console()
20
+
21
+
22
+ def run_update(project_dir: Path = Path(".")) -> None:
23
+ """Apply the latest template changes to the project.
24
+
25
+ Raises RuntimeError when the working tree has uncommitted changes.
26
+ """
27
+ # Resolve to absolute path before any subprocess/cookiecutter calls that may change CWD
28
+ project_dir = project_dir.resolve()
29
+
30
+ if not is_working_tree_clean(project_dir):
31
+ raise RuntimeError("Project has uncommitted changes. Please commit or stash them before updating.")
32
+
33
+ config = CruftConfig.load(project_dir)
34
+ old_commit = config.commit
35
+ new_commit = get_template_head_commit(config.template, checkout=config.checkout)
36
+
37
+ console.print(f"Updating from [cyan]{old_commit[:8]}[/cyan] → [cyan]{new_commit[:8]}[/cyan]")
38
+
39
+ old_context = config.context.get("cookiecutter", {})
40
+
41
+ with tempfile.TemporaryDirectory() as tmpdir:
42
+ tmp = Path(tmpdir)
43
+
44
+ # Clone template at old and new commits to compute the diff
45
+ old_template_dir = tmp / "old_template"
46
+ new_template_dir = tmp / "new_template"
47
+ clone_at_commit(config.template, old_commit, old_template_dir)
48
+ clone_at_commit(config.template, new_commit, new_template_dir)
49
+
50
+ # Detect variables added in the new template and prompt the user
51
+ new_vars = detect_new_variables(new_template_dir, old_context)
52
+ extra_context = {}
53
+ if new_vars:
54
+ console.print("[yellow]New template variables detected. Please provide values:[/yellow]")
55
+ extra_context = prompt_new_variables(new_vars)
56
+
57
+ merged_context = {**old_context, **extra_context}
58
+
59
+ # Render both template versions with the merged context
60
+ old_output = tmp / "old_output"
61
+ new_output = tmp / "new_output"
62
+ old_output.mkdir()
63
+ new_output.mkdir()
64
+ old_rendered = render_template(old_template_dir, merged_context, old_output)
65
+ new_rendered = render_template(new_template_dir, merged_context, new_output)
66
+
67
+ patch = generate_diff(old_rendered, new_rendered)
68
+
69
+ if patch:
70
+ success, stderr = apply_patch(patch, project_dir)
71
+ if not success:
72
+ rej_files = sorted(project_dir.rglob("*.rej"))
73
+ console.print("[yellow]![/yellow] Some hunks could not be applied.")
74
+ if rej_files:
75
+ console.print("Resolve conflicts and delete the following [bold].rej[/bold] files:")
76
+ for f in rej_files:
77
+ console.print(f" [bold]{f.relative_to(project_dir)}[/bold]")
78
+ if stderr:
79
+ console.print(stderr)
80
+ else:
81
+ console.print("[green]✓[/green] Patch applied successfully.")
82
+ else:
83
+ console.print("[green]✓[/green] No changes to apply.")
84
+
85
+ # Persist the new commit hash and any newly prompted variables.
86
+ # Save even on partial apply so the next run starts from the new baseline
87
+ # rather than re-applying the same diff.
88
+ config.commit = new_commit
89
+ config.context["cookiecutter"] = merged_context
90
+ config.save(project_dir)
File without changes