#!/bin/sh
# $Id: cvs-snapshot,v 1.11 2004/11/25 23:27:37 willy Exp $
# cvs-snapshot - make a CVS branch into a snapshot of a set of sources
#
# Copyright (C) Hewlett-Packard Company (Paul Bame <bame@debian.org>)
# cvs-snapshot is licensed according to the terms of GPL version 2 or later

# TODO:
# Instructions for converting an old 'cvs import'-ed branch into
# an isolated branch (mostly 'cvs admin -b').
#
# Need to revisit failure mechanisms and the restartability of this
# script, possibly saving command-line options for error-free restart.
#
# Fix this to work with repository names containing slashes.  That's
# handy for snapshotting a sub-directory from upstream for example
#
# Give some brief instructions about the next usually-required step
# aka the 'cvs up -j -j'

CVS='cvs -z5 -fq'		# Don't use .cvsrc, be quiet, and compress

usage()
{
    echo "Usage: cvs-snapshot [-C] [-I] repository vendor-tag release-tags..." >&2
    echo "	-C	My tree has CVS subdirectories and that's ok" >&2
    echo "	-I	Initialize -- this tree is not yet in CVS" >&2
    exit 2
}

CVSOK=false
INITIAL=false
while [ $# != 0 ]
do
    case "$#:$1" in
	*:-C) CVSOK=true; shift 1;;
	*:-I) INITIAL=true; shift 1;;
	*:-*) echo "Unknown option '$1'" >&2; usage;;
	3:*) REPOSITORY="$1"; VENDOR="$2"; RELEASE="$3"; break;;
	*) echo "Eh?" >&2; usage;;
    esac
done

if [ -z "$REPOSITORY" ]
then
    echo "No repository, vendor-tag, or release-tags" >&2
    usage
fi

WORKDIR="$(echo _cvs-snapshot-$REPOSITORY | tr / -)"

if [ -z "$CVSROOT" ]
then
    echo "You must set \$CVSROOT first." >&2
    usage
fi

if [ -d ./CVS -a "$CVSOK" = false ]
then
    echo "You are trying to snapshot a directory which is a CVS" >&2
    echo "directory.  This is normal if you are snapshotting sources" >&2
    echo "you pulled from another CVS repository but it might be an error." >&2
    echo "If this is OK please try again adding the -C option." >&2
    usage
fi

if [ -d $WORKDIR ]
then
    echo "There is already a cvs-snapshot working directory ($WORKDIR)." >&2
    echo "Move it out of the way or rename it and try again." >&2
    usage
fi

error()
{
    echo "ERROR: $@" >&2
    # Could offer advice here about re-starting
    exit 2
}

# Yuck -- too many people use Windoze and like blanks in file names :-(
# Make xargs take one file name per line and ignore blanks.
XARGS()
{
    tr '\012' '\000' | xargs -0 "$@"
}

mkdir $WORKDIR || error "Could not create working directory $WORKDIR"
TREE=$PWD
cd $WORKDIR

repository=$PWD/$REPOSITORY

if [ $INITIAL = true ]
then
    mkdir -p $repository
    echo "need this to trick CVS" > $repository/_TrickCVS_
    (cd $repository && $CVS import -m "create $REPOSITORY" $REPOSITORY $VENDOR $RELEASE)> import.log || error "Can't create repository $REPOSITORY, was -I correct?"
fi

vendor_flag=-r$VENDOR
[ $VENDOR = HEAD ] && vendor_flag=-A

echo -e "Checking out latest version of $REPOSITORY from branch $VENDOR...\c"
# grab the latest version of the CVS branch
$CVS co -R -ko $vendor_flag $REPOSITORY > initial-co.log ||
    error "'$CVS co -R -ko $vendor_flag $REPOSITORY' failed.  If this \
	    is the initial check-in of this repository use -I."

FIND()
{
    typeset tree=$1
    shift 1
    (cd $tree && find . "$@" |
    	grep -v -e "/CVS$" -e "/CVS/" -e "$WORKDIR" -e '/\.cvsignore$' |
	    sort)
}

