#!/bin/bash
#       A tool to move commits from svn to git and from git to svn
#       Copyright (C) 2009  Warzone Resurrection Project
#
#       This program is free software; you can redistribute it and/or modify
#       it under the terms of the GNU General Public License as published by
#       the Free Software Foundation; either version 2 of the License, or
#       (at your option) any later version.
#
#       This program is distributed in the hope that it will be useful,
#       but WITHOUT ANY WARRANTY; without even the implied warranty of
#       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#       GNU General Public License for more details.
#
#       You should have received a copy of the GNU General Public License
#       along with this program; if not, write to the Free Software
#       Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#       MA 02110-1301, USA.

function echo_c {
	echo -n -e "$color"
	echo $*
	echo -n -e "$reset"
}

function is_merge {
	# check if this commit has two parents
	git show --pretty=format:%P $1 | head -n1 | grep -q " "
}

function cherry_pick_from {
	if is_merge $1
	then
		mainline=-m1
	else
		mainline=
	fi
	git cherry-pick $mainline $1
}

function same_commit {
	log_lines=`git log --pretty=oneline $1..$2 | wc -l`
	if [ $log_lines -eq 0 ]
	then
		return 0
	else
		return 1
	fi
}

function branch_exists {
	git rev-parse --verify $1 > /dev/null 2>&1
}

function usage {
	cat << EOF
Usage: gitsvngateway [options] LOCAL_BRANCH REMOTE_SVN_BRANCH
Put new commits from LOCAL_BRANCH and REMOTE_SVN_BRANCH on top of each other and commit to SVN and push to REMOTE
REMOTE is "origin" by default
Example: gitsvngateway master svn/trunk

Options:
  -h, --help           Display this message
  -n, --dry-run        Do not push or commit to SVN and leave branches in their original state
  -p, --prefix=PREFIX  Use PREFIX instead of "_gitsvngateway/" for branches
  -r, --remote=REMOTE  git push to REMOTE instead of to "origin"
  -a, --automatic      When there is a merge conflict clean up and abort
  -f, --no-fetch       Do not do an initial fetch to see if there are new commits from SVN
EOF
	exit 1
}

function revert_and_exit {
	echo_c "== Reverting changes and exitting"
	git rebase --abort > /dev/null
	git reset -q --hard > /dev/null
	git checkout -q $p/new/${git}
	git branch -f ${git} $p/new/${git}
	git checkout -q ${git}
	git branch -f $p/${git} $p/backup/${git}
	git branch -D $p/backup/${git} > /dev/null
	git branch -D $p/tocommit/${svn} > /dev/null
	git branch -D $p/new/${git} > /dev/null
	rm -f $mergefile
	exit $1
}

color="\033[35m"
reset="\033[0m"

p=_gitsvngateway
dry_run=false
remote=origin
automatic=false
no_fetch=false
squash=false

TEMP=`getopt -o hnp:r:afs --long help,dry-run,prefix:,remote:,automatic,no-fetch,squash \
     -n 'gitsvngateway' -- "$@"`

if [ $? != 0 ] ; then usage; fi

# Note the quotes around `$TEMP': they are essential!
eval set -- "$TEMP"

while true ; do
	case "$1" in
		-h|--help) usage; ;;
		-n|--dry-run) dry_run=true; shift;;
		-p|--prefix) p="$2"; shift 2;;
		-r|--remote) remote="$2"; shift 2;;
		-a|--automatic) automatic=true; shift;;
		-f|--no-fetch) no_fetch=true; shift;;
		-s|--squash) squash=true; shift;;
		--) shift ; break ;;
		*) echo "Internal error!" ; exit 1 ;;
	esac
done

