#!/bin/sh
#-------------------------------------------------------------------------
#    Author:            Ross Mark (rossm@controllingedge.com.au)
#    Date:              Tue Mar 11 10:02:57 EST 2003
#
#    Copyright (C) 2003-2004 Ross Mark
#
#-------------------------------------------------------------------------
#
#    Description:
#    Archive SVN (asvn) will allow the recording of file types not
#    normally handled by svn. Currently this includes devices,
#    symlinks and file ownership/permissions.
#
#    Every file and directory has a 'file:permissions' property set and
#    every directory has a 'dir:devices' and 'dir:symlinks' for
#    recording the extra information.
#       
#    Run this script instead of svn with the normal svn arguments.
#
#
#    Licensing:
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#
#-------------------------------------------------------------------------

# $HeadURL$
# $LastChangedDate$
# $LastChangedBy$
# $LastChangedRevision$

PROG="`basename $0`"
VERSION=1.3


## default settings

SVN=${SVN:-svn}
FIND=${FIND:-find}
DEV_PROP="dir:devices"
SYM_PROP="dir:symlinks"
FILE_PROP="file:permissions"
SKIPSVN='\( -name .svn -prune -false \)'


## environment sanity check

die () { rc="$1"; shift; echo 1>&2 "$PROG: ERROR: $@"; exit $rc; }
have_command () { type "$1" >/dev/null; }
require_command () 
{ 
    if have_command "$1"; then
        return 0 # ok
    else
        die 1 "Cannot find required command '$1' -- check the PATH environment variable."
    fi
}

require_command cmp
require_command $FIND
require_command mktemp
require_command $SVN

# check that 'find' supports the '-printf' option
$FIND "`dirname \"$0\"`" -type f -name "$PROG" -printf "%f %l %p %m %u %U %g %G\n" >/dev/null 2>&1
if [ $? -ne 0 ]; then
    die 1 "The '$FIND' utility (`type $FIND`) does not support the '-printf' switch -- aborting."
fi

## internal variables

ACTION=""
PCWD=`pwd`
TMPDIR="`mktemp -d -t ${PROG}.XXXXXXXXXX`"
TMPFILE=${TMPDIR}/tmp0
TMPFILE1=${TMPDIR}/tmp1
IGNORE_TMP=${TMPDIR}/ignorefile.tmp

trap "cleanup" HUP INT QUIT TERM ABRT PIPE USR1 USR2 ALRM

cleanup()
{
    rm -f $TMPFILE $TMPFILE1 $IGNORE_TMP;
    rmdir $TMPDIR
}

basedirname()
{
    refname="$1"
    dir="`dirname \"$2\"`"
    ref="`expr \"$dir\" : \"$refname/\(.*\)\"`"
    if [ -z "$ref" ]
    then
        echo .
    else
        echo $ref
    fi
}

# Modifies IGNORE_TMP
addignorefile()
{
    file="`basename \"$1\"`"
    dir="`dirname \"$1\"`"

    efile="`echo $file |sed -e 's!\([\[\(\$]\)!\\\\\1!g'`"
    gefile="`echo $efile |sed -e 's!\(\\\\\)!\\\\\\\\\1!g'`"
    if ($SVN propget svn:ignore "$dir" | grep "^$gefile\$" >/dev/null 2>&1); 
    then
        : # nothing to do
    else
        $SVN propget svn:ignore "$dir"  |sed -e '/^$/d' >$IGNORE_TMP
        echo "$efile" >>$IGNORE_TMP 
        $SVN propset svn:ignore -F $IGNORE_TMP "$dir"
        verbose setting ignore
        #cat $IGNORE_TMP >&2
    fi
}

