Re: [PATCH 02/18] Add a new builtin: branch-diff

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



Hi Elijah,

On Fri, 4 May 2018, Elijah Newren wrote:

> On Thu, May 3, 2018 at 11:40 PM, Johannes Schindelin
> <Johannes.Schindelin@xxxxxx> wrote:
> > I actually have a hacky script to fixup commits in a patch series. It lets
> > me stage part of the current changes, then figures out which of the
> > commits' changes overlap with the staged changed. If there is only one
> > commit, it automatically commits with --fixup, otherwise it lets me choose
> > which one I want to fixup (giving me the list of candidates).
> 
> Ooh, interesting.  Are you willing to share said hacky script by chance?

It is part of a real huge hacky script of pretty much all things I add as
aliases, so I extracted the relevant part for you:

-- snip --
#!/bin/sh

fixup () { # [--upstream=<branch>] [--not=<tip-to-skip>]
	upstream=
	not=
	while case "$1" in
	--upstream) shift; upstream="$1";;
	--upstream=*) upstream="${1#*=}";;
	--not) shift; not="$not $1";;
	--not=*) not="$not ${1#*=}";;
	-*) die "Unknown option: $1";;
	*) break;;
	esac; do shift; done

	test $# -le 1 ||
	die "Need 0 or 1 commit"

	! git diff-index --cached --quiet --ignore-submodules HEAD -- || {
		git update-index --ignore-submodules --refresh
	        ! git diff-files --quiet --ignore-submodules -- ||
		die "No changes"

		git add -p ||
		exit
	}
	! git diff-index --cached --quiet --ignore-submodules HEAD -- ||
	die "No staged changes"

	test $# = 1 || {
		if test -z "$upstream"
		then
			upstream="$(git rev-parse --symbolic-full-name \
				HEAD@{upstream} 2> /dev/null)" &&
			test "$(git rev-parse HEAD)" != \
				"$(git rev-parse $upstream)" ||
			upstream=origin/master
		fi

		revs="$(git rev-list $upstream.. --not $not --)" ||
		die "Could not get commits between $upstream and HEAD"

		test -n "$revs" ||
		die "No commits between $upstream and HEAD"

		while count=$(test -z "$revs" && echo 0 || echo "$revs" | wc -l | tr -dc 0-9) &&
			test $count -gt 1
		do
			printf '\nMultiple candidates:\n'
			echo $revs | xargs git log --no-walk --oneline | cat -n

			read input
			case "$input" in
			[0-9]|[0-9][0-9]|[0-9][0-9][0-9])
				revs="$(echo "$revs" | sed -n "${input}p")"
				count=1
				break
				;;
			h|hh)
				revs=$(history_of_staged_changes $upstream..)
				continue
				;;
			hhhh)
				history_of_staged_changes -p $upstream..
				continue
				;;
			p)
				git log -p --no-walk $revs
				continue
				;;
			[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f])
				revs=$input
				continue
			esac
			revs="$(git rev-list --no-walk --grep="$input" $revs)"
		done

		test $count = 1 ||
		die "No commit given"

		set $revs
	}

	git commit --fixup "$1"
	message="$(git show -s --format=%s%n%n%b)"
	case "$message" in
	'fixup! fixup! '*)
		message="${message#fixup! }"
		message="${message#fixup! }"
		message="${message#fixup! }"
		git commit --amend -m "fixup! $message"
		;;
	esac
}

