#!/bin/bash
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#  http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
#

# displays a summary list of commits in your current repo vs upstream branch and upstream master,
# and of working tree changes Added, Modified, Deleted, or not in index ("??")

THIS_COMMAND="$0 $@"

while true; do
  case $1 in
    -o|--offline)
      OFFLINE=true
      ;;
    -r|--recurse|--recursive)
      RECURSE=true
      ;;
    -m|--master)
      shift
      UPSTREAM_MASTER=$1
      MASTER_UPSTREAM_REPO=`echo $1 | cut -d / -f 1`
      ;;
    -h|--help)
      cat <<EOF
Usage: git-summary [--help] [-o|--offline] [-r|--recursive] [-m|--master REPO/BRANCH] [<path>]"

This will display a one-line git log for differences between the local branch, the upstream 
pull source of the local branch, and the push target of the local branch.  With the "central"
(one-repo) workflow the latter two are the same (and you won't see < or > below).
In a triangle workflow, these might be `master`, `upstream/master`, and `origin/master`
(or `your_github_id/master` instead of `origin`). See `man git-config` pages on `push.default`
and `remote.pushDefault` for help configuring triangle workflow. Or use these names and
the script will do the right thing!

The following prefixes are shown on commits which are in some but not all locations:

  * local change, not in either the pull or push upstream (do a git push, then open a pull request)
  > changes not yet upstream, in your push upstream but not your pull upstream (open a pull request)
  ^ someone else's change, in your pull upstream but not local (do a git pull to update)
  < someone else's change, local but not in the push target (do a git push to update)

The optional <path> can be used to specify one or more files or paths to look at.

The following options are supported:

  -o  to operate offline, skipping the fetch done by default (aka --offline)
  -r  to recurse (aka --recurs{e,ive})
  -m  to specify a master "repo/branch" to compare upstream with, or "" for none (aka --master);
      defaults to "upstream/master" or "origin/master" (the first existing)
 

EOF
      exit 1
      ;;
    *)
      break
      ;;
    esac
    shift || break
done

# find pull and push sources using canonical git commands
[ -n "${PULL_BRANCH}" ] || \
  PULL_BRANCH=$(git for-each-ref --format='%(upstream:short)' $(git symbolic-ref -q HEAD)) && \
    git show $PULL_BRANCH > /dev/null 2> /dev/null || \
  unset PULL_BRANCH && true

[ -n "${PUSH_BRANCH}" ] || \
  PUSH_BRANCH=$(git for-each-ref --format='%(push:short)' $(git symbolic-ref -q HEAD)) && \
    git show $PUSH_BRANCH > /dev/null 2> /dev/null || \
  unset PUSH_BRANCH && true

# attempt some inferencing if the above didn't work
# or if pulling and pushing from same repo, see if others are available
# (remove this if you normally use central workflow)

if [ "${PULL_BRANCH}" == "${PUSH_BRANCH}" ] ; then
  [ "${PULL_BRANCH#upstream/}" == "${PULL_BRANCH}" ] || unset PUSH_BRANCH
  [ "${PULL_BRANCH#origin/}" == "${PULL_BRANCH}" ] || unset PULL_BRANCH
fi

[ -n "${PULL_REPO}" ] || \
  { [ -n "${PULL_BRANCH}" ] && PULL_REPO=$(echo $PULL_BRANCH | cut -f 1 -d '/') ; } || \
  PULL_REPO=upstream && git config remote.${PULL_REPO}.url > /dev/null || \
  PULL_REPO=origin && git config remote.${PULL_REPO}.url > /dev/null || \
  unset PULL_REPO

# set default PULL BRANCH if needed
[ -z "${PULL_REPO}" ] || [ -n "${PULL_BRANCH}" ] || \
  PULL_BRANCH=${PULL_REPO}/master

[ -z "${PULL_REPO}" ] || [ -n "${OFFLINE}" ] || \
  git fetch ${PULL_REPO}

[ -n "${PUSH_REPO}" ] || \
  { [ -n "${PUSH_BRANCH}" ] && PUSH_REPO=$(echo $PUSH_BRANCH | cut -f 1 -d '/') ; } || \
  PUSH_REPO=origin && git config remote.${PUSH_REPO}.url > /dev/null || \
  unset PUSH_REPO

# set default PUSH BRANCH if needed
[ -z "${PUSH_REPO}" ] || [ -n "${PUSH_BRANCH}" ] || \
  PUSH_BRANCH=${PUSH_REPO}/master


THIS_BRANCH_DISPLAY_NAME=$(git symbolic-ref --short -q HEAD)

if [ -z "${THIS_BRANCH_DISPLAY_NAME}" ] ; then
  THIS_BRANCH_DISPLAY_NAME="(DETACHED)"
  unset PUSH_BRANCH
else
  PUSH_REPO=$(echo $PUSH_BRANCH | cut -f 1 -d '/')
  # fetch the branch's upstream repo if different to the master
  [ -n "${OFFLINE}" ] || [ -z "${PUSH_REPO}" ] || [ "${PUSH_REPO}" == "${PULL_REPO}" ] || git fetch ${PUSH_REPO}
fi
[ -n "${PUSH_BRANCH}" ] && PUSH_BRANCH_DISPLAY_NAME="${PUSH_BRANCH}" || PUSH_BRANCH_DISPLAY_NAME="(no upstream)"
[ -n "$PUSH_BRANCH}" ] || PUSH_BRANCH=${PULL_BRANCH}
[ -n "$PULL_BRANCH}" ] || PULL_BRANCH=${PUSH_BRANCH}

