fix(new): answers point at gitea, an existing repo is adopted #1
No files matched your search
@@ -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`:
|
||||
|
||||
|
||||
Binary file not shown.
+51
-1
@@ -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 -- <file>), 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:
|
||||
|
||||
Reference in new issue
Block a user