This article describes a Git methodology that I developed managing software development.
I successfully applied these approaches in different kinds of teams: cross-functional, product-based, and outsourcing teams. Team sizes ranged from three to twenty people.
Main benefits and problems it solves:
Stable releases and stable
master.
Reducing risks by stabilizing the code that gets merged intomasterright before a release.Synchronizing Git with planning.
Changes in Git are explicitly linked to issues in the issue tracker. This makes it easy to find the reason behind any change in the code history and makes releases more predictable.Synchronizing Git with deployment.
A clear link between the Git history, build artifacts, and production instance tags simplifies incident management.A branching canonical Git history.
The history graph gives you overview the current state of development or the cost of a project without using time tracking.
Abstract
The process is built on the feature branch workflow: the master branch is always stable, and there is one branch per task.
Releases use tags created from the master branch. Tags are named using SemVer.
To keep master stable, Release Candidate (RC) branches are used. All the changes that are going to be released live in the current RC branch. RC branches go through a stabilization phase before being merged into master.
The branch for the next release, like every other branch, is always created from the up-to-date master. The team always works with the latest version of the code.
Changes for an individual task are not merged into master directly but through the RC branch. For code review, a Merge Request (MR) is created from the task branch into master. Each MR is closed automatically when the Release Candidate is merged into master.
If needed, you can also apply the green trunk practice. When several tasks share code that does not affect users, it can be merged directly into master, bypassing the release cycle. Such situations should be treated as exceptions and require extra control over the changes being merged.
Prerequisites
Besides the requirements described below, CI/CD automation is implied. However, the lack of automation does not prevent you from adopting the process.
It is recommended to have the following pipelines in CI/CD:
- automatic builds of fixed environments from their corresponding branches;
- automatic build of an environment for an individual task on demand;
- running static code analyzers and automated tests in the MRs of individual tasks.
Issue tracker
Since the process relies on a close link between the change history and planning, you need an issue tracker that automatically creates a short, human-readable identifier for each task or issue.
Common trackers such as Jira or YouTrack provide a short auto-increment task ID out of the box.
You can also use the Issues functionality in GitHub, GitLab, or Gitea. In that case, you can use either the local or the global issue identifier for the branch name (for example, 42 or acme/service#42).
Tools like Trello, Asana, or Notion probably won't fit the process, because they don't generate a readable unique ID for a task.
Testing process
The methodology gives you flexibility in how you organize the testing process.
The most important thing is testing the release branch. This step cannot be skipped. When you test the release branch (or release candidate, RC), you stabilize the code that will end up in master and, later, in production.
Besides release testing, you can also test a task in isolation or use a shared test environment. The table below lists all the possible environments for testing.
| Environment | Description |
|---|---|
<issue_id> |
Environments for testing an individual task in isolation. + No false-positives caused by overlaps with other tasks. - Extra requirements for server resources and infrastructure that can automate environment creation (for example k8s or Coolify). |
test |
A shared test environment, the least stable one. + Cheap and simple to set up. - Problematic changes can break the test environment for everyone on the team. |
stage |
The code of the last release with test dependencies. Can be used to debug production issues on test data. |
rc-<version> or rc
|
An environment for testing the changes that go into the next release. It must be used during development. |
The release process
First, you put together the list of tasks that go into the release. The selected tasks must meet the following requirements:
- The code has passed review in the Merge Request (MR) for that task.
- The MR has no conflicts with
master. - The changes for the individual task have been tested.
Based on the task list and following SemVer (Semantic Versioning), you determine the number of the next version. Using that version number, you create a new Release Candidate (RC) branch named rc-* (for example, rc-v1.5.24).
With the task list and the next version number in hand, you can create a release entity (explicitly or just as a parent task). This step is optional, but it improves the connection between the tracker and Git.
The release branch rc-* is created from the up-to-date master. It is best to explicitly sync master before creating the release branch:
git checkout master
git pull
git checkout -b rc-<next_version>
When the RC branch is created, you go through the task list and merge each task one by one. If merge conflicts come up, you resolve them on the spot, in the RC branch. After merging the branches of all required tasks, you sync the RC branch with the remote repository.
git merge ABC-42
git merge ABC-73
git push -u origin rc-<next_version>
The next step is stabilizing the release. You test the release on a fixed or dynamic environment. If you find problems, they should be fixed in the branch of the corresponding task and then merged into the RC again.
There may be cases where a fix cannot be tied to any specific task. In such cases, it is acceptable to make changes directly in the RC branch.
Once the RC branch is stable, the changes can be merged into master. At the moment of the merge, all MRs of the release's tasks are closed automatically.
git checkout master
git merge rc-<next_version>
git push
Immediately or some time later, a new tag is created from master. The tag name is used as a key in the project's build artifacts, for example in Docker image tags.
git tag -a "<next_version>" -m "<next_version>"
git push && git push --tags
Deploying the artifacts linked to the Git history is done automatically in CI/CD or manually.
The target change history
The diagram below shows an example of a Git commit graph between two releases. The example reflects the main ideas: asynchronous work in feature branches and stabilizing the RC branch before merging into master.
Git configuration
Every developer should have the following lines in their ~/.gitconfig file:
[merge]
ff = false
[pull]
rebase = true
- Disabling fast-forwarding merge is used to maintain the canonical structure of the history.
- Automatically using
rebaseonpullhelps avoid "loops" in the history when you fetch changes from the remote repository that differ from your local ones.
A Git hook for commit messages
Having a link to the task in the commit message makes investigating the reasons behind changes with git blame much easier. After a few releases, a single commit gets many parent branches, and it becomes hard to find the original one.
An issue identifier in the commit message links every line of code to a specific task.
To avoid mistakes and forgetting to reference tasks, this should be automated.
Here is an example .git/hooks/prepare-commit-message script that adds an issue reference to the commit message:
#!/bin/bash
COMMIT_MSG_FILE="$1"
COMMIT_SOURCE="${2:-}"
BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
if [[ -z "$BRANCH_NAME" || "$BRANCH_NAME" == "HEAD" ]]; then
exit 0
fi
PREFIX="$BRANCH_NAME"
if [[ "$BRANCH_NAME" =~ ^[0-9]+$ ]]; then
PREFIX="#$BRANCH_NAME"
fi
FIRST_LINE=$(head -n1 "$COMMIT_MSG_FILE")
if [[ "$FIRST_LINE" == "$PREFIX "* ]]; then
exit 0
fi
sed -i.bak -e "1s|^|$PREFIX - |" "$COMMIT_MSG_FILE"
Git working rules
-
Commits
- Commits should be as atomic as possible. 1
- Many small commits are better than one big one.
- A commit message should describe the changes it contains.
- A commit message should contain a link to the issue. 2
- In a task branch, it is acceptable to commit and push non-working code marked with
WIP(Work In Progress). - It is preferred to keep
WIPcommits in the history. 3
-
Creating feature branches
- Each task gets a unique branch for making changes.
- A task branch is always created only from the up-to-date
master. - The branch name should exactly match the task identifier in the tracker. 4
- Several developers can work on the same task branch at the same time.
- If the same new code is needed in two different task branches, it is preferred to use
git cherry-pickor to push the shared code through the release cycle. - It is not allowed to merge task branches into each other.
-
Merging branches
-
git mergeis always used to merge branches. - Merging branches must create a merge commit.
- When merging branches, all previously created commits stay in the history unchanged (forever).
-
-
Merge Requests
- Before changes from a task branch are integrated into
master, a Merge Request (MR) must be created. - Before a task gets into the release build, the code must pass review.
- If an MR has conflicts with
master, they must be resolved by mergingmasterinto the task branch. 5
- Before changes from a task branch are integrated into
-
Integrating into
master- Changes from feature branches get into
masterthrough Release Candidate (RC) branches. - It is acceptable to make changes directly in the RC branch. 6
- Changes from feature branches get into
Translated from Russian original.
-
A good time to create a commit is when you finish a separate logical block of work. It is implied that a single commit can be rolled back with
git revertor moved to another branch withgit cherry-pick. ↩ -
Adding the issue reference should be automated with a Git hook. ↩
-
Squashing commits and other history rewriting while working in a task branch is acceptable when the following conditions are met: (1) no more than one developer works in the task branch; (2) the branch has not been merged into any other; (3) no other branches have been merged into it. To avoid problems with the Git state, it is better not to rewrite the commit history. ↩
-
It is preferred to limit the branch name to just the task identifier and not use
feature,fixprefixes or a description suffix. Unlike the identifier, such prefixes and suffixes are subjective. ↩ -
To resolve conflicts, you need to sync with the up-to-date
master, then rungit merge masterin the task branch and resolve the conflicts if needed. This will create a merge commit frommasterinto the task branch. If there are conflicts withmaster, the task cannot be included in the release build. ↩ -
In Release Candidate branches it is acceptable to make changes that are not tied to any specific task. This can be meta-information changes (changelog, package version) or fixing hidden integration conflicts between two different task branches. ↩

Top comments (0)