deleteignorefile()
{
    file="`basename \"$1\"`"
    dir="`dirname \"$1\"`"
    efile="`echo $file |sed -e 's!\([\[\(\$]\)!\\\\\1!g'`"
    gefile="`echo $efile |sed -e 's!\(\\\\\)!\\\\\\\\\1!g'`"
    verbose "deleting ignore setting for '$file'"
    if ($SVN propget svn:ignore "$dir" | grep "^$gefile\$" >/dev/null 2>&1)
    then
        $SVN propget svn:ignore "$dir" |sed -e '/^$/d'  |grep -v "^$gefile\$" >$IGNORE_TMP
        $SVN propset svn:ignore -F $IGNORE_TMP "$dir"
        #cat $IGNORE_TMP >&2
    fi
}

recorddirinfo ()
{
    eval "$FIND $PCWD $SKIPSVN -o \( -type d ! -name .svn  -print \)" | while read dirlist
    do
        updatedirdevices $1 "$dirlist"
    done
}

updatedirdevices ()
{
    CHECKIN=false
    if [ "$1" = "-ci" ]
    then
        CHECKIN=true
        shift
    fi
    dir="$1"

    verbose checking $dir for devices
    #
    # Obtain the list of devices in this directory
    #
    $FIND "$dir" \( \( -type b -o -type c -o -type p \) -print \)  \
        -o  -type d ! -name "`basename \"$dir\"`" -prune \
        | sort \
        | (while read file; do
            prop="`$FIND \"$file\" -printf \"file='%f' mode=%m user=%u(%U) group=%g(%G)\"`"
            if [ -b "$file" ]; then prop="$prop type=b"; fi
            if [ -c "$file" ]; then prop="$prop type=c"; fi 
            if [ -p "$file" ]; then prop="$prop type=p"; fi
            if [ -b "$file" -o -c "$file" ] 
            then
                prop="$prop `ls -l \"$file\" | sed -e 's/^[-lcpbrdwxXstugoTS]* *[0-9] [^ ]* *[^ ]* *\([0-9]*\), *\([0-9]*\) .*/major=\1 minor=\2/'`"
            fi
            # In this case file is the full path.
            addignorefile "$file"
        done) > $TMPFILE

    #
    # Obtain the currently defined devices
    #
    $SVN propget $DEV_PROP "$dir" >$TMPFILE1 2>/dev/null
    if [ $? -eq 0 ]; then
        # If the two list are the same then there is nothing to do.
        if cmp $TMPFILE1 $TMPFILE >/dev/null
        then
            return 0
        fi

        if [ -s $TMPFILE ]
        then
            # There are devices in this directory
            if [ "$CHECKIN" = "true" ]
            then
                # Add the current devices to the property
                $SVN propset $DEV_PROP "$dir" -F $TMPFILE
            else
                # Delete all the unwanted devices ie not in TMPFILE1
                cat $TMPFILE |while read line
                do
                    file="`expr \"$line\" : \"file='\(.*\)' mode\"`"
                    if ! fgrep "file='$file'" $TMPFILE1 >/dev/null 2>&1
                    then
                        rm "$file"
                        deleteignorefile "$file"
                    fi
                done
            fi
        else
            # There are no devices in this directory
            if [ "$CHECKIN" = "true" ]
            then
                $SVN propdel $DEV_PROP "$dir"
            fi
        fi
    fi

    #
    # If we are not a checkin then make sure all the devices are defined
    #
    if [ "$CHECKIN" != "true" ]
    then
        cat $TMPFILE1 |while read info
        do
            #echo info = $info
            [ -z "$info" ] && continue
            fgrep "$info" $TMPFILE  >/dev/null 2>&1 && continue # This line still matches
            file="`expr \"$info\" : \"file='\(.*\)' \"`"
            mode=`expr "$info" : ".*' mode=\([0-9]*\) "`
            user=`expr "$info" : ".* user=\([^(]*\)("`
            uid=`expr "$info" : ".* user=[^(]*(\([0-9]*\) "`
            group=`expr "$info" : ".* group=\([^(]*\)("`
            gid=`expr "$info" : ".* group=[^(]*(\([0-9]*\) "`
            type=`expr "$info" : ".* type=\(.\)"`
            major=`expr "$info" : ".* major=\([0-9]*\)"`
            minor=`expr "$info" : ".* minor=\([0-9]*\)"`
            #
            # This file is either missing or wrong
            # Delete the old and create it anew.
            #
            rm -f "$dir/$file"
            mknod "$dir/$file" $type $major $minor
            chmod $mode "$dir/$file"
            chown $user:$group "$dir/$file"
            addignorefile "$dir/$file"
        done
    fi
}

