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"

Reply via email to