#!/bin/bash
#
# Copyright (c) Josef "Jeff" Sipek, 2006, 2007
#

GUILT_VERSION="0.25"
GUILT_NAME="Lone Wolf"

# If the first argument is one of the below, display the man page instead of
# the rather silly and mostly useless usage string
case $1 in
	-h|--h|--he|--hel|--help)
	shift
	exec "guilt-help" "$@"
	exit
esac

# we change directories ourselves
SUBDIRECTORY_OK=1

. git-sh-setup

function guilt_commands
{
	local command
	for command in $0-*
	do
		if [ -f "$command" -a -x "$command" ]
		then
			echo ${command##$0-}
		fi
	done
}

if [ `basename $0` = "guilt" ]; then
	# being run as standalone

	# by default, we shouldn't fail
	cmd=

	if [ $# -ne 0 ]; then
		# take first arg, and try to execute it

		arg="$1"
		dir=`dirname $0`

		if [ -x "$dir/guilt-$arg" ]; then
			cmd=$arg
		else
			# might be a short handed
			for command in $(guilt_commands); do
				case $command in
				$arg*)
					if [ -x "$dir/guilt-$command" ]; then
						cmd=$command
					fi
					;;
				esac
			done
		fi
		if [ $cmd ]; then
			shift
			exec "$dir/guilt-$cmd" "$@"

			# this is not reached because of the exec
			die "Exec failed! Something is terribly wrong!"
		else
			echo "Command $arg not found" >&2
			echo "" >&2
		fi
	fi

	# no args passed or invalid command entered, just output help summary

	echo "Guilt v$GUILT_VERSION"
	echo ""
	echo "Pick a command:"
	for x in `dirname $0`/guilt-*; do
		[ -x $x ] && echo -e ${x##$0-}
	done | sort | column | column -t | sed -e $'s/^/\t/'

	echo ""
	echo "Example:"
	echo -e "\tguilt-push"
	echo "or"
	echo -e "\tguilt push"

	# now, let's exit
	exit 1
fi

########

#
# Library goodies
#

# usage: valid_patchname <patchname>
function valid_patchname
{
	[ `echo "$1" | grep -e '^/' | wc -l` -gt 0 ] && return 1
	[ `echo "$1" | grep -e '^\./' | wc -l` -gt 0 ] && return 1
	[ `echo "$1" | grep -e '^\.\./' | wc -l` -gt 0 ] && return 1
	[ `echo "$1" | grep -e '/\./' | wc -l` -gt 0 ] && return 1
	[ `echo "$1" | grep -e '/\.\./' | wc -l` -gt 0 ] && return 1
	[ `echo "$1" | grep -e '/\.$' | wc -l` -gt 0 ] && return 1
	[ `echo "$1" | grep -e '/\.\.$' | wc -l` -gt 0 ] && return 1
	[ `echo "$1" | grep -e '/$' | wc -l` -gt 0 ] && return 1
	return 0
}

function get_branch
{
	git-symbolic-ref HEAD | sed -e 's,^refs/heads/,,'
}

function verify_branch
{
	local b=$branch

	[ ! -d "$GIT_DIR/patches" ] &&
		echo "Patches directory doesn't exist, try guilt-init" >&2 &&
		return 1
	[ ! -d "$GIT_DIR/patches/$b" ] &&
		echo "Branch $b is not initialized, try guilt-init" >&2 &&
		return 1
	[ ! -f "$GIT_DIR/patches/$b/series" ] &&
		echo "Branch $b does not have a series file" >&2 &&
		return 1
	[ ! -f "$GIT_DIR/patches/$b/status" ] &&
		echo "Branch $b does not have a status file" >&2 &&
		return 1
	[ -f "$GIT_DIR/patches/$b/applied" ] &&
		echo "Warning: Branch $b has 'applied' file - guilt is not compatible with stgit" >&2 &&
		return 1

	return 0
}

function get_top
{
	tail -1 "$GUILT_DIR/$branch/status" | cut -d: -f 2-
}

function get_prev
{
	local n=`wc -l < "$GUILT_DIR/$branch/status"`
	local n=`expr $n - 1`

	local idx=0
	cat "$GUILT_DIR/$branch/status" | while read p; do
		idx=`expr $idx + 1`
		[ $idx -lt $n ] && continue
		[ $idx -gt $n ] && break

		echo "$p"
	done
}

function get_series
{
	# ignore all lines matching:
	#	- empty lines
	#	- whitespace only
	#	- optional whitespace followed by '#' followed by more
	#	  optional whitespace
	grep -ve '^[[:space:]]*\(#.*\)*$' < "$series"
}

# usage: do_make_header <hash>
function do_make_header
{
	# which revision do we want to work with?
	local rev="$1"

	# we should try to work with commit objects only
	if [ `git-cat-file -t "$rev"` != "commit" ]; then
		echo "Hash $rev is not a commit object" >&2
		echo "Aborting..." >&2
		exit 2
	fi

	# get the author line from the commit object
	local author=`git-cat-file -p "$rev" | grep -e '^author ' | head -1`

	# strip the timestamp & '^author ' string
	author=`echo "$author" | sed -e 's/^author //' -e 's/ [0-9]* [+-]*[0-9][0-9]*$//'`

	git-cat-file -p "$rev" | awk "
BEGIN{ok=0}
(ok==1){print \$0; print \"\nFrom: $author\"; ok=2; next}
(ok==2){print \$0}
/^\$/ && (ok==0){ok=1}
"
}

# usage: do_get_header patchfile
function do_get_header
{
	# The complexity arises from the fact that we want to ignore the
	# From line and the empty line after it if it exists

	# 2nd line skips the From line
	# 3rd line skips the empty line right after a From line
	do_get_full_header "$1" | awk '
BEGIN{skip=0}
/^From:/{skip=1; next}
/^[ \t\f\n\r\v]*$/ && (skip==1){skip=0; next}
{print $0}
END{}
'
}

# usage: do_get_full_header patchfile
function do_get_full_header
{
	# 2nd line checks for the begining of a patch
	# 3rd line outputs the line if it didn't get pruned by the above rules
	cat "$1" | awk '
BEGIN{ok=1}
/^(diff|---)/{ok=0}
(ok==1){print $0}
END{}
'
}

# usage: assert_head_check
function assert_head_check
{
	local eh=`tail -1 < "$applied" | cut -d: -f 1`

	if ! head_check "$eh"; then
		die "aborting..."
	fi

	return 0
}

# usage: head_check <expected hash>
function head_check
{
	# make sure we're not doing funky things to commits that don't
	# belong to us
	local ch=`cat "$GIT_DIR/refs/heads/$branch"`

	# if the expected hash is empty, just return
	[ -z "$1" ] && return 0

	if [ "$ch" != "$1" ]; then
		echo "Expected HEAD commit $1" >&2
		echo "                 got $ch" >&2
		return 1
	fi
	return 0
}

# usage: series_insert_patch <patchname>
function series_insert_patch
{
	local top=`get_top | sed -e 's,/,\\\\/,g'`
	local new=`echo "$1" | sed -e 's,/,\\\\/,g'` 

	if [ ! -z "$top" ]; then
		sed -i -e "s/^$top\$/$top\n$new/" "$series"
	else
		# don't use $new here as it would only complicate things
		echo "$1" > "$series.tmp"
		cat "$series" >> "$series.tmp"
		mv "$series.tmp" "$series"
	fi
}

# usage: series_remove_patch <patchname>
function series_remove_patch
{
	grep -v "^$1\$" < "$series" > "$series.tmp"
	mv "$series.tmp" "$series"
}

# usage: series_rename_patch <oldname> <newname>
function series_rename_patch
{
	local old=`echo "$1" | sed -e 's,/,\\\\/,g'`
	local new=`echo "$2" | sed -e 's,/,\\\\/,g'` 

	sed -i -e "s/^$old\$/$new/" "$series"
}

# Beware! This is one of the few (only?) places where we modify the applied
# file directly
#
# usage: applied_rename_patch <oldname> <newname>
function applied_rename_patch
{
	local old=`echo "$1" | sed -e 's,/,\\\\/,g'`
	local new=`echo "$2" | sed -e 's,/,\\\\/,g'` 

	sed -i -e "s/^\\([0-9a-f]\\{40\\}\\):$old\$/\\1:$new/" "$applied"
}

# usage: pop_many_patches <commitish> <number of patches>
function pop_many_patches
{
	assert_head_check

	cd "$TOP_DIR"

	git-reset --hard "$1" > /dev/null
	head -n "-$2" < "$applied" > "$applied.tmp"
	mv "$applied.tmp" "$applied"

	cd - 2>&1 >/dev/null

	# update references to top, bottom, and base
	update_stack_tags
}

# usage: pop_all_patches
function pop_all_patches
{
	local x=`head -1 "$applied" | cut -d: -f1`
	local n=`wc -l < "$applied"`

	pop_many_patches $x^ $n
}

# usage: update_stack_tags
function update_stack_tags
{
	# bail if autotagging is not enabled
	if [ $autotag -eq 0 ]; then
		return 0
	fi

	if [ `wc -l < $applied` -gt 0 ]; then
		# there are patches applied, therefore we must get the top,
		# bottom and base hashes, and update the tags

		local top_hash=`git-rev-parse HEAD`
		local bottom_hash=`head -1 < $applied | cut -d: -f1`
		local base_hash=`git-rev-parse $bottom_hash^`

		echo $top_hash > "$GIT_DIR/refs/tags/${branch}_top"
		echo $bottom_hash > "$GIT_DIR/refs/tags/${branch}_bottom"
		echo $base_hash > "$GIT_DIR/refs/tags/${branch}_base"
	else
		# there are no patches applied, therefore we must remove the
		# tags to old top, bottom, and base

		rm -f "$GIT_DIR/refs/tags/${branch}_top"
		rm -f "$GIT_DIR/refs/tags/${branch}_bottom"
		rm -f "$GIT_DIR/refs/tags/${branch}_base"
	fi
}

# usage: push_patch patchname [bail_action]
function push_patch
{
	local p="$GUILT_DIR/$branch/$1"
	local pname="$1"
	local bail_action="$2"

	local bail=0
	local reject="--reject"

	assert_head_check

	cd "$TOP_DIR"

	# apply the patch if and only if there is something to apply
	if [ `git-apply --numstat "$p" | wc -l` -gt 0 ]; then
		if [ "$bail_action" = abort ]; then
		    reject=""
		fi
		git-apply -C$guilt_push_diff_context \
			$reject "$p" > /dev/null 2> /tmp/guilt.log.$$
		bail=$?

		if [ $bail -ne 0 ]; then
			cat /tmp/guilt.log.$$ >&2
			if [ "$bail_action" = abort ]; then
				return $bail
			fi
		fi

		# FIXME: Path munging is being done, we need to convince
		# git-apply to just give us list of files with \0 as a
		# delimiter, and pass -z to git-update-index
		git-apply --numstat "$p" | cut -f 3- | git-update-index --add --remove --stdin
	fi

	# grab a commit message out of the patch
	do_get_header "$p" > /tmp/guilt.msg.$$

	# make a default commit message if patch doesn't contain one
	[ ! -s /tmp/guilt.msg.$$ ] && echo "patch $pname" > /tmp/guilt.msg.$$

	# extract a From line from the patch header, and set
	# GIT_AUTHOR_{NAME,EMAIL}
	local author_str=`cat "$p" | grep -e '^From: ' | sed -e 's/^From: //'`
	if [ ! -z "$author_str" ]; then
		local backup_author_name="$GIT_AUTHOR_NAME"
		local backup_author_email="$GIT_AUTHOR_EMAIL"
		GIT_AUTHOR_NAME=`echo $author_str | sed -e 's/ *<.*$//'`
		GIT_AUTHOR_EMAIL=`echo $author_str | sed -e 's/[^<]*//'`

		if [ -z "$GIT_AUTHOR_NAME" ]; then
			GIT_AUTHOR_NAME=" "
		fi

		export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL
	fi
	local backup_author_date="$GIT_AUTHOR_DATE"
	local backup_committer_date="$GIT_COMMITTER_DATE"
	export GIT_AUTHOR_DATE=`stat -c %y "$p"`
	export GIT_COMMITTER_DATE=$GIT_AUTHOR_DATE

	# commit
	local treeish=`git-write-tree`
	local commitish=`git-commit-tree $treeish -p HEAD < /tmp/guilt.msg.$$`
	echo $commitish > $GIT_DIR/`git-symbolic-ref HEAD`

	# mark patch as applied
	echo "$commitish:$pname" >> $applied

	cd - 2>&1 >/dev/null

	# update references to top, bottom, and base of the stack
	update_stack_tags

	# restore original GIT_AUTHOR_{NAME,EMAIL}
	if [ ! -z "$author_str" ]; then
		if [ ! -z "$backup_author_name" ]; then
			export GIT_AUTHOR_NAME="$backup_author_name"
		else
			unset GIT_AUTHOR_NAME
		fi

		if [ ! -z "$backup_author_name" ]; then
			export GIT_AUTHOR_EMAIL="$backup_author_email"
		else
			unset GIT_AUTHOR_EMAIL
		fi
	fi
	if [ ! -z "$backup_author_date" ]; then
		export GIT_AUTHOR_DATE="$backup_author_date"
	else
		unset GIT_AUTHOR_DATE
	fi
		if [ ! -z "$backup_committer_date" ]; then
		export GIT_COMMITTER_DATE="$backup_committer_date"
	else
		unset GIT_COMMITTER_DATE
	fi

	rm -f /tmp/guilt.msg.$$ /tmp/guilt.log.$$

	return $bail
}

# usage: must_commit_first
function must_commit_first
{
	[ `git-diff-files | wc -l` -eq 0 ]
	return $?
}

# usage: fold_patch patchname
function fold_patch
{
	local top_patch=`get_top`

	assert_head_check

	push_patch "$1"

	__refresh_patch "$top_patch" HEAD^^ 2

	series_remove_patch "$1"
}

# usage: refresh_patch patchname
function refresh_patch
{
	__refresh_patch "$1" HEAD^ 1
}

# usage: __refresh_patch patchname commitish number_of_commits
function __refresh_patch
{
	local p="$GUILT_DIR/$branch/$1"

	assert_head_check

	cd "$TOP_DIR"

	git-diff-files --name-only | (while read n; do git-update-index "$n" ; done)

	# get the patch header
	do_get_full_header "$p" > /tmp/guilt.diff.$$

	# get the new patch
	git-diff "$2" >> /tmp/guilt.diff.$$

	# move the new patch in
	mv "$p" "$p~"
	mv /tmp/guilt.diff.$$ $p

	cd - 2>&1 >/dev/null

	# drop the currently applied patch, pop_many_patches does it's own
	# cd $TOP_DIR
	pop_many_patches "$2" "$3"

	# push_patch does it's own cd $TOP_DIR
	push_patch "$1"
}

# usage: munge_hash_range <hash range>
#
# this means:
#	<hash>			- one commit
#	<hash>..		- hash until head (excludes hash, includes head)
#	..<hash>		- until hash (includes hash)
#	<hash1>..<hash2>	- from hash to hash (inclusive)
#
# The output of this function is suitable to be passed to git-rev-list
function munge_hash_range
{
	[ -z "$1" ] && return 1

	local l=`echo "$1" | sed -e 's/\.\./ /'`

	local h1=`echo "$l" | cut -s -d' ' -f 1`
	local h2=`echo "$l" | cut -s -d' ' -f 2`

	if [ -z "$h1" -a -z "$h2" ]; then
		# e.g., "v0.19"
		echo "$l^..$l"
	elif [ -z "$h1" ]; then
		# e.g., "..v0.10"
		echo "$h2"
	elif [ -z "$h2" ]; then
		# e.g., "v0.19.."
		echo "$h1..HEAD"
	elif [ ! -z "$h1" -a ! -z "$h2" ]; then
		# e.g., "v0.19-rc1..v0.19"
		echo "$h1..$h2"
	else
		# unknown hash range format
		return 1
	fi

	return 0
}

#
# Some constants
#

# used for: git-apply -C <val>
guilt_push_diff_context=1

#
# Parse any part of .git/config that belongs to us
#

# autotag?
autotag=`git-config guilt.autotag`
[ -z "$autotag" ] && autotag=1

#
# The following gets run every time this file is source'd
#

TOP_DIR=`git-rev-parse --show-cdup`
if [ -z "$TOP_DIR" ]; then
	TOP_DIR="./"
fi

GUILT_DIR="$GIT_DIR/patches"

branch=`get_branch`

# most of the time we want to verify that the repo's branch has been
# initialized, but every once in a blue moon (e.g., we want to run guilt-init),
# we must avoid the checks
if [ -z "$DO_NOT_CHECK_BRANCH_EXISTENCE" ]; then
	verify_branch || exit 1
fi

# very useful files
series="$GUILT_DIR/$branch/series"
applied="$GUILT_DIR/$branch/status"

# determine an editor to use for anything interactive (fall back to vi)
editor="vi"
[ ! -z "$EDITOR" ] && editor="$EDITOR"

# determine a pager to use for anything interactive (fall back to more)
pager="more"
[ ! -z "$PAGER" ] && pager="$PAGER"
