#!/bin/bash

# A simple script to checkout or update a svn or git repo as source service
#
# (C) 2010 by Adrian Schröter <adrian@suse.de>
#  
# 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.  
# See http://www.gnu.org/licenses/gpl-2.0.html for full license text.  

SERVICE='tar_scm'

set_default_params () {
  MYSCM=""
  MYURL=""
  MYVERSION="_auto_"
  MYFORMAT=""
  MYPREFIX=""
  MYFILENAME=""
  MYREVISION=""
  MYPACKAGEMETA=""
  USE_SUBMODULES=enable
#  MYHISTORYDEPTH=""
  INCLUDES=""
  MYTARFILENAME='${NAME}-${VERSION}'
}

get_config_options () {
  # config options for this host ?
  if [ -f /etc/obs/services/$SERVICE ]; then
    . /etc/obs/services/$SERVICE
  fi
  # config options for this user ?
  if [ -f "$HOME"/.obs/$SERVICE ]; then
    . "$HOME"/.obs/$SERVICE
  fi
}

parse_params () {
  while test $# -gt 0; do
    case $1 in
      *-scm)
        MYSCM="$2"
        shift
      ;;
      *-url)
        MYURL="$2"
        shift
      ;;
      *-subdir)
        MYSUBDIR="$2"
        shift
      ;;
      *-revision)
        MYREVISION="$2"
        shift
      ;;
      *-version)
        MYVERSION="$2"
        shift
      ;;
      *-include)
        INCLUDES="$INCLUDES $2"
        shift
      ;;
      *-versionformat)
        MYFORMAT="$2"
        shift
      ;;
      *-versionprefix)
        MYPREFIX="$2"
        shift
      ;;
      *-exclude)
        EXCLUDES="$EXCLUDES --exclude=${2#/}"
        shift
      ;;
      *-filename)
        MYFILENAME="${2#/}"
        shift
      ;;
      *-package-meta)
        MYPACKAGEMETA="${2#/}"
        shift
      ;;
      *-outdir)
        MYOUTDIR="$2"
        shift
      ;;
      *-history-depth)
        echo "history-depth parameter is obsolete and will be ignored"
        shift
      ;;
      *-submodules)
        USE_SUBMODULES="$2"
        shift
      ;;
      *-tarfilename)
        MYTARFILENAME="$2"
        shift
      ;;
      *)
        echo "Unknown parameter: $1"
        echo 'Usage: $SERVICE --scm $SCM --url $URL [--subdir $SUBDIR] [--revision $REVISION] [--version $VERSION] [--include $INCLUDE]* [--exclude $EXCLUDE]* [--versionformat $FORMAT] [--versionprefix $PREFIX] [--filename $FILENAME] [--package-meta $META] [--submodules disable] [--tarfilename "${NAME}-${VERSION}"] --outdir $OUT'
        exit 1
      ;;
    esac
    shift
  done
}

error () {
  echo "ERROR: $*"
  exit 1
}

debug () {
  [ -n "$DEBUG_TAR_SCM" ] && echo "$*"
}

safe_run () {
  if ! "$@"; then
    error "$* failed; aborting!"
  fi
}

sanitise_params () {
  TAR_VERSION="$MYVERSION"

  if [ -z "$MYSCM" ]; then
    error "no scm is given via --scm parameter (git/svn/hg/bzr)!"
  fi
  if [ -z "$MYURL" ]; then
    error "no checkout URL is given via --url parameter!"
  fi
  if [ -z "$MYOUTDIR" ]; then
    error "no output directory is given via --outdir parameter!"
  fi

  FILE="$MYFILENAME"
  WD_VERSION="$MYVERSION"
  if [ -z "$MYPACKAGEMETA" ]; then
    EXCLUDES="$EXCLUDES --exclude-vcs"
  fi
  # if [ "$MYHISTORYDEPTH" == "full" ]; then
  #   MYHISTORYDEPTH="999999999"
  # fi
}

detect_default_filename_param () {
  if [ -n "$FILE" ]; then
    return
  fi

  case "$MYSCM" in
    git)
      FILE="${MYURL%/}"
      FILE="${FILE##*/}"
      FILE="${FILE%.git}"
      FILE="${FILE#*@*:}"
      ;;
    svn|hg|bzr)
      FILE="${MYURL%/}"
      FILE="${FILE##*/}"
      ;;
    *)
      error "unknown SCM '$MYSCM'"
  esac
}

