#!/bin/sh
set -e

usage () {
    echo "usage: git cleave [-h] [-t <prefix>] <path1> [<path2> ...]" >&2
    echo >&2
    echo "Splits the last commit into two or more commits. Takes one or more regex values" >&2
    echo "that are matched against the paths of the files from the commit." >&2
    echo "" >&2
    echo "Options:" >&2
    echo "-t <prefix>  Use this tag to prefix to commit messages" >&2
    echo "-h           Show this help" >&2
}

locally_changed_files () {
  # New files
  git status --porcelain | grep -Ee '^\?' | cut -c4-
  # Changes/deleted files
  git diff --name-only --relative
}

prefix=
while getopts t:h flag; do
    case "$flag" in
        t) prefix=$OPTARG; shift ;;
        h) usage; exit 2;;
        *) usage; exit 2;;
    esac
done

main () {
  if [ "$#" -lt 1 ]; then
    usage
    exit 2
  fi

  # Ensure we have a clean working tree
  git is-clean -v

  ORIG_SHA="$(git sha HEAD)"
  echo "Original commit is: $ORIG_SHA"

  LAST_COMMIT_MSG="$(git log --pretty='%s' -1)"
  git reset HEAD~1
  BASE_SHA="$(git sha HEAD)"

  for expr in "$@"; do
    locally_changed_files | grep -Ee "$expr" | while read f; do
      git add "$f"
    done

    # If not using a fixed prefix, use the path match as the prefix
    if [ -z "$prefix" ]; then
      if git is-dirty -i; then
        # This weird construct is to keep the original commit date intact
        git commit --no-verify -C "$ORIG_SHA"
        git commit --amend --no-verify -m "[$expr] $LAST_COMMIT_MSG"
      fi
    fi
  done

  # Else, if using the prefix strategy, add a single commit combining all of
  # the path's matched
  if [ -n "$prefix" ]; then
    if git is-dirty -i; then
      # This weird construct is to keep the original commit date intact
      git commit --no-verify -C "$ORIG_SHA"
      git commit --amend --no-verify -m "[$prefix] $LAST_COMMIT_MSG"
    fi
  fi

  locally_changed_files | while read f; do
    git add "$f"
  done

  # If there are leftover changes (or cleave didn't commit anything yet, in the
  # case of an empty original commit), commit it now
  if git is-dirty -i || [ "$BASE_SHA" = "$(git sha HEAD)" ]; then
    git commit --allow-empty --no-verify -C "$ORIG_SHA"
    git commit --allow-empty --amend --no-verify -m "$LAST_COMMIT_MSG"
  fi

  # Sanity check. No files may get lost during this process!
  if ! git diff --exit-code "$ORIG_SHA" "$(git sha HEAD)"; then
    echo "Warning! There were differences found between the current state and the" >&2
    echo "original commit.  You may want to revert back to the original commit:" >&2
    echo "" >&2
    echo "    git reset --hard $ORIG_SHA" >&2
    echo "" >&2
  else
    echo "Success! Original commit was: $ORIG_SHA"
  fi
}

( cd "$(git root)" && main "$@" )
