#!/usr/bin/env bash # The git bridge end to end: importing a repository's history, bringing a # branch in as a change, publishing with the original git history, and the # hosted git view (apps/vcs under `wrangler dev`, pushing to a local git # server that stands in for git.coline.app): new versions as commits, # private paths left out, a push from outside pausing it, and resuming. # Usage: tests/git.sh [path/to/target/debug] set -uo pipefail ROOT=$(cd "$(dirname "$0")/.." && pwd) BIN=${1:-$ROOT/target/debug} VCS=$BIN/vcs APP=$ROOT/../apps/vcs WORK=$(mktemp -d) export VCS_SAVER=0 VCS_HOME=$WORK/home VCS_NAME=Alice VCS_EMAIL=alice@acme.test PORT=$((20000 + RANDOM % 20000)) GPORT=$((PORT + 2)) HOST="127.0.0.1:$PORT" WORKER_PID= GIT_PID= cleanup() { [ -n "$WORKER_PID" ] && kill -- "-$WORKER_PID" 2>/dev/null [ -n "$GIT_PID" ] && kill "$GIT_PID" 2>/dev/null wait 2>/dev/null rm -rf "$WORK" } trap cleanup EXIT cd "$WORK" pass=0 fail=0 check() { local name=$1 shift if "$@" >/dev/null 2>&1; then pass=$((pass + 1)); else fail=$((fail + 1)); echo "FAIL: $name"; fi } jq_() { python3 -c "import json,sys; d=json.load(sys.stdin); print($1)"; } g() { git -c user.name=Ada -c user.email=ada@example.com -c init.defaultBranch=main -c commit.gpgsign=false "$@"; } # A git repository with files, an executable, a link, a big file, folders, # a submodule, a merge and a tag. g init -q lib (cd lib && printf 'lib\n' > lib.txt && g add -A && g commit -qm "Lib") g init -q src ( cd src printf '# Shop\n' > README.md && mkdir -p src && printf 'fn main() {}\n' > src/main.rs g add -A && g commit -qm "First commit" -m "With a body." printf '#!/bin/sh\necho hi\n' > run.sh && chmod +x run.sh && ln -s README.md link g add -A && g commit -qm "Script and link" head -c 3000000 /dev/urandom > big.bin && mkdir -p a/b && printf 'deep\n' > a/b/c.txt g -c protocol.file.allow=always submodule add -q ../lib vendor/lib >/dev/null 2>&1 g add -A && g commit -qm "Big file, folders and a submodule" g checkout -qb side && printf 'side\n' > side.txt && g add -A && g commit -qm "Side work" && g checkout -q main printf '# Shop\n\nSells things.\n' > README.md && g rm -q src/main.rs g add -A && g commit -qm "Readme, no main.rs" g merge -q --no-ff side -m "Merge side" g tag v1 g checkout -qb feature && printf 'feature\n' > feature.txt && printf 'deeper\n' > a/b/c.txt g add -A && g commit -qm "Feature" -m "Adds feature.txt." && g checkout -q main ) COMMITS=$(git -C src rev-list --first-parent --count main) check "import" "$VCS" import src shop check "a version per first-parent commit" test "$("$VCS" --json -C shop log -n 1 | jq_ 'd["versions"][0]["n"]')" = "$COMMITS" check "last title" sh -c "'$VCS' -C shop log -n 1 | grep -q 'Merge side'" mkdir -p want && git -C src archive main | tar -x -C want check "files match git" diff -r --exclude=.vcs --exclude=lib want shop LIB=$(git -C src rev-parse main:vendor/lib) check "the submodule is an empty folder" sh -c "test -d shop/vendor/lib && test -z \"\$(ls -A shop/vendor/lib)\"" check "and nothing to save" sh -c "'$VCS' --json -C shop status | python3 -c 'import json,sys; d=json.load(sys.stdin); assert not d.get(\"files\"), d.get(\"files\")'" check "executable kept" test -x shop/run.sh check "link kept" test -L shop/link check "big file intact" cmp src/big.bin shop/big.bin check "first version has its body" sh -c "'$VCS' -C shop log 1 | grep -q 'With a body.'" # Native work on top, then a git branch comes in as a change and lands. printf 'native\n' > shop/native.txt check "native land" "$VCS" -C shop land -m "Native work" check "branch comes in as a change" "$VCS" -C shop import ../src --change feature check "it's the workspace's change" sh -c "'$VCS' -C shop status | grep -q Feature" check "workspace has the branch" test -f shop/feature.txt check "branch lands" "$VCS" -C shop land check "sync" "$VCS" -C shop sync check "main has both" sh -c "test -f shop/native.txt && test -f shop/feature.txt && grep -q deeper shop/a/b/c.txt" check "unknown branch refused" sh -c "! '$VCS' -C shop import ../src --change nope" # The hosted git view. mkdir -p gitroot/acme git init -q --bare gitroot/acme/shop.git python3 "$ROOT/tests/git-server.py" "$WORK/gitroot" "$GPORT" & GIT_PID=$! for _ in $(seq 100); do curl -s -o /dev/null "http://127.0.0.1:$GPORT/acme/shop.git/info/refs?service=git-upload-pack" && break; sleep 0.1; done kill -0 "$GIT_PID" 2>/dev/null || { echo "git server didn't start (port $GPORT)"; exit 1; } ALICE=vcs_test_alice_0123456789 USERS="{\"$ALICE\":{\"handle\":\"alice\",\"name\":\"Alice\",\"email\":\"alice@acme.test\"}}" WRANGLER_SEND_METRICS=false setsid "$APP/node_modules/.bin/wrangler" dev --cwd "$APP" --name "vcs-test-$PORT" --port "$PORT" --ip 127.0.0.1 --inspector-port "$((PORT + 1))" \ --persist-to "$WORK/wrangler" --var AUTH_MODE:test --var "TEST_USERS:$USERS" --var "GIT_BASE:http://127.0.0.1:$GPORT" >"$WORK/worker.log" 2>&1 & WORKER_PID=$! for _ in $(seq 300); do curl -s "http://$HOST/.well-known/vcs.json" 2>/dev/null | grep -q api && break; sleep 0.2; done api() { curl -s -H "authorization: Bearer $ALICE" -H 'content-type: application/json' "$@"; } gv() { git --git-dir="$WORK/gitroot/acme/shop.git" "$@"; } wait_view() { # version number to wait for in the git view for _ in $(seq 100); do gv log -1 --format=%B main 2>/dev/null | grep -q "Vcs-Version: $1\$" && return 0; sleep 0.2; done return 1 } check "login" "$VCS" login "$HOST" --token "$ALICE" api -X POST -d '{"owner":"acme","name":"shop","git":"view"}' "http://$HOST/v1/projects" >/dev/null check "import and publish with history" "$VCS" import src hosted --to "$HOST/acme/shop" check "git history copied" test "$(gv rev-parse main)" = "$(git -C src rev-parse main)" check "tags copied" test "$(gv rev-parse v1)" = "$(git -C src rev-parse v1)" check "branches copied" test "$(gv rev-parse feature)" = "$(git -C src rev-parse feature)" N=$(("$COMMITS" + 1)) printf 'from vcs\n' > hosted/fromvcs.txt check "hosted land" "$VCS" -C hosted land -m "From vcs" -m "Landed natively." check "git view gets the version" wait_view "$N" check "commit follows the last git commit" test "$(gv rev-parse main~1)" = "$(git -C src rev-parse main)" check "commit has the file" sh -c "git --git-dir='$WORK/gitroot/acme/shop.git' show main:fromvcs.txt | grep -q 'from vcs'" check "commit has the message" sh -c "git --git-dir='$WORK/gitroot/acme/shop.git' log -1 --format=%B main | grep -q 'Landed natively.'" check "git fsck passes" gv fsck --strict --no-dangling check "the submodule is still in git" sh -c "git --git-dir='$WORK/gitroot/acme/shop.git' ls-tree main vendor/lib | grep -q '^160000 commit $LIB'" check "vcs git shows it" sh -c "'$VCS' -C hosted git | grep -q 'version $N is commit'" # Tags reach the git view as git tags; removing one removes it there. wait_tag() { # tag, commit (empty: gone) for _ in $(seq 100); do [ "$(gv rev-parse -q --verify "refs/tags/$1" 2>/dev/null)" = "$2" ] && return 0; sleep 0.2; done return 1 } check "tag a version" "$VCS" -C hosted tag release-1 "$N" check "the git view has the tag" wait_tag release-1 "$(gv rev-parse main)" check "remove the tag" "$VCS" -C hosted tag release-1 --remove check "the git view drops it" wait_tag release-1 "" # Private paths stay out of the git view. check "make secret/ private" "$VCS" -C hosted access secret/ @alice mkdir -p hosted/secret && printf 'key\n' > hosted/secret/key.txt && printf 'open\n' > hosted/open.txt N=$((N + 1)) check "land a secret" "$VCS" -C hosted land -m "Secret and open" check "git view gets it" wait_view "$N" check "open file in the git view" sh -c "git --git-dir='$WORK/gitroot/acme/shop.git' show main:open.txt | grep -q open" check "secret not in the git view" sh -c "! git --git-dir='$WORK/gitroot/acme/shop.git' cat-file -e main:secret/key.txt" check "secret still in vcs" test -f hosted/secret/key.txt wait_sync() { # a file that should show up in the workspace once vcs has git main's commits for _ in $(seq 60); do "$VCS" -C hosted sync >/dev/null 2>&1; [ -e "hosted/$1" ] && return 0; sleep 0.5; done return 1 } wait_paused() { for _ in $(seq 60); do "$VCS" -C hosted sync >/dev/null 2>&1; "$VCS" -C hosted git | grep -q paused && return 0; sleep 0.5; done return 1 } # Someone pushes to git main: their commits come into vcs, each a version. g clone -q "http://127.0.0.1:$GPORT/acme/shop.git" outside (cd outside && printf 'outside\n' > outside.txt && g add -A && g commit -qm "Pushed with git" -m "From a git user." && printf 'two\n' > outside2.txt && g add -A && g commit -qm "Second git commit" && g push -q origin main) OUTSIDE=$(git -C outside rev-parse main) N=$((N + 2)) check "a git push comes into vcs" wait_sync outside2.txt check "a version per commit" test "$("$VCS" --json -C hosted log -n 2 | jq_ '" | ".join(str(v["n"]) + " " + v["title"] for v in d["versions"])')" = "$N Second git commit | $((N - 1)) Pushed with git" check "with its message" sh -c "'$VCS' -C hosted log $((N - 1)) | grep -q 'From a git user.'" check "git main is still their commit" test "$(gv rev-parse main)" = "$OUTSIDE" check "vcs git says so" sh -c "'$VCS' -C hosted git | grep -q 'version $N is commit ${OUTSIDE:0:12}'" printf 'after\n' > hosted/after.txt N=$((N + 1)) check "landing on top" "$VCS" -C hosted land -m "After the git push" check "goes on from their commit" wait_view "$N" check "their commit is the parent" test "$(gv rev-parse main~1)" = "$OUTSIDE" # A git push and a vcs land at once: both end up in main and in git. (cd outside && g pull -q --rebase origin main && printf 'from git\n' > both-git.txt && g add -A && g commit -qm "Git side" && g push -q origin main) GITSIDE=$(git -C outside rev-parse main) printf 'from vcs\n' > hosted/both-vcs.txt check "vcs lands meanwhile" "$VCS" -C hosted land -m "Vcs side" N=$((N + 2)) check "git's side comes into vcs" wait_sync both-git.txt check "the git view has both" wait_view "$N" check "the git commit stays in git's history" gv merge-base --is-ancestor "$GITSIDE" main check "git main has both files" sh -c "git --git-dir='$WORK/gitroot/acme/shop.git' show main:both-git.txt && git --git-dir='$WORK/gitroot/acme/shop.git' show main:both-vcs.txt" check "git fsck passes after" gv fsck --strict --no-dangling # A git push into a private folder can't land: the view pauses and says why. (cd outside && g pull -q --rebase origin main && mkdir -p secret && printf 'nope\n' > secret/new.txt && g add -A && g commit -qm "Into the private folder" && g push -q origin main) check "the view pauses" wait_paused check "and says why" sh -c "'$VCS' -C hosted git | grep -q 'secret/new.txt, which is private'" check "vcs main is untouched" test ! -e hosted/secret/new.txt check "resume" "$VCS" -C hosted git resume printf 'resumed\n' > hosted/resumed.txt N=$((N + 1)) check "land after resuming" "$VCS" -C hosted land -m "Resumed" check "git view catches up" wait_view "$N" check "the git commit stays in git's history" gv merge-base --is-ancestor "$(git -C outside rev-parse main)" main check "git fsck passes after resuming" gv fsck --strict --no-dangling # A team that's left git behind turns the git view off; on again catches up. check "git view off" sh -c "'$VCS' -C hosted settings git=off | grep -q 'off:'" AT=$(gv rev-parse main) for i in 1 2 3; do printf '%s\n' "$i" > "hosted/off-$i.txt" N=$((N + 1)) check "land $i while off" "$VCS" -C hosted land -m "Off $i" done sleep 2 check "git main didn't move" test "$(gv rev-parse main)" = "$AT" check "git view on" "$VCS" -C hosted settings git=on check "it catches up" wait_view "$N" check "a commit per version" test "$(gv rev-list --count "$AT"..main)" = 3 check "git fsck passes at the end" gv fsck --strict --no-dangling # Git and vcs both change the same lines: the view pauses until someone # resolves it. (The view is off while they diverge, so the order is sure.) "$VCS" -C hosted settings git=off >/dev/null printf 'vcs line\n' > hosted/clash.txt check "vcs lands its line" "$VCS" -C hosted land -m "Vcs clash" (cd outside && g pull -q --rebase origin main && printf 'git line\n' > clash.txt && g add -A && g commit -qm "Git clash" && g push -q origin main) "$VCS" -C hosted settings git=on >/dev/null check "the view pauses" wait_paused check "and names the file" sh -c "'$VCS' -C hosted git | grep -q 'both changed clash.txt'" check "vcs main keeps its line" grep -q 'vcs line' hosted/clash.txt echo "git: $pass passed, $fail failed" [ "$fail" = 0 ]