#!/usr/bin/env bash
#
# Make a log of changes in a GIT branch.
# Copyright (c) Petr Baudis, 2005.
# Copyright (c) David Woodhouse, 2005.
#
# Display log information for files or a range of commits. The output
# will automatically be displayed in a pager unless it is piped to
# a program.
#
# OPTIONS
# -------
# Arguments not interpreted as options will be interpreted as filenames;
# cg-log then displays only changes in those files.
#
# -c::
#	Colorize the output. The used colors are listed below together
#	with information about which log output (summary, full or both)
#	they apply to:
#		- `author`:	'cyan'		(both)
#		- `committer`:	'magenta'	(full)
#		- `header`:	'green'		(full)
#		- `files`:	'blue'		(full)
#		- `signoff`:	'yellow'	(full)
#		- `commit_id`:	'blue'		(summary)
#		- `date`:	'green'		(summary)
#		- `trim_mark`:	'magenta'	(summary)
#
# -f::
#	List affected files. (No effect when passed along `-s`.)
#
# -r FROM_ID[..TO_ID]::
#	Limit the log information to a set of revisions using either
#	'-r FROM_ID[..TO_ID]' or '-r FROM_ID -r TO_ID'. In both cases the
#	option expects IDs which resolve to commits and will include the
#	specified IDs. If 'TO_ID' is omitted all commits from 'FROM_ID'
#	to the initial commit is shown. If no revisions is specified,
#	the log information starting from 'HEAD' will be shown.
#
# -d DATE::
#	Limit the log information to revisions newer than given DATE,
#	and on second time further restrain it to revisions older than
#	given date. Therefore, '-d "2 days ago" -d "yesterday"' will
#	show all the commits from the day before yesterday.
#
# -m::
#	End the log listing at the merge base of the -r arguments
#	(defaulting to HEAD and origin).
#
# -s::
#	Show a one line summary for each log entry. The summary contains
#	information about the commit date, the author, the first line
#	of the commit log and the commit ID. Long author names and commit
#	IDs are trimmed and marked with an ending tilde (~).
#
# --summary::
#	Generate the changes summary, listing the commit titles grouped
#	by their author. This is also known as a "shortlog", suitable
#	e.g. for contribution summaries of announcements.
#
# -uUSERNAME::
#	List only commits where author or committer contains 'USERNAME'.
#	The search for 'USERNAME' is case-insensitive.
#
# ENVIRONMENT VARIABLES
# ---------------------
# PAGER::
#	The pager to display log information in, defaults to `less`.
#
# PAGER_FLAGS::
#	Flags to pass to the pager.
#
# CG_LESS::
#	This is what the $LESS environment variable value will be set
#	to before invoking $PAGER. It defaults to $LESS concatenated
#	with the `R` and `S` flags to allow displaying of colorized output
#	and to avoid long lines from wrapping when using `-s`.
#
# EXAMPLE USAGE
# -------------
# To show a log of changes between two releases tagged as 'releasetag-0.9'
# and 'releasetag-0.10' do:
#
#	$ cg-log -r releasetag-0.9..releasetag-0.10
#
# Similarily, to see which commits are in branch A but not yet in branch B,
#
#	$ cg-log -r B..A
#
# (meaning "all the commits needed along the way from B to A").
#
# NOTES
# -----
# The ':' is equivalent to '..' in revisions range specification (to make
# things more comfortable to SVN users). See cogito(7) for more details
# about revision specification.

USAGE="cg-log [-c] [-f] [-m] [-s] [-uUSERNAME] [-d DATE] [-r FROM_ID[..TO_ID]] [--summary] FILE..."

. ${COGITO_LIB}cg-Xlib || exit 1
# Try to fix the annoying "Broken pipe" output. May not help, but apparently
# at least somewhere it does. Bash is broken.
trap exit SIGPIPE


