Git workflow
We primarily have two separate workflows, depending on what you need.
If you do not need to backport your changes, the Standard workflow should be a good starting point.
Jump to the FAQ.
Standard workflow
If you only need your changes on master, the basic workflow will suffice. No need to complicate things further:
- Make sure
master
is up-to-date - Create a branch for your changes
- Implement and commit your changes, and repeat.
- Open a PR from your branch to
master
- Use squash-merge when merging to master We follow the Conventional Commits specification, so you need to make sure the squash commit message is valid manually.
Backport workflow
If you know that you are going to have to backport your changes to multiple versions of the code base you can save yourself a lot of trouble by basing your changes on the correct commit.
Too long; didn't read
- Figure out the target versions (should be stated in the JIRA ticket)
- Make sure the target version branches are up-to-date
- Check out the latest common ancestor between the branches
- Create a patch branch
- Implement your changes
- Create a feature branch from your target version branches
- Merge your patch branch into the target version feature branch
- Raise a PR from the feature branches to the respective version branch
- Use squash-merge when merging to master We follow the Conventional Commits specification, so you need to make sure the squash commit message is valid manually.
Too short; didn't understand
In general, if you are going to target version v1
, v2
, and v3
, you want to
find the commit that is the latest common ancestor. In this example
it would be the fork point between v1
and v2
.
Git can help you figure this out:
git merge-base --octopus v1 v2 v3
<commit hash>
git show-branch --merge-base v1 v2 v3
<commit hash>
These two commands are equivalent. Verify that the commit makes sense
through git show <commit hash>
on the commit that was output. Does it seem to be the
oldest possible commit?
If it looks good, check out that commit and detach your HEAD
:
git checkout <commit hash>
This is the commit you will base your branch on, let's give it a sane
name so we can keep track of it. This will be backported to v1
through v3
, so we are going to end up with feature branches for all
those branches, based on this patch branch:
dhis2-1234/my-feature-branch-v1
dhis2-1234/my-feature-branch-v2
dhis2-1234/my-feature-branch-v3
So we are going to use the -patch
suffix for this one:
git checkout -b dhis2-1234/my-feature-branch-patch
If you now start developing on this branch, it is easier to merge everything into the version branches.
Once your changes are committed to the patch branch, this branch can be used to merge your changes into the different target branches.
Sometimes it is possible to set up a PR from the patch branch straight to the target version branch, e.g.:
dhis2-1234/my-feature-patch
tov1
dhis2-1234/my-feature-patch
tov2
dhis2-1234/my-feature-patch
tov3
If that gives you conflicts you need to add an intermittent step, where you create a feature branch for each target version branch:
git checkout v1
git checkout -b dhis2-1234/my-feature-branch-v1
Then you merge your patch branch into the target version feature branch:
git checkout dhis2-1234/my-feature-branch-v1
git merge dhis2-1234/my-feature-branch-patch
You will get the conflicts at this point, and here you can safely resolve them and test that it still works. This is now the branch you will setup a pull request for to merge into the target version branch:
dhis2-1234/my-feature-branch-v1
tov1
dhis2-1234/my-feature-branch-v2
tov2
dhis2-1234/my-feature-branch-v3
tov3
The Conventional Commits rules apply, so when squash-merging you need to make sure you write a message which is compliant.
Being a good Gitizen
Here are some ideas on how you can be a good citizen with regards to Git.
Save your changes on a granular level
It makes sense not to clutter the master
/version branches with your
development commits, so you might be thinking about squashing your work
before pushing your branch to GitHub.
We use squash-merges when merging a branch to master
, so there is no
need to squash your commits before pushing the branch for code review.
When reviewing code, it is sometimes useful to go through the branch commit by commit to see the feature's evolution, so it is helpful to not squash the commits before submitting it for code review for that as well.
When merging your branch after it being approved, you'll be asked to provide a new merge commit message, make sure you put in a Conventional Commits message.
Cleanup after your branch has been merged
We have several long-lived branches on GitHub as we use a strategy where
we have one branch per version of DHIS2 we support in addition to the
development trunk which is the master
branch. These should not be
deleted.
Once you've merged your branch, you'll be given the option to delete your remote branch on GitHub. Normally you should be able to just delete your branch, so do it.
Otherwise please keep in mind that your branch is still on the remote repository, and if it becomes stale, it might be deleted at any point.
You should never work on a branch that has been merged already.
If a ticket returned because didn't pass the review, create a new branch with the same ticket id and an updated description that reflects what you'll do on that branch.
This means that you can also delete your local branches once you've merged your pull request if you do not need a local copy for a while.
FAQ
What is the convention for commit messages, and also, why?
We use the Conventional Commits to structure our commits in a uniform way, and the why is a big part of this.
When all commits follow the specification we get two massive wins in exchange for a minor annoyance.
-
Auto-generate changelogs
Change logs are a very important part of software documentation, and it is extremely easy to omit during the development process. It is also difficult and time-consuming to create an accurate changelog for software as time passes. For example, the manually updated changelog in d2 lacks updates between 2017 and 2019, but looking at the commit history a lot has happened in that timespan.
By autogenerating the changelogs we can integrate it into the release scripts to always have it up-to-date.
Example: ui/CHANGELOG.md
-
Automatically determine the correct semantic version
Should the release be a patch, minor, or major version? To know you have to go through everything that has changed since the last version and understand if it is a breaking change, a new feature, or a bug fix.
This is also time-consuming and very sensitive work as an incorrect bump will have consequences for the consumers of the library or application.
By following the Conventional Commits specification we can infer what version should be the next version from the Git history itself which removes the human factor from the equation when determining the next version bump.
The price of these two wins is to write commit messages complying to a standard, i.e. a developer needs to learn and understand the specification. Thankfully, this is a rather easy spec to understand and in the front-end code bases we have an automated tool to make sure you comply.
The structure of a commit message is this:
<type>[optional scope]: <description>
[optional body]
[optional footer]
The Conventional Commits site has examples and the rules documented, but the cheat sheet is this:
-
fix:
a commit which fixes a bug and will result in aPATCH
version change. -
feat:
a commit which introduces a new feature and will result in aMINOR
version change. -
BREAKING CHANGE:
if the commit message has the textBREAKING CHANGE:
in its optional body/footer part, the version will change to the nextMAJOR
version.A
BREAKING CHANGE:
commit can be of any type (fix:
,feat:
, etc.). -
There are additional types allowed, for example
chore:
,ci:
,docs:
, etc.
Why is cherry-pick
a problem when used to port code between branches?
This article outlines problems with
cherry-pick
ing.
It is recommended material to read and understand.
We have seen instances where these problems happen in our code base, and
until we understood the problem avoided using cherry-pick
ing as a
backport workflow. Since then, we have learned what was the root cause
of the problems and are no longer strict to avoid cherry-pick
.
So, when is cherry-pick
ing a commit considered safe?
A fundamental requirement for safe cherry-pick
ing commits is that the
branches that are used as base and the target will never be
combined.
As long as the branches are kept separate the problems outlined in the article above can be avoided. This requires knowing upfront that branches will never, at any point, be combined in the future.
Do we have a convention for our branch names?
Yes. If you have a JIRA ticket that relates to the changes you are making, you will need to reference that ticket somehow.
Get into the habit of prefixing your branch with the ticket key and then you don't have to think about it. Some examples below.
git branch dhis2-1337/my-feature-branch
git branch DHIS2-1337/my-feature-branch
git branch dhis2-1337_my-feature-branch
git branch DHIS2-1337_my-feature-branch
After the ticket key prefix it is good to include a string that describes the changes, as it is common to split a feature into multiple branches and pull requests which would make just using the ticket key as the name unwieldy.
git branch dhis2-1337/initialise-new-app
git branch dhis2-1337/add-routing
git branch dhis2-1337/refactor-action-creators
This also helps when backporting:
git branch dhis2-1337/my-bug-fix-branch-v33
git branch dhis2-1337/my-bug-fix-branch-v32
git branch dhis2-1337/my-bug-fix-branch-v31