#!/usr/bin/env bash
# Inspect commits, working changes, and their publication baseline.
set -euo pipefail

usage() {
    cat <<'HELP'
Usage: git work COMMAND [OPTIONS] [REVISION...] [-- PATH...]

Commands:
  log       Compact history (HEAD by default)
  files     Files changed by a commit (HEAD by default)
  diff      Commit patches (HEAD by default)
  status    Branch, published baseline, and working-tree summary
  base      Print the publication baseline ref

Scopes (mutually exclusive):
  REVISION...        A commit or Git revision range; log traverses history
  --work             All current unpublished work
  --staged           Changes staged for commit (files/diff)
  --unstaged         Changes not yet staged (files/diff)

Options:
  --base REF         Override the publication baseline (--work/status/base)
  --status           Show file change types (files)
  --stat             Show change statistics (files)
  --word             Show a colored word diff (diff)
  -n N              Limit log output to N commits
  -h, --help         Show this help

Configure work.base to override the baseline, or work.mainBranch to name
an integration branch. No command fetches. Untracked files appear in status.
Merge commit patches use the first parent; ranges show individual patches.
HELP
}
fail() { printf 'git work: %s\n' "$*" >&2; exit 2; }

command_name=${1:-help}
if (($#)); then shift; fi
case "$command_name" in
    help|-h|--help) usage; exit 0 ;;
    log|files|diff|status|base) ;;
    *) fail "unknown command '$command_name' (see git work --help)" ;;
esac
scope=default
presentation=default
base_override=
base_given=false
limit=
revisions=()
paths=()
select_scope() {
    [[ $scope == default ]] || fail 'choose only one of --work, --staged, and --unstaged'
    scope=$1
}
while (($#)); do
    case "$1" in
        --work) select_scope work ;;
        --staged) select_scope staged ;;
        --unstaged) select_scope unstaged ;;
        --base)
            (($# >= 2)) || fail '--base requires a ref'
            [[ -n $2 ]] || fail '--base requires a nonempty ref'
            base_override=$2; base_given=true; shift ;;
        --status|--stat|--word)
            [[ $presentation == default ]] || fail 'choose only one presentation option'
            presentation=${1#--} ;;
        -n)
            (($# >= 2)) || fail '-n requires a positive integer'
            [[ $2 =~ ^[1-9][0-9]*$ ]] || fail '-n requires a positive integer'
            limit=$2; shift ;;
        --) shift; paths=("$@"); break ;;
        -h|--help) usage; exit 0 ;;
        -*) fail "unsupported option '$1' (see git work --help)" ;;
        *) revisions+=("$1") ;;
    esac
    shift
