#!/usr/bin/env bash
#
# Commit into a GIT repository.
# Copyright (c) Petr Baudis, 2005
# Based on an example script fragment sent to LKML by Linus Torvalds.
#
# Commits changes to a GIT repository. Accepts the commit message from
# `stdin`. If the commit message is not modified the commit will be
# aborted.
#
# The `GIT_AUTHOR_NAME`, `GIT_AUTHOR_EMAIL`, `GIT_AUTHOR_DATE`,
# `GIT_COMMITTER_NAME` and `GIT_COMMITTER_EMAIL` environment variables
# are used in case values other than that returned by `getpwuid(getuid())`
# are desired when performing a commit.
#
# OPTIONS
# -------
# -C::
#	Make `cg-commit` ignore the cache and just commit the thing as-is.
#	Note, this is used internally by Cogito when merging. This option
#	does not make sense when files are given on the command line.
#
# -mMESSAGE::
#	Specify the commit message, which is used instead of starting
#	up an editor (if the input is not `stdin`, the input is appended
#	after all the '-m' messages). Multiple '-m' parameters are appended
#	to a single commit message, each as separate paragraph.
#
# -e::
#	Force the editor to be brought up even when '-m' parameters were
#	passed to `cg-commit`.
#
# -E::
#	Force the editor to be brought up and do the commit even if
#	the default commit message is not changed.
#
# FILES
# -----
# $GIT_DIR/commit-template::
#	If the file exists it will be used as a template when creating
#	the commit message. The template file makes it possible to
#	automatically add `Signed-off-by` line to the log message.
#
# $GIT_DIR/hooks/commit-post::
#	If the file exists and is executable it will be executed upon
#	completion of the commit. The script is passed two arguments.
#	The first argument is the commit ID and the second is the
#	branchname. A sample `commit-post` script might look like:
#
#	#!/bin/sh
#	id=$1
#	branch=$2
#	echo "Committed $id in $branch" | mail user@host
#
# ENVIRONMENT VARIABLES
# ---------------------
# GIT_AUTHOR_NAME::
#	Author's name
#
# GIT_AUTHOR_EMAIL::
#	Author's e-mail address
#
# GIT_AUTHOR_DATE::
#	Date, useful when applying patches an e-mail
#
# GIT_COMMITTER_NAME::
#	Committer's name
#
# GIT_COMMITTER_EMAIL::
#	Committer's e-mail address
#
# EDITOR::
#	The editor used for entering revision log information.

USAGE="cg-commit [-mMESSAGE]... [-C] [-e | -E] [FILE]..."

. ${COGITO_LIB}cg-Xlib


[ -s $_git/blocked ] && die "committing blocked: $(cat $_git/blocked)"

forceeditor=
ignorecache=
commitalways=
msgs=()
while [ "$1" ]; do
	case "$1" in
	-C)
		ignorecache=1
		shift
		;;
	-e)
		forceeditor=1
		shift
		;;
	-E)
		forceeditor=1
		commitalways=1
		shift
		;;
	-m*)
		msgs=("${msgs[@]}" "${1#-m}")
		shift
		;;
	*)	break;;
	esac
done

if [ "$1" ]; then
	[ "$ignorecache" ] && die "-C and listing files to commit does not make sense"
	[ -s $_git/merging ] && die "cannot commit individual files when merging"

	eval commitfiles=($(git-diff-cache -r -m HEAD -- "$@" | \
		sed 's/^\([^	]*\)\(.	.*\)\(	.*\)*$/"\2"/'))
	customfiles=1

else
	# We bother with added/removed files here instead of updating
	# the cache at the time of cg-(add|rm), since we want to
	# have the cache in a consistent state representing the tree
	# as it was the last time we committed. Otherwise, e.g. partial
	# conflicts would be a PITA since added/removed files would
	# be committed along automagically as well.

	if [ ! "$ignorecache" ]; then
		# \t instead of the tab character itself works only with new
		# sed versions.
		eval commitfiles=($(git-diff-cache -r -m HEAD | \
			sed 's/^\([^	]*\)\(.	.*\)\(	.*\)*$/"\2"/'))
	fi

	merging=
	[ -s $_git/merging ] && merging=$(cat $_git/merging | sed 's/^/-p /')
fi


LOGMSG=$(mktemp -t gitci.XXXXXX)
LOGMSG2=$(mktemp -t gitci.XXXXXX)

written=
if [ "$merging" ]; then
	echo -n 'Merge with ' >>$LOGMSG
	[ "$msgs" ] && echo -n 'Merge with '
	[ -s $_git/merging-sym ] || cp $_git/merging $_git/merging-sym
	for sym in $(cat $_git/merging-sym); do
		uri=$(cat $_git/branches/$sym)
		[ "$uri" ] || uri="$sym"
		echo "$uri" >>$LOGMSG
		[ "$msgs" ] && echo "$uri"
	done
	echo >>$LOGMSG
	written=1
fi
first=1
for msg in "${msgs[@]}"; do
	if [ "$first" ]; then
		first=
	else
		echo >>$LOGMSG
	fi
	echo "$msg" | fmt -s >>$LOGMSG
	written=1
