#!/usr/pkg/bin/bash
#
# Show status of the repository and your working tree
# Copyright (c) Petr Baudis, 2005
# Copyright (c) Pavel Roskin 2005
#
# The output includes the list of branches and merge status.
# Current branch is marked with ">", remote branches are marked with "R".
# Branches with shelved local changes (currently produced only by
# `cg-switch -l`) are marked with "s".
#
# Then, the files in the working tree are printed out. The output has
# the following format:
#
#	<status flag> <file>
#
# where '<status flag>' can be one of the following:
#
# ?::
#	'<file>' is not known to Cogito. See the IGNORING section below.
# A::
#	'<file>' has been added.
# D::
#	'<file>' has been deleted.
# !::
#	'<file>' is gone from your working copy but not deleted by `cg-rm`.
# M::
#	'<file>' has been touched or modified.
# m::
#	'<file>' has been touched or modified, but will not be automatically
#	committed the next time you call `cg-commit`. This is used during a
#	merge to mark files which contained local changes before the merge.
#
# OPTIONS
# -------
# If neither -g or -w is passed, both is shown; otherwise, only the
# corresponding parts are shown.
#
# -g:: Show the GIT repository information
#	Show the GIT repository information.
#
# -n:: Do not show status flags
#	Do not show status flags. This is probably useful only when you filter
#	the flags for a single specific flag using the '-s' option.
#
# -s STATUS:: Limit to files matching the STATUS flags
#	Show only files with the given status flag, e.g. '-s D'. You can list
#	multiple flags ('-s MmA') to filter for all of them. You can prepend
#	'^' to the STATUS argument to invert the filter - only items with flags
#	NOT listed in the STATUS string will be printed out.
#
# -S:: Do not squash directories
#	By default, cg-status will not list full contents of untracked
#	directories but only their name. This option will make it show the
#	all untracked files inside as well.
#
# -w:: Show working files
#	Show the working tree file list.
#
# -x:: Disable file exclusion
#	Don't exclude any files from listing.
#
# DIRPATH::
#	Path to the directory to use as the base for the working tree
#	file list (instead of the current directory).
#
# NOTES
# -----
# If a file has been removed with `cg-rm` without using the `-f` option
# to remove it physically from the tree it will be reported as both being
# deleted and unknown. The reason for this is that the file is internally
# marked as deleted and thus also untracked. After next commit it will only
# be reported as being untracked.
#
# IGNORING
# --------
# You can declare some files to be ignored: this means that they will
# not be listed as unknown in `cg-status`, `cg-clean` will not remove
# them (unless '-x' is passed), and `cg-init` and `cg-add -a` will
# not add them. However, the moment you explicitly tell Cogito about
# them using `cg-add`, Cogito will stop ignoring them; it will commit
# any modifications in them, etc.: the concept is the same as e.g. in CVS.
# Typically, autogenerated and backup files are marked as ignored.
#
# Which files to ignore is determined by lists of exclude patterns
# stored in various files. There is one pattern per line and the
# patterns are classic shell glob patterns (with '*' and '?' wildcards).
# The pattern can be prefixed by '!' to unignore matching files.
# If the pattern does not contain a slash, it is applied in all
# directories; otherwise, only to the given path in the tree; use
# leading slash to denote the tree root.
#
# For example, consider the following:
#
#	.*
#	!.gitignore
#	!/.list
#
# This will ignore all hidden files except '.gitignore' in all
# directories and the '.list' file in project root.
#
# When collecting the ignore patterns, first the default ignore
# patterns are loaded from '/usr/share/cogito/default-exclude'
# (or a slightly different path depending on your installation prefix).
# Then, '.git/info/exclude' from your working copy is loaded. At last,
# during the actual tree traversal '.gitignore' in each visited directory
# is loaded for the time of its traversal.
#
# FILES
# -----
# $GIT_DIR/info/exclude::
#	The list of ignore patterns; the list itself is not version-tracked
#	and is local to this particular clone.
#
# .gitignore::
#	.gitignore in the working tree will be also scanned for ignore
#	patterns. Contrary to the exclude file, it is usually version-tracked.
#
# BUGS
# ----
# One known bug is that when you `cg-add` a new file and then delete it
# but do not call `cg-rm`, it will not be listed in `cg-status` output,
# but from the merging point of view there will still be "local changes"
# and `cg-diff` will show a diff.

# Testsuite: Marginal (part of t9202-merge-on-dirty)

USAGE="cg-status [-g] [[-n] -s STATUS] [-w] [-x] [DIRPATH]"
_git_wc_unneeded=1

. "${COGITO_LIB:-/usr/pkg/lib/cogito/}"cg-Xlib || exit 1


should_show_flag() {
	[[ -z "$flagfilter" || "$flagfilter" == *"$1"* ]]
}


