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"