I liked this puzzle, it's got its subtleties. Source this file, say init foo.rb 1000,1005
and follow the instructions. When you're done, file @changes
will have the correct list of commits in topological order and @blames
will have the actual blame output from each.
This is dramatically more complex than the accepted solution above. It produces output that will sometimes be more useful, and hard to reproduce, and it was fun to code.
The problem with trying to track line-number ranges automatically while stepping backward through history is if a change hunk crosses line-numbered range boundaries you can't automatically determine where in that hunk the new range boundary should be, and you'll either have to include a big range for big additions and so accumulate (sometimes lots of) irrelevant changes, or drop into manual mode to be sure it's right (which of course gets you right back here), or accept extreme lossage at times.
If you want your output to be exact, use the answer above with trustworthy regex ranges like `/^type function(/,/^}/', or use this, which isn't actually that bad, a couple seconds per step back in time.
In exchange for the extra complexity, it does produces the hitlist in topological sequence and it does at least (fairly successfully) try to ameliorate the pain at each step. It never runs a redundant blame, for instance, and update-ranges makes adjusting line numbers easier. And of course there's the reliability of having had to individually eyeball the hunks... :-P
To run this on full auto, say { init foo.rb /^class foo/,/^end/; auto; } 2>&-
### functions here create random @-prefix files in the current directory ###
#
# git blame history for a range, finding every change to that range
# throughout the available history. It's somewhat, ahh, "intended for
# customization", is that enough of a warning? It works as advertised
# but drops @-prefix temporary files in your current directory and
# defines new commands
#
# Source this file in a subshell, it defines functions for your use.
# If you have @-prefix files you care about, change all @ in this file
# to something you don't have and source it again.
#
# init path/to/file [<start>,<end>] # range optional
# update-ranges # check range boundaries for the next step
# cycle [<start>,<end>] # range unchanged if not supplied
# prettyblame # pretty colors,
# blue="child commit doesn't have this line"
# green="parent commit doesn't have this line"
# brown=both
# shhh # silence the pre-cycle blurb
#
# For regex ranges, you can _usually_ source this file and say `init
# path/to/file /startpattern/,/endpattern/` and then cycle until it says 0
# commits remain in the checklist
#
# for line-number ranges, or regex ranges you think might be unworthy, you
# need to check and possibly update the range before each cycle. File
# @next is the next blame start-point revision text; and command
# update-ranges will bring up vim with the current range V-selected. If
# that looks good, `@M` is set up to quit even while selecting, so `@M` and
# cycle. If it doesn't look good, 'o' and the arrow keys will make getting
# good line numbers easy, or you can find better regex's. Either way, `@M`
# out and say `cycle <start>,<end>` to update the ranges.
init () {
file=$1;
range="$2"
rm -f @changes
git rev-list --topo-order HEAD -- "$file" \
| tee @checklist \
| cat -n | sort -k2 > @sequence
git blame "-ln${range:+L$range}" -- "$file" > @latest || echo >@checklist
check-cycle
cp @latest @blames
}
update-latest-checklist() {
# update $latest with the latest sha that actually touched our range,
# and delete that and everything later than that from the checklist.
latest=$(
sed s,^^,, @latest \
| sort -uk1,1 \
| join -1 2 -o1.1,1.2 @sequence - \
| sort -unk1,1 \
| sed 1q \
| cut -d" " -f2
)
sed -i 1,/^$latest/d @checklist
}
shhh () { shhh=1; }
check-cycle () {
update-latest-checklist
sed -n q1 @checklist || git log $latest~..$latest --format=%H\ %s | tee -a @changes
next=`sed 1q @checklist`
git cat-file -p `git rev-parse $next:"$file"` > @next
test -z "$shh$shhh$shhhh" && {
echo "A blame from the (next-)most recent alteration (id `git rev-parse --short $latest`) to '$file'"
echo is in file @latest, save its contents where you like
echo
echo you will need to look in file @next to determine the correct next range,
echo and say '`cycle its-start-line,its-end-line`' to continue
echo the "update-ranges" function starts you out with the range selected
} >&2
ncommits=`wc -l @checklist | cut -d\ -f1`
echo $ncommits commits remain in the checklist >&2
return $((ncommits==0))
}
update-ranges () {
start="${range%,*}"
end="${range#*,}"
case "$start" in
*/*) startcmd="1G$start"$'\n' ;;
*) startcmd="${start}G" ;;
esac
case "$end" in
*/*) endcmd="$end"$'\n' ;;
[0-9]*) endcmd="${end}G" ;;
+[0-9]*) endcmd="${end}j" ;;
*) endcmd="echohl Search|echo "can\'t" get to '${end}'\"|echohl None" ;;
esac
vim -c 'set buftype=nofile|let @m=":|q'$'\n"' -c "norm!${startcmd}V${endcmd}z.o" @next
}
cycle () {
sed -n q1 @checklist && { echo "No more commits to check"; return 1; }
range="${1:-$range}"
git blame "-ln${range:+L$range}" $next -- "$file" >@latest || echo >@checklist
echo >>@blames
cat @latest >>@blames
check-cycle
}
auto () {
while cycle; do true; done
}
prettyblames () {
cat >@pretty <<-\EOD
BEGIN {
RS=""
colors[0]="\033[0;30m"
colors[1]="\033[0;34m"
colors[2]="\033[0;32m"
colors[3]="\033[0;33m"
getline commits < "@changes"
split(commits,commit,/\n/)
}
NR!=1 { print "" }
{
thiscommit=gensub(/ .*/,"",1,commit[NR])
printf "%s\n","\033[0;31m"commit[NR]"\033[0m"
split($0,line,/\n/)
for ( n=1; n<=length(line); ++n ) {
color=0
split(line[n],key,/[1-9][0-9]*)/)
if ( NR!=1 && !seen[key[1]] ) color+=1
seen[key[1]]=1;
linecommit = gensub(/ .*/,"",1,line[n])
if (linecommit==thiscommit) color+=2
printf "%s%s\033[0m\n",colors[color],line[n]
}
}
EOD
awk -f @pretty @blames | less -R
}
12345
the code on those lines might be on lines 55 - 60 for commit12345^
. – Ersatz