How to git

A full guide based on Atlassian branching model

Using the git-workflow method, in this document we explain some best practices and (naming) conventions on how to keep things organized and ‘easy’.

In this document, there are multiple references to git push --force related to rebasing on branches other than develop or master, please try to use git push –force-with-lease, or, when using my gitconfig, use git please :)

Scroll to the bottom for a TL;DR

Branching

Master and develop

In our repository we have both the master develop branches. master as main branch. Master is used to deploy to production. Develop serves as an integration branch for new features.

Feature, bug and maintenance branches

During development, we’d like to refrain from working directly on the master and develop branches. Instead, we’d like you to use feature branches, based off of develop. Each feature should exist in its own branch. Feature branches should use develop as their parent branch. When a ticket is assigned to you and you are going to start working on it, please make sure develop is up to date with the latest commits and create a feature branch first (git checkout -b) using this convention: feature/123-short-ticket-description. This indicates it’s a feature, the ticket number and a description on what it is about. Bug branches have a similar convention, but then use bugfix/123-short-ticket-description instead of feature/. For maintenance / support the same applies. Do not use bare numbers! This offers a few advantages:

  1. If you need help on a ticket, the person who’s helping you can easily find the branch and check it out.
  2. It is easier to get an overview of branches based on ticket type: git branch --list "feature/*" or git branch --list "bugfix/*".
  3. In an exceptional circumstance, multiple branches of the same ticket could exist.

Release branches

Once develop has enough features for a release, you create a new release branch off of develop. Creating this branch will start the next release cycle, so new features can be added after this point. Even if a release needs to be created from an earlier feature, you can always reset back to a certain commit in history.

Only documentation, bugfixes (whilst not preferred) should go into this branch. Once it is ready to ship, the release branch gets merged into master and develop and should be tagged with a version number (both explained below). It’s important to merge back into develop because critical updates may have been added to the release branch and they need to be accessible to new features.

Note: Using git-flow all this will be done for you as soon as you utilize the git flow release start command.

Committing your work

