8
\$\begingroup\$

I found nvie's git branching model simple enough to grasp and suitable for my projects - but the frontend supplied at GitHub was far too complex for me to understand. Hence, I wrote this script to start and finish tasks, automatically running the related commands to speed it all up a bit (without it becoming too abstract/complex).

I've read a handful of tutorials on writing shell scripts but found it difficult to find one that emphasized on portability/POSIX compliancy. I'd like the script to run just fine in sh, dash, bash and zsh. Any feedback is appreciated - for one thing, I'm a bit unsure about where to use $variable and ${variable}...

Intended usage

When I want to create a new branch for a task I run this:

gittask.sh new feature name_of_feature

When I'm done:

gittask.sh done

Finishing a task has it automatically deduce by the branch name prefix (feature/release/hotfix) what to do next.

  • Features are merged back into the development branch.
  • Releases are continuously merged back into the development branch (bugfixes) and, once the release branch is considered stable enough (decided upon by the user), merged into master (with a tag), ending the release branch.
  • Hotfixes are merged back into both the master (with a tag) and the development branch.

Branch model

I use two persistent/remote branches, master and development, as well as three temporary/local branches, feature, release and hotfix.

Master branch

  • Persistent
  • Remote branch
  • Each merge into master equals a new tag

Development branch

  • Persistent
  • Remote branch

Feature branches

  • Temporary
  • Branch off from development
  • Merge back into development
  • Naming: "subject" or "issue#" if on issue tracker ("feature/" is prepended)

Release branches

  • Temporary
  • Branch off from development
  • Merge back into development (continuously) and master (when done)
  • Naming: "major.minor.z", e.g. "1.2.0" (z is defined by hotfix, "release/" is prepended)
  • Signed tag needed when merging with master

Hotfix branches

  • Temporary
  • Branch off from master
  • Merge back into development and master
  • Naming: "x.y.fix", e.g. "1.2.1" (x, y is defined by release, "hotfix/" is prepended)
  • Signed tag needed when merging with master

Script

#!/bin/sh

# gittask.sh: taskbased git branching utility

# This script requires that git has been installed and properly configured,
# that the remote "master" and "development" branches exist (locally too) 
# and that a network connection to the "origin" repository is established.

set -o errexit

usage()
{
    echo
    echo "Usage:"
    echo "  gittask.sh new feature name_of_feature"
    echo "    - Creates a new branch off from 'development' named"
    echo "      'feature/name_of_feature'."
    echo "  gittask.sh new release name_of_release"
    echo "    - Creates a new branch off from 'development' named"
    echo "      'release/name_of_release'."
    echo "  gittask.sh new hotfix name_of_hotfix"
    echo "    - Creates a new branch off from 'master' named"
    echo "      'hotfix/name_of_hotfix'."
    echo "  gittask.sh done"
    echo "    - Merges current branch into master and/or development"
    echo "      depending on if it's a feature, release or hotfix."
}

delete_branch()
{
    # Infinite loop, only way out (except for Ctrl+C) is to answer yes or no.
    while true; do
        echo "Delete $current branch? "
        read yn
        case $yn in
            [Yy]* ) 
                git branch -d ${current}
                break
                ;;
            [Nn]* )
                echo "Leaving $current branch as it is."
                break
                ;;
            * )
                echo "Error: Please answer (y)es or (n)o."
                ;;
        esac
    done
}

define_tag()
{
    # Don't proceed until both variables have been set.
    while [ -z ${version_number} ] && [ -z ${version_note} ]; do
        echo "Enter version number (major.minor.fix): "
        read version_number
        echo "Enter version number note: "
        read version_note
    done
}

# Confirm that user is in a git repository, abort otherwise.
git status >/dev/null 2>&1 || { echo "Error: You're not in a git repository."; exit 1; }

# If "new", confirm that the required arguments were provided.
if [ "$1" == "new" ] && [ -n "$2" ] && [ -n "$3" ]; then
    
    # Validate $3, only allow a-z (lower case), 0-9 and _ (underscore) in branch names.
    [ "${3//[0-9a-z_]/}" = "" ] || { echo "Error: Branch names may only consist of a-z, 0-9 and underscore."; exit 1; }
    case $2 in
        feature )
            git checkout development
            git checkout -b "feature/$3"
            exit 0
            ;;
        release )
            git checkout development
            git checkout -b "release/$3"
            exit 0
            ;;
        hotfix )
            git checkout master
            git checkout -b "hotfix/$3"
            exit 0
            ;;
        * )
            echo "Error: You didn't specify feature, release or hotfix."
            exit 1
            ;;
    esac

# If "done", proceed to determine current branch and by that what to do next.
elif [ "$1" == "done" ]; then
    current=`git branch | awk '/\*/{print $2}'`
    case ${current} in
        feature* )
            echo "Merging into development branch..."
            git checkout development
            git merge ${current}
            git push origin development
            delete_branch
            exit 0
            ;;
        release* )
            echo "Merging into development branch..."
            git checkout development
            git merge ${current}
            git push origin development

            # Infinite loop, only way out (except for Ctrl+C) is to answer yes or no.
            while true; do
                echo "Merge into master (make a release)? "
                read yn
                case $yn in
                    [Yy]* )
                        echo "Merging into master branch..."
                        git checkout master
                        git merge ${current}
                        define_tag
                        git tag -s ${version_number} -m ${version_note}
                        git push --tags origin master
                        delete_branch
                        break
                        ;;
                    [Nn]* )
                        echo "Leaving branch master as it is."
                        break
                        ;;
                    * )
                        echo "Error: Please answer (y)es or (n)o."
                        ;;
                esac
            done
            exit 0
            ;;
        hotfix* )
            git checkout master
            git merge ${current}
            define_tag
            git tag -s ${version_number} -m ${version_note}
            git push --tags origin master
            git checkout development
            git merge ${current}
            git push origin development
            delete_branch
            exit 0
            ;;
        * )
            echo "Error: You're not on a feature, release or hotfix branch."
            exit 1
            ;;
    esac
else
    echo "Error: You didn't provide the needed arguments."
    usage
    exit 1
fi
\$\endgroup\$
  • 3
    \$\begingroup\$ I've revised this post and script several times, only to discover a new shortcoming each time it has been submitted. I don't expect myself to write a perfect piece of text or code at the first attempt - but it's odd how some things only become apparent once spoken out aloud. \$\endgroup\$ – totte Sep 18 '12 at 9:54
1
\$\begingroup\$

The following is one of my favorite "rosetta stone" like references:

Unix Shells

| improve this answer | |
\$\endgroup\$
1
\$\begingroup\$

It seems fine for me. Just three minor things:

  1. I'd consider calling the define_tag before the git merge or any other command which changes the current state of the repository. If a user press Ctrl+C they will find the repository in a different state than the beginning of the script.

  2. In the define_tag I'd print the existing tags (or the last five or ten tags). It could help users to choose the next one.

  3. There are same requirements in the comments:

    # This script requires that git has been installed and properly configured,
    # that the remote "master" and "development" branches exist (locally too) 
    # and that a network connection to the "origin" repository is established.
    

    You could check that git is installed properly and the required branches are exist at the beginning of the script.

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.