Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
A script to transform a linear flow in a git repository (all in master) in a flow named "git-flow" (http://nvie.com/posts/a-successful-git-branching-model/) with a master branch, a develop one, and one for each feature, hotfixe and release.
#!/bin/bash
# Explanations : http://bit.ly/gs2qpL
# give optionnaly a revision from where to start
# if not given initial_ref will be the first ref available
initial_ref='757794e524755d2eebc6d881fea026c308a11fd5'
# declare below all the steps whith this format :
# 'version type name ref' \
# - version is the tag which will be applied on the future master branch. Can be "-" to no tag it Iit when will be just merged in the develop branch without release and merge to master)
# - type is the type of branch to create. Can be "feature" or "hotfix".
# - name is the name of the branch (full name for a feature, but will be prefixed by "hotfix-" in other case)
# (if no name is provided, the version will be used, usefull for a hotfix)
# - ref if the ref in the current master branch from which the new branch will start
# dont forget to add a space before the slash at the end of each line, and do not add any space or quote in each parameter
declare -a all_steps=(\
'1.0.1 feature better-1.0-usability 24be26a54b6a116d5c4843b23e37944ff11a51ea' \
'1.0.2 feature first-full-rewrite-1.0 a3179cc084536194fcd86297f5134a86c74958c4' \
'1.0.3 feature base-debug-engine 2c20dbf610eb0536c4f08b948ed441aab19188cf' \
'1.0.4 feature webkit-long-press-bug 64e7492ebe21513033c37175ec9de917092f3fc6' \
'1.0.5 feature unicode-categories 6056a7efbadc59718d784cc38a47086eb4e4af28' \
'1.0.6 feature quick-context-access a88fdf2f1ecf91a6b2ebd1f7fc65dd9adbd7bf82' \
'1.0.7 feature simple-gestures 7581e38bbab36f4bb61f0030820c36f43278fab6' \
'1.0.7-2 hotfix - 2fdf164768eafa65a2256534ebc1cb87ad663628' \
'1.0.8 feature quick-itemview-context-access 74f0d41a8e18c3d8cf42f29bbc0bc9b3118cc68e' \
) # all_steps
# change this to "no" if you don't want branches (features, releases and hotfixes) to be pushed to remote
sync_branches_remotely="no"
# change this to "yes" if you want branches (features, releases and hotfixes) to be deleted after being merged
delete_branches="yes"
# change this to "no" if you don't want the old "master" branch be deleted (will be renamed as "legacy")
delete_old_master="yes"
# you can change some git names here
origin=origin
master=master
newmaster=newmaster
develop=develop
base_release=release-
base_hotfix=hotfix-
########## you should not change anything below this line ##########
# some usefull functions
# return the date of the merge (penultimate one : the one before the merge to have the date of the original last commit)
function git-merge-date {
echo `git log | grep 'Date:' | head -n 2 | tail -n 1 | sed 's/Date:\s\+//'`
} # git-merge-date
# return the last ref (used to get the one of the last commit)
# if a parameter is given, it's a branch used to restric the list of refs
function git-last-ref {
local branch="$1"
echo `git log $branch | grep '^commit' | head -n 1 | sed 's/commit\s\+//'`
} # git-last-ref
# get the current branch
function git-current-branch {
echo `git branch | grep '^*' | sed 's/^*\s\+//'`
} # git-current-branch
# change the date of a commit
# https://git.wiki.kernel.org/index.php/GitFaq#How_can_I_tweak_the_date_of_a_commit_in_the_repo.3F
# take 2 parameters :
# - the ref of the commit
# - the new date (you can use git-rev-date to see the format)
function git-update-commit-date {
local commit="$1"
local date="$2"
if [ "$date" ]
then
git filter-branch --env-filter \
"if test \$GIT_COMMIT = '$commit'
then
export GIT_AUTHOR_DATE
export GIT_COMMITTER_DATE
GIT_AUTHOR_DATE='$date'
GIT_COMMITTER_DATE='$date'
fi" &&
rm -fr "$(git rev-parse --git-dir)/refs/original/"
fi # date
} # git-update-commit-date
# cherry pick a commit and change the date of the new commit to be the same
# you can also pass a branch to get the date (else the current will be used)
function git-cherry-pick-and-date {
local commit="$1"
local branch="$2"
local date=`git log $branch --pretty=format:'%H %cD' | grep "^$commit" | sed "s/^$commit //"`
git cherry-pick $commit
last_error=$?
if [ "$last_error" == 0 ]
then
local last_ref=`git-last-ref`
git-update-commit-date $last_ref "$date"
fi # error
}
# merge with a branch given as parameter, and set the merge date to the last commit merged
function git-merge {
local branch="$1"
local current=`git-current-branch`
if [ "$current" == "$newmaster" ]
then
current=$master
fi # newmaster
git merge --no-ff -m "Merge branch '$branch' into '$current'" $branch
local last_ref=`git-last-ref`
local merge_date=`git-merge-date`
git-update-commit-date $last_ref "$merge_date"
}
# create a branch from a starting ref, and optionnaly sync it remotely
# with the help of http://djwonk.com/blog/2009/04/18/tracking-remote-git-branches/
# take 3 parameters :
# - the branch name
# - the ref to use as a starting point
# - "yes" if you want the branch to be sync remotely
# The branch will be the current one after being created
function git-create-branch {
local branch="$1"
local start_ref="$2"
local sync_branch_remotely="$3"
git checkout -b $branch $start_ref
if [ "$sync_branch_remotely" == "yes" ]
then
git push $origin $branch
git checkout $master
git branch -f $branch $origin/$branch
git checkout $branch
fi # sync_branch_remotely
}
# start here
echo "==========> START"
git checkout $master
last_error=0
# find existing branches
old_branches=`git branch | grep -v '^*'`
# check initial ref
if [ -z "$initial_ref" ]
then
initial_ref=`git log | grep ^commit | tail -n 1 | sed 's/commit\s\+//'`
fi # initial_ref
# some configuration
git config branch.autosetupmerge true
# prepare main branches
git-create-branch "$develop" "$initial_ref" "yes"
git-create-branch "$newmaster" "$initial_ref" "yes"
# prepare initial vars
last_develop_ref=$initial_ref
last_master_ref=$initial_ref
prev_ref=$initial_ref
# loop on each step
for i_step in `seq 0 \`expr ${#all_steps[@]} - 1\``
do
# split fields for this step
declare -a step=(${all_steps[$i_step]})
version=${step[0]}
mode=${step[1]}
name=${step[2]}
ref=${step[3]}
# new branch name
branch=$name
source=$develop
if [ "$mode" == "hotfix" ]
then
source=$newmaster
if [ "$name" == "-" ]
then
if [ "$version" == "-" ]
then
branch="$base_hotfix$i_step"
else # version -
branch="$base_hotfix$version"
fi # version -
else # name -
branch="$base_hotfix$name"
fi # name -
fi # hotfix
echo "==========> Step $i_step: version=$version, mode=$mode, name=$name, ref=$ref => branch $branch"
# create branch
git-create-branch "$branch" "$source" "$sync_branches_remotely"
# cherry pick commits from master
for commit in `git rev-list --reverse $prev_ref..$ref`
do
git-cherry-pick-and-date $commit $master
if [ "$last_error" != 0 ]
then
read -p "The Cherry pick resulted in an error. You may want to press CTRL-C to abort (or any other key to let the script continue)"
last_error=0
fi # last_error
done # for commit
# if hotfix, merge into master
if [ "$mode" == "hotfix" ]
then
if [ "$sync_branches_remotely" == "yes" ]
then
git push $origin $branch
fi # sync_branches_remotely
fi # hotfix
# always merge into develop
git checkout $develop
git-merge $branch
git push $origin $develop
# create a release
if [ "$version" != "-" ]
then
# create the branch
release_branch="$base_release$version"
git-create-branch "$release_branch" "$develop" "$sync_branches_remotely"
if [ "$sync_branches_remotely" == "yes" ]
then
git push $origin $release_branch
fi # sync_branches_remotely
# merge into master
git checkout $newmaster
git-merge $release_branch
git push $origin $newmaster
# merge into develop
git checkout $develop
git-merge $release_branch
git push $origin $develop
# then tag the release
git tag -a -m "version $version" "$version"
git push --tags
# delete the release branch
if [ "$delete_branches" == "yes" ]
then
git branch -d "$release_branch"
fi # delete_branches
fi # version
# delete branch
if [ "$delete_branches" == "yes" ]
then
git branch -d "$branch"
fi # delete_branches
# and now the save the ref as the prev_ref
prev_ref="$ref"
done # for i_step
# recreate old branches
for old_branch in $old_branches
do
echo "==========> Work on branch $old_branch"
# ask confirmation to see if we continue with this branch
echo "Do you want to recreate this branch ("$old_branch") ? Type Y/y to validate (any other key else to cancel)"
REPLY=""
read -p "> " -n 1
echo ""
if [[ $REPLY =~ ^[Yy]$ ]]
then
git checkout $master
# temporary branch name
branch="gf_new_$old_branch"
# is the old branch remotely synced ?
sync_branch_remotely=""
if [ `git branch -r | grep "$origin/$old_branch"` ]
then
sync_branch_remotely="yes"
fi # git branch
# find the new start_ref
old_start_ref=`git merge-base $old_branch $master`
branch_info=`git log $old_branch --pretty=format:'%H %cD %s' | grep "^$old_start_ref" | sed "s/^$old_start_ref//"`
start_ref=`git log $develop --pretty=format:'%H %cD %s' | grep "$branch_info" | awk '{ print $1 }'`
# create the branch
git-create-branch "$branch" "$start_ref" "$sync_branch_remotely"
# apply all commits
old_last_ref=`git-last-ref $old_branch`
for commit in `git rev-list --reverse $old_start_ref..$old_last_ref`
do
git-cherry-pick-and-date $commit $old_branch
if [ "$last_error" != 0 ]
then
read -p "The Cherry pick resulted in an error. The work on this branch ($old_branch) will stop and the new branch will be deleted (Press any key to continue)"
echo ""
last_error=0
git checkout $develop
git branch -D $branch
git push $origin :$branch
break
fi # last_error
done # for commit
# sync remotely if needed
if [ $sync_branch_remotely ]
then
git push $origin $branch
fi # sync_branch_remotely
git checkout $develop
# delete old branch
git branch -D $old_branch
if [ $sync_branch_remotely ]
then
git push $origin :$old_branch
fi # sync_branch_remotely
# rename new branch
git branch -m $branch $old_branch
if [ $sync_branch_remotely ]
then
git push $origin :$branch
git push $origin $old_branch:$old_branch
fi # sync_branch_remotely
git checkout $develop
git push $origin $develop
fi # REPLY (manage branch)
done # old_branch
# set newmaster the new master branch
# http://limi.co.uk/posts/renaming-master-branch-on-github
echo "==========> Move $newmaster to $master"
read -p "On Github, change the default branch to \"$newmaster\" (go to \"Admin\"). Then press any key to continue"
git checkout $develop
git branch -m $master legacy
git branch -m $newmaster $master
git push $origin :$master
git push $origin $master:refs/heads/$master
git push $origin legacy:refs/heads/legacy
read -p "On Github, change the default branch to \"$master\" (go to \"Admin\"). Then press any key to continue"
git push $origin :$newmaster
# delete the old master branch if wanted
if [ "$delete_old_master" == "yes" ]
then
git branch -D legacy
git push $origin :legacy
fi # $delete_old_master
echo "
==========> DONE !"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment