3.18. Git Template
The Git Template sets up a local Git repository for a generated project and gives it the tooling to work with it: a configurable branching strategy, Conventional Commits, automatic next-version detection, and stage-based releases. It doesn’t need a hosting platform — install the GitHub Template (or a future platform template) to publish the repository.
It’s standalone, so appcraft init git works on its own.
3.18.1. What it sets up
A local repository with the branches your branching strategy defines (see below), created from the first commit.
A
commit-msggit hook that blocks breaking changes from landing directly on major-version branches (see Breaking changes)..gitignoreandconfig/git.toml, which describes the branching strategy.
3.18.2. Setting up the branching strategy
The first python tools git init creates the branches. If the strategy hasn’t been chosen yet (configured = false in config/git.toml), it asks, in a terminal, whether to use major-version branches, the current major version, and the stages. Press Enter to keep each default.
The answers can also come as arguments, which skips the questions and is what automation uses:
python tools git init use_major_version=true major_version=2 stages=alpha,beta,main
python tools git init use_major_version=false stages=none trunk_branch=main
With interactive=false, init never asks: without arguments it uses the values in config/git.toml. Once the strategy is set up, init refuses arguments: change config/git.toml by hand and create the branches you need.
3.18.3. Branching strategy (config/git.toml)
Everything about branches is driven by one file:
use_major_version = false # branch names get a "v{N}-" prefix when true
major_version = 0 # current major line; only read when use_major_version = true
stages = ["alpha", "beta", "main"] # ordered, least- to most-stable; empty = no stage concept
trunk_branch = "main" # only used when stages = [] (single-branch case)
use_major_version and stages are independent, which gives four layouts:
Layout |
Config |
Branches created |
|---|---|---|
Major version and stages |
|
|
Major version only |
|
|
Stages only |
|
|
Single branch |
|
|
The most stable branch — the last stage, or the single branch when there are no stages — is the base branch. Feature and fix branches are always created from it.
3.18.4. Feature and fix branches
Work happens on short-lived branches named <type>/<name>, created from the base branch. The type is any Conventional Commits type, so the branch name says what kind of change it holds:
python tools git start feat login-page
python tools git start fix null-pointer
python tools git start refactor parser
Branch types don’t change the version by themselves; the commit types do.
Each change is merged into a stage on its own. Once validated, the same branch is merged into the next stage — there’s never a direct merge from beta into main:
python tools git merge feat/login-page beta # test it on beta
python tools git merge feat/login-page main # promote it once validated
A stage branch that doesn’t exist yet is created the first time something is merged into it, from the stage just before it. For example, the first merge into v1-beta creates it from v1-alpha. The command says so when it happens.
3.18.5. Conventional Commits
Commit messages follow Conventional Commits. The type decides how the next version is computed:
feat:— a new feature (minor bump).fix:andperf:— a fix or performance change (patch bump).feat!:,fix!:, or aBREAKING CHANGE:footer — a breaking change. It bumps the major version, except while the version is0.x: then it bumps the minor, following semver.1.0.0comes from cutting a new major line once the current one has a stable release.docs:,chore:,refactor:,test:,style:,build:,ci:,revert:— don’t change the version on their own.
3.18.6. Breaking changes
When use_major_version = true, a breaking change is treated according to the state of the current major line:
If the line has no stable release yet, or has only pre-releases, a breaking change is merged like any other change. Nothing is redirected.
If the line has a stable release (for example
0.1.0onv0), a breaking change can’t land on it. It has to go into the next major line.
This is enforced in two places:
Merging with
python tools git merge <source> <target>checks the incoming commits. If one is breaking and the line has a stable release, the merge is redirected to a newv{N+1}-alphabranch, created from the current base branch, and the target isn’t touched. Only the entry stage (alpha) is created: the new major line starts unproven and is promoted like anything else.Committing or merging directly with
git commit/git mergeis blocked by thecommit-msghook when the current branch is a major-version branch, the commit is breaking, and the line has a stable release. The hook doesn’t redirect anything; it tells you to make the change on a feature branch and merge it with the command above.
With use_major_version = false there’s no major line to protect, and breaking changes are allowed everywhere.
3.18.7. Pausing at the end of a command
detect_version and changelog wait for a keypress at the end when they run in a terminal, so their output stays on screen. Pass pause=false to skip the pause, for example in scripts:
python tools git detect_version pause=false
3.18.8. Releases
Releases are cut with one command, run from a stage branch:
python tools git release
It reads the commits that no tag contains yet, computes the next version for the current stage, shows it and asks for confirmation. Pass interactive=false to publish without the question, as in a script. On confirmation it writes the version to pyproject.toml, commits chore(release): vX.Y.Z, creates the tag and — if the GitHub template is installed and the repository has a remote — publishes a GitHub release too. python tools git detect_version shows what the release would be without changing anything.
The stage of the branch decides what kind of release it is:
Branch |
Release |
Example |
|---|---|---|
|
Pre-release |
|
|
Pre-release |
|
Base branch ( |
Stable |
|
The base is the highest release of the line across every stage, so a release from main sees the releases made on alpha and beta, whatever branch they were cut from. The next version then follows these rules:
A major line without stages is a single branch (
v0), and all of its releases are stable: the first is0.0.0, then the usual SemVer bumps.After a pre-release
X, afix:stays in the same version:0.6.0a1→0.6.0a2. Afeat:or a breaking change opens the next one:0.6.0a2→0.7.0a1. Promoting the same commits to a later stage keeps the version:0.8.0a1→0.8.0b1→0.8.0.After a stable release, the usual SemVer bump applies:
0.8.0→0.8.1a1(fix),0.9.0a1(feat),1.0.0a1(breaking).A breaking change is a minor bump only while there are no releases at all. Once any release exists, it’s a major bump.
Each pre-release label counts on its own: a second alpha of
0.6.0is0.6.0a2.A version can stop at any stage:
0.6.0a1with no beta and no stable is perfectly valid.
The version is written in PEP 440 format, which pip and poetry understand; pip install skips pre-releases unless you pass --pre.
Releases can only be cut from a stage branch of the current major line. Running release from a feature branch is refused.
When a stable release of a higher major version is cut, the repository’s default branch on the platform moves to that major’s base branch. After 1.0.0 is released from v1-main, GitHub’s default branch becomes v1-main. Pre-releases never change it.
The project’s version lives in one place, pyproject.toml ([project] or [tool.poetry]). The application reads it at runtime, so there’s no second copy to keep in sync. Edits to pyproject.toml and config/git.toml only touch the value that changes, so comments in those files are kept.
3.18.9. Cutting a major version on purpose
A new major line is normally cut automatically when a breaking change is merged into a major-version branch. To cut it explicitly, without a breaking commit, run this (requires use_major_version = true). The new major is always the one right above the highest existing major, whichever branch you’re on:
python tools git new_major
It creates the entry stage of the next line (v1-alpha) from the base branch, bumps major_version on that branch, and switches to it. Running it again is safe: it doesn’t create or bump anything twice.
A new major line only starts once the highest major has shipped a stable release (for example 1.0.0 on v1). Before that, new_major is refused, and no major is created above it. Breaking changes merged into the highest major are accepted until then.
A breaking change only goes to a new major when the highest major is the one it’s merged into. If a major above the target already exists, the merge is refused with an alert, whether the target is the highest major or not.
3.18.10. Changelog
python tools git changelog prints the commits that the next release would contain (the ones no tag contains yet), grouped into breaking changes, features, fixes, performance and other changes. Release commits and merge commits are left out.
python tools git changelog output=CHANGELOG.md also writes that section to the top of the file, above anything already there.
3.18.11. Cleaning up merged branches
python tools git cleanup lists the feature and fix branches already merged into the base branch, both locally and on origin, asks for confirmation, and deletes them from both places. Branches that aren’t merged are never touched. The remote is refreshed with a fetch first, so the list reflects what’s on the server.
3.18.12. Remote repository
python tools git set_remote <url> points origin at an existing repository and pushes every branch. To create the repository on a hosting platform instead, install a platform template such as the GitHub Template and use the Remote Repository Template commands.
3.18.13. Limitations
A squash merge leaves only the squashed message in history, so the individual commit types are lost for version detection. Prefer regular merges.
python tools git initalso works on a repository that already exists: it keeps the existing history and only creates the configured branches that are missing. Existing branches are never renamed. Run it once to get thecommit-msghook installed.git commit --no-verifyskips the hook, as in any git repository.