Subject: Re: [RFC/PATCH 1/4] Add git-sequencer shell prototype

From: Junio C Hamano <[email-masked]>
Date: Wed, 02 Jul 2008 18:45:06 -0700

Stephan Beyer <[email-masked]> writes:

> git sequencer is planned as a backend for user scripts
> that execute a sequence of git instructions and perhaps
> need manual intervention, for example git-rebase or git-am.

...
> +output () {
> +	case "$VERBOSE" in
> +	0)
> +		"$@" >/dev/null
> +		;;
> +	1)
> +		output=$("$@" 2>&1 )
> +		status=$?
> +		test $status -ne 0 && printf '%s\n' "$output"
> +		return $status
> +		;;
> +	2)
> +		"$@"
> +		;;
> +	esac
> +}

Perhaps misnamed?  This feels more like "do" or "perform" or "run".

> +require_clean_work_tree () {
> +	# test if working tree is dirty
> +	git rev-parse --verify HEAD >/dev/null &&
> +	git update-index --ignore-submodules --refresh &&
> +	git diff-files --quiet --ignore-submodules &&
> +	git diff-index --cached --quiet HEAD --ignore-submodules -- ||
> +	die 'Working tree is dirty'
> +}

When is it necessary to ignore submodules and why?  Are there cases where
submodules should not be ignored?

> +LAST_COUNT=
> +mark_action_done () {
> +	sed -e 1q <"$TODO" >>"$DONE"
> +	sed -e 1d <"$TODO" >"$TODO.new"
> +	mv -f "$TODO.new" "$TODO"
> +	if test "$VERBOSE" -gt 0
> +	then
> +		count=$(grep -c '^[^#]' <"$DONE")
> +		total=$(expr "$count" + "$(grep -c '^[^#]' <"$TODO")")

Here we are not counting lines that are comments as insns (I am not
complaining; just making a mental note).

> +		if test "$LAST_COUNT" != "$count"
> +		then
> +			LAST_COUNT="$count"
> +			test "$VERBOSE" -lt 1 ||
> +				printf 'Sequencing (%d/%d)\r' "$count" "$total"
> +			test "$VERBOSE" -lt 2 || echo
> +		fi
> +	fi
> +}
> +
> +# Generate message, patch and author script files
> +make_patch () {
> +	parent_sha1=$(git rev-parse --verify "$1"^) ||
> +		die "Cannot get patch for $1^"
> +	git diff-tree -p "$parent_sha1..$1" >"$PATCH"

Could there be a case where we need/want to deal with a root commit
without parents?

> +	test -f "$MSG" ||
> +		commit_message "$1" >"$MSG"
> +	test -f "$AUTHOR_SCRIPT" ||
> +		get_author_ident_from_commit "$1" >"$AUTHOR_SCRIPT"
> +}
> +
> +# Generate a patch and die with "conflict" status code
> +die_with_patch () {
> +	make_patch "$1"
> +	git rerere
> +	die_to_continue "$2"
> +}
> +
> +restore () {
> +	git rerere clear
> +
> +	HEADNAME=$(cat "$SEQ_DIR/head-name")
> +	HEAD=$(cat "$SEQ_DIR/head")

Perhaps

	read HEADNAME <"$SEQ_DIR/head-name"

provided if these values are $IFS safe?

> +	case $HEADNAME in
> +	refs/*)
> +		git symbolic-ref HEAD "$HEADNAME"
> +		;;
> +	esac &&
> +	output git reset --hard "$HEAD"
> +}
> +
> +has_action () {
> +	grep '^[^#]' "$1" >/dev/null
> +}
> +
> +# Check if text file $1 contains a commit message
> +has_message () {
> +	test -n "$(sed -n -e '/^Signed-off-by:/d;/^[^#]/p' <"$1")"
> +}

Makes one wonder if we would want to special case other kinds like
"Acked-by:" as well...

> +# Usage: pick_one (cherry-pick|revert) [-*|--edit] sha1
> +pick_one () {
> +	what="$1"
> +	# we just assume that this is either cherry-pick or revert
> +	shift
> +
> +	# check for fast-forward if no options are given
> +	if expr "x$1" : 'x[^-]' >/dev/null
> +	then
> +		test "$(git rev-parse --verify "$1^")" = \
> +			"$(git rev-parse --verify HEAD)" &&
> +			output git reset --hard "$1" &&
> +			return
> +	fi
> +	test "$1" != '--edit' -a "$what" = 'revert' &&
> +		what='revert --no-edit'

This looks somewhat wrong.

When the history looks like ---A---B and we are at A, cherry-picking B can
be optimized to just advancing to B, but that optimization has a slight
difference (or two) in the semantics.

 (1) The committer information would not record the user and time of the
     sequencer operation, which actually may be a good thing.

 (2) When $what is revert, this codepath shouldn't be exercised, should it?

 (3) If B is a merge, even if $what is pick, this codepath shouldn't be
     exercised, should it?

As to the syntax I tend to prefer

	case "$1" in
        -*)	... do option thing ... ;;
        *)	... do other thing... ;;
        esac

So how about...

	case "$what,$1" in
        revert,--edit)
        	what='revert --no-edit' ;;
        revert,* | cherry-pick,-* )
        	;;
        *)
		if ! git rev-parse --verify "$1^2" &&
	                test "$(git rev-parse --verify "$1^") = \
                	"$(git rev-parse --verify HEAD)"
		then
                	output git reset --hard "$1"
			return
		fi
		;;
	esac

> +make_squash_message () {
> +	if test -f "$squash_msg"
> +	then
> +		count=$(($(sed -n -e 's/^# This is [^0-9]*\([1-9][0-9]*\).*/\1/p' \
> +			<"$squash_msg" | sed -n -e '$p')+1))
> +		echo "# This is a combination of $count commits."
> +		sed -e '1d' -e '2,/^./{
> +			/^$/d
> +		}' <"$squash_msg"
> +	else
> +		count=2
> +		echo '# This is a combination of 2 commits.'
> +		echo '# The first commit message is:'
> +		echo
> +		commit_message HEAD
> +	fi
> +	echo
> +	echo "# This is the $(nth_string "$count") commit message:"
> +	echo
> +	commit_message "$1"
> +}
> +
> +make_squash_message_multiple () {
> +	echo '# This is a dummy to get the 0.' >"$squash_msg"
> +	for cur_sha1 in $(git rev-list --reverse "$sha1..HEAD")
> +	do
> +		make_squash_message "$cur_sha1" >"$MSG"
> +		cp "$MSG" "$squash_msg"
> +	done
> +}

