#!/bin/bash
# YummyYummySourceControl - Simple and convenient Git wrapper
#
# Copyright (C) 2007 Tim Janik <timj@gtk.org>
#
# 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 3 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.
#
# If you have not received a copy of the GNU General Public License
# along with this program, see: http://www.gnu.org/licenses/

#set -e	# exit on errors
#set -x	# show commands

VERSION=YummyYummySourceControl-0.9

# setup
SETBOLD=true ; SETNORM=true ; COLOR= ; OUT=cat ; YYHELP=

# perform wildcard string matches
function match() {
	pattern="$1"
	searchtext="$2"
	case "$searchtext" in
	$pattern) return 0 ;; # $pattern must be unquoted to allow *?[]
	esac
	return 1
}
function exit_err() {
	ecode=127
	test -n "$1" && { ecode="$1"; shift; }
	test -n "$*" && { echo -ne "$*\n" >&2 ; }
	exit $ecode
}

# configure for terminals with ANSI color escapes
TAB=$'\t'	# joe(1) syntax highlighting is broken for $''
test -t 1 && match $'*\e[31m*' "`tput setaf 1 2>/dev/null`" && {
	SETBOLD=echo\ -ne\ '\e[1m' ; SETNORM=echo\ -ne\ '\e[0m'
	BOLD=$'\e[1m'; GREEN=$'\e[32m'; TURK=$'\e[36m'; NORM=$'\e[0m';
	COLOR=--color ; OUT=less\ -R
	#' # work around syntax highlighting in old joe(1) versions wrg $''
	# we hardcode ANSI escapes directly because less -R can only deal with \e[*m
}

# find repository
match '*/yyhelp*' "/$0" && YYHELP=1	# no repo required
gitdir="$(git-rev-parse --git-dir 2>/dev/null)"
if test -f "$gitdir/HEAD" ; then
	githead="$gitdir/`git-symbolic-ref -q HEAD || echo HEAD`"; test -e "$githead" || githead=
	gitprefix="$(git-rev-parse --show-prefix)" ; test -z "$gitprefix" && gitprefix=. # no subdir support if GIT_DIR is set
elif ! test "$YYHELP" ; then
	echo "`basename \"$0\"`: No repository" >&2 ; exit 9
fi
gitsvn=false
SVNURL="`git-config --get svn-remote.svn.url`";
GITURL="`git-config --get remote.origin.url`"; test -z "$GITURL" && GITURL="`cat $gitdir/remotes/origin 2>/dev/null | sed -n '/^URL: /{ s/^URL: //; p; q; }'`"
test -z "$GITURL" -a -r "$gitdir/branches/origin" && GITURL="`cat $gitdir/branches/origin`" # cogito location
test -n "$SVNURL" -a -z "$GITURL" && gitsvn=true # use git-svn for local git repos with svn url
test -z "$GITURL" && GITURL="`cd \"$gitdir\" && pwd`" # no url, must be local

# functions
function list_committable() { # [FILES...]
    test -s "$gitdir/commit-ignore" && commitignore="$gitdir/commit-ignore" || commitignore=
    test -z "$*" && commitdir=. || commitdir=
    stripprefix="$gitprefix"
    test -n "$stripprefix" -a "${stripprefix: -1}" != / && stripprefix="$stripprefix/" # force trailing /
    git-diff-index --name-status --no-renames HEAD -- $commitdir "$@" | while IFS="$TAB" read -r mode file ; do
	case "$mode" in
	    D)	test -n "$(git-diff-files -- "$file")" && mode=! ;;
	    M)	test -n "$commitignore" && fgrep -qx "$file" "$commitignore" && mode=n ;;
	esac
	echo "$mode  ${file#$stripprefix}"
    done
}

# GIT commands
case "/$0" in
*/yyadd)	exec git-update-index --add -- "$@" ;;
*/yyblame)	test -z "$*" && exec yyhelp
		git-blame -- "$@" | {
		  test -n "$COLOR" &&
		    sed "/^[^()]\+(Not Committed Yet\b/{ s/^\([^()]\+\)(\([^()]\+\))\(.*\)/$GREEN\1$TURK(\2)$GREEN\3$NORM/; p; d; };
                                                       s/^\([^()]\+\)(\([^()]\+\))/$BOLD\1$NORM$TURK(\2)$NORM/" \
		    || cat ; } | $OUT ;;
*/yybranch)	FORCE=; test "x$1" = "x-f" && { FORCE=-f; shift ; }
	        test "$#" = 1 && BRANCH="$1" || exec yyhelp
		exec git-branch $FORCE "$BRANCH" ;;
