#!/bin/sh -efu
#
# Copyright (C) 2006-2008  Alexey Gladkov <legion@altlinux.org>
# Copyright (C) 2006-2009  Dmitry V. Levin <ldv@altlinux.org>
# Copyright (C) 2006  Sergey Vlasov <vsu@altlinux.org>
#
# gear-update updates subdirectory from source directory or archive file.
#
# This file 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 2 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.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
#

. gear-sh-functions

show_help()
{
	cat <<EOF
$PROG - update subdirectory from source directory or archive file.

Usage: $PROG [Options] <source> <subdirectory>

Options:

  --exclude=PATTERN   exclude files matching posix-egrep regular expression PATTERN;
  --ignore-exclude    ignore .gitignore and .git/info/exclude;
  -a,--all            extract all files and directories from archive;
  -c,--create         create the <subdirectory> before unpacking the <source>,
                      the <subdirectory> should not exist;
  -s,--subdir=DIR     extract specified directory name from archive;
  -t,--type=TYPE      define source type:
                      'dir', 'tar', 'cpio', 'cpio.gz', 'cpio.bz2' or 'zip';
  -f,--force          remove files from <subdirectory> even
                      if untracked or modified files found;
  -v,--verbose        print a message for each action;
  -V,--version        print program version and exit;
  -h,--help           show this text and exit.

Report bugs to http://bugzilla.altlinux.org/

EOF
	exit
}

print_version()
{
	cat <<EOF
$PROG version $PROG_VERSION
Written by Alexey Gladkov <legion@altlinux.org>

Copyright (C) 2006-2008  Alexey Gladkov <legion@altlinux.org>
Copyright (C) 2006-2008  Dmitry V. Levin <ldv@altlinux.org>
Copyright (C) 2006  Sergey Vlasov <vsu@altlinux.org>
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
EOF
	exit
}

source=
source_type=
get_type_by_source()
{
	if [ -d "$source" ]; then
		source_type=dir
		return
	fi

	local file_type
	file_type="$(file -b "$source")"
	case "$file_type" in
		*cpio\ archive*)
			source_type=cpio
			return
			;;
		*tar\ archive*)
			source_type=tar
			return
			;;
		*Zip\ archive*)
			source_type=zip
			return
			;;
	esac

	file_type="$(file -bz "$source")"
	case "$file_type" in
		*cpio\ archive*bzip2\ compressed\ data*)
			source_type=cpio.bz2
			return
			;;
		*cpio\ archive*gzip\ compressed\ data*)
			source_type=cpio.gz
			return
			;;
		*tar\ archive*)
			source_type=tar
			return
			;;
	esac

	fatal "$source: unrecognized source type: $file_type"
}

cpio_unpack()
{
	local file="$1"; shift
	local subdir="$1"; shift
	local compress

	case "$source_type" in
		cpio)     compress='cat' ;;
		cpio.gz)  compress='gunzip -c' ;;
		cpio.bz2) compress='bunzip2 -c' ;;
	esac
	$compress "$file" |cpio --quiet -id ${subdir:+"$subdir/*"}
}

cpio_ls_cmd()
{
	local file="$1"; shift
	local compress

	case "$source_type" in
		cpio)     compress='cat' ;;
		cpio.gz)  compress='gunzip -c' ;;
		cpio.bz2) compress='bunzip2 -c' ;;

	esac
	$compress "$file" |cpio --quiet -t
}

all_subdirs=
subdir=
validate_subdir()
{
	if [ "$source_type" = dir ]; then
		[ -z "$subdir" -o "$subdir" = "$source" ] ||
			fatal "Invalid subdir \`$subdir' for directory source \`$source'."
		subdir="$source"
		return 0
	fi

	[ -z "$all_subdirs" ] ||
		return 0

	local source_ls_cmd
	case "$source_type" in
		tar) source_ls_cmd='tar -tf' ;;
		zip) source_ls_cmd='zipinfo -1 --' ;;
		cpio*) source_ls_cmd='cpio_ls_cmd' ;;
		*) fatal "$source_type: unknown source type." ;;
	esac

	if [ -z "$subdir" ]; then
		subdir="$($source_ls_cmd "$source")"
		subdir="$(printf %s "$subdir" |
			  sed 's#\(\([./]\+\)\?[^/]\+\)/.*#\1#g' |
			  LC_ALL=C grep '^[^[:space:]]' |
			  LC_COLLATE=C sort -u)"
	fi

	[ -n "$subdir" ] ||
		fatal "$source: subdirectory not found"
	[ `printf %s "$subdir" |wc -l` -eq 0 ] ||
		fatal "More than one subdirectory specified: $(printf %s "$subdir" |tr -s '[:space:]' ' ')"

	local quoted_subdir
	quoted_subdir="$(quote_sed_regexp "$subdir")"
	$source_ls_cmd "$source" |LC_ALL=C grep -qs "^$quoted_subdir\(/\|\$\)" ||
		fatal "$source: directory \`$subdir' not found in archive."
}

