#!/usr/pkg/bin/bash
set -e

# stg-cvs - helper script to manage a mixed cvs/stgit working copy.

# Allows quick synchronization of a cvs mirror branch (does not try to
# reconstruct patchsets, creates "jumbo" commits), and commits stgit
# patches to CVS.

# Copyright (c) 2007 Yann Dirson <ydirson@altern.org>
# Subject to the GNU GPL, version 2.

# NOTES
# - you want to add a "CVS" line to .git/info/exclude
# - you may want to add a ".git" line to the top .cvsignore

# BRANCH INIT
# - ensure the cvs wc is clean (eg. with "cvsco")
# $ git init
# $ echo CVS >> .git/info/exclude
# $ git add .
# $ git commit -m "Initial import."
# $ git branch -m master cvs
# $ stg branch -c master cvs
# $ git config branch.master.stgit.parentbranch cvs (0.12.1 and earlier only)
# $ git config branch.cvs.description "CVS $(cat CVS/Root) $(cat CVS/Repository) $(cat CVS/Tag 2>/dev/null | echo HEAD)"
# $ git config branch.master.description "Changes for $(cat CVS/Repository) $(cat CVS/Tag 2>/dev/null | echo HEAD)"

# LIMITATIONS
# - this is only a proof-of-concept prototype
# - lacks an "init" command (see above)
# - "commit" does not ensure the base is uptodate before trying to
#   commit (but hey, it's CVS ;): better "stg-cvs pull" first
# - "commit" can only commit a single patch
# - not much robustness here
# - still no support for files removed in cvs (should catch "no
#   longer in the repository" message)
# - this only deals with CVS but could surely be extended to any other
#   VCS
# - lacks synchronisation of .cvsignore <-> .gitignore
# - no support for filenames with spaces (stg lacks --zero output format)
# - git-commit is too chatty when it finds nothing to commit
# - lacks a "quick cvs commit" feature

# DESIGN FLAWS
# - while fetching, if a file change was not git-update-index'd when
#   cvs-update'd (eg. because of a stg-cvs bug), it is not seen on further
#   fetches until it changes again, since we scan "cvs update" output.
#   This yields possible inconsistencies with CVS.
# - similarly, any conflict while cvs-updating (whether due to illegal
#   changes to the cvs-mirror-branch, or due to files added to cvs but
#   already-existing in working copy, or to directory moves inside the
#   cvs repository, or <fill here>) has to be dealt with by hand (although
#   the situation is better here: cvs sees the conflict on subsequent tries)
# - bad/no support for cvsutils:
#   - stg push/pop operations confuse cvsu because of timestamp changes
#   - cvspurge/cvsco would nuke .git => does not make it easy to ensure
#     synchronisation
# - should use a separate workspace for cvs branch like tailor does
# - confused by cvs keyword substitution

usage()
{
    [ "$#" = 0 ] || echo "ERROR: $*"
    echo "Usage: $(basename $0) <command>"
    echo " commands: $(do_commands)"
    exit 1
}

do_commands()
{
    echo $(grep '^[a-z-]*)' $0 | cut -d')' -f1)
}

do_fetch()
{
    local return=0
    local path

    local parent="$1"
    local branch="$2"

    # record changes from cvs into index
    stg branch "$parent" || exit $?
    cvs -fq update -dP | grep -v '^\? ' | tee /dev/tty | while read status path; do
	if [ -e "$path" ]; then
	    git update-index --add "$path" || exit $?
	else
	    git update-index --remove "$path" || exit $?
	fi
	# cvs update: `FELIN1_PEP/src/java/com/sagem/felin/ui/widget/PEPRifStateIcon.java' is no longer in the repository
    done

    # create commit
    if git commit -m "stg-cvs sync"; then
	:
    else
	return=$?
    fi

    # back to branch
    stg branch "$branch" || exit $?

    return $return
}

cvs_add_dir()
{
    local parent=$(dirname "$1")
    if [ ! -e "$parent/CVS" ]; then
	cvs_add_dir "$parent"
    fi

    cvs add "$1"
}

# get context
branch=$(stg branch)
parent=$(git-config "branch.${branch}.stgit.parentbranch") || 
    usage "no declared parent for '$branch' - set branch.${branch}.stgit.parentbranch"

# extract command

[ "$#" -ge 1 ] || usage
command="$1"
shift

case "$command" in
fetch)
    do_fetch "$parent" "$branch"
    ;;

pull)
    if do_fetch "$parent" "$branch"; then
	# update
	#  --merged
	stg rebase "$parent"
	stg clean --applied
    fi
    ;;

commit)
    # sanity asserts
    [ $(stg applied | wc -l) = 1 ] ||
	usage "you don't have exactly one patch applied"

    # context
    patch=$(stg top)
    
    # adds
    stg files | grep ^A | cut -c3- | while read file; do
	parent=$(dirname "$file")
	if [ ! -e "$parent/CVS" ]; then
	    cvs_add_dir "$parent"
	fi
	cvs -f add "$file"
    done

    # removes
    stg files | grep ^D | cut -c3- | xargs -r cvs -f remove

    # commit
    stg files --bare | xargs -r cvs -fq commit \
	-F ".git/patches/$branch/patches/$patch/description"

    # sync the parent branch
    stg branch "$parent"
    git-cherry-pick "patches/${branch}/${patch}"
    stg branch "${branch}"

    # update
    # --merged
    stg rebase "$parent"
    stg clean --applied
    ;;

_commands)
    # hint for bash-completion people :)
    do_commands
    ;;

*)
    usage "unknown command '$command'"
    ;;
esac
