#!/bin/sh -efu
#
# Copyright (C) 2006  Dmitry V. Levin <ldv@altlinux.org>
# Copyright (C) 2006  Sir Raorn <raorn@altlinux.org>
# Copyright (C) 2006  Alexey Gladkov <legion@altlinux.org>
#
# Get Every Archive from git package Repository.
#
# 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.
#

# Note about rules:
#  rules is plain text file, where each meaningful line has following format:
#  directive: args
#
#  Valid directives are:
#  spec, copy, gzip, bzip2, exclude, tar, tar.gz, tar.bz2
#
#  Each directive has own args syntax:
#  spec: path_to_file
#  (copy|gzip|bzip2): glob_pattern..
#  (copy|gzip|bzip2)?: glob_pattern..
#  exclude: glob_pattern..
#  tar(|.gz|.bz2): path_to_directory [options]
#  tar(|.gz|.bz2)?: path_to_directory [options]
#
#  Valid tar options are:
#    name=archive_name  - tar archive name, may reference to keywords;
#    base=base_name     - when specified it is added as a leading path
#                         to the files in the generated tar archive.
#    spec=path_to_file  - path to specfile which defines keywords;
#  Valid tar keywords are:
#    @dir@ - basename(path_to_directory);
#    @name@, @version@, @release@.
#  Default tar archive name is @dir@-@version@.

. gear-sh-functions

print_version()
{
	cat <<EOF
$PROG version $PROG_VERSION
Written by Dmitry V. Levin <ldv@altlinux.org>

Copyright (C) 2006  Dmitry V. Levin <ldv@altlinux.org>
Copyright (C) 2006  Sir Raorn <raorn@altlinux.org>
Copyright (C) 2006  Alexey Gladkov <legion@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
}

show_help()
{
	cat <<EOF
$PROG - extract archive from git package repository.

Usage: $PROG [options] <output-tarball-name>
or:    $PROG [options] --export-dir=<dirname>
or:    $PROG [options] --hasher -- <hsh-command>...
or:    $PROG [options] --rpmbuild -- <rpmbuld-command>...

Options:
  --no-compress             do not compress output archive;
  --bzip2                   compress output archive using bzip2;
  --gzip                    compress output archive using gzip;
  --commit                  make temporary commit prior to extract;
  --hasher                  execute hsh-like command afterwards;
  --rpmbuild                execute rpmbuild-like command afterwards;
  --update-spec             change specfile in repository if specfile changed 
                            after --rpmbuild command;
  --export-dir=DIRNAME      write source files to specified directory;
  --describe                describe package as "%{NAME} %{VERSION} %{RELEASE}";
  -r, --rules=FILENAME      name of file with rules, default is .$PROG-rules;
  -t, --tree-ish=ID         tree, commit or tag object name;
  -q, --quiet               try to be more quiet;
  -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://bugs.altlinux.ru/

EOF
	exit
}

tmpdir=
need_git_reset=
exit_handler()
{
	local rc=$?
	trap - EXIT
	[ -z "$tmpdir" ] ||
		rm -rf -- "$tmpdir"
	if [ -n "$need_git_reset" ]; then
		git-reset --soft HEAD^ &&
			verbose "Reverted temporary commit."
	fi
	exit $rc
}

subst_key()
{
	local var_name="$1" && shift
	local key_name="$1" && shift
	local key_value="$1" && shift

	local quoted var_value
	eval "var_value=\"\$$var_name\""

	if [ "$var_value" != "${var_value#*$key_name*}" ]; then
		check_name "$key_name" "$key_value"
		quoted="$(quote_sed_regexp "$key_value")"
		var_value="$(printf %s "$var_value" |sed "s/$key_name/$quoted/g")"
	fi

	eval "$var_name=\"$(quote_shell_arg "$var_value")\""
}

subst_keywords()
{
	local var="$1" && shift
	local dir="$1" && shift
	local name="$1" && shift
	local version="$1" && shift
	local release="$1" && shift

	subst_key "$var" '@dir@' "$dir"
	subst_key "$var" '@name@' "$name"
	subst_key "$var" '@version@' "$version"
	subst_key "$var" '@release@' "$release"

	eval check_name "$var" "\"\$$var\""
}

