#!/bin/sh -efu
#
# Copyright (C) 2006  Dmitry V. Levin <ldv@altlinux.org>
# Copyright (C) 2006  Alexey Gladkov <legion@altlinux.org>
#
# Incrementally import source packages to git 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.
#

. 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  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 - incrementally import source packages to git repository.

Usage: $PROG [options] <source-package>...
or:    $PROG [options] --stdin

Options:
  --branch=BRANCH           branch name where to import (default is "srpms");
  --import-only             do not perform a checkout after importing;
  --no-untar                do not unpack source tarballs;
  --rules=FILENAME          gear rules filename (default is ".gear-rules");
  --spec-pattern=PATTERN    specfile pattern list (default is "*.spec");
  --stdin                   read source package names from stdin;
  -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
}

exit_handler()
{
	local rc=$?
	trap - EXIT
	cd "$saved_cwd"
	# Recover index file.
	if [ -z "$index_already_recovered" ]; then
		[ -f "$tmpdir/index" ] &&
			cp -p "$tmpdir/index" "$index_orig" ||
			rm -f "$index_orig" ||:
	fi
	rm -rf -- "$tmpdir"
	# Recover HEAD.
	[ "$branch_orig" = "refs/heads/$branch_import" ] ||
		git-symbolic-ref HEAD "$branch_orig"
	exit $rc
}

# heuristically shorten tar directory name.
shorten_dir_name()
{
	local subdir="$1" name="$2" no_mv="$3" short
	shift 3

	short="$(printf %s "$name" |LC_COLLATE=C sed -e 's/\(-[0-9]\+\([.[:alpha:]]\+[.[:alnum:]]*\)\?\)\+$//')"
	if [ -n "$short" -a "$short" != "$name" ] && [ ! -e "$short" ]; then
		[ -n "$no_mv" ] ||
			mv "$subdir/$name" "$subdir/$short"
		name="$short"
	fi

	printf %s "$name"
}

optimize_rules()
{
	local rules
	rules="$1" && shift
	[ -s "$rules" ] || return 0

	tmp="$(mktemp rules.XXXXXX)"
	sed -ne 's/^[^:]\+: [^ ]*\.\([^ .]\+\).*/\1/p' "$rules" |
		LC_COLLATE=C sort -u |
		while read suffix; do
			local quoted
			quoted="$(quote_sed_regexp "$suffix")"
			method="$(sed -ne 's/^\([^:]\+\): [^ ]*\.'"$quoted"'$/\1/p' "$rules" |sort -u)"
			case "$method" in
				copy|gzip|bzip2)
					printf '%s: *.%s\n' "$method" "$suffix"
					;;
				*)
					sed -ne 's/^[^:]\+: [^ ]*\.'"$quoted"'$/&/p' "$rules"
					;;
			esac
		done >"$tmp"
	sed -ne 's/^[^:]\+: \([^ ]\+ .*\|[^ .]\+$\)/&/p' "$rules" >>"$tmp"
	mv "$tmp" "$rules"
	git-add $verbose "$rules"
}

