#!/usr/bin/env bash
#
# Commit only the paths you name, when something else is also writing the tree.
#
# ## The failure this catches
#
# Two agents, or an agent and a person, sharing one checkout share one **git
# index**. Staging is global state, and the window between staging a change and
# committing it is however long it takes to write the commit message. Anything
# that runs `git add -A` inside that window takes your files with it.
#
# Two real instances, one afternoon, one repository — both while the rule
# "stage by explicit path, and re-check the index immediately before committing"
# was being followed to the letter:
#
#   - A commit about reviving a game carried two unrelated documentation edits.
#   - A commit about art carried an entire renderer fix, a new class, its test
#     and two documents. Its message had no `closes #N`, so the issue that work
#     finished stayed open and had to be closed by hand afterwards.
#
# Nothing was lost either time. The attribution was wrong, and once the tracker
# was wrong with it. **The rule is not the fix, because the danger is the
# window** — so this closes the window instead: the message is written first and
# passed in, staging and scanning and committing happen back to back, and the
# commit itself names its paths.
#
# ## Why a pathspec commit rather than unstaging theirs
#
# `git commit -- <paths>` takes the working-tree content of exactly those paths
# and ignores the rest of the index. The other writer's staged work is neither
# swept into your commit nor removed from their index, so there is no step here
# that can break *their* commit either. What they have staged is reported, so you
# know somebody else is mid-flight before you add to the race.
#
# ## Usage
#
#   bash scripts/commit-mine.sh <message-file> <path> [path…]
#   bash scripts/commit-mine.sh --push <message-file> <path> [path…]
#
# ## Exit codes
#
#   0  committed (and pushed, with `--push`)
#   1  the secret scan objected, or a push left something unpushed
#   2  nothing was committed: bad arguments, or a path that does not exist.
#      **Two is not a pass**
set -euo pipefail

HERE="$(cd "$(dirname "$0")" && pwd)"
SECRETS="$HERE/secrets.sh"

PUSH="no"
if [ "${1:-}" = "--push" ]; then PUSH="yes"; shift; fi

if [ "$#" -lt 2 ]; then
  sed -n '2,45p' "$0" >&2
  exit 2
fi

MESSAGE_FILE="$1"; shift
[ -f "$MESSAGE_FILE" ] || { echo "commit-mine: no message file: $MESSAGE_FILE" >&2; exit 2; }

git rev-parse --is-inside-work-tree >/dev/null 2>&1 || {
  echo "commit-mine: not inside a git repository" >&2
  exit 2
}

for path in "$@"; do
  [ -e "$path" ] || { echo "commit-mine: no such path: $path" >&2; exit 2; }
done

# Whatever anyone else has in flight. Not an error and not touched — but you
# should see it before committing into the same index.
OTHERS="$(git diff --cached --name-only | grep -vxF -f <(printf '%s\n' "$@") || true)"
if [ -n "$OTHERS" ]; then
  echo "commit-mine: NOTE — the index also holds work that is not yours:"
  printf '  %s\n' $OTHERS
  echo "  (left staged, and left out of this commit)"
  echo
fi

# Staged only so the scanner sees exactly these paths, and only for as long as
# the scan takes. A credential is the one mistake here that cannot be undone by
# a later commit.
git add -- "$@"
if [ -x "$SECRETS" ] || [ -f "$SECRETS" ]; then
  set +e
  bash "$SECRETS"
  SCAN=$?
  set -e
  # 2 means it scanned nothing, which the convention in this template treats as
  # a failure rather than a pass — a scanner that did not run has not cleared
  # anything.
  if [ "$SCAN" -ne 0 ]; then
    git restore --staged -- "$@"
    echo "commit-mine: secret scan exited $SCAN; nothing committed" >&2
    exit 1
  fi
else
  echo "commit-mine: WARNING — $SECRETS not found, committing unscanned" >&2
fi

git commit -F "$MESSAGE_FILE" -- "$@"

echo
echo "commit-mine: committed"
git show --stat --oneline HEAD | head -20

if [ "$PUSH" = "yes" ]; then
  BRANCH="$(git rev-parse --abbrev-ref HEAD)"
  git push origin "$BRANCH"
  # Finished work is pushed work. A local commit is invisible to everything that
  # reports on the project, so the push is verified rather than assumed.
  UNPUSHED="$(git log --oneline "origin/$BRANCH..HEAD" 2>/dev/null || true)"
  if [ -n "$UNPUSHED" ]; then
    echo "commit-mine: WARNING — still unpushed after push:" >&2
    echo "$UNPUSHED" >&2
    exit 1
  fi
  echo "commit-mine: pushed; origin/$BRANCH..HEAD is empty"
fi
