Version Control

Hello everybody,

I have just spent (AKA wasted :slight_smile: ) today learning the basics of RCS. While I accept that it’s extremely old, it did fit my use-case rather well… But stretching my testing to other computers, I quickly found that it’s not available in (standard) RHEL8 - so fear that’s the start of it phased out of other distros.

My network consists of various computers, all of which have similar config files and custom scripts which I’m keen to version control. Using RCS, I “had” created a central NFS stored directory, and used symlinks for the “RCS” directories (where it stores the version details) allowing me to easily include them within my backup process.

I guess I’m about to move to GIT - but does anybody else use GIT to version control O/S level config files, and have any tips on how to structure such a repository? And/or how to pull/get changes to separate directories - like say I want to version control: “/etc/host”, “/var/www/config.php” and /home/brian/my_script.sh (for examples) - for ALL my assets.

While I’m largely a single user I guess I’d have 1 repository per asset - but is that a fair assumption and is there anything I need to consider?

Thanks in advance…
Brian

…For those not using RHEL8 (or similar) - you may be interested in this script.

It uses RCS; being a simple versions control system which exists on many distros/repos.

apt-get install rcs

which is a tiny package!

I have scripted the basic use-cases, which could be used to version control any file you may have. While RCS relies on subdirectories (./RCS) the below uses a central directory and creates a symlink instead - this way all of the version data is kept in 1 place (opposed to scattered around your drive).

I include the below for anybody who may be interested in version controlling their files, but doesn’t want the bother of installing something like GIT (you lucky things! :slight_smile: ) - but I don’t intent to maintain this any further, and I’ve therefore added a few extra comments to aid those who may find it useful.

NB: I’m not suggesting it’s 100%, but based on my own basic tests it showed promise…

examples:

brian000s_vc_script.sh add /etc/fstab 

This will create a new directory within ${rcsdir} called “etc” which is symlinked to “/etc/RCS” - adding this initial version to it.

After making changes, run:

brian000s_vc_script.sh lock /etc/fstab
brian000s_vc_script.sh update /etc/fstab 

Which will apply the new changes (perhaps you’d consider merging those two into one statement!).

To view the revision history:

brian000s_vc_script.sh version /etc/fstab

If you find yourself changing multiple files at the same time:

brian000s_vc_script.sh bulk

this will version control (creating new revisions, or updating existing ones) for any thing changed “today”

For more info please refer to the script below…

Hope this helps somebody…


#!/bin/sh

##X-ref: http://linuxforums.org.uk/index.php?topic=14199.0

CI=`which ci`
CO=`which co`
RCS=`which rcs`
RCSDIFF=`which rcsdiff`
RLOG=`which rlog`
DIRNAME=`which dirname`
dollar=$
success=0
today=$(date +"%Y-%m-%d")

##update as required!
user=`whoami`.`hostname -s`
rcsdir=/shared/RCS
rcsdirtmp=${rscdir}/tmp

_HELP() {
  echo "#########################################################"
  echo "File: ${0}"
  echo "$Id:$"
  echo "$Source:$"
  echo "#########################################################"
  echo "Purpose: Use RCS for version control"
  echo "         This uses symlinks saving revisions to 'rcsdir'"
  echo "#########################################################"
  echo "Known Issue or Risk:"
  echo "  Limited testing -  may contain errors"
  echo "  RCS removes the file after it's created, so this performs a 'get' after"
  echo "  'Bulk' may encounter issues where the revision is locked"
  echo "  During development, I lost a few test file, so a copy of the original script is saved to 'rcsdirtmp'"
  echo "  RCS doesn't seem to permit un/lock with a specific user - so 'user' may not be fully functional"
  echo "#########################################################"
  echo ""
  echo "checkout - Bookout and lock the latest revision"
  echo "           ${0} checkout <file>"
  echo ""
  echo "bulk     - Batch add/update all changes made <today>"
  echo "           ${0} bulk"
  echo ""
  echo "diff     - Get differences between the file and latest revision"
  echo "           ${0} diff <file>"
  echo ""
  echo "get      - Bookout and unlock the latest revision"
  echo "           ${0} get <file>"
  echo ""
  echo "header     - Check file for inclusion of the version detail parameters"
  echo "           ${0} header <file>"
  echo ""
  echo "help    -  View this text"
  echo ""
  echo "lock     - Lock a file for changes (will leave the file unchanged)"
  echo "           ${0} lock <file>"
  echo ""
  echo "new      - add a new file; which includes the 'get' and 'header' actions"
  echo "           ${0} new <file>"
  echo ""
  echo "unlock   - Release a Lock from a file (will leave the file unchanged)"
  echo "           ${0} unlock <file>"
  echo ""
  echo "update   - Push changes to version control"
  echo "           ${0} update <file>"
  echo ""
  echo "version  - view revision history for a specific file"
  echo "ver      - synonym for 'version'"
  echo "           ${0} version <file>"
  echo "           ${0} ver <file>"
  echo ""
  echo "#########################################################"
}

