#!/bin/bash

# ----------------------------------------------------------------------
# config
# ----------------------------------------------------------------------
#
# See help section below or --help for config details
#
# MIRROR_USER=<daemon user name>   optional, e.g. git-mirror
# MIRROR_GROUP=<daemon group name> optional, e.g. git-mirror
# PID_FILE=<file>                  default: /tmp/git-mirror.pid
# MIRROR_OWNER=<user name>         default: $MIRROR_USER

# ----------------------------------------------------------------------
# auxiliary functions / globals
# ----------------------------------------------------------------------

function eecho() {
  echo -e "$@" >&2
}

function exit_script() {
  cd "$PWD" >/dev/null 2>&1
  if test "$1" = ""; then
    exit 0
  else
    exit $1
  fi
}

function die() {
  eecho $@
  exit_script 1
}

function git_bin() {
  local GIT_BIN=$(/usr/bin/which git)
  if test "$GIT_BIN" = ""; then
    if test -x /usr/bin/git; then
      GIT_BIN=/usr/bin/git
    else
      eecho "git is not installed"
      exit_script 1
    fi
  fi
  echo $GIT_BIN
  return 0
}

# ----------------------------------------------------------------------
# daemon / service functions
# ----------------------------------------------------------------------

function daemon_pid() {
  echo $(/bin/ps -C git-daemon --User "$MIRROR_USER" --no-headers --format "%p")
}

function daemon_pid_from_file() {
  echo $(/bin/cat "$PID_FILE" 2>/dev/null)
}

function daemon_status() {
  if test "$(daemon_pid)" = ""; then
    echo "stopped"
    return 1
  else
    echo "running"
    return 0
  fi
}

function daemon_start() {
  if test "$(daemon_status)" = "running"; then
    echo "already running"
    return 0
  else
    local GITD_USER=""
    local GITD_GROUP=""
    if test "$(/usr/bin/whoami)" != "root"; then
      echo "notice: Not started as root, means git-daemon will not run "
      echo "        with the configured user/group, but with the current"
      echo "        user/group (user=$(/usr/bin/whoami))"
    else
      if test "$MIRROR_USER" = ""; then
        echo "error: No daemon user specified, and refusing to run the daemon as root"
        return 1
      fi
      GITD_USER="--user=$MIRROR_USER"
      if test "$GITD_GROUP" != ""; then
        GITD_GROUP="--group=$MIRROR_GROUP"
      fi
    fi

    $(git_bin) daemon \
      --informative-errors \
      --reuseaddr \
      --detach \
      --export-all \
      --init-timeout=3 \
      --port=9418 \
        $GITD_USER \
        $GITD_GROUP \
      --disable=upload-archive \
      --disable=receive-pack \
      --enable=upload-pack \
      --forbid-override=upload-archive \
      --forbid-override=receive-pack \
      --forbid-override=upload-pack \
      --pid-file="$PID_FILE" \
      --base-path="$REPOSITORIES/" \
      "$REPOSITORIES/" \
    2>&1

    if test $? -eq 0; then
      if test "$(daemon_pid)" = "$(daemon_pid_from_file)"; then
        if test "$1" != "-q"; then
          echo "started"
        fi
        return 0
      else
        echo "warning: Determined PID differs from the created PID file "
        echo "         content (started, but something is suspiceous)"
        return 0
      fi
    else
      echo "error: Failed to start git daemon, output is: \"$GIT_OUT\""
      return 1
    fi
  fi
}

function daemon_stop() {
  local DPID=$(daemon_pid)
  if test "$(daemon_status)" != "running" -o "$DPID" = ""; then
    echo "not running"
  elif test "$DPID" != "$(daemon_pid_from_file)"; then
    echo "error: Determined PID does not match the PID file contents of git-daemon"
  elif test "$(kill $DPID >/dev/null 2>&1; echo $?)" != "0"; then
    if test "$(/usr/bin/whoami)" != "root"; then
      echo "error: You are lacking the permissions to stop the daemon"
    else
      echo "error: Could not kill the git-daemon process (PID=$DPID)"
    fi
  else
    # stfwi: dblchk gnu corutils float sleep compatability
    sleep 0.25
    test "$(daemon_status)" != "running" || sleep 4
    if test "$(daemon_status)" = "running"; then
      echo "error: Git daemon still running after killing the process (PID=\"$DPID\")"
    else
      if test "$1" != "-q"; then
        echo "stopped"
      fi
      return 0
    fi
  fi
  return 1
}