TMP=/tmp/git-summary-$(uuidgen)
rm -f ${TMP}-*
touch ${TMP}-{1-pull-source-ahead,2-up,3-push-target-ahead,4-local}

# basically there are 6 modes where a commit is not in all three repos
# * pull ahead - show ^ - IMPORTANT means new commits we must get
# * pull behind - show > - IMPORTANT means PR not yet merged
# * push ahead - show ?> - WEIRD means someone else pushed to our origin
# * push behind - show < - means push needed to bring origin back into sync
# * local ahead - show * - IMPORTANT means new commits we must push
# * local behind - show ?^ - WEIRD means someone else pushed to our origin

if [ -z "${PULL_BRANCH}" ] ; then
  true # nothing to do
else
  if [ "${PULL_BRANCH}" == "${PUSH_BRANCH}" ] ; then
    git log --pretty=" ^ %h %aN, %ar: %s" ..${PULL_BRANCH} "$@" >> ${TMP}-2-up
    git log --pretty=" * %h %aN, %ar: %s" ${PULL_BRANCH}.. "$@" >> ${TMP}-4-local
  else
    # items in pull source not yet in our push target, will be correlated with items not in local
    git log --pretty=" < %h %aN, %ar: %s" ${PUSH_BRANCH}..${PULL_BRANCH} "$@" >> ${TMP}-1-pull-source-ahead
    git log --pretty="?^ %h %aN, %ar: %s" ..${PULL_BRANCH} "$@" >> ${TMP}-1-pull-source-ahead-of-local

    # now filter items from 1-pull-source-ahead-of-local correcting items in 1-pull-source-ahead
    cat ${TMP}-1-pull-source-ahead-of-local | while read line ; do
      word=$(echo "$line" | awk '{print $2}')
      if grep $word ${TMP}-1-pull-source-ahead > /dev/null 2> /dev/null ; then
        # if missing from both, show ^
        sed -i .bak "s/ < $word/ ^ $word/" ${TMP}-1-pull-source-ahead
      else
        # in pull and push but not local, show ?
        echo " $line" >> ${TMP}-2-up
      fi
    done

    git log --pretty=" > %h %aN, %ar: %s" ${PULL_BRANCH}..${PUSH_BRANCH} "$@" >> ${TMP}-3-push-target-ahead
    git log --pretty="%h %aN, %ar: %s" ..${PUSH_BRANCH} "$@" >> ${TMP}-3-push-target-ahead-of-local
    cat ${TMP}-3-push-target-ahead | while read line ; do
      word=$(echo "$line" | awk '{print $3}')
      if grep $word ${TMP}-3-push-target-ahead-of-local > /dev/null 2> /dev/null ; then
        # if missing from both, show ?>
        sed -i .bak "s/ > $word/?> $word/" ${TMP}-3-push-target-ahead
      fi
    done


    git log --pretty="%h %aN, %ar: %s" ${PUSH_BRANCH}.. "$@" >> ${TMP}-4-local-ahead-of-push
    git log --pretty="%h %aN, %ar: %s" ${PULL_BRANCH}.. "$@" >> ${TMP}-4-local-ahead-of-pull
    cat ${TMP}-4-local-ahead-of-push | while read line ; do
      word=$(echo "$line" | awk '{print $1}')
      if grep $word ${TMP}-4-local-ahead-of-pull > /dev/null 2> /dev/null ; then
        # in local but not push or pull
        echo " * $line" >> ${TMP}-4-local
      # else ignore, we reported it above (in local and pull target, but not in push)
      fi
    done

    rm -f ${TMP}-{1,2,3,4}-*{-of-*,.bak}
  fi
fi

git status --porcelain --ignore-submodules "$@" > ${TMP}-5-commits
cat ${TMP}-* > ${TMP}

# TODO unreliable way to get project name
SUMMARY=$(basename $(pwd))

# append files if needed
[ -z "$1" ] || SUMMARY="${SUMMARY} ($@)"

SUMMARY="${SUMMARY}: ${THIS_BRANCH_DISPLAY_NAME} -> ${PUSH_BRANCH_DISPLAY_NAME}"
[ -z ${PULL_BRANCH} ] || [ "${PULL_BRANCH}" == "${PUSH_BRANCH}" ] ||
  SUMMARY=${SUMMARY}" -> ${PULL_BRANCH}"

if [ -s ${TMP} ] ; then
  AHEAD=$(wc ${TMP}-1-* | awk '{print $1}')
  BEHIND=$(wc ${TMP}-3-* | awk '{print $1}')
  UP=$(wc ${TMP}-2-* | awk '{print $1}')
  LOCAL=$(wc ${TMP}-4-* | awk '{print $1}')
  [ "${AHEAD}" == "0" ] || COUNTS="push target ${AHEAD} behind upstream"
  [ "${BEHIND}" == "0" ] || COUNTS="${COUNTS}${COUNTS:+, }upstream ${BEHIND} behind push target"
  [ "${UP}" == "0" ] || COUNTS="${COUNTS}${COUNTS:+, }local ${UP} behind"
  [ "${LOCAL}" == "0" ] || COUNTS="${COUNTS}${COUNTS:+, }local ${LOCAL} unpushed"
  echo "${SUMMARY} (${COUNTS:-uncommitted changes only})"
  cat ${TMP} | sed 's/^/ /'
else
  echo "${SUMMARY} (all up to date)"
fi
rm -f ${TMP} ${TMP}-*

# submodules ignored; if using them, set up

if [ "$RECURSE" ] ; then
  echo
  git submodule --quiet foreach --recursive "${THIS_COMMAND}"
fi

