Git Workflow
The Code Repository page says what to name a branch. This page says where to create it from, and how the work travels from that branch into the long lived branches.
Which branch model a repository uses
This is not a choice. It follows from how many environments the project is deployed to.
Nearly all of our work is client work, deployed to more than one environment, where each step has to be approved before the change goes further. Those repositories use environment branches, and the rest of this page is written for them.
Convention
- A repository deployed to more than one environment uses environment branches: one long lived branch per environment, and no commits on any of them.
- A repository with one environment only — an internal tool, a shared library, a documentation site — uses a single long lived branch. See the last section on this page.
- The README must say which one the repository uses. A developer should not have to guess from the branch list.
- Don't mix the two in one repository.
- An existing repository keeps working the way it does today until it is moved over on purpose. This standard applies to every new repository, and to an old one from the day it is migrated.
Starting work on a work item
Where a branch starts decides what its Pull Request contains. Starting from the wrong branch is the most common cause of a Pull Request full of other people's work, and it is free to get right and expensive to fix later.
Create the branch from the project's most stable long lived branch. That is the production branch when the project has one, otherwise the working branch. Every other long lived branch already contains it, so a branch cut from there merges cleanly into any of them, carrying only your own commit.
Create it before you edit the first file, not once you are ready to commit. A file changed while a long lived branch is checked out is changed against a base your branch will not have: the working branch holds work that production does not, so the change may behave differently once it sits on the right base — or apply cleanly and quietly carry unreleased lines with it. It also leaves the work one git commit away from landing on a long lived branch.
Convention
- Create the branch before the first edit, not before the first commit. Nothing should be changed in the working tree while a long lived branch is checked out.
- Fetch first, then create the branch from the remote branch, not from whatever is checked out locally:
git fetch origin
git switch -c feat/PROJ-123-multi-shift-support origin/main- Don't create the branch from the working branch when the project has environment branches. The branch would then carry work that has not been released, and merging it into production later would take that unreleased work with it.
- Don't create the branch from a stale local copy. The Pull Request will show other people's commits as part of your change.
- Put the work item ID in the branch name, following the Code Repository page.
- If you have already started editing on a long lived branch, move the changes across instead of committing them where they are:
git stash push -u
git switch -c fix/PROJ-123-fractional-price origin/main
git stash pop- After moving them, run the build and the tests again. The base is not the one you were editing against, so an earlier passing run proves nothing.
What the target branch means
The target is the branch a Pull Request merges into. It is not the place the change ends up in the end.
One work item usually has more than one target over its life. The same branch is merged into each of them, one at a time, as the work item moves through its statuses:
| Work item status | What has just happened | Merge the branch into |
|---|---|---|
| Ready for testing | the developer has finished the code | both the working branch and the testing branch, merged together just before the status is moved |
| Testing | QA is checking it on the shared development environment | — |
| Testing failed | QA found a problem, so it goes back to In progress | — |
| Ready for deployment | testing passed | the production branch |
| Done | it is deployed and live in production | — |
Done means deployed to production, not finished coding. Moving a work item to Ready for testing is what says the code is written and merged into the working branch. Status names differ between projects, but the shape does not.
Convention
- One work item gets one branch, and that branch is merged into every target in turn. Don't create a second branch for the same work item.
- One Pull Request per target, opened when the work item reaches that status. So one branch can have several Pull Requests over its life, at different times.
- Because it is the same branch every time, the change keeps the same commit ID in every branch it reaches. That is what makes this command tell the truth:
git branch -a --contains <commit>- Don't merge one long lived branch into another. Promotion is always a work item branch merging into a target. Merging the working branch into the testing branch would carry every unapproved work item forward at once, which is exactly what the statuses exist to prevent.
- A work item that is reopened after Done starts a new branch. The old one already reached production and was deleted.
- The working branch and the testing branch are merged together, right before the work item moves to Ready for testing. In most of our projects the two carry the same code and differ only by where they are deployed — internal QA uses one, the client the other. They are tested in parallel, not one after the other, so the testing branch is not an approval step. The only gate is the one before production.
- Unless the client needs a stable environment. When a client is doing formal acceptance testing, a half-finished work item landing mid-test makes their testing unreliable. In that case hold the merge into the testing branch, and say so on the work item so nobody merges it by habit.
- The work item assignee merges their own Pull Requests into the working branch and the testing branch. A merge commit is authored by whoever performs the merge, so this keeps their name on the whole of their work, not only on the change itself. They need write access on the repository for this.
- The Pull Request into production is merged by whoever is responsible for releases, not by the assignee. That merge is the release, and it is the one gated by the client's approval.
- A Pull Request opened by one person and merged by another is normal here. Don't treat it as a mistake to correct.
Work that depends on something not yet in production
This is the exception to starting from the production branch. Sometimes a fix or a feature builds on another work item that is still sitting in the working branch, not yet released.
Convention
- When your work needs a change that is not in production yet, create your branch from the branch of the work item you depend on, not from the production branch. Building on production code that lacks it simply won't work.
- Those two work items can no longer move on their own. Promote them together, and promote the one you depend on first.
- Say so on the work item, so whoever approves the promotion knows the two travel as a pair.
- Prefer to avoid this. If the two pieces of work are that tightly bound, it is usually better as one work item.
One work item, one branch, one commit
The commit that lands on a long lived branch is the unit that everything later works on: it is reviewed, promoted, traced and reverted as one piece. That only works if a work item is one commit.
Convention
- Aim for one commit per work item, and squash the branch down to one commit before the first push. That is the only moment squashing is allowed, because rewriting a branch after it is pushed is banned anyway.
- Merge every Pull Request with a plain merge. Never use "Squash and merge". A squash creates a brand new commit on the target that does not exist on the branch. The next target then merges the branch's original commits instead, so the same change ends up under two different commit IDs — the exact problem that using one branch was meant to avoid.
- A commit added after the first merge stays its own commit. Work that comes back from testing is normal, and it is not worth breaking the rule above to force the work item back down to one commit. Two commits with the same IDs in every branch are better than one commit with a different ID in each.
- The work item ID goes in the branch name and in the commit subject, so the change can be found later:
git log --all --oneline --grep "PROJ-123"- Don't put two work items in one branch. They cannot be reviewed, promoted or reverted separately after that.
Working on several work items at once
A branch lives until it reaches production, so several branches are usually open at the same time. That does not mean several are being worked on at the same time — a branch waiting for testing or approval needs no working copy at all. Only the one being written does.
Convention
- Prefer to finish one work item before starting the next in the same repository. Working on several repositories at once costs nothing. It is two work items in one repository that is expensive.
- One working copy can only be on one branch, and switching disturbs whatever else is in that copy. So when two work items genuinely have to be open in the same repository, add a git worktree for the second instead of switching branches:
git worktree add ../myrepo-PROJ-123 feat/PROJ-123-multi-shift-support- Don't run
git switchinside a worktree. When you need another branch, create another worktree. - Git refuses to check out the same branch in two worktrees. That is a safeguard, not a problem.
- Remove the worktree once the first Pull Request is merged — not when the work item reaches production:
git worktree remove ../myrepo-PROJ-123The branch stays on the remote, and the later Pull Requests into the other targets are opened from the web interface, which needs no local copy. A long lived branch does not need a long lived worktree.
- Count the cost before adding a worktree. For a web application a second working copy is a second running system, not just more files: its own port or virtual host, its own installed dependencies, and often its own database.
- Keep the per worktree settings in a file the repository ignores, such as
.env, loaded relative to the worktree's own directory. Each worktree then points at its own database and its own address without any tracked file changing. - A shared local database only works when nothing environment specific is stored in it. Some applications keep their own base URL in a settings table, and build session cookie names and request routing from it. With one shared database every worktree reads the same address, and login and routing break in all but one of them. Those applications need a database copy per worktree, with the address row set to that worktree's own URL.
- Even when the database holds nothing environment specific, a work item that changes the schema needs its own copy. Otherwise it changes the schema under the other work items while they are still being tested.
- When that setup costs more than the parallel work saves, don't do it. Finish one work item at a time instead. Worktrees are a workaround: when parallel work in one repository becomes normal, a container based local environment per branch, or an environment deployed automatically for each Pull Request, is the better answer.
Name the branch before the first push
A branch name looks easy to change later. It is not, once a Pull Request is open.
Renaming a branch that already has an open Pull Request closes that Pull Request. The PR keeps pointing at the old name, which no longer exists. GitHub moves the base branch of a PR, but never the head branch. A closed PR cannot be reopened once its head branch is gone, so the only way forward is a new Pull Request, with a new number and none of the review comments.
Convention
- Check the branch name against the Code Repository page before the first
git push. - Never rename a branch that already has an open Pull Request.
Don't rewrite a branch after pushing
Once a branch is on the remote, other people and the Pull Request itself depend on the commits being where they are. Here the branch also stays alive across several promotions, so more than one Pull Request may be pointing at it.
Convention
- After the first push, don't use
git commit --amend,git rebase, or anything that needsgit push --force. - Fix a mistake with a new commit on top.
- This applies to a temporary branch too, not only to long lived branches.
Pull Request
A Pull Request is where the change is explained and reviewed. The work item holds the background; the Pull Request holds what a reviewer needs in front of them.
Convention
- Title is the same as the commit subject, so the list of open Pull Requests reads like a list of changes.
- When a project has environment branches, start the title with the target branch in square brackets, because the same branch produces several near identical Pull Requests:
[staging] fix(order): [PROJ-123] correct the order total when a voucher is removedThe commit subject itself does not get this prefix.
- Body has these sections: Problem, Root cause, Fix, Testing. Keep each one short.
- Don't add a tool attribution footer, such as a "Generated with" line.
- Read the list of changed files before asking for review. A file you didn't touch means the branch was created from the wrong branch, and the branch has to be redone.
- Delete the branch only after it has reached its last target. Deleting it after the first merge would leave nothing to promote.
Repositories with environment branches
Here each long lived branch is deployed to its own environment. A work item is approved for one environment before it moves to the next, so the branches hold different amounts of work: the working branch holds the most, production the least.
What keeps this workable is that they stay nested. Everything in production is also in testing, and everything in testing is also in the working branch. They are never equal, because the working branch always holds work that is not approved yet, but nothing exists in an earlier branch that is missing from a later one.
Convention
- Never commit on a long lived branch.
- Never merge one long lived branch into another. That is what breaks the nesting and drags unapproved work forward.
- Create the branch from production, and merge that same branch into each target as the work item moves. Don't copy the commit with
git cherry-pick— a copy has a different commit ID, and then no command can tell you where the change actually is. - Open the Pull Request for every target early, and make only the production one a draft. The Pull Requests into the working branch and the testing branch are opened ready for review, whatever the work item status is — they are the everyday flow, and QA cannot move the status until the change is on the shared development environment.
- Only the production Pull Request waits as a draft, because that is the one that must not be merged by accident before the client approves. A draft cannot be merged, so the gate holds by itself.
- Mark the production Pull Request ready for review when the work item reaches the status for it. An approval for testing is not an approval for production.
- Opening all of them early also means GitHub shows a conflict as soon as it appears, instead of on the day of the release.
- The branches not yet merged into production are the list of work in flight. Sorted by date, it is also a staleness report:
git fetch origin --prune
git for-each-ref --no-merged=origin/main --sort=committerdate --format='%(committerdate:short) %(refname:short)' refs/remotes/originUse it to find work that fell out of the work item tracker, and to spot branches that were abandoned and should be deleted.
Hotfix
A hotfix repairs something already broken in production. It skips the usual order and goes to production first, which is the only thing that makes it different.
Convention
- Create it from the production branch, the same as any other branch.
- Merge it into production first, then into the testing branch and the working branch as well. A hotfix that lives only on production breaks the nesting, and the bug comes back the next time an older branch is promoted.
- Keep it small. A hotfix skips the testing the other branches would have given it.
Repositories with one environment
An internal tool, a shared library or a documentation site is deployed to one place, and nobody approves its releases step by step. There is nothing to promote, so there is one long lived branch and it is the only target.
Convention
- Use a temporary branch and a Pull Request when the change is large, touches code other people are working on, or needs review before it lands. A Pull Request is preferred whenever there is doubt.
- Small and low risk changes may be committed on the long lived branch directly.
- Delete the branch after the merge. There is only one target, so there is nothing left to promote.
- Everything else on this page still applies: one work item, one branch, one commit, the ID in the branch name, and the name settled before the first push.