#!/bin/sh
#
# $Id: svn_cvsinject 452 2004-05-25 07:47:58Z sam $
# svn_cvsinject: reinject SVN commits to a CVS repository
# see http://sam.zoy.org/writings/programming/svn2cvs.html for more details
#
#    (c) 2004 Sam Hocevar <sam@zoy.org>
#
#   This program is free software; you can redistribute it and/or
#   modify it under the terms of the Do What The Fuck You Want To
#   Public License as published by Banlu Kemiyatorn. See
#   http://sam.zoy.org/projects/COPYING.WTFPL for more details.
#
#
# usage:
#   svn_cvsinject [options] <SVNrepo> <CVSrepo>
#
# or in your hooks/post-commit:
#   svn_cvsinject -r "$2" "$1" "/var/lib/cvs/videolan/vlc"
#

CVSREPO=""
SVNREPO=""
SVNREV=""
ALIASES=""
DEBUG="no"

while test $# -gt 0; do
  opt="$1"
  shift
  case "$opt" in
    -h|--help)
      echo "usage: $0 [options] <SVNrepo> <CVSrepo>"
      echo "options are:"
      echo " -r <rev>          reinject SVN revision <rev> (default: latest)"
      echo " -a <svn>/<cvs>    alias SVN branch <svn> to CVS branch <cvs>"
      echo " -d, --debug       debug mode"
      echo " -h, --help        display help message and quit"
      exit 0
      ;;
    -d|--debug)
      DEBUG="yes"
      ;;
    -r)
      SVNREV="$1"
      shift
      ;;
    -a)
      SUBST="`echo "$1" | sed -ne 's,^\([^/]*\)/\([^/]*\)$,\\\\<\1\\\\>/\2,p'`"
      if test -z "$SUBST"; then
        echo "$0 error: bad substitution ($1)"
        exit 1
      fi
      ALIASES="${ALIASES} ; s/$SUBST/g"
      shift
      ;;
    *)
      if test -z "$SVNREPO"; then
        SVNREPO="$opt"
      elif test -z "$CVSREPO"; then
        CVSREPO="$opt"
      else
        echo "$0 error: too many arguments"
        echo "Try \`$0 --help' for more information."
        exit 1
      fi
      ;;
  esac
done

# If no repositories were specified, abort
if test -z "$CVSREPO"; then
  echo "$0 error: too few arguments"
  echo "Try \`$0 --help' for more information."
  exit 1
fi

# If no revision was specified, use the latest commit
LASTSVNREV="`svn ls -v "file://$SVNREPO" 2>/dev/null | awk '{ print $1 }' | sort -n | tail -n 1`"
if test -z "$LASTSVNREV"; then
  echo "$0 error: repository file://$SVNREPO not found"
fi

if test -z "$SVNREV"; then
  SVNREV="$LASTSVNREV"
fi

if test "$DEBUG" = "yes"; then
  set -x
fi

TMPDIR="/tmp/svn_cvsinject-`date +%Y%m%d-%H%M%S`-$$"
COMMITLOG="${TMPDIR}/commit.log"
FILELOG="${TMPDIR}/file.log"
SVNDIFF="${TMPDIR}/patch.diff"
CVSROOT="`echo "${CVSREPO}" | sed 's,/[^/]*$,,'`"
CVSMODULE="`echo "${CVSREPO}" | sed 's,.*/,,'`"

#
# Create our working directory
#
mkdir -p "${TMPDIR}"
cd "${TMPDIR}"

# Get a commit log and extract the filenames
# FIXME: using --xml would make this less error-prone
svn log -v "file://${SVNREPO}" -r "${SVNREV}" | sed -e '/^--*$/d' > "${COMMITLOG}"
sed -ne '1,/^Changed/d' -e '/^$/q' -e 's/ (from.*)$//' -e 'p' < "${COMMITLOG}" > "${FILELOG}"

# CVS uses $LOGNAME
LOGNAME="`awk '{ print $3 }' "${COMMITLOG}" | sed -ne 1p`"