fetch_upstream () {
  TOHASH="$MYURL"
  [ "$MYSCM" = 'svn' ] && TOHASH="$TOHASH/$MYSUBDIR"
  HASH=`echo "$TOHASH" | sha256sum | cut -d\  -f 1`
  REPOCACHE=
  if [ -n "$CACHEDIRECTORY" ]; then
    REPOCACHEINCOMING="$CACHEDIRECTORY/incoming"
    REPOCACHEROOT="$CACHEDIRECTORY/repo"
    REPOCACHE="$REPOCACHEROOT/$HASH"
    REPOURLCACHE="$CACHEDIRECTORY/repourl/$HASH"
  fi

  if [ -z "$MYREVISION" ]; then
    case "$MYSCM" in
      git)
        MYREVISION=master
        ;;
      hg)
        MYREVISION=tip
        ;;
      # bzr)
      #   MYREVISION=HEAD
      #   ;;
    esac
    if [ -n "$MYREVISION" ]; then
      debug "no revision specified; defaulting to $MYREVISION"
    fi
  fi

  debug "check local cache if configured"
  if [ -n "$CACHEDIRECTORY" -a -d "$REPOCACHE/.$MYSCM" ]; then
    debug "cache hit: $REPOCACHE/.$MYSCM"
    check_cache
    echo "Found $TOHASH in $REPOCACHE; updating ..."
    update_cache
    REPOPATH="$REPOCACHE"
  else
    if [ -n "$CACHEDIRECTORY" ]; then
      debug "cache miss: $REPOCACHE/.$MYSCM"
    else
      debug "cache not enabled"
    fi

    calc_dir_to_clone_to
    debug "new $MYSCM checkout to $CLONE_TO"
    initial_clone

    if [ -n "$CACHEDIRECTORY" ]; then
      cache_repo
      REPOPATH="$REPOCACHE"
    else
      REPOPATH="$MYOUTDIR/$FILE"
    fi
  fi

  safe_run cd "$REPOPATH"
  switch_to_revision
  if [ "$TAR_VERSION" == "_auto_" -o -n "$MYFORMAT" ]; then
    detect_version
  fi
}

calc_dir_to_clone_to () {
  if [ -n "$CACHEDIRECTORY" ]; then
    safe_run cd "$REPOCACHEINCOMING"
    # Use dry-run mode because git/hg refuse to clone into
    # an empty directory on SLES11
    debug mktemp -u -d "tmp.XXXXXXXXXX"
    CLONE_TO=`mktemp -u -d "tmp.XXXXXXXXXX"`
  else
    CLONE_TO="$FILE"
  fi
}

initial_clone () {
  echo "Fetching from $MYURL ..."

  case "$MYSCM" in
    git)
      # Clone with full depth; so that the revision can be found if specified
      safe_run git clone "$MYURL" "$CLONE_TO"
      if [ "$USE_SUBMODULES" == "enable" ]; then
        safe_run cd "$CLONE_TO"
        safe_run git submodule update --init --recursive
        safe_run cd ..
      fi
      ;;
    svn)
      args=
      [ -n "$MYREVISION" ] && args="-r$MYREVISION"
      if [[ $(svn --version --quiet) > "1.5.99" ]]; then
        TRUST_SERVER_CERT="--trust-server-cert"
      fi
      safe_run svn checkout --non-interactive $TRUST_SERVER_CERT \
        $args "$MYURL/$MYSUBDIR" "$CLONE_TO"
      MYSUBDIR= # repo root is subdir
      ;;
    hg)
      safe_run hg clone "$MYURL" "$CLONE_TO"
      ;;
    bzr)
      args=
      [ -n "$MYREVISION" ] && args="-r $MYREVISION"
      safe_run bzr checkout $args "$MYURL" "$CLONE_TO"
      ;;
    *)
      error "unknown SCM '$MYSCM'"
  esac
}