_LOG() {
  ##Consider logging instead of just "echo"!
  if [ ! "${1}" = "" ]; then
    echo ${1} 
  fi
}

_Success() {
  ##Log and report failures.
  ##${success} holds the greatest of any errors.
  if [ ${1} -eq 0 ]; then
    _LOG "${2} - ${3} Successful."
  else
    _LOG "${2} - Unexpected Error(${1}): ${3}."
    if [ ${success} -lt ${1} ]; then
      success=${1}
    fi
  fi
}

_Bulk() {
  ##Add/Update all files in the current directory into RCS.
  ### not 100% - issues around when the file is locked.
  message="Bulk update ${today}"
  description="Created via Bulk"
  for file in `find . -maxdepth 1 -newermt "${today}"`; do
    ##disrgared old errors
    success=0

    ##Does the file needs to added or (locked and) updated?
    if [ -f "${file}" ]; then
      _TestInputFile "${file}"
      if [ ${success} -eq 0 ]; then
        ##Use LOG to establish if the file already exists (and therefore needs to updated)
        ${RCS} log "${dirname}/${basename}" >> /dev/nul 2>>/dev/nul
        if [ ${?} -eq 0 ]; then
          _LOG "Processing: '${dirname}/${basename}' (update)"
          ## remember to lock the file before an update
          lock=-l
          _LOCK
          _UPDATE
        else
          ##If it's not an update, it needs to be added.
          _LOG "Processing: '${dirname}/${basename}' (new)"
          _NEW
          if [ ${success} -eq 0 ]; then
            lock=-u
            _CHECKOUT
          fi
        fi
      fi
    else
      ##RCS does not handle directories...
      _LOG "${file} is not a file (or was not found)."
    fi
  done
}

_CheckHeader() {
  ##Capture those files which shoud have variables but don't
  if [ ! "`grep \#\!/bin/ ${dirname}/${basename}`" ]; then
    _LOG "Consider adding '#!/bin/sh'"
  fi

  ##use ${dolar} to stop RCS from expanding them, mistaking them for it's own parameters!
  if [ ! "`grep \${dollar}Id ${dirname}/${basename}`" ]; then
   _LOG "Consider adding '${dollar}Id:${dollar}' and ${dollar}Source:${dollar}'"
  fi
}

_TestInputFile() {
  ##build the target directory based on in the input filename
  cd `${DIRNAME} ${1}`
  basename=`basename ${1}`
  dirname=${PWD}

  if [ ! -f "${dirname}/${basename}" ]; then
    _Success 9 "${dirname}/${basename}" "Input file not found"
  fi
}