get_tar_name()
{
	local dir="$1" && shift

	if [ "$dir" = . ]; then
		dir=
		tar_name='@name@-@version@'
	else
		tar_name='@dir@-@version@'
	fi
	# Special non-empty marker.
	tar_base=' '

	local opt quoted spec= spec_name= spec_version= spec_release=

	for opt; do
		case "$opt" in
			spec=*) spec="${opt#spec=}"
				check_path specfile "$spec"
				;;
			name=*) tar_name="${opt#name=}"
				;;
			base=*) tar_base="${opt#base=}"
				;;
			*) rules_error "Unrecognized option: $opt"
				;;
		esac
	done

	if [ -n "$spec" ]; then
		cat_blob "$tree_id" "$spec" >"$workdir/spec"
		spec_name="$(sed '/^name:[[:space:]]*/I!d;s///;q' "$workdir/spec")"
		spec_version="$(sed '/^version:[[:space:]]*/I!d;s///;q' "$workdir/spec")"
		spec_release="$(sed '/^release:[[:space:]]*/I!d;s///;q' "$workdir/spec")"
	fi
	[ -n "$spec_name" ] || spec_name="$pkg_name"
	[ -n "$spec_version" ] || spec_version="$pkg_version"
	[ -n "$spec_release" ] || spec_release="$pkg_release"

	subst_keywords tar_name "$dir" "$spec_name" "$spec_version" "$spec_release"
	[ -z "$tar_base" -o "$tar_base" = ' ' ] ||
		subst_keywords tar_base "$dir" "$spec_name" "$spec_version" "$spec_release"
}

make_tar()
{
	local optional="$1" && shift
	local cmd="$1" && shift
	local dir="$1" && shift
	local name="$1" && shift
	tar_base="$1" && shift

	[ "$tar_base" != ' ' ] ||
		tar_base="$name"

	local id
	if [ "$dir" = . ]; then
		id="$tree_id"
	else
		local dir_name base_name quoted rc=0
		dir_name="$(dirname -- "$dir")"
		base_name="$(basename -- "$dir")"
		tree="$(traverse_tree "$tree_id" "$dir_name" "$optional")" ||
			rc=$?
		if [ "$rc" = 1 ]; then exit 1
		elif [ "$rc" != 0 ]; then return 0
		fi
		quoted="$(quote_sed_regexp "$base_name")"
		id="$(git-ls-tree "$tree" "$base_name" |
			sed -ne 's/^[^[:space:]]\+[[:space:]]\+tree[[:space:]]\+\([^[:space:]]\+\)[[:space:]]\+'"$quoted"'$/\1/p')"
		if [ -z "$id" ]; then
			if [ "$optional" = 1 ]; then
				return 0
			else
				rules_error "tree $dir not found in $tree"
			fi
		fi

	fi

	if [ -z "$tar_base" ]; then
		git-tar-tree "$id" >"$outdir/$name.tar"
	else
		git-tar-tree "$id" "$tar_base" >"$outdir/$name.tar"
	fi
	verbose "Extracted \"$name.tar\" tarball."
	case "$cmd" in
		tar.gz)
			gzip -9 "$outdir/$name.tar"
			;;
		tar.bz2)
			bzip2 -9 "$outdir/$name.tar"
			;;
	esac
}