for BRANCH in `sed -ne 's,[^/]*/branches/\([^/]*\).*,\1,p' "${FILELOG}" | sort | uniq ; grep -q '[^/]*/trunk\>' "${FILELOG}" && echo trunk`; do

  # Convert the SVN branch name to the CVS branch name
  case "${BRANCH}" in
    trunk) CVSBRANCH=HEAD;
           SVNBRANCH=trunk ;;
    *) CVSBRANCH="`echo "${BRANCH}" | sed "$ALIASES"`"
       SVNBRANCH="branches/${BRANCH}" ;;
  esac

  # Get the SVN diff, skip this version if failed
  svn diff "file://${SVNREPO}/${SVNBRANCH}" -r "$((${SVNREV} - 1)):${SVNREV}" > "${SVNDIFF}-${CVSBRANCH}" || continue

  # Get the subset of modified files from the CVS repository
  sed -ne 's,^--- \(.*\) *(revision.*,\1,p' "${SVNDIFF}-${CVSBRANCH}" | while read entry; do
    cvs -d "${CVSROOT}" checkout -l `test "${CVSBRANCH}" = "HEAD" || echo -r "${CVSBRANCH}"` "${CVSMODULE}/$entry" || true
  done

  # Also get the removed stuff
  sed -ne 's,^   D /'"${SVNBRANCH}"'/,,p' "${FILELOG}" | while read entry; do
    cvs -d "${CVSROOT}" checkout -l `test "${CVSBRANCH}" = "HEAD" || echo -r "${CVSBRANCH}"` "${CVSMODULE}/$entry" || true
    if test -d "${CVSMODULE}/$entry"; then
      (cd "${CVSMODULE}/$entry" && cvs update -d)
    fi
  done

  # If no directory was created (which may happen if only new directories and
  # files were checked in), we just retrieve the parent directory
  if ! test -d "${CVSMODULE}"; then
    cvs -d "${CVSROOT}" checkout -l `test "${CVSBRANCH}" = "HEAD" || echo -r "${CVSBRANCH}"` "${CVSMODULE}"
  fi

  # Patch the repository (if there is anything to patch)
  cd "${CVSMODULE}"
  if grep -q '^+++' "${SVNDIFF}-${CVSBRANCH}"; then
    patch -p0 < "${SVNDIFF}-${CVSBRANCH}"
  fi

  # Add all new directories and files in branch
  sed -ne 's,^   A /'"${SVNBRANCH}"'/,,p' "${FILELOG}" | while read entry; do
    if test -d "$entry"; then
      # if $entry is a directory copied from somewhere else, the SVN
      # changeset may be incomplete, we need to add all subdirectories
      # and files manually.
      find "$entry" -type d -a '!' -name CVS -exec cvs add '{}' ';'
      sed -ne 's,^+++ '"$entry/"'\(.*\) *(revision.*,\1,p' "${SVNDIFF}-${CVSBRANCH}" | while read file; do
        cvs add "$entry/$file"
      done
    else
      # if $entry is a file, just add it.
      cvs add "$entry"
    fi
  done

  # Get rid of removed files in branch
  sed -ne 's,^   D /'"${SVNBRANCH}"'/,,p' "${FILELOG}" | while read entry; do
    if test -d "$entry"; then
      # if $entry is a directory, the SVN changeset will be incomplete,
      # we need to remove all subfiles manually.
      find "$entry" -type d -a '!' -name CVS | while read dir; do
        find "$dir" -type f -maxdepth 1 | while read file; do
          rm -f "$file"
          cvs delete "$file"
        done
      done
    else
      # if $entry is a file, just remove it. Also, it might be a previously
      # removed directory, hence the || true.
      rm -f "$entry" || true
      cvs delete "$entry" || true
    fi
  done

  # Commit our changes using the same commit message
  cvs commit -m "`cat "${COMMITLOG}"`"

  # Clean up our mess
  cd ..
  rm -Rf "${CVSMODULE}"

done

# Clean up the remaining mess
cd /tmp
rm -Rf "${TMPDIR}"