_MKRCSDIR() {
  ##Create symlink for the RCS directory.
  ##Nb: to bypass this, simply manually create the RCS directory as required.
  if [ -d "${dirname}/RCS" ]; then
    _LOG "RCS subdirectory already exists."
  else

    ##Create the "tmp" direcory too
    if [ ! -d "${rcsdirtmp}" ]; then
      mkdir -p "${rcsdirtmp}"
    fi

    ##to use a local RCS direcory, simply create the directory first
    ##translate characters because the path will become the directory name
    rcsloc=`echo "${dirname}" | tr "/ " __`
    _LOG "Making RCS subdirectory - ${rcsloc}"
    mkdir -p "${rcsdir}/${rcsloc}" 
    if [ ${?} -ne 0 ]; then
      _Success 9 "${rcsdir}/${rcsloc}" "Unable to create RCS directory"
    fi

    ##Create the symlink.
    ln -s "${rcsdir}/${rcsloc}" "${dirname}/RCS"
    if [ ${?} -ne 0 ]; then
      _Success 9 "${dirname}/RCS" "Failed to create SymLink"
    fi
  fi
}

_BackupSource() {
  #take a copy of the file before we start.
  #these are not required, but will save the file in the event of errors
  if [ "${backup}" = "" ]; then
    cp -f "${dirname}/${basename}" "${rcsdirtmp}/${basename}"
    _Success ${?} "${basename}" "Copied to 'tmp'"
    backup=no
  fi
}

_NEW() {
  ##Check that the RSC directory exists (or create it)
  _MKRCSDIR
  if [ ${success} -ne 0 ]; then
    _LOG "Failed to make RCS directory."
    exit 9
  fi

  ##Take a backup of the source file
  _BackupSource

  ##Add a new file (also called from _Bulk - which passes the message and description)
  if [ "${message}" = "" ]; then
    ${CI} -T -i -w${user} ${dirname}/${basename}  
  else
    ${CI} -T -i -w${user} -m"${message}" -t-"${description}" ${dirname}/${basename}  
  fi
  _Success ${?} ${basename} "Add to version control"
}

_LOCK() {
  ##Lock the file - required before updating the revision 
  ${RCS} ${lock} ${dirname}/${basename} 
  _Success ${?} ${basename} "Lock"
}
_UPDATE() {
  ##Take a backup of the source file
  _BackupSource

  ##Add a new file (also called from _Bulk - which passes the message and description)
  if [ "${message}" = "" ]; then
    ${CI} -T -u -w${user} ${dirname}/${basename}  
  else
    ${CI} -T -u -w${user} -m"${message}" -t-"${description}" ${dirname}/${basename}  
  fi
  _Success ${?} ${basename} "Update"
}

_CHECKOUT() {
  ##Get the latest revision.
  _BackupSource
  ${CO} -T -w${user} ${lock} ${dirname}/${basename} 
  _Success ${?} ${basename} "Check out"
}

_VERSION() {
  ##display the revision details
  ${RLOG} -b ${dirname}/${basename} 
}

_DIFF() {
  ##Display a difference between the file and latest revision
  ${RCSDIFF} ${dirname}/${basename} 
}

#############################################

if [ "${RCS}" = "" ]; then
  echo "RCS doesn't appear to exist on this system!"
  exit 9
elif [ "${1}" = "help" ]; then
  _HELP
elif [ "${1}" = "bulk" ]; then
  _LOG ""
else
  _TestInputFile "${2}"
  if [ ${success} -ne 0 ]; then
    _LOG "Unable to process file '${2}'."
    exit 9
  fi
fi

#############################################

if [ "${1}" = "bulk" ]; then
  _Bulk
elif [ "${1}" = "header" ]; then
  _CheckHeader
elif [ "${1}" = "checkout" ]; then
  lock=-l
  _CHECKOUT
elif [ "${1}" = "diff" ]; then
  _DIFF
elif [ "${1}" = "get" ]; then
  lock=-u
  _CHECKOUT
elif [ "${1}" = "lock" ]; then
  lock=-l
  _LOCK
elif [ "${1}" = "new" ]; then
  _NEW
  if [ ${success} -eq 0 ]; then
    lock=-u
    _CHECKOUT
    _CheckHeader
  fi
elif [ "${1}" = "unlock" ]; then
  lock=-u
  _LOCK
elif [ "${1}" = "update" ]; then
  _UPDATE
elif [ "${1}" = "ver" ] || [ "${1}" = "version" ]; then
  _VERSION
fi