force=
canon_destdir=
validate_destdir()
{
	[ "$#" -eq 2 ] ||
		fatal "validate_destdir: invalid ($#) number of arguments."
	local src_dir="$1"; shift
	local dst_dir="$1"; shift
	[ -z "$force" ] ||
		return 0
	if [ "$dst_dir" = '.' -a -d '.git' -a -d "$src_dir/.git" ]; then
		fatal "$canon_destdir/.git: Cowardly refusing to replace git directory."
	fi
}

update_destdir()
{
	[ "$#" -eq 3 ] ||
		fatal "update_destdir: invalid ($#) number of arguments."
	local command="$1"; shift
	local src_dir="$1"; shift
	local dst_dir="$1"; shift
	local cmd=
	case "$command" in
		copy) cmd="cp -a" ;;
		move) cmd="mv" ;;
	esac
	[ -n "$cmd" ] ||
		fatal "update_destdir: invalid command: $command"

	[ "$dst_dir" = '.' ] ||
		mkdir $verbose -p -- "$dst_dir"

	find "$src_dir" -mindepth 1 -maxdepth 1 \! \( -name '.git' -o -name '.gear' -o -name '.gear-rules' -o -name '.gear-tags' \) -print0 |
		xargs -r0 -i $cmd -- \{\} "$dst_dir/"
}

workdir=
destdir=
dest_create=
cleanup_handler()
{
	[ -z "$workdir" -o ! -d "$workdir" ] ||
		rm -rf -- "$workdir"
	if [ "$*" != 0 ]; then
		# Rollback actions
		if [ -d "$destdir" ]; then
			find "$destdir" -mindepth 1 -maxdepth 1 \! -name '.git' -print0 |
				xargs -r0 rm $verbose -rf --
		else
			rm $verbose -f -- "$destdir"
		fi

		if [ -z "$dest_create" ]; then
			mkdir -p -- "$destdir"
			cd "$destdir"
			git checkout-index -a
		fi
		message 'failed to update target directory.'
	fi
}

TEMP=`getopt -n $PROG -o a,c,f,t:,s:,v,V,h -l all,create,exclude:,ignore-exclude,force,type:,subdir:,verbose,version,help -- "$@"` || show_usage
eval set -- "$TEMP"

exclude_pattern=
ignore_exclude=
while :; do
	case "$1" in
	    -a|--all) all_subdirs=1
		;;
	    -c|--create) dest_create=1
		;;
	    --exclude) shift
		[ -z "$1" ] || exclude_pattern="$1"
		;;
	    --ignore-exclude) ignore_exclude=1
		;;
	    -f|--force) force=1
		;;
	    -t|--type) shift; source_type="$1"
		;;
	    -s|--subdir) shift; subdir="$1"
		;;
	    -v|--verbose) verbose=-v
		;;
	    -V|--version) print_version
		;;
	    -h|--help) show_help
		;;
	    --) shift; break
		;;
	    *) fatal "unrecognized option: $1"
		;;
	esac
	shift
done

[ "$#" -ge 2 ] ||
	show_usage 'Not enough arguments.'
[ "$#" -eq 2 ] ||
	show_usage 'Too many arguments.'

source="$(readlink -ev -- "$1")"; shift
if [ -n "$dest_create" ]; then
	destdir="$(readlink -fv -- "$1")"
	[ ! -e "$destdir" ] ||
		fatal "$destdir: Destination directory already exists."
else
	destdir="$(readlink -ev -- "$1")"
fi
shift
canon_destdir="$destdir"

[ "$source" != "$destdir" ] ||
	fatal "\`$source' and \`$destdir' are the same place."
[ -n "$dest_create" -o -d "$destdir" ] ||
	fatal "$destdir: not a directory"

[ -z "$subdir" -o -z "$all_subdirs" ] ||
	fatal "Cannot use --all and --subdir at the same time."