untar()
{
	local f version method_tar method_copy tar_dir=
	f="$1" && shift
	version="$1" && shift
	method_tar="$1" && shift
	method_copy="$1" && shift

	if [ -z "$no_untar" -a -z "${f%%*.tar}" ]; then
		local tarlist
		tarlist="$(tar -tf "$f")"
		# Avoid unpacking archives with paths containing /../
		printf %s "$tarlist" |egrep -qs '^\.\.(/|$)|/\.\.(/|$)' ||
			# Prefix leading "/" with ".", strip leading "./"
			tar_dir="$(printf %s "$tarlist" |
				   tr -s / |
				   sed 's|^/|.&|' |
				   sed 's|^\(\./\)\+||g' |
				   sed -n 's|^\([^/]\+\)\(/.*\)\?$|\1|p' |
				   LC_COLLATE=C sort -u)"
	fi
	if [ -n "$tar_dir" ]; then
		local base= short subdir unpack_into_subdir= tar_name="${f%.tar}"
		if [ "$tar_dir" != "$(printf %s "$tar_dir" |LC_COLLATE=C tr -d '[:space:]')" ] ||
		   [ -n "$(printf %s "$tar_dir" |LC_COLLATE=C tr -d -- '-[:alnum:]_.,+')" ]; then
			unpack_into_subdir=1
			tar_dir="$tar_name"
		else
			base="$tar_dir"
		fi
		subdir="$(mktemp -d subdir.XXXXXX)"
		tar -x -C "$subdir" -f "$f"
		chmod -Rf u+rwX,go-w "$subdir"
		rm "$f"

		# Heuristics.
		tar_dir="$(shorten_dir_name "$subdir" "$tar_dir" "$unpack_into_subdir")"

		# Avoid file names clash.
		if [ -e "$tar_dir" -a -z "$unpack_into_subdir" ]; then
			mv "$subdir/$tar_dir" "$subdir/$tar_name"
			tar_dir="$(shorten_dir_name "$subdir" "$tar_name" "$unpack_into_subdir")"
		fi
		if [ -e "$tar_dir" ]; then
			mv "$subdir/$tar_dir" "$subdir/$tar_name.tar"
			tar_dir="$tar_name.tar"
		fi

		if [ -n "$unpack_into_subdir" ]; then
			mv "$subdir" "$tar_dir"
		else
			mv "$subdir/$tar_dir" .
			rmdir "$subdir"
		fi

		local quoted
		quoted="$(quote_sed_regexp "$version")"

		if [ "$tar_name" = "$base" ]; then
			if [ "$tar_dir-$version" = "$tar_name" ]; then
				printf '%s: %s\n' "$method_tar" "$tar_dir"
			else
				printf '%s: %s name=%s\n' "$method_tar" "$tar_dir" "$tar_name"
			fi
		else
			if [ "$tar_dir-$version" = "$tar_name" ]; then
				printf '%s: %s base=%s\n' "$method_tar" "$tar_dir" "$base"
			else
				printf '%s: %s name=%s base=%s\n' "$method_tar" "$tar_dir" "$tar_name" "$base"
			fi
		fi |sed "s/${quoted:-@version@}/@version@/g" >>"$gear_rules"
		git-add $verbose "$tar_dir"
	else
		printf '%s: %s\n' "$method_copy" "$f" >>"$gear_rules"
		git-add $verbose "$f"
	fi
}

import()
{
	local srpm="$1" && shift
	cd "$saved_cwd"
	verbose "Processing $srpm"
	srpm="$(readlink -ev "$srpm")"

	local header name verrel buildtime filelist changelog author
	header="$(od -A n -N 8 -t x1 -- "$srpm")" &&
	[ -n "$header" -a -z "${header## ed ab ee db ?? ?? 00 01}" ] &&
	name="$(rpmquery -p --qf '%{NAME}' -- "$srpm")" &&
	verrel="$(rpmquery -p --qf '%{VERSION}-%{RELEASE}' -- "$srpm")" &&
	buildtime="$(rpmquery -p --qf '%{BUILDTIME}' -- "$srpm")" &&
	changelog="$(rpmquery -p --qf '%|CHANGELOGTEXT?{%{CHANGELOGTEXT}}|' -- "$srpm")" &&
	filelist="$(rpm2cpio "$srpm" |cpio --list --quiet )" ||
		fatal "$srpm: Not a valid source rpm package"

	author="$(rpmquery -p --qf '%|CHANGELOGNAME?{%{CHANGELOGNAME}}|' -- "$srpm" |
		sed -ne 's/^\([^<]*<[^@]\+\(@\| at \)[^@]\+>\).*/\1/p' |
		sed -e s/alt-linux/altlinux/g)"

	# Use own directory for checkout.
	rm -rf "$workdir"
	mkdir "$workdir"
	cd "$workdir"
	rpm2cpio "$srpm" |cpio --extract $([ -n "$verbose" ] || echo --quiet)
	verbose "Unpacked ${srpm##*/}"

	# Find specfile.
	local spec= spec_version=
	if spec="$(find_specfile_in_cwd "${srpm##*/}: " ${filelist})"; then
		[ -z "${spec##$def_spec_pattern}" -o -z "${spec%%$def_spec_pattern}" ] ||
			printf 'spec: %s\n' "$spec" >>"$gear_rules"
		spec_version="$(sed '/^version:[[:space:]]*/I!d;s///;q' "$spec")"
	fi

	rm -f "$index_orig"
	local f
	for f in ${filelist}; do
		if [ -L "$f" -o ! -f "$f" ]; then
			info "$f: Non-regular file ignored."
			continue
		fi
		if [ "$f" = "$spec" ]; then
			git-add $verbose "$f"
			continue
		fi
		if [ "$f" = "$gear_rules" ]; then
			info "$f: rules file $gear_rules ignored."
			continue
		fi
		case "$f" in
			*.tar)
				untar "$f" "$spec_version" tar copy
				;;
			*.gz)
				gunzip "$f"
				untar "${f%.gz}" "$spec_version" tar.gz gzip
				;;
			*.bz2)
				bunzip2 "$f"
				untar "${f%.bz2}" "$spec_version" tar.bz2 bzip2
				;;
			*)
				printf 'copy: %s\n' "$f" >>"$gear_rules"
				git-add $verbose "$f"
				;;
		esac
	done

	optimize_rules "$gear_rules"

	local message
	message="$(printf '%s\n\n%s\n' "$verrel" "$changelog")"
	TZ=UTC faketime -d "1970-01-01 $buildtime seconds" -- \
		git-commit -a -m "$message" ${author:+--author "$author"}
	verbose "Committed $name $verrel"
	TZ=UTC faketime -d "1970-01-01 $buildtime seconds" -- \
		git-tag -a -m "$name $verrel" "$verrel" ||:
	info "Imported $srpm"
}