list_commit_files()
{
	tree1="$1"
	tree2="$2"
	line=
	sep="    * $colfiles"
	# List all files for for the initial commit
	if [ -z $tree2 ]; then
		list_cmd="git-ls-tree -r $tree1"
	else
		list_cmd="git-diff-tree -r $tree1 $tree2"
	fi
	echo
	$list_cmd | cut -f 2- | while read -r file; do
		echo -n "$sep"
		sep=", "
		line="$line$sep$file"
		if [ ${#line} -le 74 ]; then
			echo -n "$file"
		else
			line="      $file"
			echo "$coldefault"
			echo -n "      $colfiles$file"
		fi
	done
	echo "$coldefault:"
}

process_commit_line()
{
	if [ "$key" = "%" ] || [ "$key" = "%$colsignoff" ]; then
		# The fast common case
		[ "$summary" ] || [ "$skip_commit" ] || echo "    $rest"
		return
	fi
	case "$key" in
	"commit")
		[ "$summary" ] || [ "$skip_commit" ] || { [ "$commit" ] && echo; }
		commit="$rest"
		parents=()
		skip_commit=
		;;
	"tree")
		tree="$rest"
		;;
	"parent")
		parents[${#parents[@]}]="$rest"
		;;
	"committer")
		committer="$rest"
		;;
	"author")
		author="$rest"
		;;
	"")
		if [ ! "$commit" ]; then
			# Next commit is coming
			[ "$summary" ] || echo
			return
		fi

		if [ "$user" ]; then
			if ! [[ "$author" == *"$user"* || "$committer" == *"$user"* ]]; then
				skip_commit=1
				return
			fi
		fi
		if [ "$summary" ]; then
			# Print summary
			commit="${commit%:*}"
			author="${author% <*}"
			date=(${committer#*> })
			showdate ${date[*]} '+%F %H:%M'; date="$_showdate"
			read -r title
			if [ "${#author}" -gt 15 ]; then
				author="${author:0:14}$coltrim~"
			fi
			if [ "${COLUMNS:-0}" -le 90 ]; then
				commit="${commit:0:12}$coltrim~"
			fi

			printf "$colcommit%s $colauthor%-15s $coldate%s $coldefault%s\n" \
				"${commit%:*}" "$author" "$date" "${title:2}"
			commit=
			return
		fi

		echo ${colheader}commit ${commit%:*} $coldefault
		echo ${colheader}tree $tree $coldefault

		for parent in "${parents[@]}"; do
			echo ${colheader}parent $parent $coldefault
		done

		date=(${author#*> })
		showdate ${date[*]}; pdate="$_showdate"
		[ "$pdate" ] && author="${author%> *}> $pdate"
		echo ${colauthor}author $author $coldefault

		date=(${committer#*> })
		showdate ${date[*]}; pdate="$_showdate"
		[ "$pdate" ] && committer="${committer%> *}> $pdate"
		echo ${colcommitter}committer $committer $coldefault

		if [ -n "$list_files" ]; then
			list_commit_files "$tree" "${parents[0]}"
		fi
		echo
		commit=
		;;
	esac
}

print_commit_log()
{
	commit=
	author=
	committer=
	tree=

	sed -e '
		s/^    \(.*\)/% \1/
		/^% *[Ss]igned-[Oo]ff-[Bb]y:.*/ s/^% \(.*\)/% '$colsignoff'\1'$coldefault'/
		/^% *[Aa][Cc][Kk]ed-[Bb]y:.*/ s/^% \(.*\)/% '$colsignoff'\1'$coldefault'/
	' | while read -r key rest; do
		trap exit SIGPIPE
		process_commit_line
	done
}


[ "$COLUMNS" ] || COLUMNS="$(tput cols)"

colheader=
colauthor=
colcommitter=
colfiles=
colsignoff=
colcommit=
coldate=
coltrim=
coldefault=

list_files=
log_start=
log_end=
summary=
shortlog=
user=
mergebase=
date_from=
date_to=

while optparse; do
	if optparse -c; then
		# See terminfo(5), "Color Handling"
		colheader="$(tput setaf 2)"    # Green
		colauthor="$(tput setaf 6)"    # Cyan
		colcommitter="$(tput setaf 5)" # Magenta
		colfiles="$(tput setaf 4)"     # Blue
		colsignoff="$(tput setaf 3)"   # Yellow

		colcommit="$(tput setaf 4)"
		coldate="$(tput setaf 2)"
		coltrim="$(tput setaf 5)"

		coldefault="$(tput op)"        # Restore default
	elif optparse -f; then
		list_files=1
	elif optparse -u=; then
		user="$OPTARG"
	elif optparse -r=; then
		if echo "$OPTARG" | fgrep -q '..'; then
			log_end=${OPTARG#*..}
			[ "$log_end" ] || log_end="HEAD"
			log_start=${OPTARG%..*}
		elif echo "$OPTARG" | grep -q ':'; then
			log_end=${OPTARG#*:}
			[ "$log_end" ] || log_end="HEAD"
			log_start=${OPTARG%:*}
		elif [ -z "$log_start" ]; then
			log_start="$OPTARG"
		else
			log_end="$OPTARG"
		fi
	elif optparse -d=; then
		if [ -z "$date_from" ]; then
			date_from="--max-age=$(date -d "$OPTARG" +%s)" || exit 1
		else
			date_to="--min-age=$(date -d "$OPTARG" +%s)" || exit 1
		fi
	elif optparse -m; then
		mergebase=1
	elif optparse -s; then
		summary=1
	elif optparse --summary; then
		shortlog=1
	else
		optfail
	fi
done


if [ "$mergebase" ]; then
	[ "$log_start" ] || log_start="HEAD"
	[ "$log_end" ] || log_end="origin"
	id1=$(cg-object-id -c "$log_start") || exit 1
	id2=$(cg-object-id -c "$log_end") || exit 1
	conservative_merge_base $id1 $id2 || exit 1
	[ "$_cg_base_conservative" ] &&
		echo -e "Warning: Multiple merge bases, picking the most conservative one\a" >&2
	log_start=$_cg_baselist
fi

if [ "$shortlog" ]; then
	revls="git-rev-list --pretty=short"
else
	revls="git-rev-list --pretty=raw"
fi
revls="$revls $date_from $date_to"

id1="$(cg-object-id -c "$log_start")" || exit 1
if [ "$log_end" ]; then
	id2="$(cg-object-id -c "$log_end")" || exit 1
	revls="$revls $id2 ^$id1"
else
	revls="$revls $id1"
fi

# Later, we will want to use git-diff-tree --stdin for the filtering
# instead of git-rev-list, so that we can do custom renames detection.
# See also <Pine.LNX.4.64.0510212025260.10477@g5.osdl.org>.
sep=
[ "${ARGS[*]}" ] && sep=--

# Translate arguments to relpath:
if [ "$_git_relpath" ]; then
	for (( i=0; i<${#ARGS[@]}; i++ )); do
		ARGS[$i]="$_git_relpath${ARGS[$i]}"
	done
fi


if [ "$shortlog" ]; then
	# Special care here.
	$revls $sep "${ARGS[@]}" | git-shortlog | pager
	exit
fi


# LESS="S" will prevent less to wrap too long titles to multiple lines;
# you can scroll horizontally.
$revls $sep "${ARGS[@]}" | print_commit_log | _local_CG_LESS="S" pager

exit 0