function daemon_restart() {
  if test "$(daemon_pid)" = ""; then
    echo "not running"
    return 1
  else
    daemon_stop -q
    if test $? -ne 0; then
      echo "error: Failed to restart"
    else
      daemon_start -q
      if test $? -eq 0; then
        if test "$1" != "-q"; then
          echo "restarted"
        fi
        return 0
      else
        daemon_stop -q >/dev/null 2>&1
        echo "error: Failed to restart"
        return 1
      fi
    fi
  fi
}

function daemon_user_check() {
  if test "$(/usr/bin/whoami)" != "root"; then
    die "You are not root (git will not be able to switch the \
          user to \"$MIRROR_USER\")"
  fi
}

function repository_user_check() {
  if test "$(/usr/bin/whoami)" = "root"; then
    eecho "(dropping root permissions, user=\"$MIRROR_OWNER\")"
    if test -x /usr/bin/sudo; then
      /usr/bin/sudo -n -u $MIRROR_OWNER -- /bin/true
      if test $? -ne 0; then
        die "error: Could not switch user to \"$MIRROR_OWNER\""
      else
        /usr/bin/sudo -n -u $MIRROR_OWNER -- $0 $@
        exit_script $?
      fi
    else
      die "Need the 'sudo' command to drop permissions (switch to user \"$MIRROR_OWNER\")"
    fi
  elif test "$(/usr/bin/whoami)" != "$MIRROR_OWNER"; then
    eecho "error: The user operating on repositories does not match the configured repository owner, aborting."
    eecho "       \"$(/usr/bin/whoami)\" != \"$MIRROR_OWNER\""
  else
    return 0
  fi
  exit_script 1
}

# ----------------------------------------------------------------------
# repository functions
# ----------------------------------------------------------------------

function repository_clone() {
  if test "$(/usr/bin/whoami)" = "root"; then
    die "error: !!! Still root after user check"
  elif test "$1" = "" -o "$2" = "" -o "$3" != ""; then
    die "Need a remote to clone (arg1) and the local repository name (arg2)"
  elif test ! -d "$REPOSITORIES"; then
    die "Missing repository base directory (\"$REPOSITORIES\")"
  elif test "$(basename $2)" != "$2"; then
    die "Your local mirror repository name contains slashes, not ok"
  elif test -d "$REPOSITORIES/$2"; then
    die "The local mirror repository directory exists already (\"$REPOSITORIES/$2\")"
  else
    case "$2" in
      *.git)
      ;;
    *)
      die "You local mirror repository name must end with \".git\""
      ;;
    esac
    echo -en "cloning \"$(basename $1)\" ... "
    local CPWD=$(pwd)
    cd "$REPOSITORIES" || die "Failed to switch to repository base directory"
    $(git_bin) clone --mirror "$1" "$2"
    local RES=$?
    cd "$CPWD"
    if test $RES -eq 0; then
      return 0
    fi
  fi
  return 1
}

function repository_update() {
  if test "$(/usr/bin/whoami)" = "root"; then
    die "error: !!! Still root after user check"
  elif test ! -d "$1"; then
    echo "update error: \"$1\" is not a directory"
  else
    echo -en "fetching \"$(basename $1)\" ... "
    local CPWD=$(pwd)
    cd "$1"
    $(git_bin) fetch --all >/dev/null
    local RES=$?
    cd "$CPWD"
    if test $RES -eq 0; then
      echo "ok"
      return 0
    else
      echo "failed"
    fi
  fi
  return 1
}