cache_repo () {
  if [ -e "$REPOCACHE" ]; then
    error "Somebody else beat us to populating the cache for $MYURL ($REPOCACHE)"
  else
    # FIXME: small race window here; do source services need to be thread-safe?
    debug "mv #1: $CLONE_TO -> $REPOCACHE"
    safe_run mv "$CLONE_TO" "$REPOCACHE"
    echo "$MYURL" > "$REPOURLCACHE"
    echo "Cached $MYURL at $REPOCACHE"
  fi
}

check_cache () {
  CACHEDURL=`cat "$REPOURLCACHE"`
  [ -z "$CACHEDURL" ] && CACHEDURL='<unknown URL>'
  if [ "$MYURL" != "$CACHEDURL" ]; then
    error "Corrupt cache: cache for repo $MYURL was recorded as being from $CACHEDURL"
  fi
}

update_cache () {
  safe_run cd "$REPOCACHE"

  case "$MYSCM" in
    git)
      safe_run git fetch
      ;;
    svn)
      args=
      [ -n "$MYREVISION" ] && args="-r$MYREVISION"
      safe_run svn update $args
      MYSUBDIR= # repo root is subdir
      ;;
    hg)
      if ! out=`hg pull`; then
        if [[ "$out" == *'no changes found'* ]]; then
          # Contrary to the docs, hg pull returns exit code 1 when
          # there are no changes to pull, but we don't want to treat
          # this as an error.
          :
        else
          error "hg pull failed; aborting!"
        fi
      fi
      ;;
    bzr)
      args=
      [ -n "$MYREVISION" ] && args="-r$MYREVISION"
      safe_run bzr update $args
      ;;
    *)
      error "unknown SCM '$MYSCM'"
  esac
}

switch_to_revision () {
  case "$MYSCM" in
    git)
      # $MYREVISION may refer to any of the following:
      #
      # - explicit SHA1: a1b2c3d4....
      #   - the SHA1 must be reachable from a default clone/fetch (generally, must be
      #     reachable from some branch or tag on the remote).
      # - short branch name: "master", "devel" etc.
      # - explicit ref: refs/heads/master, refs/tags/v1.2.3, refs/changes/49/11249/1
      #
      if ! git rev-parse --verify --quiet tar_scm_tmp >/dev/null; then
        safe_run git checkout -b tar_scm_tmp
      fi
      if git rev-parse --verify --quiet "origin/$MYREVISION" >/dev/null; then
          safe_run git reset --hard "origin/$MYREVISION"
      else
          safe_run git reset --hard "$MYREVISION"
      fi
      ;;
    svn|bzr)
      : # should have already happened via checkout or update
      ;;
    hg)
      safe_run hg update "$MYREVISION"
      ;;
    # bzr)
    #   safe_run bzr update
    #   if [ -n "$MYREVISION" ]; then
    #     safe_run bzr revert -r "$MYREVISION"
    #   fi
    #   ;;
    *)
      error "unknown SCM '$MYSCM'"
  esac
}

detect_version () {
  if [ -z "$MYFORMAT" ]; then
    case "$MYSCM" in
      git)
        MYFORMAT="%ct"
        ;;
      hg)
        MYFORMAT="{rev}"
        ;;
      svn|bzr)
        MYFORMAT="%r"
        ;;
      *)
        error "unknown SCM '$MYSCM'"
        ;;
    esac
  fi

  safe_run cd "$REPOPATH"
  [ -n "$MYPREFIX" ] && MYPREFIX="$MYPREFIX."
  get_version
  TAR_VERSION="$MYPREFIX$version"
}