Hmm, I know this is how rebase-i is written, but we should be able to do
better than writing and flipping temporary times N times, shouldn't we?

> +peek_next_command () {
> +	sed -n -e '1s/ .*$//p' <"$TODO"
> +}

... which could respond "the next command is '#' (comment)", so we are
actively counting a comment as a step here.  Does this contradict with the
mental note we made earlier, and if so, does the discrepancy hurt us
somewhere in this program?

> +# If $1 is a mark, make a ref from it; otherwise keep it
> +mark_to_ref () {
> +	arg="$1"
> +	ref=$(expr "x$arg" : 'x:0*\([0-9][0-9]*\)$')

You might want to leave comments to describe constraints that led to this
slightly awkward regexp:

 * :0 is allowed
 * :01 is the same as :1

> +strategy_check () {
> +	case "$1" in
> +	resolve|recursive|octopus|ours|subtree|theirs)
> +		return
> +		;;
> +	esac
> +	todo_warn "Strategy '$1' not known."
> +}

Hmm.  Do we need to maintain list of available strategies here and then in
git-merge separately?

> +### Author script functions
> +
> +clean_author_script () {
> +	cat "$ORIG_AUTHOR_SCRIPT" >"$AUTHOR_SCRIPT"
> +}
> +
> +# Take "Name <e-mail>" in stdin and outputs author script
> +make_author_script_from_string () {
> +	sed -e 's/^\(.*\) <\(.*\)>.*$/GIT_AUTHOR_NAME="\1"\
> +GIT_AUTHOR_EMAIL="\2"\
> +GIT_AUTHOR_DATE=/'
> +}

If you are going to "."-source or eval the output from this, you would
need to quote the values a lot more robustly, wouldn't you?  Is this safe
against shell metacharacters in names, mails and/or space between unixtime
and the timezone information?

> +	if test -z "$AUTHOR"
> +	then
> +		sed -n -e '
> +			s/^Author: \(.*\)$/GIT_AUTHOR_NAME="\1"/p;
> +			s/^Email: \(.*\)$/GIT_AUTHOR_EMAIL="\1"/p;
> +			s/^Date: \(.*\)$/GIT_AUTHOR_DATE="\1"/p
> +		' <"$infofile" >>"$AUTHOR_SCRIPT"

The same comment on quoting applies here, I think.

> +		# If sed's result is empty, we keep the original
> +		# author script by appending.
> +	fi
> ...
> +	failed=
> +	with_author git apply $apply_opts --index "$PATCH" || failed=t
> +
> +	if test -n "$failed" -a -n "$threeway" && (with_author fallback_3way)
> +	then
> +		# Applying the patch to an earlier tree and merging the
> +		# result may have produced the same tree as ours.
> +		git diff-index --quiet --cached HEAD -- && {
> +			echo 'No changes -- Patch already applied.'
> +			return 0
> +			# XXX: do we want that?
> +		}
> +		# clear apply_status -- we have successfully merged.
> +		failed=
> +	fi
> +
> +	if test -n "$failed"
> +	then
> +		# XXX: This is just a stupid hack:
> +		with_author git apply $apply_opts --reject --index "$PATCH"

Please don't do this without being asked, if you are planning to use this
in "am" when 3-way fallback was not asked.  It _may_ make sense to give an
option to the users to ask for .rej if they prefer to work that way better
than working with 3-way merge fallback, but doing this without being asked
is not acceptable.

> +		die_to_continue 'Patch failed. See the .rej files.'
> +		# XXX: We actually needed a git-apply flag that creates
> +		# conflict markers and sets the DIFF_STATUS_UNMERGED flag.

That is what -3way is all about, and this codepath is when the user did
not ask for it, isn't it?

> +# Check the "pick" instruction
> +check_pick () {
> +	revert=
> +	mainline=
> +	while test $# -gt 1
> +	do
> ...
> +	done
> +
> +	if test -n "$mainline"
> +	then
> +		test -z "$revert" ||
> +			todo_error "Cannot use $revert together with --mainline."

Why not?  If you have this...

	---A---C---D
              /
          ---B

and you are at D, you may want to undo the merge you made at C and go back
to either A or B, which essentially is same as cherry-picking diff between
C and D on top of either A or B.  Both are valid operations aren't they?

The remainder of the review will have to be in a separate message..