cat >/dev/null <<EOM
Hi all.
One thing that I always miss in CVS is ability to pick up separate
changes from a modified file. So many times doing the
cp-edit-commit-rm-update dance I've tired and started this utility.
In the beginning it was very simple, but now it's more like the
features Mercurial and Git provide.
I'm highly doubt this will go in base or ports tree (although I do
not have anything against it), but maybe someone else will find
this utility useful. I did a few dozens of commits using it and I
think that most critical bugs are squashed. But be my guest and
feel free to find more. :) Any other comments are welcome, too.
Oh, and this mail could be used to run this script without cutting
a single bit from message body: "ksh /path/to/this/letter" ;)
--
WBR,
Vadim Zhukov
EOM
#!/bin/ksh
set -e
vcs_opts=
unset vcs_opts[0]
while (($# > 1)); do
[[ $1 == -?* ]] || break
vcs_opts[${vcs_opts[#]}]=$1
shift
done
debug=false
[[ -n $DEBUG ]] && debug=true
function usage {
echo "usage: ${0##*/} [file] ..." >&2
exit 0
}
p_full=
p_actual=
p_hunk=
cleanup() {
if $debug; then
echo "FULL DIFF: $p_full"
echo "ACTUAL DIFF: $p_actual"
echo "HUNK DIFF: $p_hunk"
else
[[ -n $p_full ]] && rm -- "$p_full"
[[ -n $p_actual ]] && rm -- "$p_actual"
[[ -n $p_hunk ]] && rm -- "$p_hunk"
fi
}
p_full=$(mktemp -t cip.full.XXXXXXXX)
p_actual=$(mktemp -t cip.actual.XXXXXXXX)
p_hunk=$(mktemp -t cip.hunk.XXXXXXXX)
commit_all=false
skip_all=false
hunk=
index=
unset hunk_start
last_add_index=
add_hunk() {
if [[ $last_add_index != "$index" ]]; then
echo "--- $index.orig" >>"$p_actual"
echo "+++ $index" >>"$p_actual"
last_add_index=$index
fi
local l
for l in "${hunk[@]}"; do
printf '%s\n' "$l" >>"$p_actual"
done
set -A hunk --
}
parse_hunk_header() {
set -A hunk_start -- $(printf '%s\n' "$1" |
perl -ne
'/^@@\s*-([0-9]+)(?:,[0-9]*)?\s*\+([0-9]+)(?:,[0-9]*)?\s*@@/ and print "$1
$2\n"' ||
true)
((${#hunk_start[@]} == 2))
}
edit_hunk() {
local greeting="Welcome to interactive hunk editor!"
local err_line='(unknown)'
local l header_read new_hunk nold nnew
# Loop until no error
while true; do
eval "greeting=\"${greeting}\""
cat >"$p_hunk" <<EOF
$greeting
Do not worry about counters, but do not remove first line
in a hunk. Everything above "@@" line will be ignored.
You can cancel addition of the hunk by clearing everything
below the "@@" line.
--- $index.orig
+++ $index
EOF
greeting='Patch hunk is corrupt at line $err_line, please
re-edit.'
for l in "${hunk[@]}"; do
printf '%s\n' "$l" >>"$p_hunk"
done
${VISUAL:-vi} -- "$p_hunk"
set -A new_hunk -- "${hunk[0]}"
header_read=false
err_line=0
nold=0
nnew=0
IFS=
while read -r l; do
unset IFS
if ! $header_read; then
parse_hunk_header "$l" && header_read=true
continue
fi
((++err_line))
case "$l" in
-*)
((++nold))
;;
+*)
((++nnew))
;;
" "*)
((++nold))
((++nnew))
;;
*)
continue 2
;;
esac
new_hunk[${#new_hunk[@]}]=$l
done <"$p_hunk"
unset IFS
err_line='(unknown)'
$header_read || continue
((${#new_hunk[@]} == 0)) && return
new_hunk[0]="@@ -${hunk_start[0]},$nold +${hunk_start[1]},$nnew
@@"
# Check if hunk is still applicable.
{
echo "Index: $index"
echo "--- $index.orig"
echo "+++ $index"
for l in "${new_hunk[@]}"; do
printf '%s\n' "$l"
done
} | patch -CR || continue
break
done
set -A hunk -- "${new_hunk[@]}"
add_hunk
}
help_for_hunk() {
cat >&2 <<EOF
[Y]es - add this hunk to commit, proceed to next hunk.
[N]o - ignore this hunk in commit, proceed to next hunk.
[A]ll - add this and all remaining hunks to commit.
[P]roceed - ignore this and all remaining hunks, proceed to commit.
[H]elp - show this message.
You can safely quit at any time before committing by pressing ^C.
EOF
}
last_ask_index=
ask_for_hunk() {
if $commit_all; then
add_hunk
return
fi
if $skip_all; then
return
fi
if [[ $last_ask_index != "$index" ]]; then
echo "================================================"
echo "Index: ${index}"
last_ask_index=$index
fi
local l ans
for l in "${hunk[@]}"; do
printf '%s\n' "$l"
done
echo '[END OF HUNK]'
while true; do
read 'ans?Add this hunk to the commit?
[Yes/No/Edit/All/Proceed/Help] '
case $ans in
[Aa])
commit_all=true
add_hunk
break
;;
[Ee])
edit_hunk
break
;;
[Hh])
help_for_hunk
;;
[Nn])
break
;;
[Pp])
skip_all=true
break
;;
[Yy])
add_hunk
break
;;
*)
echo -n 'Wrong answer! ' >&2
;;
esac
done
}
trap cleanup EXIT
if ${VCSCMD:=cvs} diff "${@:-.}" >"$p_full"; then
echo "${0##*/}: no changes to commit" >&2
exit 0
fi
cat <"$p_full" |&
while true; do
IFS=
read -pr l || break
unset IFS
# skip global patch header
while [[ -z $index && $l != Index:* ]]; do
continue 2
done
if [[ $l == Index:* ]]; then
if [[ -n $hunk ]]; then
ask_for_hunk
fi
index=${l##Index:*( )}
set -A hunk -- ""
continue
fi
if parse_hunk_header "$l"; then
if [[ -n $hunk ]]; then
ask_for_hunk
fi
set -A hunk -- "$l"
continue
fi
# skip particular file's patch header
[[ -z $hunk ]] && continue
if [[ $l != @(-|+| )* ]]; then
# hunk ended, some other text begun
if [[ -n $hunk ]]; then
ask_for_hunk
fi
set -A hunk -- "$l"
continue
fi
hunk[${#hunk[@]}]="$l"
done
unset IFS
if [[ -n $hunk ]]; then
ask_for_hunk
fi
help_for_approval() {
cat >&2 <<EOF
[C]ommit - proceed with commit.
[S]how - show the diff using pager, more(1) by default.
[H]elp - show this message.
You can safely quit at any time before committing by pressing ^C.
EOF
}
echo '[END OF PATCH]'
while true; do
read 'ans?Ready to commit? [Commit/Show/Help] '
case $ans in
[Cc])
break
;;
[Hh])
help_for_approval
;;
[Ss])
${PAGER:-more} -- "$p_actual"
;;
*)
echo -n 'Wrong answer! ' >&2
;;
esac
done
if ! patch -CR <"$p_full"; then
echo "Cannot unapply initial diff. Did something touched files?" >&2
exit 1
fi
if ! patch -R <"$p_full"; then
echo "Cannot unapply initial diff. Did something touched files?" >&2
echo "Sorry, but your working directory is dirty now! :(" >&2
exit 1
fi
if ! patch -C <"$p_actual"; then
echo "Cannot apply your final diff, reverting" >&2
patch <"$p_full"
exit 1
fi
if ! patch <"$p_actual"; then
echo "Cannot apply your final diff." >&2
echo "Sorry, but your working directory is dirty now! :(" >&2
echo "Your original diff is in $p_full" >&2
# Avoid removing it on exit
p_full=
exit 1
fi
${VCSCMD:-cvs} "${vcs_opts[@]}" commit "${@:-.}" || true
patch -R <"$p_actual" && patch <"$p_full"