A simple Merge Git workflow

5 minute read

In this post I explain the Git workflow I’ve found most useful, easy, clean and safe to work with using only git merge.

The Git repository should have a trunk branch that I will refer to as Master, which should be as clean as possible and working at any moment. Then, a new branch should be created for each feature, bug fix or improvement. I will refer to this branch as Topic. I will show an example of the workflow with Gitlab as the remote and one without. Instead of Gitlab, I imagine it would work the same way with Github or similar.

I won’t discuss whether you should have a Develop and a Master branch, or only Master, or a different branch for each release. In the end, this will depend on the kind of software you are developing and how you release it.

Here I focus on the most basic part of any workflow, which is not usually discussed in detail: when to create a new branch, how to update it when Master gets ahead and how to merge it with Master.

In a nutshell

  1. Create a Topic branch from Master for each new feature, improvement, bug fix,etc. Start developing.

    git checkout Master
    git checkout -b Topic
    
    • (optional): if during development Master gets new commits, you might want to update Topic with a merge:
      git merge Master
      
  2. Once development is finished, merge Master to ensure any new commits in Master are applied to the Topic branch.

    git merge Master
    

    You can also do this many times during development, but it’s important to do it once finished.

    This is necessary as the Master branch should be as stable as possible, so integration should be tested first in Topic.

  3. Create a merge request in Gitlab, tick the “Squash commits” option and assign a reviewer. The review should happen shortly, in case other branches are merged to Master. If that happened, Master should be merged again to the Topic branch. It’s very important that merges to Master are as clean as possible.

  4. The reviewer accepts the merge request. Make sure the “Squash commits” option is ticked and the “Delete branch” unticked. We want to keep Topic’s history at least for some time.

Manual equivalent with merge --squash

If you don’t use Gitlab as the remote, you can achieve the same manually:

  1. Same as before
  2. Same as before
  3. Create a temporary branch from Master
    git checkout Master
    git checkout -b merge_squash
    
  4. Use a merge --squash to squash the Topic branch
    git merge --squash Topic
    git commit
    
  5. Merge the squashed Topic to Master and delete the temporary branch
    git checkout Master
    git merge --no-ff merge_squash
    git branch -d merge_squash
    

Example

Here we can see a graphical example. Workflow

  • The blue circles are Merge commits in Master
  • The green circles are commits in a Topic (Feature) branch
  • The orange circles are squash commits created by Gitlab
  • The orange rectangles are Merge Requests in Gitlab
  • Each vertical line represents a unit of time.

Explanation: one developer starts branch Feature A. At the same time, another starts Feature B. When A is finished, the developer creates a Merge Request in Gitlab which is reviewed and accepted by another developer. Gitlab squashes the commits A1 and A2 in a single commit (Squashed A) and creates a new merge commit in Master (Merge A into Master).

The other developer is still working on B. When he is finished, he sees Master has advanced, so he merges Master to Feature B and resolves potential conflicts. Finally, he creates a Merge Request, which is reviewed and accepted.

As you can see in the picture, it’s a very simple workflow.

Here’s the manual equivalent with merge --squash. The result is exactly the same: Workflow

Reasoning

This is my preferred Git workflow for the following reasons.

First, the whole project history is kept while having a concise Master history at the same time.

Gitlab merges Topic branches to Master as two commits. The first combines every commit in the Topic branch. The second is the merge commit, which doesn’t bring any change but is useful to make the merge explicit in the history, and to show who accepted the merge request.

This way, the Master history is clean because there’s only two commits for each feature. The first commit contains the changes and indicates the author of the feature, while the second commit indicates who merged the feature. This makes clear who was involved.

If we ever want to examine the history in detail, we can do so by checking out the relevant Topic branch, whose full history is kept in the remote (at least for a reasonable time, but it could be wiped some time later).

The second reason is that the workflow is very safe. Conflict resolutions with merge are easier than with rebase, as conflicts appear all together instead of separated by commit. Since rebase is never used, it’s never necessary to git push --force-with-lease (do not use --force), which is a destructive operation.

The third reason is that it’s possible to know when a Topic branch started, i.e. its first commit and its parent:

first=$(git log --pretty=%h Topic ^Master | tail -1)
parent=$(git log --pretty=%p $first)

Possible mistakes

The workflow is very safe, but the only probable mistake one could do is to forget to tick “Squash commits” in the merge request. This will add every Topic commit to Master, which will pollute history and cannot be undone. It’s unlikely that this will happen because the option should be checked by both the merge requester and the reviewer. In any case, the mistake is not destructive, so one shouldn’t worry if it happens.

Workflows that don’t work

During my research I tried to find different ways to replicate the effects of a merge request manually. I tried to use git rebase --interactive and git reset --soft to replicate the squash effect, however, they all have the same problem: they don’t retain conflict resolutions from Master to Topic. This means that resolved conflicts will appear again when merging Topic to Master. Example:

git checkout -b topic_squash
first=$(git log --pretty=%h Topic ^Master | tail -1)
root=$(git log --pretty=%p $first)
git rebase --interactive $root
git diff Topic topic_squash # should have no differences
git rebase Master # conflicts will appear again here
git checkout Master
git merge topic_squash

Categories:

Updated:

Leave a comment