For commits, please make sure you reference the complete ticket number (#123) and add a clear description on what the commit is about: #123 - implementation of specific feature. If you need to commit multiple things which are related, please use git commit --amend --no-edit (or git commend if you use our gitconfig) - this adds the change to the latest commit (HEAD) without creating a new commit and commit message. Please note that you change history this way and it requires a git push --force. sidenote: force pushing to master and develop should not be enabled, it should only be possible in feature branches

Rebasing vs merging

To get a clear overview of the differences, please check the following link: https://www.atlassian.com/git/tutorials/merging-vs-rebasing

Merge explanation

Merging is nice because it’s a non-destructive operation. The existing branches are not changed in any way. This avoids all of the potential pitfalls of rebasing (discussed below). On the other hand, this also means that the feature branch will have an extraneous merge commit every time you need to incorporate upstream changes. If master or develop is very active, this can pollute your feature branch’s history quite a bit.

Rebase explanation

The major benefit of rebasing is that you get a much cleaner project history. First, it eliminates the unnecessary merge commits required by git merge. Second, rebasing also results in a perfectly linear project history. You can follow the tip of feature all the way to the beginning of the project without any forks. This makes it easier to navigate your project with commands like git log

Rebase is the way to go

As we’d like to keep everything clean and readable, rebase is the choice of having a clean history in the git log. In the beginning it will take some getting-used-to.

Interactive rebasing

A cleanup of your commit history (interactive rebasing)

Having a history with many commits which do not have a decent commit message will not make any sense: you don’t know what it is about and what has changed. There are a few ways to prevent this from happening and if it happened - how to clean it up (using git rebase -i).

Prevent your mess: amend without editing the latest commit message

To prevent new commits being added all the time (note: only if they are basically the same, or the addition is not big enough to have its own commit) you can use git commit --amend --no-edit. This adds your staged change to the latest commit (HEAD) without changing the commit message. Congratulations, you’ve rewritten history! Don’t forget to force push your change (git push -f).

Cleaning up your commit message mess

So you were not able to contain the mess beforehand, or you work in somebody else’s feature branch, you need to clean up the history. This can be achieved using git rebase -i. A step by step plan:

  1. pull the latest feature branch changes (git pull on your feature branch)
  2. Fetch the git log (git log OR if you want a clear overview: git log --graph --pretty=format:'%Cred%h%Creset %C(yellow)%an%d%Creset %s [%N] %Cgreen(%ar)%Creset' --date=relative)
  3. Determine from which commit hash you’d like to start rebasing
  4. Let’s say we want to start from bd1b4d1: git rebase -i bd1b4d1

We now get an overview which looks like:

$ pick 9010820 refs #591 added a reset option to every store module and created a clearstate method in the vuex root index.js so we can use each module mutation seperately
$ pick 0bb292e refs #591 fetch current user permissions and store them in account, handle front interactions based on these permissions
$ pick d74b147 refs #591 made decent use of store getters, removed RoleUtils as everything is done through getters now
$ pick 3915b49 refs #591 squash later: projectmembers api integration and temporarily disabled fetchTasks for quarantine
$ # Rebase bd1b4d1..3915b49 onto bd1b4d1 (4 commands)
$ #
$ # Commands:
$ # p, pick = use commit
$ # r, reword = use commit, but edit the commit message
$ # e, edit = use commit, but stop for amending
$ # s, squash = use commit, but meld into previous commit
$ # f, fixup = like "squash", but discard this commit's log message
$ # x, exec = run command (the rest of the line) using shell
$ # d, drop = remove commit
$ #
$ # These lines can be re-ordered; they are executed from top to bottom.
$ # If you remove a line here THAT COMMIT WILL BE LOST.
$ # However, if you remove everything, the rebase will be aborted.
$ # Note that empty commits are commented out

First thing you will notice is that the list is turned around: your latest commit is now at the bottom. Rules are executed from top to bottom. Please read all the commands carefully when executing this.
In this case we’d like to fixup (fixup because we can disregard its commit message) the latest commit, so in our rebase it will look like this:

pick 9010820 refs #591 added a reset option to every store module and created a clearstate method in the vuex root index.js so we can use each module mutation seperately
pick 0bb292e refs #591 fetch current user permissions and store them in account, handle front interactions based on these permissions
pick d74b147 refs #591 made decent use of store getters, removed RoleUtils as everything is done through getters now
f 3915b49 refs #591 squash later: projectmembers api integration and temporarily disabled fetchTasks for quarantine

Save this and you will see git applying the results, which will show refs #591 made decent use of store getters, removed RoleUtils as everything is done through getters now as your latest commit message (but containing the fixed up commit). Don’t forget to force push your changes.

Note: Only do this on branches other than master or develop!

Merge / pull requests

For the contributor

So you’ve finished your ticket, all tests pass and you decide to create a merge request? Good. Here’s a few key things about merge requests:

  1. Only create a merge request for functionality that is in your perspective done and matches the Acceptance Criteria of the ticket (so please no WIP: merge requests as this makes no sense, ticket progress can and should be monitored in the ticket itself. Update your tickets!).
  2. Make sure the latest develop (checkout to develop and git pull first) is in your feature branch by doing a git rebase develop in your feature branch. You will either see your commit(s) being placed on top of master (possibly rewriting history if it isnt up to date and you should git push --force) or it will notify you it is already up to date. In the latter, no more action is required. Worst case, you’ll get a conflict which you will have to resolve first. Follow the onscreen steps if that’s the case (or contact Mycha with a request for help :))
  3. A default set of reviewers/approvers is added when you create a merge / pull request. If this is not the case, please check if all the appropriate reviewers are selected and add or remove where necessary.

Resolving conflicts

Conflicts can occur if you rebase onto your featurebranch and there’s a change in the same file. In my experience, PHPStorm has an excellent conflict resolver which will guide you through the process, even showing you a side-by-side overview of things that are changed on both branches. You can decide to pick one, or to merge them onto a single one which you will then have to commit.

Pull request approvals

Before a pull request should be merged, please make sure everybody who should approve also approves.

Merging a merge request

Merge requests can be handled manually by the user via commandline or via the bitbucket interface. Both should be done in a fastforward way.

To prevent mistakes, we prefer the merging of pull requests be done through the bitbucket interface.

Note: If you decide to do it manually: As the to-be-merged feature branch only contains new changes on top of master, you must use git merge to merge to master! Do not use git rebase to merge the feature branch. You should never ever force push to master as the possibility to accidentally delete history is a thing! You will not create a new merge commit as only the feature branch commits are placed on top!**

Tagging

When preparing for a demo or production release, a tag of the master branch should be created. Tags are used to mark a point in the commit history as important. A release is an important milestone, so in our git history it should be marked as such. It can also be used as a reference point (if a developer is looking for code from a previous release, this tag as a reference will limit the time spent on searching)

Tags should follow the semantic versioning convention, where it is always MAJOR.MINOR.PATCH.

  • MAJOR means it is an incompatible / non backwards capable change.
  • MINOR is when changes are added but they are (backwards) compatible
  • PATCH is used for compatible bug fixes

Note:

  • MAJOR version 0.y.z is used for development only. Please check SymVer specification #4.
  • Using git-flow we will not have to manually create tags. A release is started and finished, creating a tag.

In the full example below I also added a section on how to create a tag both manually and using git-flow

Forks