[ -n "$source_type" ] ||
	get_type_by_source

if [ "$source_type" = "dir" ]; then
	[ -z "$all_subdirs" ] ||
		fatal "Cannot use --all for updating from a directory source."

	[ -z "$subdir" ] ||
		fatal "Cannot use --subdir for updating from a directory source."
fi

# Change to toplevel directory
chdir_to_toplevel
topdir="$(readlink -ev .)"

# Make destdir relative
destdir="$destdir/"
destdir="${destdir#$topdir/}"
[ "$destdir" = "${destdir#/}" ] ||
	fatal "$destdir: directory out of \`$topdir' tree."
[ -n "$destdir" ] &&
	destdir="${destdir%/}" ||
	destdir='.'

initial_commit=
git rev-parse --verify HEAD >/dev/null 2>&1 || initial_commit=1

if [ -z "$force" -a -z "$initial_commit" ]; then
	out="$(git diff-index --cached --name-only HEAD -- "$destdir")"
	[ -z "$out" ] ||
		fatal "$destdir: Changed files found in the index."

	out="$(git diff --name-only -- "$destdir" &&
	       git ls-files --directory --others --exclude-per-directory=.gitignore -- "$destdir")"
	[ -z "$out" ] ||
		fatal "$destdir: Untracked or modified files found."
fi

[ -n "$dest_create" -o -n "$initial_commit" ] ||
	git rm -n -r ${force:+-f} -- "$destdir" >/dev/null

validate_subdir

# Trap for rollback
install_cleanup_handler cleanup_handler

# Remove $destdir
if [ -z "$dest_create" ]; then
	find "$destdir" -mindepth 1 -maxdepth 1 \! \( -name '.git' -o -name '.gear' -o -name '.gear-rules' -o -name '.gear-tags' \) -print0 |
		xargs -r0 rm $verbose -rf --
	[ "$destdir" = '.' ] ||
		rmdir $verbose -- "$destdir"
fi

# Copy new sources
case "$source_type" in
	cpio*)
		workdir="$(mktemp -d "$PROG.XXXXXXXXX")"
		cd "$workdir"
		cpio_unpack "$source" "$subdir"
		cd - >/dev/null
		validate_destdir "$workdir/$subdir" "$destdir"
		update_destdir move "$workdir/$subdir" "$destdir"
		;;
	dir)
		validate_destdir "$subdir" "$destdir"
		update_destdir copy "$subdir" "$destdir"
		;;
	tar)
		workdir="$(mktemp -d "$PROG.XXXXXXXXX")"
		tar -x -f "$source" -C "$workdir" -- ${subdir:+"$subdir/"}
		validate_destdir "$workdir/$subdir" "$destdir"
		update_destdir move "$workdir/$subdir" "$destdir"
		;;
	zip)
		workdir="$(mktemp -d "$PROG.XXXXXXXXX")"
		unzip -q "$source" ${subdir:+"$subdir/*"} -d "$workdir"
		validate_destdir "$workdir/$subdir" "$destdir"
		update_destdir move "$workdir/$subdir" "$destdir"
		;;
esac
[ -d "$destdir" ] ||
	fatal "$destdir: result of extraction is not a directory."

[ -z "$workdir" -o ! -d "$workdir" ] ||
	rm -rf -- "$workdir"

if [ -n "$exclude_pattern" ]; then
	cd "$destdir"
	find . -regextype 'posix-egrep' -regex "$exclude_pattern" -print0 |
			xargs -r0 rm $verbose -rf --
	cd - >/dev/null
fi

if [ -n "$(find "$destdir" -maxdepth 0 -empty)" ]; then
	echo '.gitattributes export-ignore' >"$destdir/export-ignore"
else
	export_ignore="$(mktemp -- "$destdir/$PROG.XXXXXXXX")"
	echo '.gitattributes export-ignore' >"$export_ignore"
	find "$destdir" -type d -empty -exec ln -- "$export_ignore" '{}/.gitattributes' ';'
	rm -- "$export_ignore"
fi

if [ -n "$ignore_exclude" ]; then
	git ls-files -z -- "$destdir" |
		git update-index --remove --force-remove -z --stdin
	git ls-files -z --others --modified -- "$destdir" |
		git update-index --add ${verbose:+--verbose} -z --stdin
else
	[ -n "$dest_create" -o -n "$initial_commit" ] ||
		git rm -f -r --cached -- "$destdir" >/dev/null
	git add $verbose -- "$destdir"
fi
