#!/bin/bash

# (C) 2014 David Greaves david.greaves@jolla.com
#
# 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.

SVCDIR=/srv/service
STORE=$SVCDIR/repo
# Note the CACHE *must* be on the same fs as the STORE (and OUTDIR)
# to avoid inter-fs copying.
CACHE=$SVCDIR/.cache
SVC=""
REPO=""
OUTDIR=""
URL=""
LOCKFILE=service_lockfile
LOCK="$SVCDIR/repo/$LOCKFILE" # It does not need to be in $STORE.
repocmd=/usr/bin/repo
SHA1ONLY=no
MANIFEST_NAME=default.xml
MANIFEST=.repo/manifests/$MANIFEST_NAME
REFXML=.repo/manifests/reference.xml
REPOCONFIG=/etc/obs/services/repo
CACHE_SHA1=""
REPOSYNC_OPTS="--no-clone-bundle --fetch-submodules"
MYPID=$$

WAIT_MINS=480 # in mins

if [ -f $REPOCONFIG ]; then
    SERVICES=$(cut -f1 -d\  $REPOCONFIG | paste -s -d\|)
else
    echo "Configuration file $REPOCONFIG for obs-service-repo missing."
    exit 1
fi


if [ -f /etc/sysconfig/proxy ]; then
    source /etc/sysconfig/proxy
    export http_proxy=$HTTP_PROXY
    export https_proxy=$HTTPS_PROXY
    export no_proxy=$NO_PROXY
fi

if [ -z "$OBS_SERVICE_REPO_SYNC_JOBS" ] ; then
    OBS_SERVICE_REPO_SYNC_JOBS=$(($(nproc)/2))
 fi

usage() {
  cat <<EOF
Usage: $0 --service <service> --initrepo <path/pkg> --branch <branch> | --tag <tag>  [--outdir <outdir>]
Options:
  --service <service>      Git hosting service to use ($SERVICES)
  --initrepo <path/pkg>    Repository path to check out
  --branch <branch>        Branch to use for repo init
  --tag <tag>              Tag to use for repo init
  --sha1-only              Don't run the service unless every <project> has a sha1 revision
  --manifest-name <name>   Use the manifest called 'name' (default: default.xml)
  --outdir <outdir>        Move files to outdir after checkout (optional)

Examples:
  $0 --service github --initrepo mer-hybris/android --branch hybris-10.1

Only one instance of this service can run at once.

EOF
}

fatal() {
    usage
    echo "$@"
    exit 1
}

repo_init() {
    # Get the manifest
    if ! $repocmd init $repourl -u $URL -b $BRANCH -m $MANIFEST_NAME --platform=linux --no-clone-bundle $REPOREF; then
	fatal "command 'repo init -u $URL -b $BRANCH -m $MANIFEST_NAME $REPOREF' returned an error"
    fi
}

# Example reference.xml file
#  <reference>
#     <device name="f5121" url="https://github.com/mer-hybris/android.git" branch="hybris-sony-aosp-6.0.1_r80-20170902"/>
#     <device name="aosp" url="https://android.googlesource.com/platform/manifest" branch="master"/>
#  </reference>
#
# Currently only "name" attributes are taken into account, but in the future,
# the url and branch could be used to initialize a reference mirror first.
#
# Reference devices should be ordered by preference.

