#!/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 to 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.
#
# -m::
#	End the log listing at the merge base of the -r arguments
#	(defaulting to master 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 (~).
#
# -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. By default `R` and `S` is added to the
#	`LESS` environment variable 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

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

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

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

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

list_files=
log_start=
log_end=
summary=
user=
mergebase=

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" | grep -q ':'; then
			log_end=$(echo "$OPTARG" | cut -d : -f 2)
			[ "$log_end" ] || log_end="HEAD"
			log_start=$(echo "$OPTARG" | cut -d : -f 1)
		elif [ -z "$log_start" ]; then
			log_start="$OPTARG"
		else
			log_end="$OPTARG"
		fi
	elif optparse -m; then
		mergebase=1
	elif optparse -s; then
		summary=1
	else
		optfail
	fi
done
files=("${ARGS[@]}")

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 $tree1"
	else
		list_cmd="git-diff-tree -r $tree1 $tree2"
	fi
	echo
	$list_cmd | cut -f 2- | while read 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 ! echo -e "author $author\ncommitter $committer" \
			     | grep -qi "$user"; then
				skip_commit=1
				return
			fi
		fi
		if [ "$files" ]; then
			parent="${parents[0]}"
			diff_ops=
			[ "$parent" ] || diff_ops=--root
			if ! [ "$(git-diff-tree -r $diff_ops $commit $parent "${files[@]}")" ]; then
				skip_commit=1
				return
			fi
		fi
		if [ "$summary" ]; then
			# Print summary
			commit="${commit%:*}"
			author="${author% <*}"
			date=(${committer#*> })
			date="$(showdate ${date[*]} '+%F %H:%M')"
			read 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#*> })
		pdate="$(showdate ${date[*]})"
		[ "$pdate" ] && author="${author%> *}> $pdate"
		echo ${colauthor}author $author $coldefault

		date=(${committer#*> })
		pdate="$(showdate ${date[*]})"
		[ "$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]cked-[Bb]y:.*/ s/^% \(.*\)/% '$colsignoff'\1'$coldefault'/
	' | while read key rest; do
		trap exit SIGPIPE
		process_commit_line
	done
}

if [ "$mergebase" ]; then
	[ "$log_start" ] || log_start="master"
	[ "$log_end" ] || log_end="origin"
	log_start=$(git-merge-base $(commit-id "$log_start") $(commit-id "$log_end"))
fi

id1="$(commit-id "$log_start")" || exit 1
if [ "$log_end" ]; then
	id2="$(commit-id "$log_end")" || exit 1
	revls="git-rev-list --pretty=raw $id2 ^$id1"
else
	revls="git-rev-list --pretty=raw $id1"
fi

# LESS="S" will prevent less to wrap too long titles to multiple lines;
# you can scroll horizontally.
$revls | print_commit_log | LESS="S$LESS" pager