*/yybranchdel)	DEL=-d; test "x$1" = "x-f" && { DEL=-D; shift ; }
	        test "$#" = 1 && BRANCH="$1" || exec yyhelp
		exec git-branch $DEL "$BRANCH" ;;
*/yyChangeLog)	FIRSTSVN=
	        HASHFORMAT="	# %H (%cn)"
	        test "x$1" = "x-s" && {
		  shift;
		  FIRSTSVN=$(git-rev-list HEAD --max-count=1 --grep='^git-svn-id:.*@[0-9].*-[a-f0-9]\{12\}$');
		  HASHFORMAT= # skip commit SHA1
		}
		test -n "$*" && exec yyhelp
		git-log HEAD ${FIRSTSVN:+^$FIRSTSVN}                    \
		    --pretty="format:%ad %an$HASHFORMAT%n%n%s%n%n%b"  |
		  sed -e 's/^/	/;s/^	//;/^[ 	]*<unknown>$/d' \
                      -e 's/^[	 ]*$//'                                 |
		  $OUT ;;
*/yycommit)     # standard boilerplate
	        USAGE='[FILES...]' ; SUBDIRECTORY_OK=Yes ; . git-sh-setup ; require_work_tree
		verify_msg=true
		# check and set AUTHOR and COMMITTER
		git-var GIT_AUTHOR_IDENT    2>/dev/null | grep -q '\b[0-9]\{8,\}[ ]\+[+-][0-9]\+ *$' || exit_err 9 "$0: missing commit author information"
		git-var GIT_COMMITTER_IDENT 2>/dev/null | grep -q '\b[0-9]\{8,\}[ ]\+[+-][0-9]\+ *$' || exit_err 9 "$0: missing committer information"
		AIDENT=$(git-var GIT_AUTHOR_IDENT    | sed 's/\b\([0-9]\{8,\}[ ]\+[+-][0-9]\+ *\)$/\n\1/' | {
		  read author; read time; echo -n "  $author" ; echo $time | gawk '{ print strftime (" %F %T %z", $1) }'
		})
		CIDENT=$(git-var GIT_COMMITTER_IDENT | sed 's/\b\([0-9]\{8,\}[ ]\+[+-][0-9]\+ *\)$/\n\1/' | {
		  read author; read time; echo -n "  $author" ; echo $time | gawk '{ print strftime (" %F %T %z", $1) }'
		})
		# construct commit message
		echo > "$GIT_DIR/commit-editmsg.txt" || exit $? # abort if not writable
		cat >> "$GIT_DIR/commit-editmsg.txt" <<-_EOF_HERE
		#YY: ----------------------------------------------------------------------
		#YY: Files to be committed (detected by "#YY:F"):
		#YY:
_EOF_HERE
		list_committable "$@" | sed 's/^/#YY:F   /' >> "$GIT_DIR/commit-editmsg.txt"
		cat >> "$GIT_DIR/commit-editmsg.txt" <<-_EOF_HERE
		#YY:
		#YY: Author:
		#YY: $AIDENT
		#YY: Committer:
		#YY: $CIDENT
		#YY:
_EOF_HERE
		yyinfo | sed 's/^/#YY: /' >> "$GIT_DIR/commit-editmsg.txt"
		cat >> "$GIT_DIR/commit-editmsg.txt" <<-_EOF_HERE
		#YY:
		#YY: (The "#YY:" prefixed lines are ignored for commit messages)
