More Git Branches

In an earlier post I summarized a series of git commands to work with branches. In this post, I attempt to classify those commands, according to their purpose. My approach in this article is to take a series of common development scenarios using git and see how we can solve them. At the end of the article we will use what we have learnt (and some) to create a couple of useful git aliases.

Git branches are light and fast. You want to take advantage of these abilities by often forking and experimenting with new code, and sharing them with others when they’re ripe. Need to work on a breaking change without disrupting your main branch, no problem. Branch and push. Need to contribute to someone else’s branch, no problem. Pull, commit and push. Need to see if there’s a new branch to work on, no problem. Fetch and checkout.

These tasks are simple, but as with most things git they can be unintuitive depending on what you’re trying to do. When working with branches there are 3 important aspects that all developers will need to deal with :

  • Discoverability : You’re looking for a branch to contribute, how do you find it?
  • Contribution : You want to contribute code, especially outside mainline. How do you pull in their changes? How do you create your own new branches?
  • Clean up : How do clean up old stuff so others don’t push in old branches?

Before we move forward, let’s get some simple terminology out of the way :

  1. Local Branch : A branch that is defined on your local machine.
  2. Remote Branch : A branch that is defined on a remote server.
  3. Remote Tracking Branch : A remote branch that you have linked to a local branch so can pull and push updates from the remote branch.
  4. HEAD : The HEAD is a pointer to the latest commit on a branch. Your local branche’s HEAD is always the last commit you made. A remotely-tracked branch’s HEAD is whatever the latest commit was when you last synced up. The actual remote branch may have yet another HEAD, for example when someone pushes new commits after you.

Listing branches, local and remotely tracked

Let’s cover a few basic commands here. These commands will show you what branch you’re working on, but with slightly different outputs.

  1. git branch : This will list all the branches you have defined on your local machine, and will mark the current branch you’re on. Notice below that i’m on the master branch of my local git clone.
     workspace/eclipse/Tiny-Redis ‹master› » git branch
     adil
     gh-pages
    * master
  2. git branch --all : This command lists all local and remote-tracking branches. A remote-tracking branch is a remote branch you have linked to a local branch so you can pull and push. This will also show remotes for which the local branch has been deleted. This command will NOT show branches that have not been tracked, like any new branches on ‘origin’ (that’s later).
     workspace/eclipse/Tiny-Redis ‹master› » git branch -a       
      adil
      gh-pages
    * master
      remotes/origin/adil
      remotes/origin/gh-pages
      remotes/origin/master
      remotes/origin/v1
  3. git branch -vv : This is the same as git branch, but it shows local branches alongside their tracked remotes, and what the current HEAD is on each. This is a slightly more useful command when used during active development.
    workspace/eclipse/Tiny-Redis ‹master› » git branch -vv 
     adil 7cd590a [origin/adil] Add dub.json
     gh-pages 3108052 [origin/gh-pages] v2.0.2
    * master ab2272a [origin/master] v2.0.2
    

Discovering new branches

Here we’ll see a couple of ways to discover what branches are available on the remote server.

  1. git ls-remote --heads origin : List all branches on ‘origin‘ server (origin is usually the default server you’ve cloned from) and their respective HEADs. This includes branches that are not being tracked on your system. Use this to find a new remote branch.
    git ls-remote --heads origin
    7cd590a5dbb04594c93fee59e94bd2025e88ce78 refs/heads/adil
    310805208339955df7805f5e043724c7c961edde refs/heads/gh-pages
    ab2272a22856230a4d56ca5c541e4adfe958e873 refs/heads/master

    Note that the origin argument above can be substituted with any git URL you have access to (or even a public URL), ex :

    workspace/eclipse/Tiny-Redis ‹master› » git ls-remote --heads http://www.kernel.org/pub/scm/git/git.git             
    9b7cbb315923e61bb0c4297c701089f30e116750	refs/heads/maint
    3f1509809e728b70ea7912e4e1b40f22965e45ee	refs/heads/master
    41e5f3a738fb5958e2058f13714ffff159b26ff0	refs/heads/next
    2707ec8700faa183c55f106d5dcd387e48dcaba6	refs/heads/pu
    92d6a7876afe47c632fc9e7d1b16a8f2f40fbbaa	refs/heads/todo
    
  2. git remote show origin : Shows local branches, remote branches, which branches are being tracked and to what. This is a good command to get a summary of your git repo. The output is very human-friendly. It even shows you old branches you should have pruned! (how’s that for house keeping)
     workspace/eclipse/Tiny-Redis ‹master› » git remote show origin
    * remote origin
      Fetch URL: git@github.com:adilbaig/Tiny-Redis.git
      Push  URL: git@github.com:adilbaig/Tiny-Redis.git
      HEAD branch: master
      Remote branches:
        adil                   tracked
        gh-pages               tracked
        master                 tracked
        refs/remotes/origin/v1 stale (use 'git remote prune' to remove)
      Local branches configured for 'git pull':
        adil     merges with remote adil
        gh-pages merges with remote gh-pages
        master   merges with remote master
      Local refs configured for 'git push':
        adil     pushes to adil     (up to date)
        gh-pages pushes to gh-pages (up to date)
        master   pushes to master   (up to date)

However, this command does NOT show local branches that are not tracking remotely. Use git branch -vv for that.

Working on a brand new branch

So you’ve decided to create a new branch and share the good stuff. Here goes.

  1. git branch newfeature : Creates a new local branch called newfeature, that is untracked until you ‘git push‘. Note : Running this command will NOT switch your branch (see below). And, it does not output anything if successful.
  2. git checkout newfeature : Switch to your local newfeature branch. Errors out if the branch doesn’t exist.
    workspace/eclipse/Tiny-Redis ‹master› » git checkout newfeature
    Switched to branch 'newfeature'
  3. git checkout -b newfeature : This combines the previous 2 steps. Create a new local branch and switch to it. After you’ve made a commit you can create a new branch on remote using the next command.
  4. git push -u origin newfeature : This command pushes your commits to a new remote branch. The -u sets up a new remote branch and tracks the local branch to it. This command only needs to be run once. Subsequent pushes do not need the -u

    workspace/eclipse/Tiny-Redis ‹newfeature› » git push -u origin newfeature                                         Total 0 (delta 0), reused 0 (delta 0)
    To git@github.com:adilbaig/Tiny-Redis.git
     * [new branch]      newfeature -> newfeature
    Branch newfeature set up to track remote branch newfeature from origin.

Contributing to an existing remote branch

When you want to contribute code to an existing branch, there are three steps :

  1. Finding new branches : For that see “Discovering new branches” above.
  2. Syncing : New branches can be created anytime, and your system may not be synced with the remote server. git fetch –all will sync your local repo with the remote origin.
  3. Comitting back : This is as simple as checking out the branch (after a git fetch) and pushing upstream.

So let’s get on with it

  1. git fetch --all : This command fetches all remote branches and tags. It’s worth doing this every now and then, just to see what features are in development. You’ll need to do this before you can start contributing to a remote branch. Use the -n option if you don’t want to fetch tags
    workspace/eclipse/Tiny-Redis ‹newfeature› » git fetch --all 
    Fetching origin
  2. git checkout --track origin/newfeature : Create a new local branch called ‘newfeature‘ that tracks to an existing remote branch ‘origin/newfeature‘. Do this when you want to contribute to an existing remote branch. To use this command you must first run git fetch origin newfeature or git fetch --all, so you can track the remote branch on your system.

Deleting branches

When the time comes to finally clean up old code, here’s a few commands to delete local and remote branches

  1. git branch -D newfeature : This command deletes the newfeature local branch
     workspace/eclipse/Tiny-Redis ‹master› » git branch -D newfeature
    Deleted branch newfeature (was ab2272a).
  2. git push origin :newfeature : The ‘:’ prefix tells git to delete newfeature on the remote server
    workspace/eclipse/Tiny-Redis ‹master› » git push origin :newfeature
    To git@github.com:adilbaig/Tiny-Redis.git
     - [deleted]         newfeature
  3. git remote prune origin : Finally, this command prunes old branches on the remote server that are not being tracked by anyone.
    workspace/eclipse/Tiny-Redis ‹master› » git remote prune origin
    Pruning origin
    URL: git@github.com:adilbaig/Tiny-Redis.git
     * [pruned] origin/v1

Simplify with Git Aliases

Now that we know all of the above, let’s use what we’ve learnt to create git aliases. Aliases are user-defined git commands that can combine multiple commands into one. I won’t get into the details here, you can read them up on Git SCM. In short, they can simplify our lives by creating more intelligent, user-friendly commands.

git delete-branch <branch name>

Our first alias : delete-branch. It takes in the name of the branch and deletes both local and remote versions. Before we begin, here’s the code to create the alias :

git config --global alias.delete-branch '!_() { test -n "$1" && git branch -D $1 && git push origin :$1; }; _ '

Let’s break that down so we can understand it in parts. git config alias.delete-branch '..' creates the alias. --global makes this alias available to all repos on our system, even ones we haven’t created yet. You usually want to define aliases in a repo agnostic way and make them available globally. They’re simpler to understand that way and more generally useful.

The alias definition is as follows :

!_() { test -n "$1" && git branch -D $1 && git push origin :$1; }; _

The ‘!‘ tells git to run the rest of the string in bash (or your default shell). ‘_() { .. }; _‘ : This part wraps the string in a bash function and calls it. Git appends user provided parameters to the end of the alias. This provides a way to pass params to your alias, but it can cause your alias to behave strangely when it has a complicated setup. By wrapping it in a function we don’t need to worry about coming across strange append errors, the params are passed to the function and we don’t need to deal with them unless we want to.

test -n "$1"‘ makes sure you don’t run anything if you pass in an empty string. $1 is the first parameter you pass into this command. Finally ‘git branch -D $1 && git push origin :$1‘ deletes the local branch and it’s remote branch. In case, a local branch doesn’t exist (like when you make a typo) the command will error out. Let’s try this :

workspace/eclipse/Tiny-Redis ‹master› » git branch newfeature; git push -u origin newfeature
Total 0 (delta 0), reused 0 (delta 0)
To git@github.com:adilbaig/Tiny-Redis.git
 * [new branch]      newfeature -> newfeature
Branch newfeature set up to track remote branch newfeature from origin.
 workspace/eclipse/Tiny-Redis ‹master› » git config --global alias.delete-branch '!_() { test -n "$1" && git branch -D $1 && git push origin :$1; }; _ '
 workspace/eclipse/Tiny-Redis ‹master› » git delete-branch newfeature
Deleted branch newfeature (was ab2272a).
To git@github.com:adilbaig/Tiny-Redis.git
 - [deleted]         newfeature

git new-branch <branch name>

This alias will take a branch name and create a local branch that is remotely tracked. It will also do some sanity checks. If a remote with the same name exists, this alias will pull and track it locally. And in the case the branch already exists locally, it’ll print a message and error out.

When you create a new branch you want to be sure a branch of the same name does not exist remotely. Technically you can track a local branch to a remote branch with a different name, but it’s usually unintuitive and not recommended.

So here’s the script :

git config --global alias.new-branch '!_() { 
    
    local LOCAL_BRANCH_EXISTS=$(git branch | grep -sx $1 > /dev/null; echo $?);
    if [ "$LOCAL_BRANCH_EXISTS" -eq 0 ]; then
        echo "Branch \"$1\" already exists!"
        exit 0;
    fi

    local REMOTE_BRANCH_EXISTS=$(git ls-remote --heads | grep -x "$1" > /dev/null; echo $?);
    if [ "$REMOTE_BRANCH_EXISTS" -eq 0 ]; then
        echo "git fetch --all; git checkout -b $1 --track origin/$1; git pull origin $1;";
        git fetch --all;
        git checkout -b $1 --track origin/$1;
        git pull origin $1;
    else 
        echo "git checkout -b $1; git push origin $1;"
        git checkout -b $1; git push origin $1;
    fi 
}; _ '

This script looks long but is essentially simple. Let’s break it down into logical chunks.

local LOCAL_BRANCH_EXISTS=$(git branch | grep -sx $1 > /dev/null; echo $?);
    if [ "$LOCAL_BRANCH_EXISTS" -eq 0 ]; then
        echo "Branch '$1' already exists!"
        exit 1;
    fi

This code checks to see if the branch exists. We just grep the output of git branch for this. If we find a local branch, we print a message and error out.

local REMOTE_BRANCH_EXISTS=$(git ls-remote --heads | grep -x "$1" > /dev/null; echo $?);
    if [ "$REMOTE_BRANCH_EXISTS" -eq 0 ]; then
        echo "git fetch origin $1; git checkout -b $1 --track origin/$1; git merge origin/$1;";
        git fetch origin $1;
        git checkout -b $1 --track origin/$1;
        git merge origin/$1;

Here we check if a remote branch exists, by grepping the git ls-remote --heads output. If it does, we fetch that branch and create a local branch tracked to the remote, with ‘git checkout -b $1 --track origin/$1;'. ‘git merge origin/$1' merges the remote commits to the local one, so you can begin from where it left off.

And finally,

    else 
        echo "git checkout -b $1; git push origin $1;"
        git checkout -b $1; git push origin $1;
    fi 

We create a new branch and push it online if none of the above is true.

There you have it, two useful git aliases and an understanding of how they work. Leave your comments below.

Advertisements

One thought on “More Git Branches

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s