recordpermissions ()
{
    CHECKIN=false
    if [ "$1" = "-ci" ]
    then
        CHECKIN=true
        shift
    fi

    eval "$FIND $PCWD $SKIPSVN -o \( \( -type d ! -name .svn  \) -o -type f \) -printf \"file='%p' mode=%m user=%u(%U) group=%g(%G)\n\"" | while read info
    do
        device="`expr \"$info\" : \"file='\(.*\)' mode\"`"
        info="`expr \"$info\" : \"file='.*' \(mode.*\)\"`"

        if [ "$PCWD" = "$device" ]
        then
            dir="."
            file=""
        else
            dir="`basedirname \"$PCWD\" \"$device\"`"
            file="`basename \"$device\"`"
        fi

        # see if the properties have changed.
        prop="`$SVN propget $FILE_PROP \"$dir/$file\" 2>/dev/null`"
        if [ $? -eq 0 -a "$prop" != "$info" ]
        then
            if [ "$CHECKIN" = "true" ]
            then
                $SVN propset $FILE_PROP  "$info" "$dir/$file"
            else
                info=`$SVN propget $FILE_PROP "$dir/$file"`
                mode=`expr "$info" : "mode=\([0-9]*\) "`
                user=`expr "$info" : ".* user=\([^(]*\)("`
                uid=`expr "$info" : ".* user=[^(]*(\([0-9]*\) "`
                group=`expr "$info" : ".* group=\([^(]*\)("`
                gid=`expr "$info" : ".* group=[^(]*(\([0-9]*\) "`
                if  [ "$user" = "" -o "$group" = ""  -o "$mode" = "" ]
                then
                    verbose "property $FILE_PROP not set for $dir/$file"
                else
                    chown $user:$group  "$dir/$file"
                    chmod $mode "$dir/$file"
                fi
            fi
        fi
    done
}


pre_checkin ()
{
    verbose this is the pre checkin process
    recorddirinfo -ci
    recordpermissions -ci
}

post_checkout ()
{
    verbose this is the post checkout process
    if [ "$CHDIR" = "true" ]
    then
        shift `expr $# - 1`
        cd "`basename \"$1\"`"
        PCWD="$PCWD/`basename \"$1\"`"
    fi
    recorddirinfo 
    recordpermissions 
}


## main loop

first_non_option_argument ()
{
    for arg in "$@"; do
        case "$arg" in 
            -*) continue ;; # ignore
            *)  echo "$arg"; return 0 ;; # print it
        esac
    done
    return 1 # no non-option argument found
}

contains ()
{
    expr "$1" : ".*$2" >/dev/null 2>&1
}

has_option ()
{
    short="$1"
    long="$2"
    shift 2
    for arg in "$@"; do
        case "$arg" in
            --"$long") return 0 ;;
            -*) if contains "$arg" "$short"; then return 0; fi ;;
            *) continue ;;
        esac
    done
    return 1
}

# if option -q/--quiet given, then turn off verbose reporting
if has_option "q" "quiet" "$@"; then
    verbose () { : ; }
else
    verbose () { echo "$PROG: $@"; } 
fi

CHDIR=false
case "`first_non_option_argument \"$@\"`" in
  checkout|co)      CHDIR=true; ACTION="post";;
  commit|ci)        ACTION="pre";;
  switch|sw)        ACTION="post";;
  update|up)        ACTION="post";;
  *);;
esac

[ "$ACTION" =  "pre" ] && pre_checkin "$@"

$SVN "$@"
rc=$?

[ $rc -eq 0 -a "$ACTION" = "post" ] && post_checkout "$@"

cleanup

exit $rc

#
# vim: set ai ts=8 sw=4
#