_EOF_HERE
		# edit commit message
		grep -q '^#YY:F ' "$GIT_DIR/commit-editmsg.txt" || exit_err 0 "** Nothing to commit."
		${VISUAL:-${EDITOR:-vi}} "$GIT_DIR/commit-editmsg.txt" ||
		  exit_err 3 "** Aborting commit, editing commit message failed: $GIT_DIR/commit-editmsg.txt"
		# verify user's commit message
		grep -v -i '^\(Signed-off-by\|#YY\):' < "$GIT_DIR/commit-editmsg.txt" > "$GIT_DIR/commit-msg.txt" || exit $?
		$verify_msg && test -x "$GIT_DIR/hooks/commit-msg" && { "$GIT_DIR/hooks/commit-msg" "$GIT_DIR/commit-msg.txt" || exit $? ; }
		mlines=`git-stripspace < "$GIT_DIR/commit-msg.txt" | wc -l`
		test 0 -lt "$mlines" || exit_err 3 "** Aborting commit, missing commit message..."
		# save current index around commit (since GIT_INDEX_FILE=tmpindex git-commit is buggy)
		TMPINDEX=`mktemp "$GIT_DIR/.precommittindex$$.XXXXXX"` && cp -p "$GIT_DIR/index" "$TMPINDEX" || exit_err 9 "$0: failed to create temporary file"
		trap 'mv -f "$TMPINDEX" "$GIT_DIR/index"' 0 HUP INT QUIT TRAP USR1 PIPE TERM
		# unstage everything in index
		git-read-tree HEAD
		# stage files from commit message
		grep "^#YY:F " "$GIT_DIR/commit-editmsg.txt" |                  # filter #YY:F
		  sed -e "s/^#YY:F \+//" | {                                    # extract file names
                    while IFS=" " read -r mode file ; do
		      case "$mode" in
		      D)  	git-update-index --force-remove -- "$file" || exit_err 5 "** Aborting commit, removing failed: $file" ;;
		      A|M)	git-update-index --add          -- "$file" || exit_err 6 "** Aborting commit, adding failed: $file" ;;
		      n)        ;; # commitignore
		      '!')      ;; # missing file
		      *)        exit_err 7 "** Aborting commit, unknown file mode: $mode $file" ;;
		      esac
		    done
		  }
		rm -f "$GIT_DIR/commit-editmsg.txt"
		# abort for empty committs
		git-diff-index --cached HEAD | grep -q '.' || exit_err 0 "** Nothing to commit."
		# actually commit changes
		git-commit -F "$GIT_DIR/commit-msg.txt" ; ccode=$?
		# cleanup, restore old index and force update on comitted files
		test 0 = $ccode && rm -f "$GIT_DIR/commit-msg.txt" "$GIT_DIR/commit-editmsg.txt"
		mv -f "$TMPINDEX" "$GIT_DIR/index"
		trap - 0 HUP INT QUIT TRAP USR1 PIPE TERM
		git-update-index --refresh --again > /dev/null  # update stat info
		# handle auto pushing
		test 0 = $ccode -a "true" = "`git-config --bool yyhelp.auto-push-commits`" && yypushpull
		exit $ccode ;;
*/yydiff)	args=`getopt -n "$0" -o r: -- "$@"`; [ $? = 0 ] || exec yyhelp; eval set -- "$args"
		rev=HEAD
		while :; do case "$1" in
		-r) rev="$2"; shift 2; git-rev-parse --verify "$rev" >/dev/null 2>&1 || exit_err 3 "$0: unknown revision: $rev" ;;
		--) shift; break ;;
		esac; done
		git-update-index --refresh > /dev/null
		# 'git-diff-index -m' shows uncommittable files
		git-diff-index -r -C -p $COLOR "$rev" -- "$@" | $OUT ;;
*/yygc)		test -n "$*" && exec yyhelp
		exec git-gc --prune ;;
*/yyHistoryGrep)
		REV= ; test "x$1" = "x-r" && { shift; REV=--reverse ; }
		test "$#" = 1 || exec yyhelp
		# create temporary file
	        TMAP=`mktemp -t yyTMAP.$$XXXXXX` && touch $TMAP || exit_err 9 "$0: failed to create temporary file"
		trap "rm -f $TMAP" 0 HUP INT QUIT TRAP USR1 PIPE TERM
	        # create sed mapping from tree hashes to commit hashes
		git-rev-list --all --pretty=format:'/^%t/s/^%T:/%H:/' | grep -v ^commit >$TMAP
		# grep trees in chronological order and convert hashes on the fly
		git-rev-list --all $REV --pretty=format:%T | grep -v ^commit |
		  xargs git-grep -E -e $1 | sed -f $TMAP | $OUT
		;;
*/yyinfo)	test -n "$*" && exec yyhelp
		REPO="`cd \"$gitdir/..\" && basename \"$(pwd)\" `"
		REPODIR="`git-rev-parse --show-cdup`" ; test -z "$REPODIR" && REPODIR=.
		OC="`git-rev-parse --verify origin 2>/dev/null`"; test -n "$OC" && OC="`date -d \"$(git-log -n1 --pretty=format:%cD $OC)\" '+%F %T %z'` # $OC"
		HC="`git-rev-parse --verify HEAD   2>/dev/null`"; test -n "$HC" && HC="`date -d \"$(git-log -n1 --pretty=format:%cD $HC)\" '+%F %T %z'` # $HC"
		test -n "$SVNURL" && SVNREV=`git-cat-file commit HEAD | tail -n1 | sed -n '/^git-svn-id:.*/ { s/^[^@]\+@\([0-9]\+\).*/\1/; p; }'`
		true			&& echo "GIT-Repo: $REPO"
		true			&& echo "URL:      $GITURL"
		true			&& echo "Path:     $REPODIR"
		$gitsvn                 && echo "Method:   git-svn commands are used for push and pull"
		$gitsvn			|| echo "Method:   git-push and git-pull are used for updates"
		test -n "$SVNURL"	&& echo "SVN-URL:  $SVNURL"
		test -n "$SVNREV"	&& echo "SVN-Rev:  $SVNREV (HEAD)"
		test -n "$HC"		&& echo "HEAD:     $HC"
		test -n "$OC"		&& echo "Origin:   $OC"
		;;
