From 095b8eacbd76383dd0fdabcf55c686841badde89 Mon Sep 17 00:00:00 2001 From: h Date: Thu, 8 Oct 2026 04:39:38 +0000 Subject: [PATCH] fix(new): answers point at gitea when rendered from local checkouts, an existing repo is adopted with the rendered files on top --- README.md | 4 +- __pycache__/templates.cpython-313.pyc | Bin 0 -> 28529 bytes templates.py | 52 +++++++++++++++++++++++++- 3 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 __pycache__/templates.cpython-313.pyc diff --git a/README.md b/README.md index 7cf9368..06ec1ba 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ newproject ~/projects/my-thing --parts backend,frontend,caddy --backend python - `other` as the backend or the frontend renders the `blank` template: the part's compose files, Makefile and `.env.example` with no code, for a stack that has no template of its own, next to any other part. -One part alone is rendered at the root of the project; two parts live in `backend/` and `frontend/`. After rendering, `make env` creates `.env` and every `docker-compose.override.yml` from their examples. `updateproject` needs a clean working tree and commits each part's update separately, refreshing `uv.lock` and `bun.lock` first, so the next part starts from a clean tree again. When copier leaves conflicts it stops before committing and lists the files: resolve, commit, run it again. Templates are cloned from `https://git.kotikot.com/templates` unless `TEMPLATES_DIR` points at a directory with local checkouts; `TEMPLATES_REF=HEAD` renders their working trees instead of the latest tag. +One part alone is rendered at the root of the project; two parts live in `backend/` and `frontend/`. After rendering, `make env` creates `.env` and every `docker-compose.override.yml` from their examples. `updateproject` needs a clean working tree and commits each part's update separately, refreshing `uv.lock` and `bun.lock` first, so the next part starts from a clean tree again. When copier leaves conflicts it stops before committing and lists the files: resolve, commit, run it again. Templates are cloned from `https://git.kotikot.com/templates` unless `TEMPLATES_DIR` points at a directory with local checkouts; `TEMPLATES_REF=HEAD` renders their working trees instead of the latest tag. Either way the answers in `.copier/` point at Gitea, not at the local checkout, so `updateproject` works on any machine; it always updates from Gitea, so push a template change before updating projects with it. ## Gitea @@ -26,7 +26,7 @@ giteaproject # asks for the organization and the name giteaproject ms-agents/my-thing ``` -Run in the project directory. It creates the repo if it does not exist (private, `main` as the default branch, the description from `.copier/infra.yml`), adds it as `origin` over ssh, and when the repo is still empty and there are local commits, pushes `main` and `dev` and switches to `dev`. An existing `origin` that points elsewhere is left alone. `newproject` asks for the organization at the end, or takes `--repo org/name`; without a terminal and without `--repo` it creates nothing. +Run in the project directory. It creates the repo if it does not exist (private, `main` as the default branch, the description from `.copier/infra.yml`), adds it as `origin` over ssh, and when the repo is still empty and there are local commits, pushes `main` and `dev` and switches to `dev`. An existing `origin` that points elsewhere is left alone. When the repo already has commits and the project has none yet (a fresh `newproject` for code that lives in Gitea already), the project lands on top of it: the repo's history becomes the local one, the rendered files stay as uncommitted changes against it, and files the templates do not render come back as they are in the repo. Review with `git status` and `git diff`, keep what is yours, commit the rest. `newproject` asks for the organization at the end, or takes `--repo org/name`; without a terminal and without `--repo` it creates nothing. The organization defaults to `org` from the config, else to the name of the parent directory when it matches an organization. `~/.config/templates/config.toml`: diff --git a/__pycache__/templates.cpython-313.pyc b/__pycache__/templates.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e67e8e27bea328349e645af7f5f0a0a949b54d7b GIT binary patch literal 28529 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 literal 0 HcmV?d00001 diff --git a/templates.py b/templates.py index 963f407..10f4d27 100644 --- a/templates.py +++ b/templates.py @@ -12,7 +12,8 @@ from pathlib import Path import questionary from copier import run_copy, run_update -SOURCE = os.environ.get("TEMPLATES_DIR", "https://git.kotikot.com/templates") +PUBLISHED = "https://git.kotikot.com/templates" +SOURCE = os.environ.get("TEMPLATES_DIR", PUBLISHED) REF = os.environ.get("TEMPLATES_REF") BLANK = "blank" BACKENDS = {"python": "backend-python", "other": BLANK} @@ -26,6 +27,26 @@ def template(name: str) -> str: return str(local) if local.is_dir() else f"{SOURCE}/{name}.git" +def publish_answers(dest: Path) -> None: + """Answers rendered from local checkouts (TEMPLATES_DIR) point at Gitea. + + copier writes the path it rendered from into `_src_path`; a local path + only exists on this machine, and `updateproject` anywhere else fails. + """ + local = Path(SOURCE) + if not local.is_dir(): + return + for answers in (dest / ".copier").glob("*.yml"): + lines = answers.read_text(encoding="utf-8").splitlines(keepends=True) + for i, line in enumerate(lines): + if not line.startswith("_src_path:"): + continue + src = Path(line.split(":", 1)[1].strip().strip("'\"")) + if src.is_dir() and src.resolve().parent == local.resolve(): + lines[i] = f"_src_path: {PUBLISHED}/{src.name}.git\n" + answers.write_text("".join(lines), encoding="utf-8") + + def git_config(key: str) -> str: result = subprocess.run(["git", "config", key], capture_output=True, text=True, check=False) return result.stdout.strip() @@ -101,6 +122,7 @@ def new(args: argparse.Namespace) -> None: copy_part("frontend", FRONTENDS[frontend], {"serve": serve}) if "caddy" in parts: copy("caddy", {"project_slug": slug, "routes": routes}) + publish_answers(dest) if (dest / "Makefile").is_file(): subprocess.run(["make", "env"], cwd=dest, check=True) if args.repo or (args.repo is None and interactive()): @@ -224,6 +246,28 @@ def project_description(cwd: Path) -> str: return "" +def adopt(cwd: Path, branch: str) -> None: + """Put a freshly rendered project on top of an existing repo. + + The repo's history becomes the local one; what the templates rendered + stays in the working tree as uncommitted changes against it, files the + templates do not know are restored as they are in the repo. + """ + subprocess.run(["git", "fetch", "-q", "origin"], cwd=cwd, check=True) + # No commits yet: the branch is unborn, reset creates it at the repo's tip + # and keeps the working tree. + subprocess.run(["git", "symbolic-ref", "HEAD", f"refs/heads/{branch}"], cwd=cwd, check=True) + subprocess.run(["git", "reset", "-q", f"origin/{branch}"], cwd=cwd, check=True) + subprocess.run(["git", "branch", "-q", "-u", f"origin/{branch}"], cwd=cwd, check=True) + deleted = git("ls-files", "--deleted", cwd=cwd).splitlines() + if deleted: + subprocess.run(["git", "checkout", "-q", "--", *deleted], cwd=cwd, check=True) + print( + f"on {branch} of the existing repo: the rendered files are uncommitted changes on top of it.\n" + "Review with git status / git diff, keep what is yours (git checkout -- ), commit the rest." + ) + + def choose_repo(cwd: Path, repo: str | None, call) -> str | None: if repo: return repo @@ -258,6 +302,8 @@ def setup_repo(cwd: Path, repo: str, call) -> None: print(f"repo {repo}: created") url = info["clone_url"] if config().get("remote") == "https" else info["ssh_url"] + if not info.get("empty", True) and not git("rev-parse", "--git-dir", cwd=cwd): + subprocess.run(["git", "init", "-q"], cwd=cwd, check=True) if git("rev-parse", "--git-dir", cwd=cwd): current = git("remote", "get-url", "origin", cwd=cwd) if not current: @@ -275,6 +321,10 @@ def setup_repo(cwd: Path, repo: str, call) -> None: print("pushed main and dev, on dev now") elif info.get("empty", True): print("nothing committed yet: commit, then run giteaproject again to push main and dev") + elif not git("rev-parse", "--verify", "-q", "HEAD", cwd=cwd): + adopt(cwd, info["default_branch"]) + else: + print(f"{repo} is not empty and there are local commits: nothing pushed, merge them by hand") existing = {label["name"] for label in call("GET", f"/repos/{repo}/labels?limit=100") or []} for label, color, description in RENOVATE_LABELS: