mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 16:33:27 +00:00
Addresses review findings from a merge-time audit. - **Complete the headline multi-value `clone` fix for PRs** (was applied only to kind:30617). GitPullRequestEvent (1618) and GitPullRequestUpdateEvent (1619) carry `clone` with the same spec shape but still emitted repeated single-value tags and read only the first value — so the exact interop bug this branch set out to kill was still live for PRs, both directions (ngit keeps only the last repeated tag; we lost every URL after the first from ngit's multi-value tag). Now both emit one multi-value `["clone", …]` tag and read both forms. Verified on the wire + GitNip34InteropTest + CLI harness (40 checks). - **Android git-status spoofing (GitStatusIndex)**: newest-status-wins with no author check meant anyone could publish a kind-1632 and make someone else's issue render closed. Now filter statuses to the repository owner (from the status's own `a` tag), declared maintainers (from the cached announcement), or the target item's author — matching NIP-34 and the CLI's derivation. Pre-existing on main; this branch made the CLI/Android divergence visible. - **CLI robustness**: `git comment`/`git patch` no longer block forever reading stdin on an interactive TTY (amy is non-interactive — error instead). The local `git` subprocesses in `git init`/`git apply` now drain stdout on a side thread under a bounded `waitFor` + `destroyForcibly`, so a wedged git can't hang the CLI. Left as a follow-up (cosmetic): GitBrowseCommands.candidateUrls duplicates GitRepositoryBrowserViewModel's — worth lifting to shared code, not worth the cross-module coupling here. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UKMaNoK5M2PQKCAxhxWzPr
332 lines
17 KiB
Bash
Executable File
332 lines
17 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# git-nip34-headless.sh — drives the real `amy` binary against a real
|
|
# `amy serve` relay to prove the NIP-34 (git-over-nostr) collaboration surface
|
|
# end-to-end: repository announcement + state, patches, pull requests, issues,
|
|
# NIP-22 comments, and status events — plus the status-deriving reads.
|
|
#
|
|
# The verbs mirror the pure-Nostr surface of `ngit` and `nak git`. The git
|
|
# packfile transport (clone/fetch/push of real objects) is intentionally out of
|
|
# scope, so this harness only exercises the events amy publishes and reads back.
|
|
#
|
|
# Flow (single maintainer account against one relay):
|
|
# announce (30617) → state (30618) → issue (1621) → patch (1617) →
|
|
# pr (1618) → pr-update (1619) → comment (1111) → close the issue (1632) →
|
|
# mark the pr applied (1631) → list issues/patches/prs (status derived) →
|
|
# thread view (status timeline + comments).
|
|
#
|
|
# Usage: ./git-nip34-headless.sh [--port N] [--no-build]
|
|
set -uo pipefail
|
|
|
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd -- "$SCRIPT_DIR/../../.." && pwd)"
|
|
TESTS_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
|
|
STATE_DIR="$SCRIPT_DIR/state-git-nip34"
|
|
LOG_DIR="$STATE_DIR/logs"
|
|
RUN_TS="$(date +%Y%m%d-%H%M%S)"
|
|
LOG_FILE="$LOG_DIR/run-$RUN_TS.log"
|
|
RESULTS_FILE="$STATE_DIR/results-$RUN_TS.tsv"
|
|
|
|
AMY_BIN="$REPO_ROOT/cli/build/install/amy/bin/amy"
|
|
RELAY_HOST="127.0.0.1"
|
|
RELAY_PORT="${RELAY_PORT:-7793}"
|
|
RELAY_URL="ws://$RELAY_HOST:$RELAY_PORT"
|
|
NO_BUILD=0
|
|
LIVE=0
|
|
LIVE_REPO="${LIVE_REPO:-https://github.com/octocat/Hello-World.git}"
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--port) RELAY_PORT="$2"; RELAY_URL="ws://$RELAY_HOST:$RELAY_PORT"; shift ;;
|
|
--no-build) NO_BUILD=1 ;;
|
|
--live) LIVE=1 ;;
|
|
*) echo "unknown arg: $1" >&2; exit 2 ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
rm -rf "$STATE_DIR"
|
|
mkdir -p "$LOG_DIR"
|
|
: >"$RESULTS_FILE"
|
|
|
|
# shellcheck source=../lib.sh
|
|
source "$TESTS_DIR/lib.sh"
|
|
|
|
assert_eq() {
|
|
local actual="$1" expected="$2" test_id="$3" note="${4:-}"
|
|
if [[ "${actual// /}" == "${expected// /}" ]]; then
|
|
info "assert: $test_id \"$actual\" == \"$expected\""
|
|
record_result "$test_id" pass "$note"
|
|
return 0
|
|
fi
|
|
fail_msg "$test_id: expected \"$expected\", got \"$actual\" (${note:-})"
|
|
record_result "$test_id" fail "${note:-mismatch}"
|
|
return 1
|
|
}
|
|
|
|
assert_nonempty() {
|
|
local actual="$1" test_id="$2" note="${3:-}"
|
|
if [[ -n "$actual" && "$actual" != "null" ]]; then
|
|
info "assert: $test_id nonempty (\"$actual\")"
|
|
record_result "$test_id" pass "$note"
|
|
return 0
|
|
fi
|
|
fail_msg "$test_id: expected a value, got empty/null (${note:-})"
|
|
record_result "$test_id" fail "${note:-empty}"
|
|
return 1
|
|
}
|
|
|
|
SERVE_PID=""
|
|
cleanup() {
|
|
[[ -n "$SERVE_PID" ]] && kill "$SERVE_PID" 2>/dev/null
|
|
trap - EXIT INT TERM HUP
|
|
print_summary
|
|
}
|
|
trap cleanup EXIT
|
|
trap 'exit 130' INT
|
|
trap 'exit 143' TERM
|
|
|
|
banner "amy git — NIP-34 collaboration headless ($RUN_TS)"
|
|
|
|
# ---- build ------------------------------------------------------------------
|
|
if [[ "$NO_BUILD" -eq 0 ]]; then
|
|
step "Building amy (installDist)…"
|
|
(cd "$REPO_ROOT" && ./gradlew -q :cli:installDist) >>"$LOG_FILE" 2>&1 \
|
|
|| { fail_msg "build failed (see $LOG_FILE)"; exit 1; }
|
|
fi
|
|
[[ -x "$AMY_BIN" ]] || { fail_msg "amy binary not found at $AMY_BIN"; exit 1; }
|
|
|
|
strip() { grep -vE "Picked up JAVA_TOOL|DEBUG:|INFO:"; }
|
|
mk_home() { mktemp -d "$STATE_DIR/home.XXXXXX"; }
|
|
# amy_run <home> <account> args...
|
|
amy_run() {
|
|
local home="$1" acct="$2"; shift 2
|
|
HOME="$home" "$AMY_BIN" --account "$acct" --secret-backend plaintext --json "$@" 2>>"$LOG_FILE" | strip
|
|
}
|
|
|
|
M_HOME="$(mk_home)"
|
|
amy_run "$M_HOME" m init >/dev/null
|
|
M() { amy_run "$M_HOME" m "$@"; }
|
|
|
|
step "Starting amy serve on $RELAY_URL…"
|
|
HOME="$M_HOME" "$AMY_BIN" --account m --secret-backend plaintext \
|
|
serve --host "$RELAY_HOST" --port "$RELAY_PORT" >>"$LOG_FILE" 2>&1 &
|
|
SERVE_PID=$!
|
|
for _ in $(seq 1 60); do
|
|
grep -q "relay up at" "$LOG_FILE" && break
|
|
sleep 0.5
|
|
done
|
|
grep -q "relay up at" "$LOG_FILE" || { fail_msg "relay did not come up"; exit 1; }
|
|
|
|
# =============================================================================
|
|
# git init — bootstrap from a local (full, non-shallow) git checkout
|
|
# =============================================================================
|
|
banner "git init (from a full scratch checkout)"
|
|
INITREPO="$(mk_home)/initrepo"
|
|
git init -q "$INITREPO"
|
|
git -C "$INITREPO" config user.email a@b.c
|
|
git -C "$INITREPO" config user.name t
|
|
echo "hello" >"$INITREPO/README.md"
|
|
git -C "$INITREPO" add README.md
|
|
git -C "$INITREPO" commit -qm "initial commit"
|
|
ROOT_COMMIT="$(git -C "$INITREPO" rev-list --max-parents=0 --first-parent HEAD | tail -1)"
|
|
INIT="$(M git init --repo "$INITREPO" --relay "$RELAY_URL")"
|
|
info "init: $INIT"
|
|
assert_eq "$(echo "$INIT" | jq -r '.from_git_repo')" "true" init.from_git "init derived fields from the git repo"
|
|
assert_nonempty "$(echo "$INIT" | jq -r '.name')" init.name "repo name derived"
|
|
assert_eq "$(echo "$INIT" | jq -r '.earliest_commit')" "$ROOT_COMMIT" init.euc "earliest-unique-commit is the first-parent root"
|
|
assert_nonempty "$(echo "$INIT" | jq -r '.state_event_id')" init.state "init also published a 30618 state event"
|
|
|
|
# A SHALLOW clone must NOT invent an euc (it can't know the true root).
|
|
SHALLOWREPO="$(mk_home)/shallowrepo"
|
|
git clone -q --depth 1 "file://$INITREPO" "$SHALLOWREPO" 2>/dev/null || git clone -q --depth 1 "$INITREPO" "$SHALLOWREPO"
|
|
if [[ "$(git -C "$SHALLOWREPO" rev-parse --is-shallow-repository)" == "true" ]]; then
|
|
SINIT="$(M git init --repo "$SHALLOWREPO" --d shallow-test --relay "$RELAY_URL")"
|
|
assert_eq "$(echo "$SINIT" | jq -r '.earliest_commit')" "null" init.shallow_euc "shallow clone omits the euc (no wrong identity)"
|
|
else
|
|
skip_msg "could not create a shallow clone for init.shallow_euc"
|
|
fi
|
|
|
|
# =============================================================================
|
|
# Repository announcement (30617) + state (30618)
|
|
# =============================================================================
|
|
banner "announce + state"
|
|
ANN="$(M git announce --name demo-repo --description "a demo" \
|
|
--clone https://example.com/demo.git --earliest-commit abc123 --relay "$RELAY_URL")"
|
|
info "announce: $ANN"
|
|
ADDR="$(echo "$ANN" | jq -r '.address')"
|
|
assert_nonempty "$ADDR" announce.address "kind:pubkey:id coordinate returned"
|
|
assert_eq "$(echo "$ANN" | jq -r '.published_to | length')" "1" announce.published "relay accepted 30617"
|
|
|
|
STATE="$(M git state "$ADDR" --head main --branch main=deadbeef,dev=cafe00 --tag v1.0=abc123 --relay "$RELAY_URL")"
|
|
info "state: $STATE"
|
|
assert_eq "$(echo "$STATE" | jq -r '.branches')" "2" state.branches "two branch refs"
|
|
assert_eq "$(echo "$STATE" | jq -r '.head')" "main" state.head "HEAD=main"
|
|
|
|
# --- ngit interop: wire-format checks --------------------------------------
|
|
banner "ngit interop wire format"
|
|
# Announce a repo with TWO clone URLs; NIP-34/ngit want ONE multi-value tag.
|
|
IADDR="$(M git announce --name interop --clone https://a.git,https://b.git --web https://a.com,https://b.com --earliest-commit abc123 --relay "$RELAY_URL" | jq -r '.address')"
|
|
IEID="$(M git show "$IADDR" --relay "$RELAY_URL" | jq -r '.event_id')"
|
|
ITAGS="$(M fetch --id "$IEID" --relay "$RELAY_URL")"
|
|
assert_eq "$(echo "$ITAGS" | jq -r '[.events[0].tags[] | select(.[0]=="clone")] | length')" "1" interop.clone_single "clone is one tag (not repeated)"
|
|
assert_eq "$(echo "$ITAGS" | jq -r '.events[0].tags[] | select(.[0]=="clone") | length')" "3" interop.clone_multivalue "clone tag carries both URLs (name + 2 values)"
|
|
# The tolerant reader must round-trip both URLs back.
|
|
assert_eq "$(M git show "$IADDR" --relay "$RELAY_URL" | jq -r '.clone | length')" "2" interop.clone_read "reader returns both clone URLs"
|
|
# Issue must p-tag the repository owner (maintainer routing).
|
|
IISS="$(M git issue "$IADDR" --subject "interop" "b" --relay "$RELAY_URL" | jq -r '.event_id')"
|
|
IOWNER="$(echo "$IADDR" | cut -d: -f2)"
|
|
assert_eq "$(M fetch --id "$IISS" --relay "$RELAY_URL" | jq -r "[.events[0].tags[] | select(.[0]==\"p\" and .[1]==\"$IOWNER\")] | length")" "1" interop.issue_ptag "issue carries the repo owner p tag"
|
|
# A PR (1618) also carries clone URLs as ONE multi-value tag (same fix as 30617).
|
|
IPR="$(M git pr "$IADDR" --commit tip1 --clone https://c1.git,https://c2.git --subject s --relay "$RELAY_URL" | jq -r '.event_id')"
|
|
IPRTAGS="$(M fetch --id "$IPR" --relay "$RELAY_URL")"
|
|
assert_eq "$(echo "$IPRTAGS" | jq -r '[.events[0].tags[] | select(.[0]=="clone")] | length')" "1" interop.pr_clone_single "PR clone is one tag"
|
|
assert_eq "$(echo "$IPRTAGS" | jq -r '.events[0].tags[] | select(.[0]=="clone") | length')" "3" interop.pr_clone_multivalue "PR clone tag carries both URLs"
|
|
|
|
# GRASP server list (10317) round-trip.
|
|
GRASP="$(M git grasp set "wss://grasp.example.com,wss://grasp2.example.com" --relay "$RELAY_URL")"
|
|
assert_eq "$(echo "$GRASP" | jq -r '.kind')" "10317" grasp.kind "grasp list is kind 10317"
|
|
GRASP_READ="$(M git grasp list --relay "$RELAY_URL")"
|
|
assert_eq "$(echo "$GRASP_READ" | jq -r '.count')" "2" grasp.count "two grasp servers read back"
|
|
assert_eq "$(echo "$GRASP_READ" | jq -r '.grasps[0]')" "wss://grasp.example.com" grasp.order "preference order preserved"
|
|
|
|
# =============================================================================
|
|
# Issue (1621) + patch (1617) + PR (1618) + PR update (1619)
|
|
# =============================================================================
|
|
banner "issue / patch / pr / pr-update"
|
|
ISS="$(M git issue "$ADDR" --subject "First bug" "the issue body" --relay "$RELAY_URL")"
|
|
ISSID="$(echo "$ISS" | jq -r '.event_id')"
|
|
assert_eq "$(echo "$ISS" | jq -r '.kind')" "1621" issue.kind "issue is kind 1621"
|
|
assert_nonempty "$ISSID" issue.id "issue event id"
|
|
|
|
PATCH="$(printf 'From abc\nSubject: [PATCH] fix the thing\n\ndiff --git a/x b/x\n' \
|
|
| M git patch "$ADDR" --root --commit deadbeef --relay "$RELAY_URL")"
|
|
info "patch: $PATCH"
|
|
assert_eq "$(echo "$PATCH" | jq -r '.kind')" "1617" patch.kind "patch is kind 1617"
|
|
assert_eq "$(echo "$PATCH" | jq -r '.subject')" "fix the thing" patch.subject "subject parsed from format-patch"
|
|
|
|
PR="$(M git pr "$ADDR" --commit feed01 --clone https://example.com/demo.git \
|
|
--subject "add feature" "pr description" --relay "$RELAY_URL")"
|
|
PRID="$(echo "$PR" | jq -r '.event_id')"
|
|
assert_eq "$(echo "$PR" | jq -r '.kind')" "1618" pr.kind "pr is kind 1618"
|
|
|
|
PRUP="$(M git pr-update "$PRID" --commit feed02 --clone https://example.com/demo.git --relay "$RELAY_URL")"
|
|
assert_eq "$(echo "$PRUP" | jq -r '.kind')" "1619" prupdate.kind "pr-update is kind 1619"
|
|
|
|
# =============================================================================
|
|
# Comment (1111) + status events (1632 close, 1631 applied)
|
|
# =============================================================================
|
|
banner "comment + status"
|
|
CMT="$(M git comment "$ISSID" "thanks for reporting" --relay "$RELAY_URL")"
|
|
assert_eq "$(echo "$CMT" | jq -r '.kind')" "1111" comment.kind "comment is NIP-22 kind 1111"
|
|
|
|
LABEL="$(M git label "$ISSID" "bug,help-wanted" --relay "$RELAY_URL")"
|
|
assert_eq "$(echo "$LABEL" | jq -r '.kind')" "1985" label.kind "label is NIP-32 kind 1985"
|
|
assert_eq "$(echo "$LABEL" | jq -r '.labels | length')" "2" label.count "two labels attached"
|
|
|
|
CLOSE="$(M git close "$ISSID" "wontfix" --relay "$RELAY_URL")"
|
|
assert_eq "$(echo "$CLOSE" | jq -r '.kind')" "1632" close.kind "close status is kind 1632"
|
|
|
|
APPLIED="$(M git applied "$PRID" "merged it" --merge-commit feed99 --commit feed02 --relay "$RELAY_URL")"
|
|
assert_eq "$(echo "$APPLIED" | jq -r '.kind')" "1631" applied.kind "applied status is kind 1631"
|
|
|
|
# =============================================================================
|
|
# git apply — publish a real patch and apply it to a local working tree
|
|
# =============================================================================
|
|
banner "git apply (nostr patch → local git am)"
|
|
SCRATCH="$(mk_home)/scratch"
|
|
git init -q "$SCRATCH"
|
|
git -C "$SCRATCH" config user.email a@b.c
|
|
git -C "$SCRATCH" config user.name t
|
|
echo "line1" >"$SCRATCH/f.txt"
|
|
git -C "$SCRATCH" add f.txt
|
|
git -C "$SCRATCH" commit -qm "init"
|
|
# Make a real commit, capture its format-patch, then roll it back so `apply` can re-add it.
|
|
echo "line2" >>"$SCRATCH/f.txt"
|
|
git -C "$SCRATCH" commit -qam "add line2"
|
|
PATCHTXT="$(git -C "$SCRATCH" format-patch -1 --stdout)"
|
|
git -C "$SCRATCH" reset -q --hard HEAD~1
|
|
APATCH="$(printf '%s' "$PATCHTXT" | M git patch "$ADDR" --root --relay "$RELAY_URL")"
|
|
APID="$(echo "$APATCH" | jq -r '.event_id')"
|
|
CHECK="$(M git apply "$APID" --check --repo "$SCRATCH")"
|
|
assert_eq "$(echo "$CHECK" | jq -r '.mode')" "check" apply.check "apply --check dry-runs cleanly"
|
|
APPLY="$(M git apply "$APID" --repo "$SCRATCH")"
|
|
info "apply: $APPLY"
|
|
assert_eq "$(echo "$APPLY" | jq -r '.applied')" "true" apply.applied "patch applied via git am"
|
|
assert_eq "$(git -C "$SCRATCH" log --oneline | head -1 | sed 's/^[0-9a-f]* //')" "add line2" apply.commit "commit landed in the working tree"
|
|
|
|
# =============================================================================
|
|
# Status-deriving reads
|
|
# =============================================================================
|
|
banner "reads derive status"
|
|
ISSUES="$(M git issues "$ADDR" --relay "$RELAY_URL")"
|
|
info "issues: $ISSUES"
|
|
assert_eq "$(echo "$ISSUES" | jq -r '.items[0].status')" "closed" issues.status "closed issue reads as closed"
|
|
|
|
PRS="$(M git prs "$ADDR" --relay "$RELAY_URL")"
|
|
assert_eq "$(echo "$PRS" | jq -r '.items[0].status')" "applied" prs.status "applied pr reads as applied"
|
|
|
|
PATCHES="$(M git patches "$ADDR" --relay "$RELAY_URL")"
|
|
assert_eq "$(echo "$PATCHES" | jq -r '.items[0].status')" "open" patches.status "un-statused patch reads as open"
|
|
|
|
# --status filter: closed issues present, open issues empty.
|
|
FILTERED="$(M git issues "$ADDR" --closed --relay "$RELAY_URL")"
|
|
assert_eq "$(echo "$FILTERED" | jq -r '.count')" "1" issues.filter_closed "--closed keeps the closed issue"
|
|
OPEN_ONLY="$(M git issues "$ADDR" --open --relay "$RELAY_URL")"
|
|
assert_eq "$(echo "$OPEN_ONLY" | jq -r '.count')" "0" issues.filter_open "--open drops the closed issue"
|
|
|
|
# =============================================================================
|
|
# Thread view
|
|
# =============================================================================
|
|
banner "thread view"
|
|
THREAD="$(M git thread "$ISSID" --relay "$RELAY_URL")"
|
|
info "thread: $THREAD"
|
|
assert_eq "$(echo "$THREAD" | jq -r '.status')" "closed" thread.status "thread shows closed"
|
|
assert_eq "$(echo "$THREAD" | jq -r '.status_events | length')" "1" thread.status_events "one status event in timeline"
|
|
assert_eq "$(echo "$THREAD" | jq -r '.comments | length')" "1" thread.comments "one comment in thread"
|
|
|
|
# =============================================================================
|
|
# Read repo content over git smart-HTTP (--live only — needs a reachable host).
|
|
# =============================================================================
|
|
if [[ "$LIVE" -eq 1 ]]; then
|
|
banner "live: git browse / cat / log ($LIVE_REPO)"
|
|
BROWSE="$(M git browse "$LIVE_REPO" --json)"
|
|
info "browse: $BROWSE"
|
|
assert_nonempty "$(echo "$BROWSE" | jq -r '.head_commit')" live.browse "browse resolves a head commit"
|
|
FIRST_FILE="$(echo "$BROWSE" | jq -r '.entries[] | select(.type=="file") | .name' | head -1)"
|
|
if [[ -n "$FIRST_FILE" ]]; then
|
|
CAT="$(M git cat "$LIVE_REPO" "$FIRST_FILE" --json)"
|
|
assert_eq "$(echo "$CAT" | jq -r '.path')" "$FIRST_FILE" live.cat "cat returns the requested file"
|
|
assert_nonempty "$(echo "$CAT" | jq -r '.oid')" live.cat_oid "cat resolves a blob oid"
|
|
fi
|
|
LOG="$(M git log "$LIVE_REPO" --depth 2 --json)"
|
|
assert_nonempty "$(echo "$LOG" | jq -r '.commits[0].oid')" live.log "log returns at least one commit"
|
|
|
|
# ngit interop against the REAL amethyst repo (published by ngit to relay.ngit.dev).
|
|
# Proves our reader parses ngit's actual multi-value `clone` tag (4 URLs) — the
|
|
# exact case the pre-fix reader collapsed to one.
|
|
banner "live: reading the real ngit-published amethyst repo"
|
|
NGIT_RELAY="${NGIT_RELAY:-wss://relay.ngit.dev}"
|
|
NGIT_REPO="30617:460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c:amethyst"
|
|
RSHOW="$(M git show "$NGIT_REPO" --relay "$NGIT_RELAY")"
|
|
if [[ -n "$RSHOW" && "$(echo "$RSHOW" | jq -r '.name // empty')" == "amethyst" ]]; then
|
|
CLONES="$(echo "$RSHOW" | jq -r '.clone | length')"
|
|
if [[ "$CLONES" -ge 2 ]]; then
|
|
pass_msg "ngit.clone_multivalue: read $CLONES clone URLs from ngit's multi-value tag"
|
|
record_result ngit.clone_multivalue pass "$CLONES clone URLs"
|
|
else
|
|
fail_msg "ngit.clone_multivalue: only $CLONES clone URL parsed from ngit's multi-value tag"
|
|
record_result ngit.clone_multivalue fail "expected >=2, got $CLONES"
|
|
fi
|
|
RISS="$(M git issues "$NGIT_REPO" --relay "$NGIT_RELAY" --limit 1 | jq -r '.count')"
|
|
assert_nonempty "$RISS" ngit.issues "read ngit-published issues"
|
|
else
|
|
skip_msg "ngit live repo unreachable (relay.ngit.dev) — skipping real-repo interop check"
|
|
fi
|
|
else
|
|
skip_msg "live git smart-HTTP reads (browse/cat/log) + real ngit repo — pass --live to run"
|
|
fi
|
|
|
|
grep -q $'\tfail\t' "$RESULTS_FILE" && exit 1
|
|
exit 0
|