*/yylsbranches)	test -n "$*" && exec yyhelp
		exec git-branch $COLOR -a ;;
*/yylstags)	test -n "$*" && exec yyhelp
		exec git-tag -l '.*' ;;
*/yypull)	test -n "$*" && exec yyhelp
		$gitsvn && exec git-svn rebase || exec git-pull ;;
*/yypushpull)	test -n "$*" && exec yyhelp
		$gitsvn && exec git-svn dcommit
		git-push && git-pull || exit $? ;;
*/yyremove)	exec git-update-index --force-remove -- "$@" ;;
*/yyreset)	test -n "$*" && exec yyhelp
		exec git-checkout -f HEAD ;;
*/yyrestore)	git-ls-tree --full-name -r HEAD -- "$@" | git-update-index --index-info # resurrect deleted
		exec git-checkout-index -f -u -- "$@" ;;
*/yystatus)	test -z "$*" || match '-[tuc]' "$*" || exec yyhelp
		git-update-index --refresh > /dev/null	# update stat info
		! match '*-[tc]*' "$*" &&	# list unknown files
		  git-ls-files --others --directory --exclude-from=$gitdir/info/exclude --exclude-per-directory=.gitignore |
		    { test " $*" != " -u" && sed 's,^,?  ,' || sed 's,/$,,' ; }
		test " $*" != " -u" && {	# list non-unknown files
		  if test -z "$githead" ; then	# empty history, list new files
		    git-ls-files | sed 's/^/A  /'
		  else				# list known files, path-relative
		    list_committable
		  fi
		} | { test " $*" != " -c" && cat || sed 's/^[^ ]  \([^ ].*\)/* \1:/' ; } ;;
*/yytag)	(set -e && test -n "$*" && test -z "$3" && ! match "* -*" " $*" ) || exec yyhelp
		test -n "$2" && exec git-tag "$1" "$2"
		exec git-tag "$1" ;;
*/yytagdel)	test -z "$*" && exec yyhelp
		match "* -*" " $*" && exec yyhelp
		exec git-tag -d "$1" ;;
*/yyuncommit)	test -n "$*" && exec yyhelp
		exec git-reset --soft HEAD~1 ;;
*/yyview)	test -n "$*" && exec yyhelp
		exec gitk --all -d & ;;
*/yywarp)	test -z "$*" && exec yyhelp
		git-rev-parse --verify "$1" >/dev/null 2>&1 || exit_err 3 "$0: unknown revision: $1"
		exec git-checkout "$1" ;;
*)		test "$YYHELP" || { echo "`basename \"$0\"`: No such command" >&2 ; exit 9 ; } ;;
esac
test "$YYHELP" || exit 0 # successful command execution

YYHELP_ALIASES="yyadd yyblame yybranch yybranchdel yyChangeLog yycommit yydiff yygc yyHistoryGrep
                yyinfo yylsbranches yylstags yypull yypushpull yyremove yyreset yyrestore
                yystatus yytag yytagdel yyuncommit yyview yywarp"

test "x$1" = "x--install-aliases" && {
	git --version | egrep '^git version (1\.[5-9]\.|[2-9]\.)' -q ||
					   { echo "$0: failed to install: missing git >= 1.5.0" >&2 ; exit 2 ; }
	test -x ./yyhelp		|| { echo "$0: failed to install: missing ./yyhelp" >&2 ; exit 2 ; }
	test -x /bin/bash		|| { echo "$0: failed to install: missing /bin/bash" >&2 ; exit 2 ; }
	gawk --version 2>/dev/null | fgrep -q "GNU Awk" ||
	                                   { echo "$0: failed to install: missing GNU awk (/usr/bin/gawk)" >&2 ; exit 2 ; }
	set -e
	for i in $YYHELP_ALIASES ; do
		test -L $i || ln -vs yyhelp $i
	done
	exit 0
}
test "x$1" = "x--uninstall-aliases" && {
	set -e
	for i in $YYHELP_ALIASES ; do
		test -L $i && rm -vf $i
	done
	exit 0
}