make_archive()
{
	# format: "$optional cmd dirname options.."
	[ $# -ge 3 ] ||
		rules_error "No dirname specified"
	local optional="$1" && shift
	local cmd="$1" && shift
	local dir_name="$1" && shift

	check_path dirname "$dir_name"
	get_tar_name "$(basename -- "$dir_name")" "$@"
	make_tar "$optional" "$cmd" "$dir_name" "$tar_name" "$tar_base"
}

copy_by_pattern()
{
	# format: "$optional cmd pattern.."
	[ $# -ge 3 ] ||
		rules_error "No pattern specified"
	local optional="$1" && shift
	local cmd="$1" && shift

	for pattern; do
		[ -z "$(printf %s "$pattern" |tr -d '[:alnum:]_.?*-/')" ] ||
			rules_error "Invalid copy pattern \"$pattern\" specified"
		local dir_name base_name rc=0
		dir_name="$(dirname "$pattern")"
		base_name="$(basename "$pattern")"
		tree="$(traverse_tree "$tree_id" "$dir_name" "$optional")" ||
			rc=$?
		if [ "$rc" = 1 ]; then exit 1
		elif [ "$rc" != 0 ]; then return 0
		fi
		git-ls-tree "$tree" >"$workdir/blobs"
		echo >>"$workdir/blobs"
		local mode otype id name found=
		while read -r mode otype id name; do
			# ignore non-blobs.
			[ "$otype" = blob ] ||
				continue
			# ignore invalid filenames.
			[ "$name" = "${name#*/}" -a \
			  "$name" = "${name%/*}" ] ||
				continue
			# ignore unmatched.
			[ -z "${name##$base_name}" -o \
			  -z "${name%%$base_name}" ] ||
				continue
			local ex_pattern sample
			sample="$dir_name/$name"
			for ex_pattern in $exclude_pattern_list; do
				# ignore excluded.
				[ -n "${sample##$ex_pattern}" -a \
				  -n "${sample%%$ex_pattern}" ] ||
				  	continue 2
			done
			git-cat-file blob "$id" >"$outdir/$name"
			verbose "Extracted \"$name\" by pattern \"$pattern\"."
			[ -n "${mode%%*7??}" ] ||
				chmod a+x "$outdir/$name"
			case "$cmd" in
				gzip)
					gzip -9 "$outdir/$name"
					;;
				bzip2)
					bzip2 -9 "$outdir/$name"
					;;
			esac
			found=1
		done <"$workdir/blobs"
		[ -n "$found" -o "$optional" = 1 ] ||
			rules_error "Unmatched pattern \"$pattern\" specified"
	done
}

parse_rules()
{
	[ -s "$workdir/rules" ] || return 0

	echo >>"$workdir/rules"

	exclude_pattern_list=
	lineno=0

	local cmd options
	while read -r cmd options; do
		lineno="$((lineno+1))"
		if [ "$cmd" = 'exclude:' ]; then
			[ -z "$(printf %s "$options" |tr -d '[:alnum:]_.?*-/')" ] ||
				rules_error "Invalid exclude pattern \"$options\" specified"
			exclude_pattern_list="$exclude_pattern_list $options"
		fi
	done <"$workdir/rules"

	lineno=0
	while read -r cmd options; do
		lineno="$((lineno+1))"
		local optional=0
		case "$cmd" in
			""|\#*)
				continue
				;;
			*\?:)
				cmd="${cmd%\?:}"
				optional=1
				;;
			*:)
				cmd="${cmd%:}"
				optional=0
				;;
			*)
				rules_info "Unrecognized rule ignored"
				continue
				;;
		esac
		case "$cmd" in
			spec)
				continue
				;;
			tar|tar.gz|tar.bz2)
				make_archive "$optional" "$cmd" $options ||
					rules_error "Failed to make archive"
				;;
			copy|gzip|bzip2)
				copy_by_pattern "$optional" "$cmd" $options ||
					rules_error "Failed to copy by pattern"
				;;
			*)
				rules_info "Unrecognized directive \"$cmd\" ignored"
				continue
				;;
		esac
	done <"$workdir/rules"
	lineno=
}

TEMP=`getopt -n $PROG -o r:,t:,h,q,v,V -l no-compress,commit,bzip2,gzip,hasher,rpmbuild,update-spec,export-dir:,describe,rules:,tree-ish:,help,quiet,verbose,version -- "$@"` ||
	show_usage
eval set -- "$TEMP"

hasher=
update_spec=
rpmbuild=
outdir=
describe=
do_commit=
rules=".$PROG-rules"
tar_compress=--gzip
tree_id=HEAD
while :; do
	case "$1" in
		--) shift; break
			;;
		--no-compress) tar_compress=
			;;
		--commit) do_commit=1
			[ "$tree_id" = HEAD ] ||
				show_usage 'Options --commit and --tree-ish are mutually exclusive.'
			;;
		--bzip2) tar_compress=--bzip2
			;;
		--gzip) tar_compress=--gzip
			;;
		--hasher) hasher=1
			[ -z "$outdir$rpmbuild$describe" ] ||
				show_usage 'Options --hasher, --rpmbuild, --export-dir and --describe are mutually exclusive.'
			;;
		--rpmbuild) rpmbuild=1
			[ -z "$hasher$outdir$describe" ] ||
				show_usage 'Options --hasher, --rpmbuild, --export-dir and --describe are mutually exclusive.'
			;;
		--update-spec) update_spec=1
			;;
		--export-dir)
			outdir="$(opt_check_dir "$1" "$2")"
			shift
			[ -z "$hasher$rpmbuild$describe" ] ||
				show_usage 'Options --hasher, --rpmbuild, --export-dir and --describe are mutually exclusive.'
			;;
		--describe) describe=1
			[ -z "$hasher$outdir$rpmbuild" ] ||
				show_usage 'Options --hasher, --rpmbuild, --export-dir and --describe are mutually exclusive.'
			;;
		-r|--rules) shift; rules="$1"
			;;
		-t|--tree-ish) shift; tree_id="$1"
			[ -z "$do_commit" -o "$tree_id" = HEAD ] ||
				show_usage 'Options --commit and --tree-ish are mutually exclusive.'
			;;
		-h|--help) show_help
			;;
		-q|--quiet) quiet=-q; verbose=
			;;
		-v|--verbose) verbose=-v; quiet=
			;;
		-V|--version) print_version
			;;
		*) fatal "Unrecognized option: $1"
			;;
	esac
	shift
