#!/bin/sh

accept_rerere="--rerere-autoupdate"
generate=no
exec=:
update= diff= edit= stop_at_cut= skip_cocci= force_cocci= no_cocci=
while case "$#,$1" in 0,*) break;; *,-*) ;; esac
do
	case "$1" in
	-n)	accept_rerere= ;;
	-e)	edit=t ;;
	-c)	stop_at_cut=1 ;;
	-c?*)	stop_at_cut=${1#-c} ;;
	-d)	update=${2?"diff with what?"}
		diff=yes
		generate=yes
		shift ;;
	-u)	update=${2?"update what?"}
		generate=yes
		shift ;;
	-x)	exec=${2?exec}; shift ;;
	-x?*)	exec=${1#-x} ;;
	-ss)	skip_cocci=t ;;
	-fs)	force_cocci=t ;;
	-ns)	no_cocci=t ;;
	*)	generate=yes
		break ;;
	esac
	shift
done

annotate_merge () {
	test -f Meta/whats-cooking.txt || return 0

	# NEEDSWORK: unify with cook::wildo_match
	perl -e '
		sub wildo_match {
			s/^\s*//;
			if (/^Will (?:\S+ ){0,2}(fast-track|hold|keep|merge|drop|discard|cook|kick|defer|eject|be re-?rolled|wait)[,. ]/ ||
			    /^Not urgent/ || /^Not ready/ || /^Waiting for / || /^Under discussion/ ||
			    /^Can wait in / || /^Still / || /^Stuck / || /^On hold/ || /^Breaks / ||
			    /^Inviting / || /^Comments\?/ || /^Retracted/ ||
			    /^Needs? / || /^Expecting / || /^May want to / || /^Under review/) {
				return 1;
			}
			return 0;
		}

		sub read_message {
			my ($fh, $branch) = @_;
			my ($in_section, $in_desc);
			my @msg = ();
			while (<$fh>) {
				chomp;
				if (/^\* \Q$branch\E /) {
					$in_section = 1;
					next;
				}
				last if (/^[-*\[]/ && $in_section);
				next unless $in_section;
				s/^\s+//;
				if (/^$/) {
					$in_desc = 1;
				}
				next unless ($in_section && $in_desc);
				next if (/Originally merged to '\''next'\'' on ([-0-9]+)/);
				next if (/^source: /);
				last if (wildo_match($_));
				push @msg, "$_\n";
			}
			return ($in_section, @msg);
		}

		my ($branch) = $ARGV[0];
		my ($fh, $in_section, @msg);
		if (open $fh, "<", "Meta/whats-cooking.txt") {
			($in_section, @msg) = read_message($fh, $branch);
		}
		if (!@msg) {
			open my $revs, "-|",
				qw(git -C Meta rev-list -32 HEAD -- whats-cooking.txt);
			while (my $rev = <$revs>) {
				chomp($rev);
				open $fh, "-|",
				qw(git -C Meta cat-file blob), "$rev:whats-cooking.txt";
				($in_section, @msg) = read_message($fh, $branch);
				last if (@msg);
			}
		}
		if (@msg) {
			open(my $fh, "-|", qw(git cat-file commit HEAD));
			my @original = (<$fh>);
			close $fh;
			my @final;
			$in_section = 0;
			for (@original) {
				if (!$in_section) {
					$in_section = 1 if (/^$/);
					next;
				}
				if (/^Conflicts:$/ && $in_section == 2) {
					$in_section = 3;
				}

				if ($in_section == 3) {
					$_ = "# $_";
				}
				push @final, $_;
				if (/^$/ && $in_section == 1) {
					push @final, @msg;
					push @final, "\n";
					$in_section = 2;
				}
			}
			open($fh, "|-", qw(git commit --amend -F -));
			print $fh @final;
			close $fh;
		}
	' "$1"
}

cocci_mark="treewide: apply cocci patch"

case "$generate" in
no)
	accept_rerere () {
		git ls-files -u -z |
		perl -0 -e '
			my %path_stage = ();
			my @to_remove = ();
			while (<>) {
				my ($mode, $sha1, $stage, $path) =
					/^([0-7]+) ([0-9a-f]+) ([0-3])	(.*)$/;
				$path_stage{$path} ||= 0;
				$path_stage{$path} |= (1 << ($stage - 1));
			}

			while (my ($path, $bits) = each %path_stage) {
				if ($bits == 3 || $bits == 5) {
					push @to_remove, $path;
				}
			}
			if (@to_remove) {
				system(qw(git rm -f), @to_remove);
			}
		'

		if ! git write-tree 2>/dev/null >/dev/null
		then
			git rerere remaining
			return 1
		else
			GIT_EDITOR=: git commit --no-verify
			echo "Accepted previous resolution"
			return 0
		fi
	}

	mark_cut () {
		test -n "$stop_at_cut" && return

		count_since_last_cut=$(( $count_since_last_cut + 1 ))
		test -z "$prev_cut" && return
		git commit --allow-empty -m "$prev_cut"
		prev_cut=
	}

	detach () {
		if original_branch=$(git symbolic-ref HEAD 2>/dev/null)
		then
			original_branch=${original_branch#refs/heads/}
			git checkout --quiet --detach
			into="--into $original_branch"
		else
			original_branch=
			into=
		fi
	}

	leave () {
		if test -n "$original_branch" && ! git symbolic-ref HEAD 2>/dev/null
		then
			git checkout --quiet -B "$original_branch"
		fi
		if test -n "$1"
		then
			exit "$1"
		fi
	}

	detach
	cut_seen=0 prev_cut= count_since_last_cut=0 cocci_count=0

	while read branch eh
	do
		case "$branch" in
		'###')
			cut_seen=$(( $cut_seen + 1 ))
			echo >&2 "$branch $eh"
			;;
		esac
		if test -n "$stop_at_cut" && test $stop_at_cut -le $cut_seen
		then
			continue ;# slurp the remainder and skip
		fi

		case "$branch" in
		'###')
			if test "$count_since_last_cut" = 0
			then
				prev_cut=
			else
				prev_cut="$branch $eh"
				count_since_last_cut=0
			fi
			continue ;;
		'#cocci')
			if test -n "$no_cocci"
			then
				continue
			elif test 0 = "$cocci_count" && test -z "$force_cocci"
			then
				continue
			fi

			if test -n "$skip_cocci" && test -n "$eh"
			then
				git cherry-pick --no-commit "$eh"
			else
				rm -f contrib/coccinelle/*.patch
				Meta/Make -j8 coccicheck
				if grep coccicheck-pending Makefile >/dev/null
				then
					Meta/Make -j8 coccicheck-pending
				fi
				cat contrib/coccinelle/*.patch >cocci.patch
				if ! test -s cocci.patch
				then
					leave 0
				fi
				git apply --index -3 cocci.patch || leave $?
				rm cocci.patch
				git diff --quiet HEAD && continue
			fi
			git commit -m "$cocci_mark" || leave $?

			mark_cut
			continue
			;;
		'#'* | '')
			continue ;;
		esac

		case "$eh" in
		"" | "#"* | [0-9][0-9]-[0-9][0-9]*)
			echo >&2 "* $branch"

			save=$(git rev-parse --verify HEAD) &&
			tip=$(git rev-parse --verify "$branch^0") &&
			mb=$(git merge-base "$tip" "$save") ||
			leave $?

			test "$mb" = "$tip" && continue

			mark_cut
			cocci_count=$(( $cocci_count + 1 ))

			rebuild=$(git config "branch.$branch.rebuild" || :)

			GIT_EDITOR=: git merge --no-ff $into $rebuild $accept_rerere --edit "$branch" ||
			accept_rerere ||
			leave $?

			annotate_merge "$branch" || leave $?
			test -z "$edit" ||
			git commit --amend || leave $?

			this=$(git rev-parse --verify HEAD)
			if test "$this" = "$save"
			then
				:
			elif git show-ref -q --verify "refs/merge-fix/$branch"
			then
				echo >&2 "Fixing up the merge"
				git cherry-pick --no-commit "refs/merge-fix/$branch" &&
				git diff --stat HEAD &&
				GIT_EDITOR=: git commit --amend -a || leave $?
			fi
			;;
		pick" "*)
			echo >&2 "* $eh"

			mark_cut

			git cherry-pick --allow-empty "$branch" || leave $? ;;
		*) echo >&2 "Eh? $branch $eh"; leave $? ;;
		esac

		eval "$exec" || leave $?
	done
	leave $?
	;;
esac

if test -n "$update" && test $# = 0
then
	set x $(sed -n -e '2s/^# //p' <"$update") &&
	shift
fi

# Generation (or updating)

x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'
x40="$x40$x40$x40$x40$x40$x40$x40$x40"
LF='
'

show_merge () {
	case "$msg" in
	"Merge branch '"*"'"*)
		branch=$(expr "$msg" : "Merge branch '\(.*\)'")
		merge_hier=heads/
		;;
	"Merge remote branch '"*"'"*)
		branch=$(expr "$msg" : "Merge remote branch '\(.*\)'")
		merge_hier=
		;;
	*)
		echo >&2 "unknown merge: $msg"
		exit 1
		;;
	esac &&
	tip=$(git rev-parse --verify "refs/$merge_hier$branch" 2>/dev/null) &&
	merged=$(git name-rev --refs="refs/$merge_hier$branch" "$other" 2>/dev/null) &&
	merged=$(expr "$merged" : "$x40 \(.*\)") &&
	test "$merged" != undefined || {
		other=$(git log -1 --pretty='format:%s' $other) &&
		merged="$branch :rebased? $other"
	}
}

show_pick () {
	case "$msg" in
	"### "* | "###")
		merged="$msg$LF"
		;;
	*)
		merged="$(git rev-parse --verify "$commit") pick $msg"
		;;
	esac

}

generate () {
	PROGRAM=$1
	shift
	echo '#!/bin/sh'
	echo "# $1"
	echo 'case "$#,$1" in'
	echo '1,-u|1,-d)'
	echo "	exec $PROGRAM" '"$1" "$0"'
	echo 'esac'
	echo "$PROGRAM" '"$@" <<\EOF'
	git log --no-decorate --pretty=oneline --first-parent "$1" |
	{
		series=
		while read commit msg
		do
			if other=$(git rev-parse -q --verify "$commit^2")
			then
				show_merge
			elif test "$msg" = "$cocci_mark"
			then
				merged="#cocci "$(git rev-parse "$commit^0")
			else
				show_pick
			fi

			if test -z "$series"
			then
				series="$merged"
			else
				series="$merged$LF$series"
			fi
		done
		echo "$series"
	}
	echo EOF
}

if test -z "$update"
then
	generate "$0" "$@"
elif test -z "$diff"
then
	tmp=/tmp/regenerate.$$
	trap 'rm -f "$tmp"*' 0
	generate "$0" "$@" >"$tmp"
	diff -w -u "$update" "$tmp"
	if test $? = 0
	then
		echo >&2 "No changes."
	else
		echo >&2 -n "Update [y/N]? "
		read yesno
		case "$yesno" in
		[Yy]*)
			sed -e 's/ :rebased?.*//' "$tmp" >"$update" ;;
		*)
			echo >&2 "No update then." ;;
		esac
	fi
else
	generate "$0" "$@" | diff -w -u "$update" -
fi