A fork is a copy of a repository. Forking a repository allows you to freely experiment with changes without affecting the original project. We mainly want to use forks for longer running projects.

Example from start to finish

Creating a simple feature branch, committing a new feature

$ git branch
* develop
  master
$ git flow feature start 123-new-cool-feature
$ git add path/to/file
$ git commit -m 'refs #123 added file to handle expected functionality'
$ git push -u origin feature/123-new-cool-feature

We are on branch develop and from the current tree, we checkout to a new branch called feature/123-new-cool-feature. Now we edited a file and we commit that file, push that to origin. (-u equals to --set-upstream because the feature branch does not exist on remote).

Updating our feature branch after parent develop is updated

Let’s say in the meantime develop was updated with new commits and we need to update our feature branch to avoid conflicts later on.

$ git branch
* feature/123-new-cool-feature
$ git checkout develop
$ git pull
Updating 63ee230..d761996
Fast-forward
 .dockerignore               |  1 +
 .bitbucket-pipelines.yml              |  2 +-
$ git checkout feature/123-new-cool-feature
Switched to branch 'feature/123-new-cool-feature'
Your branch is up to date with 'origin/feature/123-new-cool-feature'.
$ git rebase develop
First, rewinding head to replay your work on top of it...
Applying: refs #123 added file to handle expected functionality
$ git push -f

We are on our feature branch, now we are going to update the feature branch with the latest master changes by using git rebase (Please note that for the reviewer / person who merges to develop, this is required otherwise you will get merge conflicts on develop). After this you’ve updated history and a git push --force is required.

Merging our feature into develop using a simple git merge

$ git branch
* feature/123-new-cool-feature
$ git pull 
Already up to date.
Current branch feature/123-new-cool-feature is up to date.
$ git checkout develop
$ git pull
Already up to date.
Current branch develop is up to date.
$ git flow feature finish 123-new-cool-feature
Updating 63ee230..d761996
Fast-forward
 path/to/file               |  1 +
$ git push

Now we’ve successfully merged the feature branch into develop and now the feature branch will be closed / deleted. Note here we merge (and not rebase) to develop. Because the feature branch is up to date with develop, git performes a fastforward merge which will not create a merge commit.

Git flow release start

Once we are done with all our changes, all tests are created and passed, the code is production-ready and we would like to deploy soon, we are going to create a tag of the current situation.

$ git branch
develop
$ git flow release start 1.0.0
Switched to a new branch 'release/1.0.0'

Summary of actions:
- A new branch 'release/1.0.0' was created, based on 'develop'
- You are now on branch 'release/1.0.0'

Follow-up actions:
- Bump the version number now!
- Start committing last-minute fixes in preparing your release
- When done, run:

     git flow release finish '1.0.0'
$ git flow release finish 1.0.0
Switched to branch 'master'
Deleted branch release/1.0.0 (was 168c993).

Summary of actions:
- Release branch 'release/1.0.0' has been merged into 'master'
- The release was tagged '1.0.0'
- Release branch 'release/1.0.0' has been locally deleted
- You are now on branch 'master'
$ git push origin develop master 1.0.0

Creating a tag, ready for production, the manual situation

$ git branch
develop
$ git checkout -b release/1.0.0
Switched to a new branch 'release/1.0.0'
// some final bugfixes can be pushed to the release branch
$ git checkout master
$ git merge release/1.0.0
// merge release/1.0.0 into master, also merge it into develop
$ git tag -a 1.0.0 -m "First major release of frontend!"
$ git push origin --tags
Counting objects: 1, done.
Writing objects: 100% (1/1), 160 bytes | 0 bytes/s, done.
Total 1 (delta 0), reused 0 (delta 0)
To [email protected]:examples/frontend.git
 * [new tag]         1.0.0 -> 1.0.0
$ git checkout develop
$ git merge master
$ git branch -D release/1.0.0
// delete your release branch as we already released.

TL;DR

  • Example of a good commit message for a bug gives either a technical description of the bug solution or “fixes ”.
  • Example of a good commit message for a feature gives a technical description of the feature that has been realised or a part of the feature.
  • Bug or features in a branch, prefixed with ‘bug/’ or ‘feature/’
  • When committing, use commend (commit --amend --no-edit) whenever possible (if the bitbucket ticket is the same and the piece of code is related) - or, if you want to adjust the commit message but still keep everything in the latest commit: git commit --amend will prompt you to change the commit message.
  • After amend or rebase, you must push --force
  • After merging branches to master, you cannot change the latest commit on the feature branch (create a new commit on the (feature|bug) branch or just create a new branch), because you cannot push --force to develop
  • Merging of feature branches to develop require a succesful pipeline execution and necessary approvals
  • When preparing for a release and all merges are complete, please tag the latest commit used in the release.

See also