echo -e "\nComputing lists of files/directories to be added/removed...\c"
# Make lists of files and directories currently in CVS and also in
# the tree to be snapshotted.  This is neither fast nor elegant, but
# is simple and maintainable, and if you have enough RAM for a nice
# FS cache, it's not *terribly* slow.
FIND $TREE -type f > tree.files
FIND $TREE -type d > tree.dirs
FIND $repository -type f > repository.files
FIND $repository -type d > repository.dirs

# Need lists of files/directories which are different between CVS
# and the new tree.  Not all these lists are used, but they are generated
# for symmetry and because they can be useful for debugging or documenting
# what changed.
comm -13 repository.files tree.files > add.files
comm -13 repository.dirs tree.dirs > add.dirs
comm -23 repository.files tree.files > remove.files
comm -23 repository.dirs tree.dirs > remove.dirs

runlog_error()
{
    typeset logfile="$1"
    shift 1
    typeset command="$@"
    echo -e "\nERROR: Something wicked happened running '$command'." >&2
    echo "Check $logfile for more information." >&2
    return 2
}

# Tell user we're about to run something, run the command with output
# to a log file, and if there's an error say so and instruct the user
# to consult the log file.  Change to the CVS repository directory first.
runlog_cvsdir()
{
    typeset logfile="$1" commentary="$2"
    shift 2
    echo -e "\n$commentary...\c"
    (cd $repository && "$@") >> $logfile 2>&1 ||
    	runlog_error "$logfile" "$@"
}

# Remove the files from CVS which aren't in the new tree.  'cvs update -P'
# will properly handle any directories which become empty as a result of this.
[ -s remove.files ] && runlog_cvsdir rm.log \
			"Removing files which aren't in the snapshot" \
			XARGS $CVS rm -f < remove.files

echo -e "\nCopying the snapshot over the checked-out CVS image...\c"
# Overwrite the CVS files with a link farm to the new tree.
(cd $TREE && FIND . | cpio -pvmud $repository) > copy.log 2>&1

# Add any new directories (there is no 'cvs add -R')
[ -s add.dirs ] && runlog_cvsdir add.log \
		    "cvs adding new directories" \
		    XARGS $CVS add -ko < add.dirs
# Add any new files
[ -s add.files ] && runlog_cvsdir add.log \
		    "cvs adding new files" \
		    XARGS $CVS add -ko < add.files
if [ $INITIAL = true ]
then
    # Remove _TrickCVS_ from the trunk
    (cd $repository
	set -x
	$CVS rm -f _TrickCVS_
	$CVS commit -m 'removed trickery' _TrickCVS_
    	$CVS up -A _TrickCVS_
	$CVS rm -f _TrickCVS_
	$CVS commit -m 'removed trickery' _TrickCVS_
    ) > cleanup.log 2>&1
fi

# Commit and tag
runlog_cvsdir commit.log \
	"Commiting the changes" \
	$CVS commit -m "updating branch $VENDOR to $RELEASE"

runlog_cvsdir tag.log \
	"Tagging" \
	$CVS tag -F $RELEASE

# Do diffs here to make sure all's well?
(cd $repository && ls | grep -v CVS | XARGS rm -fr)
runlog_cvsdir diff.log \
	"cvs updating prior to running 'diff' as quality check" \
	$CVS up -ko -RPd -r$RELEASE

echo -e "\nRunning 'diff' as quality check...\c"
cd ..
if diff -ur --exclude CVS --exclude .cvsignore --exclude $WORKDIR \
	$PWD $WORKDIR/$REPOSITORY
then
    echo -e "\nNo differences, removing temporary CVS directory...\c"
    rm -fr $WORKDIR/$REPOSITORY
    echo
    echo "I left $WORKDIR/* around in case you're interested -- you probably"
    echo "will want to delete $WORKDIR soon."
else
    echo -e "\nFound some differences so leaving the temporary CVS directory"
    echo "$WORKDIR/$REPOSITORY to help with possible diagnosis."
    echo "Note that empty directories in the snapshot appear as errors"
    echo "because 'cvs update -P' prunes empty directories.  Those directories"
    echo "are safely in CVS."
fi