done
# Always have at least one blank line, to ease the editing for
# the poor people whose text editor has no 'O' command.
[ "$written" ] || echo >>$LOGMSG

if [ -e "$_git/commit-template" ]; then
	cat $_git/commit-template >>$LOGMSG
else
	cat >>$LOGMSG <<EOT
CG: -----------------------------------------------------------------------
CG: Lines beginning with the CG: prefix are removed automatically.
EOT
fi

if [ ! "$ignorecache" ]; then
	if [ ! "${commitfiles[*]}" ]; then
		rm $LOGMSG $LOGMSG2
		die 'Nothing to commit'
	fi
	if [ ! "$merging" ]; then
		echo "CG: By deleting lines beginning with CG:F, the associated file" >>$LOGMSG
		echo "CG: will be removed from the commit list." >>$LOGMSG
	fi	
	echo "CG:" >>$LOGMSG
	echo "CG: Modified files:" >>$LOGMSG
	for file in "${commitfiles[@]}"; do
		# TODO: Prepend a letter describing whether it's addition,
		# removal or update. Or call git status on those files.
		echo "CG:F   $file" >>$LOGMSG
		[ "$msgs" ] && ! [ "$forceeditor" ] && echo $file
	done
fi
echo "CG: -----------------------------------------------------------------------" >>$LOGMSG

cp $LOGMSG $LOGMSG2
if tty -s; then
	if ! [ "$msgs" ] || [ "$forceeditor" ]; then
		${EDITOR:-vi} $LOGMSG2
		if ! [ "$commitalways" ] && ! [ $LOGMSG2 -nt $LOGMSG ]; then
			rm $LOGMSG $LOGMSG2
			die 'Commit message not modified, commit aborted'
		fi
	fi
	if [ ! "$ignorecache" ] && [ ! "$merging" ]; then
		eval newcommitfiles=($(grep ^CG:F $LOGMSG2 | sed 's/^CG:F *\(.*\)$/"\1"/'))
		if [ ! "${newcommitfiles[*]}" ]; then
			rm $LOGMSG $LOGMSG2
			die 'Nothing to commit'
		fi
		if [ "${commitfiles[*]}" != "${newcommitfiles[*]}" ]; then
			commitfiles=("${newcommitfiles[@]}")
			customfiles=1
		fi
	fi
else
	cat >$LOGMSG2
fi
# Remove heading and trailing blank lines.
grep -v ^CG: $LOGMSG2 | git-stripspace >$LOGMSG
rm $LOGMSG2


precommit_update () {
	queueN=(); queueD=(); queueM=();
	for file in "$@"; do
		op=${file%% *}
		fname=${file#* }
		[ "$op" = "N" ] || [ "$op" = "D" ] || [ "$op" = "M" ] || op=M
		eval "queue$op[\${#queue$op[@]}]=\"\$fname\""
	done
	# XXX: Do we even need to do the --add and --remove update-caches?
	[ "$queueN" ] && { git-update-cache --add -- "${queueN[@]}" || return 1; }
	[ "$queueD" ] && { git-update-cache --force-remove -- "${queueD[@]}" || return 1; }
	[ "$queueM" ] && { git-update-cache -- "${queueM[@]}" || return 1; }
	return 0
}

if [ ! "$ignorecache" ]; then
	if [ "$customfiles" ]; then
		precommit_update "${commitfiles[@]}" || die "update-cache failed"
		export GIT_INDEX_FILE=$(mktemp -t gitci.XXXXXX)
		git-read-tree HEAD
	fi
	precommit_update "${commitfiles[@]}" || die "update-cache failed"
fi


oldhead=
if [ -s "$_git/HEAD" ]; then
	oldhead=$(cat $_git/HEAD)
	oldheadstr="-p $oldhead"
fi

treeid=$(git-write-tree)
[ "$treeid" ] || die "git-write-tree failed"
if [ ! "$merging" ] && [ "$oldhead" ] && [ "$treeid" = "$(tree-id)" ]; then
	echo "Refusing to make an empty commit - the tree was not modified" >&2
	echo "since the previous commit. If you really want to make the" >&2
	echo "commit, do: commit-tree \`tree-id\` -p \`parent-id\`" >&2
	exit 2;
fi

newhead=$(git-commit-tree $treeid $oldheadstr $merging <$LOGMSG)
rm $LOGMSG

if [ "$customfiles" ]; then
	rm $GIT_INDEX_FILE
	export GIT_INDEX_FILE=
fi

if [ "$newhead" ]; then
	echo "Committed as $newhead."
	echo $newhead >$_git/HEAD
	[ "$merging" ] && rm $_git/merging $_git/merging-sym $_git/merge-base

	# Trigger the postcommit hook
	branchname=
	[ -s $_git/branch-name ] && branchname=$(cat $_git/branch-name)
	if [ -x $_git/hooks/commit-post ]; then
		# We just hope that for the initial commit, the user didn't
		# manage to install the hook yet.
		for merged in $(git-rev-tree $newhead ^$oldhead | sort -n | cut -d ' ' -f 2 | cut -d : -f 1); do
			$_git/hooks/commit-post $merged $branchname
		done
	fi

	exit 0
else
	die "error during commit (oldhead $oldhead, treeid $treeid)"
fi