git=$1
svn=$2
mergefile=$p.$git.$svn.merge
mergefile=${mergefile//\//_}

# svn -> git

if ! branch_exists $p/${git} || ! branch_exists $p/${svn}
then

	echo_c "== Storing pre-commit states"
	git branch $p/${git} ${git}
	git branch $p/${svn} ${svn}

	echo_c "== You need to run this command on all branches you want to keep in sync."
	echo_c "== Otherwise it will not be able to determine which commits from git svn fetch are new."
	echo_c "== Please do this now and restart this script (with the same arguments) to continue."
	exit
fi

if ! branch_exists $p/tocommit/${svn}
then
	if ! $no_fetch
	then
		echo_c "== Fetching commits from SVN"
		$dry_run || git svn fetch
	fi

	if same_commit $p/${svn} ${svn} && same_commit $p/${git} ${git}
	then
		echo_c "== No new commits from SVN and no new local commits"
		exit
	fi

	# so we can reset it later when the user decides to abort
	git branch $p/backup/${git} $p/${git}

	# mark current state so we know what the local changes were we need to commit to SVN
	git branch -f $p/new/${git} ${git}

	if same_commit $p/${svn} ${svn}
	then
		echo_c "== No new changes from SVN"
	else
		echo_c "== Rebasing commits from ${svn} onto ${git}"
		git branch -f $p/new/${svn} ${svn}
		if ! git rebase --onto ${git} $p/${svn} $p/new/${svn}
		then
			if $automatic
			then
				revert_and_exit 2
			else
				echo_c "== Finish the rebase and restart this script (with the same arguments)"
				exit 2
			fi
		fi
		git branch -f ${git} $p/new/${svn}
		git checkout -q ${git}
		git branch -D $p/new/${svn} > /dev/null
	fi
	git branch $p/tocommit/${svn} ${svn}
	rm -f $mergefile # make sure we start a new merge
fi

if branch_exists $p/tocommit/${svn}
then
	# git -> svn

	echo_c "== Rebasing commits from ${git} onto ${svn}"
	git checkout -q $p/tocommit/${svn}
	hashes=`git log --pretty=oneline --first-parent $p/${git}..$p/new/${git} | cut -f 1 --delimiter=" " | tac`

	for hash in $hashes; do
		if is_merge $hash
		then
			if ! $squash
			then
				echo_c "== Merge detected for $hash"
				echo_c "== Commits contained in this merge:"
				git log --pretty=oneline $p/${git}..$hash | tail -n+2
				amount=`git log --pretty=oneline $p/${git}..$hash | tail -n+2 | wc -l`
				echo_c "== $amount commit(s) found, will unravel if 15 or less"
				if [ $amount -le 15 ]
				then
					echo_c "== Going ahead with unraveling the merge"
					if ! [ -s "$mergefile" ]
					then
						git log --pretty=oneline $p/${git}..$hash | cut -f 1 --delimiter=" " | tail -n+2 | tac > $mergefile
					fi
					hashes2=`cat $mergefile`
					echo_c "== Commits still to merge:"
					cat $mergefile

					for hash2 in $hashes2; do
						echo_c "== Merging $hash2"
						# remove the first line from the merge file
						sed -i '1d' $mergefile

						if ! cherry_pick_from $hash2
						then
							if $automatic
							then
								revert_and_exit 2
							else
								echo_c "== Could not merge! Please fix, commit and restart the script."
								echo_c "== To use the already existing log message use:"
								echo_c "git commit -a -C $hash2"
								exit 2
							fi
						fi
					done
					# cleanum
					rm $mergefile
					# to keep track of where we are
					git branch -f $p/${git} $hash
					echo_c "== Done processing the merge"
					continue
				else
					echo_c "== Squashing this merge"
				fi
			fi
		fi
		# to keep track of where we are in case of a failed merge
		git branch -f $p/${git} $hash
		if ! cherry_pick_from $hash
		then
			if $automatic
			then
				revert_and_exit 2
			else
				echo_c "== Could not merge! Please fix, commit and restart the script."
				echo_c "== To use the already existing log message use:"
				echo_c "git commit -a -C $hash"
				exit 2
			fi
		fi
	done

	# everything is done, try to get it back into SVN and origin

	if ! same_commit ${svn} $p/tocommit/${svn}
	then
		echo
		echo_c "== These changes are staged for commit to SVN:"
		git log ${svn}..$p/tocommit/${svn} | cat
		if ! $automatic
		then
			echo_c -n -e "\nDo you want to commit these changes to SVN? [Y/n]: "
			read -n1 answer
			echo_c
		fi
		if $automatic || [ "x$answer" == "x" ]
		then
			echo_c "== Committing to SVN"
			$dry_run || git svn dcommit
			echo_c "== Getting back the changes from SVN"
			$dry_run || git svn fetch
		else
			revert_and_exit 1
		fi
	else
		echo_c "== Nothing to commit to SVN"
	fi

	git checkout -q ${git}

	if ! same_commit $remote/${git} ${git}
	then
		echo_c "== Pushing the changes to $remote"
		$dry_run ||  git push $remote ${git} || exit 1
	else
		echo_c "== No local changes, so not pushing to $remote"
	fi

	if ! $dry_run
	then
		echo_c "== Marking current state of ${git} and ${svn}"
		git branch -f $p/${git} ${git}
		git branch -f $p/${svn} ${svn}
	else
		revert_and_exit 0
	fi

	git branch -D $p/backup/${git} > /dev/null
	git branch -D $p/tocommit/${svn} > /dev/null
	git branch -D $p/new/${git} > /dev/null
	exit 0
fi

echo_c "== Not supposed to reach this line!"
exit 1
