Skip to content

Instantly share code, notes, and snippets.

@arempe93
Created July 25, 2018 14:39
Show Gist options
  • Save arempe93/accd3b0d441d93412d62646a46f21658 to your computer and use it in GitHub Desktop.
Save arempe93/accd3b0d441d93412d62646a46f21658 to your computer and use it in GitHub Desktop.
A guide of my git practices

Git Guide

This document is meant to serve as a repository of best practices for large, multi-environment source control management.

Table of Contents

  1. Introduction
  2. Rules of the Trunk
  3. The Basic Process
  4. Creating a topic branch
  5. Releasing 1. Preparation 2. Rebasing 3. Resolving conflicts 4. Cleanup 5. Updating topic branches 6. Tagging
  6. Managing Environments
  7. Introducing topic branches
  8. Reset, rebase, re-merge
  9. Tips and Tricks
  10. Git setup
  11. Automated reset
  12. Rewriting history

Introduction

The first thing to understand when trying to make a clean git project is that there can only be one trunk. Most commonly this branch is referred to as master, the default branch name in git. This branch serves as the source of truth in your project. Everything should be based on your trunk, and defer to your trunk when messes are made. When this idea is employed correctly, it gives us a lot of power to manage our codebase.

Rules of the Trunk

To properly use a trunk branch, a couple of rules need to be followed.

#1 - All branches are based off the trunk

The first rule is clear: all branches must use the trunk as a base. This is fairly obvious when making topic branches - everyone should already be branching off master to make features, bug-fixes, etc. The emphasis however, is on all branches, including environment specific branches, such as develop. This takes a lot of care and planning from the developer, but can be done, and will be explained in depth later on.

#2 - The trunk should never be merged into a branch

The second rule is a repeat of the first, in essence. If the first rule is always true, a merge of the trunk into the branch would be a no-op. But the trunk can be updated and topic branches can fall behind. The point of this rule is to enforce the use of rebase over merge when dealing with the trunk. If you instead merge trunk into your topic branch, the first rule will be broken, as your topic branch's history is merged with your trunk's history, rather than being based upon it.

#3 - The trunk should only contain commits from a single parent

The last rule enforces a linear history in your trunk branch. "Commits from a single parent" means no merges commits. Merge commits have two parents, one from both branch. This rule is designed to keep the history of the trunk branch clean, as it serves as the source of all branches.

The Basic Process

The basic git process for most projects uses master for production releases, other branches such as develop for environment releases, and topic branches named for the JIRA issue ticket. For the basics, we will pretend the other environment branches do not exist, only the trunk and topic branches.

Creating a topic branch

According to the first rule, all branches should be based off the trunk. So, after you ensure that you have the latest version of master in your local repository, you should use the following command to create all topic branches:

git checkout -b topic master

After you add a few commits to your topic branch, it should look something like the following:

              E---F---G  topic
             /
A---B---C---D   master

Clearly, the topic is cleanly based on master, no problems so far. So let's create another topic branch, for the next feature:

git checkout -b topic2 master

And after a few commits, we will have something like this:

              H---I---J   topic2
            /
            | E---F---G   topic
            |/
A---B---C---D   master

Both branches are still based off master, as running a quick git rebase master will confirm. Now we are done making changes, and want to release topic and topic2.

Releasing

Currently all releases are done through individual merges to master. But you might have noticed that this breaks rule #3 of dealing with trunks. The trunk branch should not contain merge commits, only fast-forward merges are acceptable. In order to achieve this, we create a release branch.

Preparation

First we create a release branch off our trunk:

git checkout -b release master

Then before we start the release process, we ensure all the branches we want to release (topic and topic2) are up to date with our trunk. Following the second rule, we do this with rebase:

git checkout topic
git rebase master

git checkout topic2
git rebase master

Rebasing

Now we need to get the commits from our topic branches, into the release branch. The release branch is a special branch, however, since it will eventually become a part of the trunk. This means we can also not merge to this branch either, as those commits will carry over into our trunk. So we instead use rebase. Using rebase, you can resolve commits from many sources into a linear history on a branch, without changing the topic branch.

git checkout release
git rebase topic2
git rebase topic

Resolving conflicts

Like doing a merge, a rebase can also have conflicts if two files are edited in different branches. This can cause some small inconvenience when creating a release branch. Let's pretend that the branches topic and topic2 both edit a shared configuration file. When doing the release rebasing process, a conflict appears:

error: could not apply sha12345...

When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".
Could not apply sha12345678901234567890... Changes configuration file in topic

From here is gives you two options to resolve: using rebase --skip or rebase --continue. Using rebase --skip is straightforward - it will skip this commit during the rebase. This would be useful if topic and topic2 both had made the same change to the configuration file, thusly only adding one commit for it in the history.

Otherwise, this is the same situation as a regular merge conflict. Git will tell you the files in conflict, and uses the same notation in the files to show you where. Once you resolve all the conflicts, the following commands will continue the rebase along.

git add .
git rebase --continue

Cleanup

Once all topic branches have been rebased into the release branch, you should have the following structure.

              H---I---J   topic2
             /
            | E---F---G   topic
            |/
            | E---F---G---H---I---J   release
            |/
A---B---C---D   master

From here you can now do a fast-forward merge from release to master (also pruning the branches):

git checkout master
git merge release
git branch -D topic
git branch -D topic2
git branch -D release

to keep a linear history in your trunk branch:

A---B---C---D---E---F---G---H---I---J   master

Updating topic branches

Let's pretend now that we had a thrid topic branch, topic3, that did not make it into this release. Giving us this structure:

              K---L---M   topic3
             /
A---B---C---D---E---F---G---H---I---J   master

After each release you want to ensure all branches follow the first rule. In order to do this, rebase the topic3 branch onto the updated trunk.

git checkout topic3
git rebase master

Any conflicts that need to be resolved become a part of the topic branch's history on a per commit level, as if it was always branched directly off the updated trunk. This keeps all changes sandboxed to their current branch. Now the structure should be:

                                      K---L---M   topic3
                                     /
A---B---C---D---E---F---G---H---I---J   master

Tagging

It should become a part of everyone's release strategy to use tags as snapshots of the trunk. This way it becomes a simple task to view the trunk at different parts of the project's lifespan. To tag the trunk, use the following commands:

git checkout master
git tag v1.0.0
git push --tags

Managing Environments

In theory, the above example is the complete process for development. We have a trunk with topics that, through a release process, eventually become a part of the trunk codebase. In reality, we have environments to maintain with different versions of the code to test against. The important thing to remember is, while these branches must follow the rules, they are not a trunk branch themselves.

Introducing topic branches

Since environment branches are not trunks, they are allowed merge commits. After each topic branch is ready to be introduced to an environment, you are free to merge the brach in normally. Resolving conflicts on the environment branch is also ok. These conflicts will be resolved again when you rebase onto the release branch later, but now you can be aware of potential problems.

Reset, rebase, re-merge

The most difficult part of managing environments is updating them after a release. Assume the following structure, directly after a release:

              K---L---M   topic3
             /
            | E---F---G---H---I---J---TX2---K---L---M---TX3   develop
            |/
A---B---C---D---E---F---G---H---I---J  master

where the TX commits indicate merges of topic branches into develop. These merge commits show that develop now has different history than the current trunk. Since environment branches are subject to the first rule, you need to reset each environment branch to the trunk. Doing a rebase is not possible, because of the messy merge commits. The following will reset develop to the current trunk:

git checkout develop
git reset --hard master

giving us this structure:

              K---L---M   topic3
             /
A---B---C---D---E---F---G---H---I---J  master, develop

Now we have two problems. First, topic3 is violating the first rule. This can be fixed by performing a rebase on all remaining topic branches after every release. Now we have this structure:

                                      K---L---M   topic3
                                     /
A---B---C---D---E---F---G---H---I---J  master, develop

Second problem, we are now missing code that used to be in develop: the commits from topic3 were overwritten in the reset. To remedy this, we simply re-merge the topic branch back into develop. Giving us this final structure:

                                      K---L---M   topic3
                                     /
                                    | K---L---M   develop
                                    |/
A---B---C---D---E---F---G---H---I---J  master

This needs to be repeated for all environment branches after each release. This ensures each branch follows trunk, instead of having its own unique history, to prevent environment specific issues.

Tips and Tricks

Git can be made a lot easier with a proper environment setup, and some knowledge of how to perform some simple git magic.

Git setup

Here is a .gitconfig file (located in ~/.gitconfig) with some very useful aliases and configurations.

[alias]
    co = checkout
    s = status
    clear = checkout -- .
    last-diff = diff HEAD~1
    hist = log --pretty=format:'%C(auto)%h%C(reset) | %s%C(auto)%d%C(reset)' --graph
    dump = cat-file -p
    reset-head = reset --hard HEAD
    reset-trunk = reset --hard master
    root = ! git log | tail -n 5 | grep ^commit | cut -d ' ' -f 2
    tip = ! git log | head -n 1 | cut -d ' ' -f 2
    b = "!f() { git checkout -b $1 master; }; f"
[push]
    default = simple

Automated reset

Here is a script to automatically reset all environment branches for multienvironment projects. This can be run after each release, then only in-progress branches will need to be updated, and re-merged.

#!/bin/sh

git fetch

# reset branches
git co develop
git reset --hard origin/master
git push -f

git co qa
git reset --hard origin/master
git push -f

git co staging
git reset --hard origin/master
git push -f

git co sandbox
git reset --hard origin/master
git push -f

git co master

Rewriting history

This blog post by Thoughtbot gives many good examples on how to fix small mistakes in your git history.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment