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