#!/bin/sh

REVISON="$Id: cvs-mkpatch,v 1.4 2003/09/02 20:11:05 bame Exp $"

### TODO -- what about .versions or patch.cvs or committing tree and/or patch

list_contains()
{
    typeset element="$1"
    shift 1
    typeset list="$2"

    case " $list " in
        "* $element *") return 0;;
    esac

    return 1
}

list_remove()
{
    typeset element="$1"
    shift 1
    typeset list="$2"
    typeset f;

    for f in $list
    do
        [ "$f" = "$element" ] && continue
	echo "$f"
    done
}

uncompress()
{
    case $1 in
        *.bz2) bunzip2 < $1;;
	*.gz) gunzip < $1;;
	*) cat $1;;
    esac
}

patch2flist()
{
    typeset p=1
    while [ $# != 0 ]
    do
        case "$1" in
	    -p) p=$2; shift 2;;
	    -?) echo 'wtf? patch2flist'; exit 2;;
	    *) break;;
	esac
    done

    uncompress $1 |
    grep "^+++ " |
    awk -vp=$p '{
		    x = $2
		    for (i = 0; i < p; i++) {
			x = substr(x, index(x, "/") + 1);
		    }
		    print x
		}'
}

usage()
{
    [ $# != 0 ] && echo "ERROR: $*" >&2

    if $LONGHELP
    then
cat <<EOFEOF
Usage: $0 -o output-patch [-i input-patch] [-C] [-A] [-r rev] [-D date] [file(s)]

	-o output-patch		name of output patch file.  If it already
				exists, will be overwritten.  The order
				of files diffed in output-patch is repeated
				so patches may themselves be diffed against
				each other with some hope of comprehension.
				May end in .bz2 or .gz

	-i input-patch		Name of a 'patch -p1' patch which is
				related to the changes you're creating a
				patch about yourself.  Unless -A is used,
				this is only used to grab a list of file
				names involved in the patch and to order
				the diffs if no output patch exists.  May
				end in .bz2 or .gz

	-A			Apply <input-patch> to your current tree
				before proceeding.  This saves a little
				typing, but since patches often need extra
				work, it usually fails, you fix, and then
				re-run $0.

	-C			Do not ask 'cvs up -n' for a list of files
				which may be different in your local CVS
				copy (faster but less thorough).

	file(s)			A list of files which may be involved in
				the patch being created.

	Deciding the CVS baseline revision:
	    -r rev		passed to 'cvs up -rrev'
	    -D date		passed to 'cvs up -Ddate'

$0 uses CVS to reproduce the baseline against which
you desire to create a patch but no changes to CVS are
performed, that is, <output-patch> is not committed nor is
the current CVS tree.

$0 goes to a lot of trouble to operate on the minimum number
of files, especially when -C is added.  This makes it fast
to produce a patch even when CVS is fairly slow and/or the
CVS repository is huge.

The only flavor of patch grokked and produced by $0 is
'patch -p1' patches.

$0 is INTERACTIVE.  The user is presented with a list of
files to edit to designate as part of the patch or not,
so you can have extra changes laying around and fine-tune
things.

$0 recreates <output-patch> every time and is currently
incapable of tweeking, or incrementally changing, it.  For
example you'd like to be able to change one little thing,
and have that one little thing updated inside the patch
without necessarily having a convenient CVS tag or date for
use with -r or -D.
EOFEOF
    else
	echo "Usage: $0 -o output-patch [-i input-patch] [-C] [-A] [-r rev] [-D date] [file(s)]" >&2
        echo "Use $0 -h for more information" >&2
    fi
    exit 2
}

apply_patch()
{
    uncompress $1 | patch -p1
}

compress()
{
    case $1 in
        *.bz2) bzip2 > $1;;
	*.gz) gzip > $1;;
	*) cat > $1;;
    esac
}

echo '$Id: cvs-mkpatch,v 1.4 2003/09/02 20:11:05 bame Exp $'

REV=			# for cvs up -rxxx
DATE=			# for cvs up -Dxxx
OP=			# Output patch, required arg
IP=			# Input patch if any
CVSN=true		# Use 'cvs up -n' to find possible changed files?
APPLY=false		# try to apply the input patch?
LONGHELP=false

[ $# = 0 ] && usage
while [ $# != 0 ]
do
    case "$1" in
        -r) REV="-r$2"; shift 2;;
        -D) DATE="-D$2"; shift 2;;
	-o) OP=$2; shift 2;;
	-A) APPLY=true; shift 1;;
	-i) IP=$2; shift 2;;
	-h) LONGHELP=true; usage;;
	-C) CVSN=false; shift 1;;
	-??*) usage space required between option and value, use -r 1 not -r1;;
	-*) usage unknown argument "$1";;
	*) break;;
    esac
done
flist="$@"

# output patch name is required because we're interactive so need stdout
# for other things
[ -n "$OP" ] || usage -o is required


[ -d CVS ] || usage "(no ./CVS) this directory must be managed by CVS"
grep / CVS/Repository >/dev/null && usage "(/ in CVS/Repository) must be run from top of your CVS tree"

if $APPLY
then
    [ -z "$IP" ] && usage -A requires -i
    echo "Applying $IP now.  If this fails you'll have to fix the problems and"
    echo "re-run this command probably without the -A"

    apply_patch $IP
fi

TMPDIR=/tmp/$$.$RANDOM.xx
#TMPDIR=/tmp/1.xx
mkdir -p $TMPDIR
trap "rm -r $TMPDIR" 0 1 2 3 15 

# get a comprehensive list of files to process
for f in $flist
do
    echo "f $f"
done > $TMPDIR/flist

$CVSN && cvs -q -n up >> $TMPDIR/flist

[ -n "$IP" ] && patch2flist -p 1 < $IP | sed 's/^/i /' >> $TMPDIR/flist
[ -n "$OP" -a -f "$OP" ] && patch2flist -p 1 < $OP | sed 's/^/o /' >> $TMPDIR/flist
echo '# !Change this to the list of files involved in this patch' >> $TMPDIR/flist

sort -t' ' +1 -u -o $TMPDIR/flist $TMPDIR/flist

answer=n
while [ "$answer" != y ]
do
    ${EDITOR:-vi} $TMPDIR/flist
    echo -n 'Are you satisfied with the file list (y/N)? '; read answer
done

# set up a temporary tree with the interesting files as of their
# pre-changed revision
mkdir -p $TMPDIR/cvs
flist=
while read marker f
do
    [ "$marker" = '#' ] && continue
    mkdir -p $TMPDIR/cvs/$(dirname $f)
    cvs up -ko -p $DATE $REV $f > $TMPDIR/cvs/$f 2>/dev/null
    flist="$flist $f"
done < $TMPDIR/flist
flist="$flist"

# If we've got a patch, use it's file list diff order for the
# beginning part of the patch, output patch preferred if exists
patchflist=
[ -n "$IP" ] && patchflist=$(patch2flist -p 1 < $IP)
[ -n "$OP" -a -f "$OP" ] && patchflist=$(patch2flist -p 1 < $OP)

basedir=$(basename $PWD)

(
    cd ..
    for f in $patchflist
    do
	if list_contains $f $flist
	then
	    diff -u $TMPDIR/cvs/$f $basedir/$f
	    flist=$(list_remove $f $flist)
	#else generate a warning that the patch is now minus this file?
	fi
    done

    # diff files not referenced in patches
    for f in $flist
    do
	diff -u $TMPDIR/cvs/$f $basedir/$f
    done
) | compress $OP

echo "patch generated in $OP" >&2