history_of_staged_changes () { # <commit-range>
	pretty=
	if test "a-p" = "a$1"
	then
		pretty=-p
		shift
	fi

	test $# -le 1 ||
	die "Usage: $0 <commit-range>"

	test $# = 1 || {
		upstream=$(git rev-parse --verify HEAD@{u} 2>/dev/null) ||
		upstream=origin/master
		set $upstream..
	}

	args=$(for file in $(git diff --no-color --cached --name-only)
		do
			for hunk in $(get_hunks --cached -- "$file")
			do
				hunk1=${hunk%:*}
				start1=${hunk1%,*}
				end1=$(($start1+${hunk1#*,}-1))
				echo "'$file:$start1-$end1'"
			done
		done)

	test -n "$args" ||
	die "No staged files!"

	eval hunk_history $pretty "$1" -- $args
}

hunk_history () { # <commit-range> -- <file>:<start>[-<end>]...
	pretty=
	if test "a-p" = "a$1"
	then
		pretty=t
		shift
	fi

	test $# -ge 3 && test a-- = "a$2" ||
	die "Usage: $0 <commit-range> -- <file>:<start>[-<end>]..."

	range="$1"
	shift; shift

	files="$(for arg
		do
			echo "'${arg%:*}'"
		done)"

	for commit in $(eval git rev-list --topo-order "$range" -- $files)
	do
		if test -z "$lines"
		then
			lines="$(echo "$*" |
				tr ' ' '\n' |
				sed "s/^/$commit /")"
		fi

		touched=
		for line in $(echo "$lines" |
				sed -n "s|^$commit ||p")
		do
			file="${line%:*}"
			curstart="${line#$file:}"
			curend="${curstart#*-}"
			curstart="${curstart%%-*}"

			diff_output=
			parentstart=$curstart
			parentend=$curend
			parents=$(git rev-list --no-walk --parents \
					$commit -- "$file" |
				cut -c 41-)
			if test -z "$parents"
			then
				touched=t
			fi

			for parent in $parents
			do
				for hunk in $(get_hunks ^$parent $commit -- \
					"$file")
				do
					hunk1=${hunk%:*}
					start1=${hunk1%,*}
					end1=$(($start1+${hunk1#*,}-1))

					hunk2=${hunk#*:}
					start2=${hunk2%,*}
					end2=$(($start2+${hunk2#*,}-1))

					if test $start2 -le $curend &&
						test $end2 -ge $curstart
					then
						touched=t
					fi

					if test $end2 -le $curstart
					then
						diff=$(($end1-$end2))
						parentstart=$(($curstart+$diff))
					elif test $start2 -le $curstart
					then
						parentstart=$start1
					fi

					if test $end2 -le $curend
					then
						diff=$(($end1-$end2))
						parentend=$(($curend+$diff))
					elif test $start2 -le $curend
					then
						parentend=$end1
					fi
				done

				if test -n "$pretty" &&
					test $curstart != $parentstart ||
					test $curend != $parentend
				then
					test -n "$(echo "$diff_output" |
						sed -n "s|^\([^ -]\[[^m]*m\)*diff --git a/$file b/||p")" ||
					diff_output="$(printf '%s%s\n' "$diff_output" \
						"$(git diff --color \
							^$parent $commit -- $file |
						sed '/^\([^ -]\[[^m]*m\)*@@ /,$d')")"
					prefix="$(git rev-parse --git-dir)"
					oldfile="${prefix}${prefix:+/}.old"
					git show $parent:$file 2>/dev/null |
					sed -n "$parentstart,${parentend}p" >$oldfile
					newfile="${prefix}${prefix:+/}.new"
					git show $commit:$file |
					sed -n "$curstart,${curend}p" >$newfile
					diff1="$(git diff --no-index --color $oldfile $newfile |
						sed '1,4d')"
					diff_output="$(printf '%s%s\n' "$diff_output" \
						"$diff1")"
				fi

				# TODO: support renames here
				prefix="$parent $file:"
				lines="$(printf '%s\n%s%s' "$lines" "$prefix" \
					"$parentstart-$parentend")"
			done
		done
		test -z "$touched" || {
			if test -z "$pretty"
			then
				echo $commit
			else
				git show --color -s $commit --
				echo "$diff_output"
			fi
		}
	done
}

# takes a commit range and a file name
# returns a list of <offset>,<count>:<offset>,<count>
get_hunks () {
	# TODO: support renames here
	git diff --no-color -U0 "$@" |
	sed -n -e 's/\([-+][0-9][0-9]*\) /\1,1 /g' \
		-e 's/^@@ -\([0-9]*,[0-9]*\) +\([0-9]*,[0-9]*\) .*/\1 \2/p' |
	fix_hunks
}

fix_hunks () {
	while read hunk1 hunk2
	do
		case $hunk1 in
		*,0)
			printf '%d,0:' $((${hunk1%,0}+1))
			;;
		*)
			printf '%s:' $hunk1
		esac
		case $hunk2 in
		*,0)
			printf '%d,0\n' $((${hunk2%,0}+1))
			;;
		*)
			printf '%s\n' $hunk2
		esac
	done
}

fixup "$@"
-- snap --

Quite a handful to read, eh? And no code comments. I always meant to
annotate it with some helpful remarks for future me, but never got around
to do that, either. These days, I would probably

1. write the whole thing based on `git log -L <line-range>:<file>`, and

2. either implement it in node.js for speed, or directly in C.

> (And as a total aside, I found your apply-from-public-inbox.sh script
> and really like it.  Thanks for making it public.)

You're welcome! I am glad it is useful to you.

Ciao,
Dscho



[Index of Archives]     [Linux Kernel Development]     [Gcc Help]     [IETF Annouce]     [DCCP]     [Netdev]     [Networking]     [Security]     [V4L]     [Bugtraq]     [Yosemite]     [MIPS Linux]     [ARM Linux]     [Linux Security]     [Linux RAID]     [Linux SCSI]     [Fedora Users]

  Powered by Linux