#!/usr/bin/env bash

# git-fresh
# https://github.com/imsky/git-fresh
# By Ivan Malopinsky - http://imsky.co
# MIT License

usage () {
cat << EOD
Usage: git fresh [-fmrtRW] [-sl] [remote] [root]
By default, git-fresh will:
 - rebase against remote current branch
 - stash changes
 - prune remote branches

git-fresh will ignore any branches listed in a .freshignore file.
.freshignore should contain branch names you would like to ignore
on separate lines. The file can exist in the current Git repo
or in the home directory, i.e. ~/.freshignore.

-f: Delete stale local and remote branches
-m: Merge remote root into current branch
-r: Rebase current branch against remote root
-t: Remove local tags that do not exist on remote
-R: Reset local root to remote root
-W: Wipe workspace clean

-s: Apply stashed changes after run
-l: Only delete local stale branches

-v: Print git-fresh version and exit

remote: remote name, origin by default
root: root branch, master by default
EOD

exit 0
}

say () {
  echo "[git-fresh] $@" 1>&2
}

die () {
  say $@
  exit 1
}

error () {
  die "Error on line $1: $(head -n $1 $0 | tail -1)"
}

trap 'error $LINENO' ERR

while getopts ":fmrtslRWTv" opt; do
  case $opt in
    f)
      FORCE_DELETE_STALE=true
      ;;
    m)
      MERGE=true
      ;;
    r)
      REBASE=true
      ;;
    t)
      TAGS=true
      ;;
    s)
      APPLY_STASH=true
      ;;
    l)
      DELETE_ONLY_LOCAL=true
      ;;
    R)
      RESET_ROOT=true
      ;;
    W)
      WIPE_WORKSPACE=true
      ;;
    T)
      TEST=true
      ;;
    v)
      VERSION=1.10.1
      ;;
    *)
      usage
      break
      ;;
  esac
done

shift $((OPTIND-1))

# Are we in version mode?

if [[ ! -z $VERSION ]]; then
  echo git-fresh $VERSION
  exit 0
fi

# Are we in testing mode?

if [[ $TEST = true ]]; then
  PATH=$(pwd):$PATH
  TEST_DIR=/tmp/git-fresh-test
  fail_test () {
    echo 'Tests failed!'
    rm -rf $TEST_DIR
    exit 1
  }

  rm -rf $TEST_DIR; mkdir -p $TEST_DIR; cd $TEST_DIR
  git init; touch test; git add test; git commit -am 'test'
  git checkout -b test; rm test; git commit -am 'delete test'
  git checkout master; git merge test; git checkout test
  git-fresh -fr; git rev-parse --verify test && fail_test || true
  git checkout -b test; git checkout master; git-fresh; git checkout -
  git rev-parse --abbrev-ref HEAD | grep -q test || fail_test
  rm -rf $TEST_DIR
  echo 'Tests passed!'
  exit 0
fi

# Are we inside a git repository?

INSIDE_GIT_REPO=$(git rev-parse --is-inside-work-tree 2> /dev/null)

if [[ -z "$INSIDE_GIT_REPO" ]]; then
  die "Not a git repository"
fi

# Are we in a non-empty git repository?

TOP_LEVEL_DIRECTORY=$(git rev-parse --show-toplevel)
REMOTE=${1:-origin}
ROOT=${2:-master}

if [[ $(ls -1 "$TOP_LEVEL_DIRECTORY/.git/refs/heads" | wc -l | xargs) -eq "0" ]]; then
  if git rev-parse --verify "$ROOT"; then
    git rev-parse "$ROOT" > "$TOP_LEVEL_DIRECTORY/.git/refs/heads/$ROOT"
  else
    (git fsck --lost-found &> /dev/null; git checkout "$ROOT") || die "No HEAD ref available"
  fi
fi

CURRENT=$(git rev-parse --abbrev-ref HEAD)

if [[ $(git remote -v | wc -l) -gt "0" ]]; then
  REMOTES=true
fi

# Is this branch in .freshignore?

FRESH_IGNORE="$TOP_LEVEL_DIRECTORY/.freshignore"

if [[ ! -f $FRESH_IGNORE ]]; then
  FRESH_IGNORE="~/.freshignore"
fi

if [[ -f $FRESH_IGNORE ]]; then
    if [[ ! -z $(grep -Fx "$CURRENT" "$FRESH_IGNORE") ]]; then
        die "Branch $CURRENT is ignored"
    fi
fi


STASH_STAMP=git-fresh-$(date +%s)

# Stash changed files

if ! git diff-files --quiet; then
  git stash save $STASH_STAMP
fi

if [[ $REMOTES = true ]]; then
  # Update remotes and prune stale remotes
  git remote prune $REMOTE
  git remote update $REMOTE
  git remote prune $REMOTE

  # If the current branch exists on the remote, rebase against it
  REMOTE_CURRENT=$(git ls-remote $REMOTE --heads 2> /dev/null | grep "heads/$CURRENT$" | cat)

  if [[ ! -z "$REMOTE_CURRENT" ]]; then
    git rebase $REMOTE $CURRENT
  fi