get_reference() {
    REFPARSE=$(python -c "
import xml.etree.cElementTree
import sys
import os
import glob
try:
    ref = xml.etree.ElementTree.parse('$REFXML').getroot()
except Exception as e:
    print ('Error parsing reference.xml file: ' + str(e))
    sys.exit(1)
for device in ref:
    if device.tag == 'device' and 'name' in device.attrib:
        # First attempt to find directory with the exact device name
        if os.path.isdir('$SVCDIR/'+device.attrib['name']):
            print(device.attrib['name'])
            sys.exit(0)
        # Otherwise, try to find a chain that leads to the device name
        # eg. aosp_f5121. This scheme allows to put order in the directories,
        # eg. 01_f5121, 02_sony-nile, or combine them, eg. 01_aosp_f5121.
        dirlist = glob.glob('$SVCDIR/*_'+device.attrib['name'])
        if len(dirlist) > 0:
            dirlist.sort()
            print(os.path.basename(dirlist[0]))
            sys.exit(0)

print('No devices found in cache that match referenced devices')
sys.exit(1)
")
    RETCODE=$?
    if [ $RETCODE -eq 0 ]; then
        REFDEV=$REFPARSE
        REFDIR=$SVCDIR/$REFDEV
        REPOREF="--reference=$REFDIR"
    else
        echo $REFPARSE
    fi
    return $RETCODE

}

use_repo_local() {
    if [[ -d .repo/manifests/local_manifests ]]; then
        echo "Using local_manifests"
        ln -s manifests/local_manifests/ .repo/
    fi
}

repo_sync() {
    # Get the git repos
    $repocmd sync $REPOSYNC_OPTS -j$OBS_SERVICE_REPO_SYNC_JOBS || fatal "'repo sync' command failed"
}

store_manifest() {
    # Store the manifest for use in packaging
    $repocmd manifest -r -o repo_service_manifest.xml  || fatal "'repo manifest' command failed"
    # Annoyingly the manifest created above is mv'ed out of the
    # SOURCE/ area by existing spec files so we add a copy of it for
    # use by the cache and for promotion purposes
    cp repo_service_manifest.xml rpm/as_built_manifest.xml
}

process_spec_files() {
    # Pre-process all the .spec files in rpm/ and actually replace %include with the included file
    for spec in rpm/*.spec; do
	perl -i -ne 'BEGIN { sub pinc {if (/^%include\s+([\w-.\/]+)$/) { print "# repo service performed : %%include $1\n"; local (*I); open I,"<$1" or print "# Couldnt open $1\n"; while (<I>) { pinc(); }; } else { print $_; }}} pinc;' $spec
	# Add the as_built_manifest.xml to the Sources
	perl -i -e 'while (<>) { print $_; last if /^Source40:/;}; print "Source755: as_built_manifest.xml\n"; while (<>) { print $_;} ' $spec

    done

}

check_sha1_manifest() {
    if [[ $SHA1ONLY == yes ]]; then
	grep "<project" $MANIFEST | grep -vq "revision" || return
	echo "This service run requires $MANIFEST to be a sha1-ised snapshot manifest"
	exit 1
    fi
}

check_cache_and_exit_if_hit() {
    # We only check the cache if *every* project has a "revision" attribute
    # since any project without one may have changed
    grep "<project" $MANIFEST | grep -vq "revision" && return

    # Check the cache:
    CACHE_SHA1=$(sha1sum $MANIFEST | cut -f1 -d" ")
    if  [[ -f ${CACHE}/${CACHE_SHA1}_repo.tar.bzip2 ]] && \
	[[ -f ${CACHE}/${CACHE_SHA1}_rpm.tar.bzip2 ]] ; then
	# Found a full cache; move the big tarball and the rpm/
	# subtree tarball to cwd
	# use a hard link to avoid a wasteful copy
	ln ${CACHE}/${CACHE_SHA1}_repo.tar.bzip2 $OUTDIR/repo.tar.bzip2
	ln ${CACHE}/${CACHE_SHA1}_rpm.tar.bzip2 $OUTDIR/rpm.tar.bzip2
	# and unpack any files into $OUTDIR
	pbzip2 -dc ${CACHE}/${CACHE_SHA1}_rpmfiles.tar.bzip2 |tar x -C $OUTDIR
	# Touch the used files so the cleanup cron job knows they've been useful
	touch ${CACHE}/${CACHE_SHA1}_repo.tar.bzip2 \
		${CACHE}/${CACHE_SHA1}_rpm.tar.bzip2 \
		${CACHE}/${CACHE_SHA1}_rpmfiles.tar.bzip2
	echo "Used cached tarballs - done"
	exit 0
    else
	echo "No cached tarballs found. Continuing to build a new tarball set"
	return
    fi
}

create_tarballs() {
    find . -mindepth 1 -maxdepth 1 -not \( -name .repo -o -name rpm -o -name repo.tar.bzip2 \) -print | tar c --files-from - | pbzip2 -c > repo.tar.bzip2  || fatal "'tar' command failed creating repo.tar.bzip2"
	# Now store any subdirs and symlinks in rpm/ that need keeping
	# SOURCE: files are moved directly to $OUTDIR
	(cd rpm; find . -mindepth 1 -maxdepth 1 \( -type d -o -type l \) -print  | tar c --files-from - | pbzip2 -c > ../rpm.tar.bzip2 ) || fatal "'tar' command failed creating rpm.tar.bzip2"
}

cache_results() {

    if [ "${CACHE_SHA1}" == "" ]; then
        echo "Not caching anything since some revisions were missing in manifest.xml"
        return   # check_cache did not find a "revision" attribute in on of the projects
    else
        mkdir -p ${CACHE}

        ln repo.tar.bzip2 ${CACHE}/${CACHE_SHA1}_repo.tar.bzip2
        ln rpm.tar.bzip2 ${CACHE}/${CACHE_SHA1}_rpm.tar.bzip2
        # The files (not dirs) in rpm/* will be needed
        (cd rpm; find . -mindepth 1 -maxdepth 1 -type f -print  | tar c --files-from - | pbzip2 -c > ${CACHE}/${CACHE_SHA1}_rpmfiles.tar.bzip2 )
        
        echo "Tarballs have been cached as :"
        ls -laF ${CACHE}/${CACHE_SHA1}*
    fi
}

move_package_files() {
    if [ ! -z "$OUTDIR" ]; then
	# Move all files to OUTDIR
	mv repo.tar.bzip2 "$OUTDIR"
	(cd rpm; find . -mindepth 1 -maxdepth 1 -type f | xargs -I '{}' mv '{}' "$OUTDIR")
	mv rpm.tar.bzip2 "$OUTDIR"
    fi
}

tidy_up() {
    # Do not remove the default repo/.repo dir since there's old-style object cache
    if [ "$STORE" == "$SVCDIR/repo" ]; then
        # Only clean up if we can enter the STORE directory
        cd $STORE && find . -mindepth 1 -maxdepth 1 -not -name .repo -and -not -name "$LOCKFILE" -print0 | xargs -0 rm -rf
    else
        rm -rf $STORE
    fi
}

fail_lock() {
    echo "$MYPID repo service could not run within $WAIT_MINS mins"
    exit 1
}

while test $# -gt 0; do
  case $1 in
    *-service)
      SVC="$2"
      shift
    ;;
    *-initrepo)
      REPO="$2"
      shift
    ;;
    *-branch)
      BRANCH="$2"
      shift
    ;;
    *-tag)
      TAG="$2"
      shift
    ;;
    *-sha1-only)
      SHA1ONLY=yes
    ;;
    *-manifest-name)
      MANIFEST_NAME=$2
      MANIFEST=".repo/manifests/$2"
      shift
    ;;
    *-outdir)
      OUTDIR="$2"
      shift
    ;;
    -h|*-help)
      usage
      exit 0
    ;;
    *)
      usage
      echo Unknown parameter $1.
      exit 1
    ;;
  esac
  shift
done

if [ -z "$SVC" ]; then
    fatal "ERROR: no --service parameter ($SERVICES)"
fi
if [ -z "$REPO" ]; then
    fatal "ERROR: no --initrepo parameter"
fi
if [ -z "$BRANCH" ]; then
    fatal "ERROR: no --branch parameter"
fi

repo_regexp="^[A-Za-z0-9_-]*/[A-Za-z0-9_-]*$"
if ! [[ $REPO =~ $repo_regexp ]]; then
    fatal "ERROR: repo '$REPO'is not in area/repo format (omit .git and any http://.../ part)"
fi


WORKDIR=""

tag_regexp="^[A-Za-z0-9_.-]*$"
if ! [[ $TAG =~ $tag_regexp ]]; then
    fatal "ERROR: repo '$TAG'is not valid (must match '$tag_regexp')"
fi

while read svc_name svc_url
do
    if [ x"$svc_name" == x"$SVC" ]; then
        break
    fi
done < $REPOCONFIG
if [ -z "$svc_name" ]; then
    echo "Sorry, git service $SVC is not whitelisted. please ask your OBS administrator to add it to the repo config file $REPOCONFIG if you need it."
    exit 1
else
    URL=${svc_url}${REPO}.git
fi

PRJDIR="$PWD"

if [[ ! -d $STORE ]]; then
    echo "Note: $STORE path not found. Creating a local repo directory."
    STORE=./repo.tmp
    mkdir -p $STORE || fatal "Could not create $STORE"
fi

cd $STORE || exit 1

echo $MYPID attempting lock

( flock -w $(($WAIT_MINS * 60)) 9 || fail_lock
  trap 'tidy_up' EXIT

  tidy_up
  repo_init
  check_sha1_manifest
  check_cache_and_exit_if_hit

  if get_reference; then
    tidy_up
    STORE=$(mktemp -d -p $SVCDIR)
    cd $STORE || exit 1
    repo_init
    REPOSYNC_OPTS="$REPOSYNC_OPTS --current-branch"
  else
    REPOSYNC_OPTS="$REPOSYNC_OPTS --force-sync"
  fi

  use_repo_local
  repo_sync
  store_manifest
  process_spec_files
  create_tarballs
  cache_results
  move_package_files
  tidy_up
) 9>$LOCK

exit 0