function repositories_update() {
  for I in $REPOSITORIES/*.git; do
    if test -d "$I"; then
      repository_update "$I"
    fi
  done
  echo "update done"
  return 0
}

# ----------------------------------------------------------------------
# help
# ----------------------------------------------------------------------

function script_help() {

cat <<'EOT'

NAME

    git-mirror

SYNOPSIS

    git-mirror [ start | stop | restart | reload | status ]

    git-mirror [ clone | fetch ]

    git-mirror [ help | --help | -h ]

SERVICE OPTIONS / COMMANDS

    help, --help, -h  Show this help

    start             Start git-daemon in background

                        If executed as root, the script will force git
                        to drop the permissions by switching to the
                        user/group specified in the script configuration.

                        If executed as non-root the deamon will run as
                        the current user/group.

    stop              Stop git-daemon

                        If the current user lacks if the required
                        permissions, an error will be raised.

    restart, reload   Restart git-daemon

                        Identical to start && stop

    status            Retrieve the status of the background daemon

                        Prints "stopped" or "running" according to
                        the daemon process state.

REPOSITORY OPTIONS / COMMANDS

    clone <rem> <loc> Clones a remote repository using the --mirror
                      option. The owner of the repositories is set
                      in the script configuration, and should be
                      different to the daemon user for the sake of
                      saftey and security. If executed as root, the
                      script drops permissions by switching to the
                      configured owner. Otherwise, if the current user
                      is not the configured owner, an error will be
                      raised.

                      <rem> is the remote repository to mirror,
                      <loc> is the local repository name.

                      Note that the local repository must end with
                      ".git" and must not contain slashes. The script
                      throws an error if these requirements are not
                      satisfied.

    fetch,update,pull Update all bare mirrors from their remote. If
                      executed as root, the permissions are dropped
                      as with `clone`, otherwise the current user must
                      match the configured owner.

DEPENDENCIES

    git, sudo

EXAMPLE SETUP

    This setup sequence initialises git-mirror (simply). Note that
    you should modify `chown git-mirror:git-mirror` to another user,
    so that git-mirror has no write access when running the git-daemon.
    For that, you also need to change `MIRROR_OWNER=<other user>` in
    the config section of this script.

    Assuming this script is currently located somewhere in your home,
    and you are in the parent directiory (script is "./git-mirror").

    $ su
    # apt-get install git
    # adduser --quiet --disabled-login --shell /bin/false \
    #         --home /home/git-mirror --no-create-home git-mirror
    # mkdir /home/git-mirror
    # mkdir /home/git-mirror/bin
    # mkdir /home/git-mirror/repositories
    # cp ./git-mirror /home/git-mirror/bin/
    # chmod 755 /home/git-mirror/bin/git-mirror
    # chown root:root /home/git-mirror/bin/git-mirror
    # chmod 755 /home/git-mirror/repositories
    # chown git-mirror:git-mirror /home/git-mirror/repositories
    #
    # cd /home/git-mirror/bin
    # ./git-mirror clone git://somehost/somerepo.git somerepo.git
    # ./git-mirror fetch
    # ./git-mirror start
    #
    # exit
    $
    $ cd /tmp/
    $ git clone git://localhost/somerepo.git
    $

    The script is suitable to be symlinked in /etc/rc?.d (/etc/init.d,
    respectively) or to be wrapped as service with minor effort.

    `/home/git-mirror/bin/git-mirror fetch` can also be used in a
    cron job:

    $ su
    # echo -e '#!/bin/sh\n/home/git-mirror/bin/git-mirror fetch \
              >/dev/null 2>&1\n' >/etc/cron.daily/git-mirror-update
    # chmod 755 /etc/cron.daily/git-mirror-update

EOT

  return 1
}


# ----------------------------------------------------------------------
# main
# ----------------------------------------------------------------------

PWD=$(pwd)
THIS_SCRIPT=$(readlink -f "$0")
BASE_DIR=$(dirname $(dirname $THIS_SCRIPT))

if test "$REPOSITORIES" = ""; then
  REPOSITORIES="$BASE_DIR/repositories"
fi

if test "$PID_FILE" = ""; then
  PID_FILE=/tmp/git-mirror.pid
fi

if test "$MIRROR_USER" = ""; then
  if test "$(dirname $BASE_DIR)" = "/home"; then
    MIRROR_USER=$(basename $BASE_DIR)
  else
    die "No MIRROR_USER configured"
  fi
fi

if test "$MIRROR_OWNER" = ""; then
  MIRROR_OWNER=$MIRROR_USER
fi

if test ! -d "$REPOSITORIES"; then
  die "error: Repository directory does not exist or not configured."
fi

case $1 in
  start)
    daemon_start
    exit_script $?
    ;;
  stop)
    daemon_stop
    exit_script $?
    ;;
  restart|reload)
    daemon_restart
    exit_script $?
    ;;
  status)
    daemon_status
    exit_script $?
    ;;
  clone)
    repository_user_check $@
    repository_clone "$2" "$3" "$4"
    exit_script $?
    ;;
  update|pull|fetch)
    repository_user_check $@
    repositories_update "$2"
    exit_script $?
    ;;
  help|-h|--help)
    script_help
    exit_script $?
    ;;
  *)
    echo "Unknown command: \"$1\""
    echo "- Server control usage: git-mirror [ start | stop | restart | status ]"
    echo "- Repository updates  : git-mirror [ pull | fetch | update ]"
    echo "- Repository init     : git-mirror clone <remote git repository>"
    exit 1
    ;;
esac
die "Unexpected script run until EOF (bug)"