gitstatus=
workstatus=
exclude=yes
flagfilter=
noflags=
squashdirs=squashdirs
while optparse; do
	if optparse -g; then
		gitstatus=1
	elif optparse -n; then
		noflags=1
	elif optparse -s=; then
		flagfilter="$OPTARG"
		echo "$flagfilter" | /usr/bin/grep -qx '[a-zA-Z?!^]*' \
			|| die "invalid -s status flag"
		if [ x"${flagfilter:0:1}" = x"^" ]; then
			flagfilter="$(echo "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ?!" | tr -d "${flagfilter:1}")"
		fi
	elif optparse -S; then
		squashdirs=nosquashdirs
	elif optparse -w; then
		workstatus=1
	elif optparse -x; then
		exclude=no
	else
		optfail
	fi
done
if [ ! "$gitstatus" ] && [ ! "$workstatus" ]; then
	gitstatus=1
	workstatus=1
fi
if [ "$_git_no_wc" ]; then
	workstatus=
fi



if [ "$gitstatus" ]; then
	mkdir -p "$_git/refs/heads"
	[ "$(find "$_git/refs/heads" -follow -type f)" ] \
	       || die "List of heads is empty."


	[ -s "$_git/branch-name" ] && echo "Branch (informal): $(cat "$_git/branch-name")"

	if [ -s "$_git/head-name" ]; then
		headsha1=$(cat "$_git/$(git-symbolic-ref HEAD)")
		echo "Seeked from head: $(cat "$_git/head-name")"
		echo "Seeked at commit: $headsha1"
		echo
	fi

	echo "Heads:"
	# -name * will prevent listing hidden heads
	maxlen="$(find "$_git/refs/heads" -name '*' -a ! -type d |
	          column_width "$_git/refs/heads/")"
	find "$_git/refs/heads" -name '*' -a ! -type d | sort | while read head; do 
		headsha1="$(cat "$head")"
		headname="${head#$_git/refs/heads/}"
		[ "$headname" = "cg-seek-point" ] && continue
		cf=" "; rf=" "; sf=" ";
		[ "$headname" = "$_git_head" ] && cf=">"
		[ -s "$_git/branches/$headname" ] && rf="R"
		[ -s "$_git/refs/heads/.cg-shelve-$headname" ] && sf="s"
		columns_print "  $sf$rf$cf" - "$headname" t$maxlen "$headsha1" -
	done

	if [ -s "$_git/merging" ]; then
		tmp="$(cat "$_git/merging")"
		echo
		echo "Merging: $(cat "$_git/merging") ($(cat "$_git/merging-sym"))"
		echo "Merge base: $(cat "$_git/merge-base")"
		[ -s "$_git/squashing" ] && echo "Squash-merge."
	fi
	if [ -s "$_git/commit-ignore" ]; then
		echo "Files not to be committed now (contained local changes before the merge):"
		sed 's/^/    /' "$_git/commit-ignore"
	fi

	if [ -s "$_git/blocked" ]; then
		echo
		echo "Changes recording BLOCKED:"
		sed 's/^/    /' "$_git/blocked"
	fi

	if [ ! -s "$_git/$(git-symbolic-ref HEAD)" ]; then
		echo
		echo "Before initial commit."
	fi

	if [ "$_git_no_wc" ]; then
		echo
		echo "Repository without attached working copy."
	fi
fi



if [ "$gitstatus" ] && [ "$workstatus" ]; then
	echo
fi



if [ "$workstatus" ]; then
	git-update-index --refresh > /dev/null

	basepath="$_git_relpath"
	[ "${ARGS[0]}" ] && basepath="$(echo "$_git_relpath${ARGS[0]}" | normpath)"

	should_show_flag '?' &&
		list_untracked_files $exclude $squashdirs | tr '\0' '\n' |
		if [ "$basepath" ]; then
			while IFS=$'' read path; do
				[ x"${path#$basepath}" != x"$path" ] &&
					echo "${path#$_git_relpath}"
			done
		else
			cat
		fi |
		sed 's,^,? ,'

	if [ ! -s "$_git/$(git-symbolic-ref HEAD)" ]; then
		# Initial commit
		should_show_flag 'A' && git-ls-files | sed 's,^,A ,'
	else
		commitignore=
		[ -s "$_git/commit-ignore" ] && commitignore=1

		git-diff-index HEAD -- "${basepath:-.}" | cut -f5- -d' ' | 
		while IFS=$'\t' read -r mode file; do
			if [ "$mode" = D ]; then
				[ "$(git-diff-files -- "$file")" ] && mode=!
			elif [ "$mode" = M ] && [ "$commitignore" ]; then
				/usr/bin/fgrep -qx "$file" "$_git/commit-ignore" && mode=m
			fi
			should_show_flag "$mode" && echo "$mode ${file#$_git_relpath}"
		done
	fi
fi | if [ "$noflags" ]; then
	sed 's/^. //'
else
	cat
fi