fi

# If we are not already on root branch, switch to root branch (master)

if [[ "$ROOT" != "$CURRENT" ]]; then
  git checkout $ROOT > /dev/null 2>&1
fi

# Wipe workspace?

if [[ $WIPE_WORKSPACE = true ]]; then
  git clean -dfx
fi

# Reset root?

if [[ $REMOTES = true ]]; then
  if [[ $RESET_ROOT = true ]]; then
    git reset --hard $REMOTE/$ROOT
  fi
fi

if [[ $REMOTES = true ]]; then
  git rebase -q $REMOTE/$ROOT
fi

# Compute stale branches

SMART_STALE=$(git branch -a --merged | tr -d "\* " | grep -Ev ">|$ROOT" | cat)

LOCAL_STALE=$(grep -Ev "^remotes/" <<< "$SMART_STALE" | cat)
REMOTE_STALE=$(grep -E "^remotes/" <<< "$SMART_STALE" | cat)
#todo: add flag to prune all remote branches
REMOTE_STALE=$(grep "^remotes/$REMOTE" <<< "$REMOTE_STALE" | cat)
REMOTE_STALE=${REMOTE_STALE//remotes\/$REMOTE\/}

if [[ ! -z "${SMART_STALE// }" ]]; then
  if [[ ! -z "${LOCAL_STALE// }" ]]; then
    STALE_BRANCHES=true
    if [[ -f "$FRESH_IGNORE" ]]; then
      LOCAL_STALE=$(echo -n $LOCAL_STALE | tr " " "\n" | grep -Fxvf "$FRESH_IGNORE" | tr "\n" " ")
      if [[ -z $LOCAL_STALE ]]; then
        STALE_BRANCHES=false
      fi
    fi
    if [[ "$FORCE_DELETE_STALE" = true ]]; then
      echo -n $LOCAL_STALE | tr " " "\0" | xargs -0 git branch -d 2> /dev/null
    else
      if [[ $STALE_BRANCHES = true ]]; then
        say "Local stale branches found:" $(echo -n $LOCAL_STALE | tr "\n" " ")
      fi
    fi
  fi

  if [[ ! -z "${REMOTE_STALE// }" ]]; then
    STALE_BRANCHES=true
    if [[ -f "$FRESH_IGNORE" ]]; then
      REMOTE_STALE=$(echo -n $REMOTE_STALE | tr " " "\n" | grep -Fxvf "$FRESH_IGNORE" | tr "\n" " ")
      if [[ -z $REMOTE_STALE ]]; then
        STALE_BRANCHES=false
      fi
    fi
    if [[ "$FORCE_DELETE_STALE" = true ]]; then
      if [[ "$DELETE_ONLY_LOCAL" != true ]]; then
        echo -n $REMOTE_STALE | tr " " "\0" | xargs -0 git push $REMOTE --delete
      fi
    else
      if [[ $STALE_BRANCHES = true ]]; then
        say "Remote stale branches found:" $(echo -n $REMOTE_STALE | tr "\n" " ")
      fi
    fi
  fi

  if [[ "$FORCE_DELETE_STALE" != true && "$STALE_BRANCHES" = true ]]; then
    say "Delete stale branches with: git fresh -f"
  fi
fi

# Remove tracking information for missing upstreams

if [[ ! -z $(git branch -vv | grep -F "[$REMOTE/$CURRENT: gone]") ]]; then
  git branch --unset-upstream $CURRENT
fi

# Rebase or merge remote root against local branch

if [[ ! -z $(git rev-parse --verify --quiet "$CURRENT") ]]; then
  if [[ "$ROOT" != "$CURRENT" ]]; then
    git checkout $CURRENT
  fi

  if [ "$REBASE" = true ] && [ "$MERGE" = true ]; then
    say "Rebase and merge enabled, skipping both"
  else
    if [[ "$REMOTES" = true ]]; then
      if [[ "$REBASE" = true ]]; then
        git rebase $ROOT
      fi

      if [[ "$MERGE" = true ]]; then
        git merge --no-edit $ROOT
      fi
    fi
  fi
else
  echo "$CURRENT branch was stale, staying on $ROOT"
fi

# Remove local tags that are missing on the remote

if [[ "$TAGS" = true ]]; then
  REMOTE_TAGS=$(git ls-remote --tags $REMOTE | cut -f 2)
  LOCAL_TAGS=$(git show-ref --tags | cut -d' ' -f 2)

  for tag in $LOCAL_TAGS; do
    if [[ -z $(grep $tag <<< "$REMOTE_TAGS" | cat) ]]; then
      MISSING_TAG="${tag//refs\/tags\/}"
      git tag -d $MISSING_TAG
    fi
  done
fi

# Restore stashed changes

if [[ ! -z $(git stash list | grep $STASH_STAMP | cat) ]]; then
  if [[ "$APPLY_STASH" = true ]]; then
    git stash pop
  else
    say "Stashed changes present, apply with: git stash pop"
  fi
fi

git gc --auto --prune=now