{	# yyhelp
	echo -e 'YummyYummySourceControl                                 YummyYummySourceControl'
	echo
	#       0\t911234567892123456789312345678941234567895123456789612345678971234567898
	$SETBOLD
	echo -e 'NAME'
	$SETNORM
	echo -e '\tYummyYummySourceControl - Simple and convenient Git wrapper'
	echo
	$SETBOLD
	echo -e 'SYNOPSIS'
	$SETNORM
	echo -e '\tyyadd          [FILES...] - add files to git repository'
	echo -e '\tyyblame        [FILES...] - annotate file source code'
	echo -e '\tyybranch    [-f] <branch> - add branch (forcable)'
	echo -e '\tyybranchdel [-f] <branch> - delete branch (forcable)'
	echo -e '\tyyChangeLog [-s]          - show git log in ChangeLog style'
	echo -e '\t                            -s: skip committed SVN revisions'
	echo -e '\tyycommit       [FILES...] - commit current working tree'
	echo -e '\tyydiff [-rREV] [FILES...] - show committable differences in'
	echo -e '\t                            working tree; given revision REV'
	echo -e '\tyygc                      - repack and prune repository'
	echo -e '\tyyhelp                    - display yy* help information'
	echo -e '\tyyHistoryGrep  [-r] <PAT> - grep commit history for POSIX extended'
	echo -e '\t                            regular expression pattern PAT'
	echo -e '\t                            -r: reverse order, search chronologically'
	echo -e '\tyyinfo                    - display repository information'
	echo -e '\tyylsbranches              - list branches'
	echo -e '\tyylstags                  - list tags'
	echo -e '\tyypull                    - pull upstream sources'
	echo -e '\tyypushpull                - push & pull upstream sources'
	echo -e '\tyyremove       [FILES...] - remove files from git repository'
	echo -e '\tyyreset                   - reset (revert to HEAD) all files in the tree'
	echo -e '\tyyrestore      [FILES...] - forcefully recheckout specific files'
	echo -e '\tyystatus       [-t|-u|-c] - display working tree status'
	echo -e '\t                            -t: trim unknown; -u: show unknown;'
	echo -e '\t                            -c: trim and use commit message style'
	echo -e '\tyytag    <tag> [revision] - add tag'
	echo -e '\tyytagdel <tag>            - delete tag'
	echo -e '\tyyuncommit                - undo the last commit (must be unpushed)'
	echo -e '\tyyview                    - start view to browse & navigate the history'
	echo -e '\tyywarp           <branch> - checkout new branch'
	echo
	$SETBOLD
	echo -e 'DESCRIPTION'
	$SETNORM
	echo -e '\tYummyYummySourceControl is a shallow wrapper around the myriads of'
	echo -e '\tcommands and options offered by the Git(7) revision control system.'
	echo -e '\tThe scope of this wrapper is confined to a farily basic set of actions'
	echo -e '\taround source control management, such as adding/removing/modifying'
	echo -e '\tfiles and the simplest forms of tag and branch handling.'
	echo -e '\tIf git-svn(1) is used to upgrade the repository, the git-svn commands'
	echo -e '\trebase and dcommit will be used to push and pull respectively.'
	echo -e '\tThe yyinfo command indicates git-svn repositories.'
	echo
	$SETBOLD
	echo -e 'CONFIGURATION'
	$SETNORM
	echo -e "\tThe yycommit command can be configured to automatically issue"
	echo -e "\tyypushpull after successful commits with the following option:"
	echo -e "\t\tgit-config yyhelp.auto-push-commits true"
	echo
	$SETBOLD
	echo -e 'INSTALLATION'
	$SETNORM
	echo -e '\tTo install YummyYummySourceControl, copy yyhelp into a bin/ directory'
	echo -e '\tfrom $PATH and invoke: ./yyhelp --install-aliases'
	echo
	$SETBOLD
	echo -e 'HISTORY'
	$SETNORM
	echo -e "\tYummyYummySourceControl was created as a very shallow porcelain script"
	echo -e "\taround git(7) tool option variants, to simplify common use cases."
	echo -e "\tDepending on programming habits, YummyYummySourceControl may or may"
	echo -e "\tnot suit a developers daily needs. It is in no case meant as a full"
	echo -e "\treplacement for the git interface (there is e.g. no yyclone)."
	echo -e "\tThe prefix 'yy' was choosen to allow conflict free shell completion."
	echo
	VERSIONSTRING___________FIXED=`printf '%-30s' "$VERSION"`
	echo -e "$VERSIONSTRING___________FIXED                          YummyYummySourceControl"
} | $OUT
exit 0