get_version () {
  case "$MYSCM" in
    git)
      #version=`safe_run git show --pretty=format:"$MYFORMAT" | head -n 1`
      if [[ "$MYFORMAT" =~ .*@PARENT_TAG@.*  ]] ; then
          MYFORMAT=${MYFORMAT/@PARENT_TAG@/$(git describe --tags --abbrev=0)}
          if [ $? -gt 0 ] ; then
              echo -e "\e[0;31mThe git repository has no tags, thus @PARENT_TAG@ can not be expanded\e[0m"
              exit 1
          fi
      fi
      version=`safe_run git log -n1 --date=short --pretty=format:"$MYFORMAT"|sed 's@-@@g'`
      ;;
    svn)
      #rev=`LC_ALL=C safe_run svn info | awk '/^Revision:/ { print $2 }'`
      rev=`LC_ALL=C safe_run svn info | sed -n 's,^Last Changed Rev: \(.*\),\1,p'`
      version="${MYFORMAT//%r/$rev}"
      ;;
    hg)
      rev=`safe_run hg id -n`
      # Mercurial internally stores commit dates in its changelog
      # context objects as (epoch_secs, tz_delta_to_utc) tuples (see
      # mercurial/util.py).  For example, if the commit was created
      # whilst the timezone was BST (+0100) then tz_delta_to_utc is
      # -3600.  In this case,
      #
      #     hg log -l1 -r$rev --template '{date}\n'
      #
      # will result in something like '1375437706.0-3600' where the
      # first number is timezone-agnostic.  However, hyphens are not
      # permitted in rpm version numbers, so tar_scm removes them via
      # sed.  This is required for this template format for any time
      # zone "numerically east" of UTC.
      #
      # N.B. since the extraction of the timestamp as a version number
      # is generally done in order to provide chronological sorting,
      # ideally we would ditch the second number.  However the
      # template format string is left up to the author of the
      # _service file, so we can't do it here because we don't know
      # what it will expand to.  Mercurial provides template filters
      # for dates (e.g. 'hgdate') which _service authors could
      # potentially use, but unfortunately none of them can easily
      # extract only the first value from the tuple, except for maybe
      # 'sub(...)' which is only available since 2.4 (first introduced
      # in openSUSE 12.3).
      version=`safe_run hg log -l1 -r$rev --template "$MYFORMAT"|sed 's@-@@g'`
      ;;
    bzr)
      #safe_run bzr log -l1 ...
      rev=`safe_run bzr revno`
      version="${MYFORMAT//%r/$rev}"
      ;;
    *)
      error "unknown SCM '$MYSCM'"
  esac
}

prep_tree_for_tar () {
  if [ ! -e "$REPOPATH/$MYSUBDIR" ]; then
    error "directory does not exist: $REPOPATH/$MYSUBDIR"
  fi

  if [ -z "$TAR_VERSION" ]; then
    TAR_BASENAME="$FILE"
  else
    NAME="$FILE"
    VERSION="$TAR_VERSION"
    TAR_BASEDIR="${FILE}-${TAR_VERSION}"
    TAR_BASENAME=`eval echo ${MYTARFILENAME}`
  fi

  MYINCLUDES=""

  for INC in $INCLUDES; do
    MYINCLUDES="$MYINCLUDES $TAR_BASEDIR/$INC"
  done
  if [ -z "$MYINCLUDES" ]; then
    MYINCLUDES="$TAR_BASEDIR"
  fi

  safe_run cd "$MYOUTDIR"

  if [ -n "$CACHEDIRECTORY" ]; then
    debug cp -a "$REPOPATH/$MYSUBDIR" "$TAR_BASEDIR"
    safe_run cp -a "$REPOPATH/$MYSUBDIR" "$TAR_BASEDIR"
  else
    debug "mv #2: $REPOPATH/$MYSUBDIR -> $TAR_BASEDIR"
    safe_run mv "$REPOPATH/$MYSUBDIR" "$TAR_BASEDIR"
  fi
}

create_tar () {
  TARFILE="${TAR_BASENAME}.tar"
  TARPATH="$MYOUTDIR/$TARFILE"
  debug tar --owner=root --group=root -cf "$TARPATH" $EXCLUDES $MYINCLUDES
  safe_run tar --owner=root --group=root -cf "$TARPATH" $EXCLUDES $MYINCLUDES
  echo "Created $TARFILE"
}

cleanup () {
  debug rm -rf "$TAR_BASEDIR" "$FILE"
  rm -rf "$TAR_BASEDIR" "$FILE"
}

main () {
  # Ensure we get predictable results when parsing the output of commands
  # like 'git branch'
  LANG=C

  set_default_params
  if [ -z "$DEBUG_TAR_SCM" ]; then
    get_config_options
  else
    # We're in test-mode, so don't let any local site-wide
    # or per-user config impact the test suite.
    :
  fi
  parse_params "$@"
  sanitise_params

  SRCDIR=$(pwd)
  cd "$MYOUTDIR"
  detect_default_filename_param

  fetch_upstream

  prep_tree_for_tar
  create_tar

  cleanup
}

main "$@"

exit 0