done
if ((${#revisions[@]})); then
    [[ $scope == default ]] || fail 'revision arguments cannot be combined with a scope option'
fi
case "$command_name" in
    log)
        [[ $scope != staged && $scope != unstaged ]] || fail 'log cannot display staged or unstaged changes'
        [[ $presentation == default ]] || fail 'presentation option requires files or diff' ;;
    files)
        [[ $presentation != word ]] || fail '--word requires diff' ;;
    diff)
        [[ $presentation == default || $presentation == word ]] || fail '--status and --stat require files' ;;
    status|base)
        [[ $scope == default && $presentation == default && ${#revisions[@]} == 0 && ${#paths[@]} == 0 ]] || fail "$command_name accepts only --base"
        ;;
esac
[[ -z $limit || $command_name == log ]] || fail '-n requires log'
if $base_given; then
    [[ $scope == work || $command_name == status || $command_name == base ]] || fail '--base requires --work, status, or base'
fi

git rev-parse --git-dir >/dev/null 2>&1 || fail 'not inside a Git repository'
head_oid=$(git rev-parse --verify --quiet HEAD^{commit}) || head_oid=
branch=$(git symbolic-ref --quiet HEAD) || branch=
branch=${branch#refs/heads/}
base_ref=
base_oid=
main_branch=

resolve_commit() { git rev-parse --verify --quiet --end-of-options "$1^{commit}"; }
set_base() {
    base_oid=$(resolve_commit "$1") || fail "baseline '$1' does not resolve to a commit"
    base_ref=$1
}
resolve_base() {
    local configured remote_head upstream candidate
    if $base_given; then set_base "$base_override"; return; fi
    if configured=$(git config --get work.base); then
        [[ -n $configured ]] || fail 'work.base must not be empty'
        set_base "$configured"; return
    fi
    main_branch=$(git config --get work.mainBranch) || main_branch=
    if [[ -n $main_branch ]]; then
        git check-ref-format "refs/heads/$main_branch" >/dev/null || fail 'work.mainBranch must be a branch name'
        if ! resolve_commit "refs/heads/$main_branch" >/dev/null && ! resolve_commit "refs/remotes/origin/$main_branch" >/dev/null; then
            [[ -z $head_oid && $branch == "$main_branch" ]] || fail "integration branch '$main_branch' does not exist"
        fi
    else
        remote_head=$(git symbolic-ref --quiet refs/remotes/origin/HEAD) || remote_head=
        if [[ $remote_head == refs/remotes/origin/* ]] && resolve_commit "$remote_head" >/dev/null; then
            main_branch=${remote_head#refs/remotes/origin/}
        else
            for candidate in main master; do
                if resolve_commit "refs/heads/$candidate" >/dev/null || resolve_commit "refs/remotes/origin/$candidate" >/dev/null || [[ $branch == "$candidate" ]]; then
                    main_branch=$candidate; break
                fi
            done
        fi
    fi
    if [[ -z $main_branch ]]; then
        [[ -n $head_oid ]] || return 0
        fail 'cannot identify the integration branch; set git config work.mainBranch BRANCH (or use --base REF)'
    fi
    if [[ $branch == "$main_branch" ]]; then
        upstream=$(git for-each-ref --format='%(upstream)' "refs/heads/$main_branch")
        # A local tracking branch is not evidence of remote publication.
        if [[ $upstream == refs/remotes/* ]] && resolve_commit "$upstream" >/dev/null; then
            set_base "$upstream"; return
        fi
    elif resolve_commit "refs/heads/$main_branch" >/dev/null; then
        set_base "refs/heads/$main_branch"; return
    fi
    if resolve_commit "refs/remotes/origin/$main_branch" >/dev/null; then
        set_base "refs/remotes/origin/$main_branch"
    fi
    return 0
}

pretty='%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%ar) %C(bold blue)<%an>%Creset'
log_args=(--graph "--pretty=format:$pretty" --abbrev-commit)
if [[ -n $limit ]]; then log_args+=("-n" "$limit"); fi
if [[ $scope == work || $command_name == status || $command_name == base ]]; then resolve_base; fi

baseline_heading() {
    if [[ -n $base_oid ]]; then
        printf 'Published baseline: %s (%s)\n' "$base_ref" "$(git rev-parse --short "$base_oid")"
    else
        printf 'No published baseline; all history is unpublished.\n'
    fi
}

if [[ $command_name == base ]]; then
    if [[ -n $base_ref ]]; then printf '%s\n' "$base_ref"; else printf 'No published baseline.\n' >&2; exit 1; fi
    exit 0
fi
if [[ $command_name == status ]]; then
    printf 'Branch: %s\n' "${branch:-detached HEAD}"
    baseline_heading
    if [[ -n $head_oid ]]; then
        if [[ -n $base_oid ]]; then
            printf 'Outstanding commits: %s\n' "$(git rev-list --count "$base_oid..$head_oid")"
        else
            printf 'Outstanding commits: %s\n' "$(git rev-list --count "$head_oid")"
        fi
    else
        printf 'No commits yet.\n'
    fi
    exec git status --short --untracked-files=normal
fi

if [[ $command_name == log ]]; then
    if [[ $scope == work ]]; then
        baseline_heading
        if [[ -z $head_oid ]]; then printf 'No commits yet.\n'; exit 0; fi
        if [[ -n $base_oid ]]; then
            count=$(git rev-list --count "$base_oid..$head_oid")
            if [[ $count == 0 ]]; then
                printf 'No unpublished commits. Published baseline tip:\n'
                exec git log "${log_args[@]}" --no-walk -n 1 "$base_oid" --
            fi
            printf 'Boundary (o) marks published ancestry; it may precede the baseline tip.\n'
            exec git log "${log_args[@]}" --boundary "$base_oid..$head_oid" -- "${paths[@]}"
        fi
        revisions=("$head_oid")
    elif ((${#revisions[@]} == 0)); then
        if [[ -z $head_oid ]]; then printf 'No commits yet.\n'; exit 0; fi
        revisions=("$head_oid")
    fi
    exec git log "${log_args[@]}" --end-of-options "${revisions[@]}" -- "${paths[@]}"
fi

# Both files and diff share scope resolution and differ only in rendering.
diff_args=()
case "$command_name:$presentation" in
    files:default) diff_args+=(--name-only) ;;
    files:status) diff_args+=(--name-status) ;;
    files:stat) diff_args+=(--stat) ;;
    diff:word) diff_args+=(--word-diff=color) ;;
esac
case "$scope" in
    staged) exec git diff "${diff_args[@]}" --cached -- "${paths[@]}" ;;
    unstaged) exec git diff "${diff_args[@]}" -- "${paths[@]}" ;;
    work)
        if [[ -n $base_oid && -n $head_oid ]]; then
            ancestors=$(git merge-base --all "$head_oid" "$base_oid") || fail 'no common ancestry with baseline (history may be shallow); choose --base REF explicitly'
            [[ $ancestors != *$'\n'* ]] || fail 'multiple merge bases; choose --base REF explicitly'
            comparison=$ancestors
        elif [[ -n $base_oid ]]; then
            fail 'HEAD has no commits; cannot establish common ancestry with the baseline'
        else
            comparison=$(git hash-object -t tree --stdin </dev/null)
        fi
        exec git diff "${diff_args[@]}" "$comparison" -- "${paths[@]}"
        ;;
esac

if ((${#revisions[@]} == 0)); then
    if [[ -z $head_oid ]]; then printf 'No commits yet.\n'; exit 0; fi
    revisions=("$head_oid")
fi
# --no-walk selects individual revisions; Git ignores it for revision ranges.
# Select before path filtering so merge commits use the same first-parent
# comparison whether or not the caller supplies a path.
selected=$(git rev-list --reverse --topo-order --no-walk=unsorted --end-of-options "${revisions[@]}" --) || exit $?
[[ -n $selected ]] || exit 0
mapfile -t commits <<< "$selected"
format=
if [[ $command_name == diff || ${#commits[@]} -gt 1 || ${revisions[*]} == *..* ]]; then
    format="commit $pretty"
fi
exec git show --root --diff-merges=first-parent "--format=$format" "${diff_args[@]}" --end-of-options "${commits[@]}" -- "${paths[@]}"
