/ devlog / devscripts / check_sync.sh
#!/bin/sh

# Gold moves by the settle script and by nothing else. Where its head
# does not carry the message of the last patch the ledger says was
# settled, something was applied, reset, or committed there by hand, and
# every patch cut afterwards is against a base that is nobody's tree.
# Gold is meant to hold the same tree Chris committed, and until now
# nothing compared the two: the check read commit messages, which match
# even where the contents do not. A patch of mine once added four
# sentences to a plan that his tree never took, and every later patch was
# cut against that difference and would not apply. The confirmed history
# is fetchable, so the tree itself is compared against it.
REMOTE=${REMOTE:-https://www.seekquarry.com/group/2/yioop-repo.git}
check_gold_tree()
{
    if [ "${SKIP_REMOTE_CHECK:-}" = "1" ]; then
        return 0
    fi
    where=`mktemp -d`
    if ! timeout -s KILL 150 git clone -q --depth 25 "$REMOTE" \
        "$where" 2>/dev/null; then
        echo "check_sync: the confirmed history could not be reached;"
        echo "  gold's tree was not compared against it"
        rm -rf "$where"
        return 0
    fi
    # Chris commits on his own machine and pushes later, so the
    # published history is often a patch or two behind gold. Where it
    # does not yet carry gold's newest commit there is nothing to
    # compare against, and saying so is the answer rather than refusing
    # a cut.
    mine=`git -C "$GOLD" log -1 --format=%s`
    if ! git -C "$where" log --format=%s | grep -qxF "$mine"; then
        echo "check_sync: gold is ahead of the published history;"
        echo "  its tree was not compared"
        rm -rf "$where"
        return 0
    fi
    # Gold's own commit is the one to compare against, not whatever the
    # published history has at its tip. Chris may have committed a patch
    # and then rejected it, which leaves the published history ahead of
    # gold with a commit gold must never take. Comparing against the tip
    # would call gold stale for holding exactly the tree it should.
    at_theirs=`git -C "$where" log --format='%H %s' | \
        grep -F " $mine" | head -1 | cut -d' ' -f1`
    theirs=`git -C "$where" rev-parse "$at_theirs^{tree}"`
    ours=`git -C "$GOLD" rev-parse HEAD^{tree}`
    ahead=`git -C "$where" log --oneline "$at_theirs"..HEAD | wc -l`
    rm -rf "$where"
    if [ "$theirs" != "$ours" ]; then
        echo "check_sync: FAIL gold's tree is not the one Chris"
        echo "  committed. Rebuild gold from $REMOTE before cutting."
        return 1
    fi
    if [ "$ahead" -gt 0 ]; then
        echo "check_sync: the published history carries $ahead commit(s)"
        echo "  past gold's; gold takes one only when Chris says so"
    fi
    return 0
}
check_gold_head()
{
    last=`grep ' settled$' "$LEDGER" | tail -1 | cut -d' ' -f2`
    if [ -z "$last" ]; then
        return 0
    fi
    head_message=`git -C "$GOLD" log -1 --format=%s`
    as_name=`echo "$head_message" | tr 'A-Z' 'a-z' | tr -c 'a-z0-9' '-' | \
        sed 's/--*/-/g; s/^-//; s/-$//'`
    # Chris names his commit as he pleases: he may add a word or a
    # number to the message the patch was cut under. What matters is
    # that his message is the patch's name with something after it,
    # not that the two match letter for letter.
    case "$as_name" in
        "$last"|"$last"-*)
            return 0
            ;;
    esac
    if [ "$as_name" != "$last" ]; then
        echo "check_sync: FAIL gold's head is \"$head_message\" but the"
        echo "  ledger last settled \"$last\". Gold was moved by hand."
        return 1
    fi
    return 0
}
# Refuses to let a patch be cut when gold and work have drifted apart, or
# when the ledger has been written by anything but the two scripts. Every
# check below stands for a failure that has happened and cost a round.
GOLD=${GOLD:-/home/claude/gold}
WORK=${WORK:-/home/claude/work}
LEDGER=${LEDGER:-/home/claude/gold_ledger.txt}
fail=0
# Gold is a vault: its working tree must equal its own confirmed-head, so
# nothing has been applied, edited or checked out inside it by hand.
gold_head=`git -C "$GOLD" rev-parse confirmed-head^{tree} 2>/dev/null`
gold_now=`git -C "$GOLD" rev-parse HEAD^{tree} 2>/dev/null`
if [ "$gold_head" != "$gold_now" ]; then
    echo "check_sync: FAIL gold's head is not its confirmed-head; something"
    echo "  was done inside gold by hand."
    fail=1
fi
if [ -n "`git -C "$GOLD" status --porcelain 2>/dev/null`" ]; then
    echo "check_sync: FAIL gold has uncommitted changes; it is a vault."
    fail=1
fi
# Work's confirmed-head must name the same tree as gold's. When gold has
# been settled and work was not re-cloned, work's tag is stale and every
# patch cut from it is against a base Chris does not have.
work_head=`git -C "$WORK" rev-parse confirmed-head^{tree} 2>/dev/null`
if [ "$work_head" != "$gold_head" ]; then
    echo "check_sync: FAIL work's confirmed-head is not gold's tree."
    echo "  Re-clone work from gold and restore work_directory."
    fail=1
fi
# The ledger is written by settle_gold.sh and cut_patch.sh alone. Each
# writes a checksum beside it; a mismatch means it was edited by hand.
if [ -f "$LEDGER.sum" ]; then
    want=`cat "$LEDGER.sum"`
    have=`cksum < "$LEDGER" | awk '{print $1"-"$2}'`
    if [ "$want" != "$have" ]; then
        echo "check_sync: FAIL the ledger was changed by something other"
        echo "  than the two scripts."
        fail=1
    fi
fi
if [ "$fail" = "0" ]; then
    if ! check_gold_head; then
        exit 1
    fi
    if ! check_gold_tree; then
        exit 1
    fi
# The settings file in work may differ from gold in the line naming
# the work directory and nothing else. A saved copy of that file put
# over it drops whatever gold has gained since, and the patch then
# reads as taking those settings away.
#
# A patch that adds a setting is a different thing from that, and it is
# told apart by whether the change is committed. What a round means to
# deliver has been committed, so only lines still uncommitted are counted
# here: a stale copy dropped over the file shows up as uncommitted
# whatever it holds, while a setting the round added is already in a
# commit and passes.
settings=`git diff HEAD --numstat -- src/configs/Config.php \
    2>/dev/null | awk '{print $1 + $2}'`
if [ -n "$settings" ] && [ "$settings" -gt 2 ]; then
    echo "check_sync: FAIL the settings file has $settings uncommitted"
    echo "  line(s); it may differ by the work directory line alone."
    echo "  Take gold copy and set that one line, or commit the"
    echo "  settings this round adds."
    exit 1
fi
    echo "check_sync: PASS gold and work agree, gold holds the tree"
    echo "  Chris committed, and the ledger is untouched by hand"
fi
exit $fail
X