done

out_file=
if [ -n "$hasher" -o -n "$rpmbuild" ]; then
	# At least one argument, please.
	[ "$#" -ge 1 ] ||
		show_usage 'Not enough arguments.'
elif [ -n "$outdir" -o -n "$describe" ]; then
	[ "$#" -eq 0 ] ||
		show_usage 'Too many arguments.'
else
	# Exactly one argument, please.
	[ "$#" -ge 1 ] ||
		show_usage 'Not enough arguments.'
	[ "$#" -eq 1 ] ||
		show_usage 'Too many arguments.'
	out_file="$1"
	shift
fi

# Save git directory for future use.
git_dir="$(git-rev-parse --git-dir)"
git_dir="$(readlink -ev "$git_dir")"

# Check given tree-ish.
type="$(git-cat-file -t "$tree_id")" ||
	fatal "$tree_id: Invalid tree-ish object"
case "$type" in
	tag|commit|tree)
		;;
	*)
		fatal "$type: Invalid tree-ish type"
		;;
esac

if [ -n "$update_spec" ]; then
	[ -n "$rpmbuild" -a -z "${GIT_DIR:-}" ] ||
		fatal "Unable to update specfile in this mode."
fi

tmpdir="$(mktemp -dt "$PROG.XXXXXXXX")"
trap exit_handler HUP PIPE INT QUIT TERM EXIT
workdir="$tmpdir/work"
mkdir "$workdir"
if [ -z "$outdir" ]; then
	outdir="$tmpdir/out"
	mkdir $verbose "$outdir"
fi

if [ -n "$do_commit" ]; then
	if git-commit -a -m "Temporary commit by $PROG."; then
		need_git_reset=1
		verbose "Temporarily committed local changes."
	fi
fi

find_specfile

if [ -n "$describe" ]; then
	printf "%s %s %s\n" "$pkg_name" "$pkg_version" "$pkg_release"
	exit 0
fi

parse_rules

install -pm644 "$workdir/specfile" "$outdir/${specfile##*/}"
verbose "Extracted \"${specfile##*/}\" specfile."

if [ -n "$rpmbuild" ]; then
	run_command "$@" --define "_specdir $outdir" --define "_sourcedir $outdir" "$outdir/${specfile##*/}"
	if [ -n "$update_spec" ]; then
		cdup="$(git-rev-parse --show-cdup |tr -d '\n')"
		cmp -s "$outdir/${specfile##*/}" "$cdup$specfile" && rc=0 || rc="$?"
		[ "$rc" -ne 1 ] || cp $verbose -f -- "$outdir/${specfile##*/}" "$cdup$specfile"
	fi
else
	[ -z "$hasher" ] ||
		out_file="$workdir/pkg.tar"
	if [ -n "$out_file" ]; then
		find "$outdir" -maxdepth 1 -type f -printf '%f\0' |
			sort -uz |
			xargs -r0 tar --create --file="$out_file" --directory="$outdir" \
				--label="${specfile##*/}" --owner=root --group=root \
				--mode=u+w,go-w,go+rX $tar_compress --
		verbose "Created \"$out_file\" output tarball."
		if [ -n "$hasher" ]; then
			# Once tarball is created, its sources are no longer needed.
			rm -rf -- "$outdir"
			run_command "$@" "$out_file"
		fi
	fi
fi