TEMP=`getopt -n $PROG -o h,q,v,V -l branch:,import-only,no-untar,rules:,spec-pattern:,stdin,help,quiet,verbose,version -- "$@"` ||
	show_usage
eval set -- "$TEMP"

branch_import=
import_only=
no_untar=
read_from_stdin=
gear_rules=".gear-rules"
while :; do
	case "$1" in
		--) shift; break
			;;
		--branch) shift; branch_import="$1"
			;;
		--import-only) import_only=1
			;;
		--no-untar) no_untar=1
			;;
		--rules) shift; gear_rules="$1"
			;;
		--spec-pattern) shift; spec_pattern="$1"
			;;
		--stdin) read_from_stdin=1
			;;
		-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

if [ -n "$read_from_stdin" ]; then
	# No arguments, please.
	[ "$#" -eq 0 ] ||
		show_usage 'Too many arguments.'
else
	# At least one argument, please.
	[ "$#" -ge 1 ] ||
		show_usage 'Not enough arguments.'
fi

GIT_DIR="$(git-rev-parse --git-dir)"
GIT_DIR="$(readlink -ev "$GIT_DIR")"
export GIT_DIR

if [ -n "$branch_import" ]; then
	# Check given branch name
	[ -f "$GIT_DIR/refs/heads/$branch_import" ] ||
		fatal "$branch_import: branch not found"
else
	branch_import=srpms
fi

branch_orig="$(git-symbolic-ref HEAD)"
head_orig="$(git-rev-parse --verify HEAD 2>/dev/null ||:)"

unset GIT_INDEX_FILE ||:
index_orig="$GIT_DIR/index"
index_already_recovered=

# Save current work directory.
saved_cwd="$(/bin/pwd)"

tmpdir="$(mktemp -dt "$PROG.XXXXXXXX")"
trap exit_handler HUP PIPE INT QUIT TERM EXIT
workdir="$tmpdir/work"

# Backup index file.
[ ! -f "$index_orig" ] ||
	cp -p "$index_orig" "$tmpdir/index"

git-symbolic-ref HEAD "refs/heads/$branch_import"

if [ -n "$read_from_stdin" ]; then
	while read REPLY; do
		import "$REPLY"
	done
else
	for REPLY; do
		import "$REPLY"
	done
fi

# Merge after import.
cd "$saved_cwd"
head_new="$(git-rev-parse --verify HEAD 2>/dev/null ||:)"
if [ "$head_orig" = "$head_new" ]; then
	verbose "Nothing imported."
elif [ -z "$import_only" ]; then
	if [ -z "$head_orig" ]; then
		git-update-ref "$branch_orig" "refs/heads/$branch_import"
		info "Created ${branch_orig#refs/heads/} branch"
		git-symbolic-ref HEAD "$branch_orig"
		git-checkout -f
		index_already_recovered=1
	elif [ -n "$head_new" ]; then
		git-symbolic-ref HEAD "$branch_orig"
		git-checkout -f
		index_already_recovered=1
		git-merge srpmimport HEAD "$branch_import"
		info "Merged $branch_import branch into ${branch_orig#refs/heads/} branch"
	fi
fi
