Onset: version control where nobody waits and nothing gets lost. The on command, its core and onset-server.
| 1 | #!/usr/bin/env bash |
| 2 | # The git bridge end to end: importing a repository's history, bringing a |
| 3 | # branch in as a change, publishing with the original git history, and the |
| 4 | # hosted git view (apps/onset under `wrangler dev`, pushing to a local git |
| 5 | # server that stands in for git.coline.app): new versions as commits, |
| 6 | # private paths left out, a push from outside pausing it, and resuming. |
| 7 | # Usage: tests/git.sh [path/to/target/debug] |
| 8 | set -uo pipefail |
| 9 |
| 10 | ROOT=$(cd "$(dirname "$0")/.." && pwd) |
| 11 | BIN=${1:-$ROOT/target/debug} |
| 12 | VCS=$BIN/on |
| 13 | APP=$ROOT/../apps/onset |
| 14 | WORK=$(mktemp -d) |
| 15 | export ONSET_SAVER=0 ONSET_HOME=$WORK/home ONSET_NAME=Alice ONSET_EMAIL=alice@acme.test |
| 16 | # Run from inside an agent, its own session would be kept with the tests' lands. |
| 17 | unset CLAUDE_CODE_SESSION_ID CLAUDECODE CLAUDE_CONFIG_DIR ONSET_SESSION_FILE ONSET_SESSIONS VCS_SESSION_FILE VCS_SESSIONS VCS_HOME VCS_TOKEN |
| 18 | export CODEX_HOME="$WORK/no-codex" |
| 19 | PORT=$((20000 + RANDOM % 20000)) |
| 20 | GPORT=$((PORT + 2)) |
| 21 | HOST="127.0.0.1:$PORT" |
| 22 | WORKER_PID= |
| 23 | GIT_PID= |
| 24 | cleanup() { |
| 25 | [ -n "$WORKER_PID" ] && kill -- "-$WORKER_PID" 2>/dev/null |
| 26 | [ -n "$GIT_PID" ] && kill "$GIT_PID" 2>/dev/null |
| 27 | wait 2>/dev/null |
| 28 | rm -rf "$WORK" |
| 29 | } |
| 30 | trap cleanup EXIT |
| 31 | cd "$WORK" |
| 32 |
| 33 | pass=0 |
| 34 | fail=0 |
| 35 | check() { |
| 36 | local name=$1 |
| 37 | shift |
| 38 | if "$@" >/dev/null 2>&1; then pass=$((pass + 1)); else fail=$((fail + 1)); echo "FAIL: $name"; fi |
| 39 | } |
| 40 | jq_() { python3 -c "import json,sys; d=json.load(sys.stdin); print($1)"; } |
| 41 | g() { git -c user.name=Ada -c user.email=ada@example.com -c init.defaultBranch=main -c commit.gpgsign=false "$@"; } |
| 42 |
| 43 | # A git repository with files, an executable, a link, a big file, folders, |
| 44 | # a submodule, a merge and a tag. |
| 45 | g init -q lib |
| 46 | (cd lib && printf 'lib\n' > lib.txt && g add -A && g commit -qm "Lib") |
| 47 | g init -q src |
| 48 | ( |
| 49 | cd src |
| 50 | printf '# Shop\n' > README.md && mkdir -p src && printf 'fn main() {}\n' > src/main.rs |
| 51 | g add -A && g commit -qm "First commit" -m "With a body." |
| 52 | printf '#!/bin/sh\necho hi\n' > run.sh && chmod +x run.sh && ln -s README.md link |
| 53 | g add -A && g commit -qm "Script and link" |
| 54 | head -c 3000000 /dev/urandom > big.bin && mkdir -p a/b && printf 'deep\n' > a/b/c.txt |
| 55 | g -c protocol.file.allow=always submodule add -q ../lib vendor/lib >/dev/null 2>&1 |
| 56 | g add -A && g commit -qm "Big file, folders and a submodule" |
| 57 | g checkout -qb side && printf 'side\n' > side.txt && g add -A && g commit -qm "Side work" && g checkout -q main |
| 58 | printf '# Shop\n\nSells things.\n' > README.md && g rm -q src/main.rs |
| 59 | g add -A && g commit -qm "Readme, no main.rs" |
| 60 | g merge -q --no-ff side -m "Merge side" |
| 61 | g tag v1 |
| 62 | g checkout -qb feature && printf 'feature\n' > feature.txt && printf 'deeper\n' > a/b/c.txt |
| 63 | g add -A && g commit -qm "Feature" -m "Adds feature.txt." && g checkout -q main |
| 64 | ) |
| 65 | COMMITS=$(git -C src rev-list --first-parent --count main) |
| 66 |
| 67 | check "import" "$VCS" import src shop |
| 68 | check "a version per first-parent commit" test "$("$VCS" --json -C shop log -n 1 | jq_ 'd["versions"][0]["n"]')" = "$COMMITS" |
| 69 | check "last title" sh -c "'$VCS' -C shop log -n 1 | grep -q 'Merge side'" |
| 70 | mkdir -p want && git -C src archive main | tar -x -C want |
| 71 | check "files match git" diff -r --exclude=.onset --exclude=lib want shop |
| 72 | LIB=$(git -C src rev-parse main:vendor/lib) |
| 73 | check "the submodule is an empty folder" sh -c "test -d shop/vendor/lib && test -z \"\$(ls -A shop/vendor/lib)\"" |
| 74 | 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\")'" |
| 75 | check "executable kept" test -x shop/run.sh |
| 76 | check "link kept" test -L shop/link |
| 77 | check "big file intact" cmp src/big.bin shop/big.bin |
| 78 | check "first version has its body" sh -c "'$VCS' -C shop log 1 | grep -q 'With a body.'" |
| 79 |
| 80 | # Native work on top, then a git branch comes in as a change and lands. |
| 81 | printf 'native\n' > shop/native.txt |
| 82 | check "native land" "$VCS" -C shop land -m "Native work" |
| 83 | check "branch comes in as a change" "$VCS" -C shop import ../src --change feature |
| 84 | check "it's the workspace's change" sh -c "'$VCS' -C shop status | grep -q Feature" |
| 85 | check "workspace has the branch" test -f shop/feature.txt |
| 86 | check "branch lands" "$VCS" -C shop land |
| 87 | check "sync" "$VCS" -C shop sync |
| 88 | check "main has both" sh -c "test -f shop/native.txt && test -f shop/feature.txt && grep -q deeper shop/a/b/c.txt" |
| 89 | check "unknown branch refused" sh -c "! '$VCS' -C shop import ../src --change nope" |
| 90 |
| 91 | # The hosted git view. |
| 92 | mkdir -p gitroot/acme |
| 93 | git init -q --bare gitroot/acme/shop.git |
| 94 | python3 "$ROOT/tests/git-server.py" "$WORK/gitroot" "$GPORT" & |
| 95 | GIT_PID=$! |
| 96 | 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 |
| 97 | kill -0 "$GIT_PID" 2>/dev/null || { echo "git server didn't start (port $GPORT)"; exit 1; } |
| 98 | ALICE=vcs_test_alice_0123456789 |
| 99 | USERS="{\"$ALICE\":{\"handle\":\"alice\",\"name\":\"Alice\",\"email\":\"alice@acme.test\"}}" |
| 100 | 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))" \ |
| 101 | --persist-to "$WORK/wrangler" --var AUTH_MODE:test --var "TEST_USERS:$USERS" --var "GIT_BASE:http://127.0.0.1:$GPORT" --var "UPSTREAM_API:http://127.0.0.1:$GPORT/api" --var "VCS_ORIGIN:http://$HOST" --var VCS_INTERNAL_SECRET:test-internal >"$WORK/worker.log" 2>&1 & |
| 102 | WORKER_PID=$! |
| 103 | for _ in $(seq 300); do curl -s "http://$HOST/.well-known/onset.json" 2>/dev/null | grep -q api && break; sleep 0.2; done |
| 104 | api() { curl -s -H "authorization: Bearer $ALICE" -H 'content-type: application/json' "$@"; } |
| 105 | gv() { git --git-dir="$WORK/gitroot/acme/shop.git" "$@"; } |
| 106 | wait_view() { # version number to wait for in the git view |
| 107 | for _ in $(seq 100); do gv log -1 --format=%B main 2>/dev/null | grep -q "Onset-Version: $1\$" && return 0; sleep 0.2; done |
| 108 | return 1 |
| 109 | } |
| 110 |
| 111 | check "login" "$VCS" login "$HOST" --token "$ALICE" |
| 112 | api -X POST -d '{"owner":"acme","name":"shop","git":"view"}' "http://$HOST/v1/projects" >/dev/null |
| 113 | check "import and publish with history" "$VCS" import src hosted --to "$HOST/acme/shop" |
| 114 | check "git history copied" test "$(gv rev-parse main)" = "$(git -C src rev-parse main)" |
| 115 | check "tags copied" test "$(gv rev-parse v1)" = "$(git -C src rev-parse v1)" |
| 116 | check "branches copied" test "$(gv rev-parse feature)" = "$(git -C src rev-parse feature)" |
| 117 |
| 118 | N=$(("$COMMITS" + 1)) |
| 119 | printf 'from vcs\n' > hosted/fromvcs.txt |
| 120 | check "hosted land" "$VCS" -C hosted land -m "From vcs" -m "Landed natively." |
| 121 | check "git view gets the version" wait_view "$N" |
| 122 | check "commit follows the last git commit" test "$(gv rev-parse main~1)" = "$(git -C src rev-parse main)" |
| 123 | check "commit has the file" sh -c "git --git-dir='$WORK/gitroot/acme/shop.git' show main:fromvcs.txt | grep -q 'from vcs'" |
| 124 | check "commit has the message" sh -c "git --git-dir='$WORK/gitroot/acme/shop.git' log -1 --format=%B main | grep -q 'Landed natively.'" |
| 125 | check "git fsck passes" gv fsck --strict --no-dangling |
| 126 | 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'" |
| 127 | check "vcs git shows it" sh -c "'$VCS' -C hosted git | grep -q 'version $N is commit'" |
| 128 |
| 129 | # Tags reach the git view as git tags; removing one removes it there. |
| 130 | wait_tag() { # tag, commit (empty: gone) |
| 131 | for _ in $(seq 100); do [ "$(gv rev-parse -q --verify "refs/tags/$1" 2>/dev/null)" = "$2" ] && return 0; sleep 0.2; done |
| 132 | return 1 |
| 133 | } |
| 134 | check "tag a version" "$VCS" -C hosted tag release-1 "$N" |
| 135 | check "the git view has the tag" wait_tag release-1 "$(gv rev-parse main)" |
| 136 | check "remove the tag" "$VCS" -C hosted tag release-1 --remove |
| 137 | check "the git view drops it" wait_tag release-1 "" |
| 138 |
| 139 | # A release line is a git branch of its name: it starts at its version of |
| 140 | # main's commit, and each of its versions is a commit on it. |
| 141 | wait_branch() { # branch, text in its last commit's message |
| 142 | for _ in $(seq 100); do gv log -1 --format=%B "refs/heads/$1" 2>/dev/null | grep -q "$2" && return 0; sleep 0.2; done |
| 143 | return 1 |
| 144 | } |
| 145 | check "start line 1.x" "$VCS" -C hosted line 1.x --from "$N" |
| 146 | check "the git view has branch 1.x at main $N" wait_branch 1.x "Onset-Version: $N\$" |
| 147 | "$VCS" get "$HOST/acme/shop" hosted-1x --line 1.x >/dev/null 2>&1 |
| 148 | printf 'line fix\n' > hosted-1x/linefix.txt |
| 149 | check "land on the line" "$VCS" -C hosted-1x land -m "Fix on the line" |
| 150 | check "it's a commit on branch 1.x" wait_branch 1.x "Onset-Line: 1.x/1\$" |
| 151 | check "on top of main $N's commit" test "$(gv rev-parse 1.x~1)" = "$(gv rev-parse "main")" |
| 152 | check "with the file" sh -c "git --git-dir='$WORK/gitroot/acme/shop.git' show 1.x:linefix.txt | grep -q 'line fix'" |
| 153 | check "main's branch doesn't have it" sh -c "! git --git-dir='$WORK/gitroot/acme/shop.git' show main:linefix.txt >/dev/null 2>&1" |
| 154 |
| 155 | # Purge: git main is written again without the path (a force push); the |
| 156 | # commits from before it keep their IDs. |
| 157 | BEFORE=$(gv rev-parse main) |
| 158 | printf 'not-a-real-key-git\n' > hosted/oops.pem |
| 159 | "$VCS" -C hosted land -m "Oops in git" --allow-secrets >/dev/null 2>&1 |
| 160 | printf 'after oops\n' > hosted/after-oops.txt |
| 161 | "$VCS" -C hosted land -m "After oops" >/dev/null 2>&1 |
| 162 | OOPS_N=$("$VCS" --json -C hosted log -n 1 | python3 -c 'import json,sys; print(json.load(sys.stdin)["versions"][0]["n"])') |
| 163 | check "git main has the key" sh -c "$(declare -f wait_view gv); WORK=$WORK; wait_view $OOPS_N && git --git-dir='$WORK/gitroot/acme/shop.git' show main:oops.pem >/dev/null" |
| 164 | check "purge it" "$VCS" -C hosted purge oops.pem --yes |
| 165 | wait_rewritten() { |
| 166 | for _ in $(seq 100); do |
| 167 | [ -z "$(gv log --format=%H main -- oops.pem 2>/dev/null)" ] && gv log -1 --format=%B main 2>/dev/null | grep -q "Onset-Version: $OOPS_N\$" && return 0 |
| 168 | sleep 0.2 |
| 169 | done |
| 170 | return 1 |
| 171 | } |
| 172 | check "git main's history no longer has it" wait_rewritten |
| 173 | check "the commits before it kept their IDs" test "$(gv rev-parse main~2)" = "$BEFORE" |
| 174 | check "the later version is there" sh -c "git --git-dir='$WORK/gitroot/acme/shop.git' show main:after-oops.txt | grep -q 'after oops'" |
| 175 | check "git fsck passes after the rewrite" gv fsck --strict --no-dangling |
| 176 | check "vcs git says it's current" sh -c "'$VCS' -C hosted git | grep -q 'version $OOPS_N is commit'" |
| 177 | N=$OOPS_N |
| 178 |
| 179 | # A submodule's commit changes from vcs, and the git view has the new one. |
| 180 | NEWLIB=$(cd lib && printf 'lib two\n' > lib.txt && g commit -qam "Lib two" && git rev-parse HEAD) |
| 181 | check "point the submodule at another commit" "$VCS" -C hosted submodule vendor/lib "$NEWLIB" |
| 182 | check "it lands" "$VCS" -C hosted land -m "Lib two in shop" |
| 183 | N=$((N + 1)) |
| 184 | check "the git view has the new commit" sh -c "$(declare -f wait_view gv); WORK=$WORK; wait_view $N && git --git-dir='$WORK/gitroot/acme/shop.git' ls-tree main vendor/lib | grep -q '^160000 commit $NEWLIB'" |
| 185 | check "git fsck still passes" gv fsck --strict --no-dangling |
| 186 |
| 187 | # Private paths stay out of the git view. |
| 188 | check "make secret/ private" "$VCS" -C hosted access secret/ @alice |
| 189 | mkdir -p hosted/secret && printf 'key\n' > hosted/secret/key.txt && printf 'open\n' > hosted/open.txt |
| 190 | N=$((N + 1)) |
| 191 | check "land a secret" "$VCS" -C hosted land -m "Secret and open" |
| 192 | check "git view gets it" wait_view "$N" |
| 193 | check "open file in the git view" sh -c "git --git-dir='$WORK/gitroot/acme/shop.git' show main:open.txt | grep -q open" |
| 194 | check "secret not in the git view" sh -c "! git --git-dir='$WORK/gitroot/acme/shop.git' cat-file -e main:secret/key.txt" |
| 195 | check "secret still in vcs" test -f hosted/secret/key.txt |
| 196 |
| 197 | wait_sync() { # a file that should show up in the workspace once vcs has git main's commits |
| 198 | for _ in $(seq 60); do "$VCS" -C hosted sync >/dev/null 2>&1; [ -e "hosted/$1" ] && return 0; sleep 0.5; done |
| 199 | return 1 |
| 200 | } |
| 201 | wait_paused() { |
| 202 | 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 |
| 203 | return 1 |
| 204 | } |
| 205 |
| 206 | # Someone pushes to git main: their commits come into vcs, each a version. |
| 207 | g clone -q "http://127.0.0.1:$GPORT/acme/shop.git" outside |
| 208 | (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) |
| 209 | OUTSIDE=$(git -C outside rev-parse main) |
| 210 | N=$((N + 2)) |
| 211 | check "a git push comes into vcs" wait_sync outside2.txt |
| 212 | 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" |
| 213 | check "with its message" sh -c "'$VCS' -C hosted log $((N - 1)) | grep -q 'From a git user.'" |
| 214 | check "git main is still their commit" test "$(gv rev-parse main)" = "$OUTSIDE" |
| 215 | check "vcs git says so" sh -c "'$VCS' -C hosted git | grep -q 'version $N is commit ${OUTSIDE:0:12}'" |
| 216 | printf 'after\n' > hosted/after.txt |
| 217 | N=$((N + 1)) |
| 218 | check "landing on top" "$VCS" -C hosted land -m "After the git push" |
| 219 | check "goes on from their commit" wait_view "$N" |
| 220 | check "their commit is the parent" test "$(gv rev-parse main~1)" = "$OUTSIDE" |
| 221 |
| 222 | # A git push and a vcs land at once: both end up in main and in git. |
| 223 | (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) |
| 224 | GITSIDE=$(git -C outside rev-parse main) |
| 225 | printf 'from vcs\n' > hosted/both-vcs.txt |
| 226 | check "vcs lands meanwhile" "$VCS" -C hosted land -m "Vcs side" |
| 227 | N=$((N + 2)) |
| 228 | check "git's side comes into vcs" wait_sync both-git.txt |
| 229 | check "the git view has both" wait_view "$N" |
| 230 | check "the git commit stays in git's history" gv merge-base --is-ancestor "$GITSIDE" main |
| 231 | 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" |
| 232 | check "git fsck passes after" gv fsck --strict --no-dangling |
| 233 |
| 234 | # A git push into a private folder can't land: the view pauses and says why. |
| 235 | (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) |
| 236 | check "the view pauses" wait_paused |
| 237 | check "and says why" sh -c "'$VCS' -C hosted git | grep -q 'secret/new.txt, which is private'" |
| 238 | check "vcs main is untouched" test ! -e hosted/secret/new.txt |
| 239 | check "resume" "$VCS" -C hosted git resume |
| 240 | printf 'resumed\n' > hosted/resumed.txt |
| 241 | N=$((N + 1)) |
| 242 | check "land after resuming" "$VCS" -C hosted land -m "Resumed" |
| 243 | check "git view catches up" wait_view "$N" |
| 244 | check "the git commit stays in git's history" gv merge-base --is-ancestor "$(git -C outside rev-parse main)" main |
| 245 | check "git fsck passes after resuming" gv fsck --strict --no-dangling |
| 246 |
| 247 | # A team that's left git behind turns the git view off; on again catches up. |
| 248 | check "git view off" sh -c "'$VCS' -C hosted settings git=off | grep -q 'off:'" |
| 249 | AT=$(gv rev-parse main) |
| 250 | for i in 1 2 3; do |
| 251 | printf '%s\n' "$i" > "hosted/off-$i.txt" |
| 252 | N=$((N + 1)) |
| 253 | check "land $i while off" "$VCS" -C hosted land -m "Off $i" |
| 254 | done |
| 255 | sleep 2 |
| 256 | check "git main didn't move" test "$(gv rev-parse main)" = "$AT" |
| 257 | check "git view on" "$VCS" -C hosted settings git=on |
| 258 | check "it catches up" wait_view "$N" |
| 259 | check "a commit per version" test "$(gv rev-list --count "$AT"..main)" = 3 |
| 260 | check "git fsck passes at the end" gv fsck --strict --no-dangling |
| 261 |
| 262 | # The repository's rules (Upstream's Rules page) send lands through change |
| 263 | # requests. Upstream merges one once it's ready, and the merge comes into |
| 264 | # vcs main as the change. |
| 265 | code() { # expected exit code, command... |
| 266 | local want=$1 |
| 267 | shift |
| 268 | "$@" >/dev/null 2>&1 |
| 269 | [ "$?" = "$want" ] |
| 270 | } |
| 271 | stand_in() { curl -s -X POST -H 'content-type: application/json' -d "$2" "http://127.0.0.1:$GPORT/api/test/$1" >/dev/null; } |
| 272 | cr() { curl -s "http://127.0.0.1:$GPORT/api/repos/acme/shop/changes/$1" | jq_ "d['data']$2"; } |
| 273 | wait_main() { # title of main's latest version to wait for |
| 274 | for _ in $(seq 60); do "$VCS" -C hosted sync >/dev/null 2>&1; "$VCS" --json -C hosted log -n 1 | jq_ 'd["versions"][0]["title"]' | grep -qF "$1" && return 0; sleep 0.5; done |
| 275 | return 1 |
| 276 | } |
| 277 | "$VCS" -C hosted sync >/dev/null 2>&1 |
| 278 | stand_in rules/acme/shop '{"minimumApprovals": 1}' |
| 279 | printf 'reviewed\n' > hosted/reviewed.txt |
| 280 | AT=$(gv rev-parse main) |
| 281 | check "a land the rules hold opens a change request (exit 5)" code 5 "$VCS" -C hosted land -m "Needs review" |
| 282 | REVIEWED=$("$VCS" --json -C hosted status | jq_ 'd["change"]["id"]') |
| 283 | check "its branch is in git" gv rev-parse --verify "refs/heads/onset/$REVIEWED" |
| 284 | check "on top of git main" test "$(gv rev-parse "refs/heads/onset/$REVIEWED~1")" = "$AT" |
| 285 | check "main didn't move" test "$(gv rev-parse main)" = "$AT" |
| 286 | check "the change request is open" test "$(cr 1 '["state"]')" = open |
| 287 | check "status shows it" sh -c "'$VCS' -C hosted status | grep -q 'change request #1'" |
| 288 | printf 'more\n' > hosted/reviewed-more.txt |
| 289 | check "landing again updates it (exit 5)" code 5 "$VCS" -C hosted land |
| 290 | check "still change request #1" test "$(cr 1 '["state"]')" = open |
| 291 | check "its branch has both files" sh -c "git --git-dir='$WORK/gitroot/acme/shop.git' show refs/heads/onset/$REVIEWED:reviewed-more.txt | grep -q more" |
| 292 | stand_in ready/acme/shop/1 '{}' |
| 293 | check "Upstream merged it" gv merge-base --is-ancestor "refs/heads/onset/$REVIEWED" main |
| 294 | check "the merge comes into vcs main" wait_main "Needs review (#1)" |
| 295 | check "as the change" test "$("$VCS" --json -C hosted log -n 1 | jq_ 'd["versions"][0]["change"]')" = "$REVIEWED" |
| 296 | check "which is landed" sh -c "'$VCS' --json -C hosted status $REVIEWED | grep -q '\"landed\"'" |
| 297 | stand_in rules/acme/shop '{}' |
| 298 | printf 'free\n' > hosted/free.txt |
| 299 | check "without rules it lands at once" "$VCS" -C hosted land -m "No rules" |
| 300 |
| 301 | # Landing at a set time: the host keeps the change as it is and lands it then. |
| 302 | printf 'later\n' > hosted/later.txt |
| 303 | check "land in a few seconds" "$VCS" -C hosted land -m "Later" --at "in 3s" |
| 304 | check "not yet" sh -c "! '$VCS' -C hosted log -n 1 | grep -q Later" |
| 305 | check "the workspace goes on on top of it" test -n "$("$VCS" --json -C hosted status | jq_ 'd["change"]["on"] or ""')" |
| 306 | check "it lands at its time" wait_main "Later" |
| 307 | check "a time it can't read (exit 2)" code 2 "$VCS" -C hosted land -m "Soon" --at "soonish" |
| 308 | # Under rules it's reviewed now and merges from its time. |
| 309 | stand_in rules/acme/shop '{"minimumApprovals": 1}' |
| 310 | printf 'timed\n' > hosted/timed.txt |
| 311 | check "a land at a time under rules opens its change request now (exit 5)" code 5 "$VCS" -C hosted land -m "Timed review" --at "in 4s" |
| 312 | TIMED=$("$VCS" --json -C hosted status | jq_ 'd["change"]["id"]') |
| 313 | stand_in ready/acme/shop/2 '{}' |
| 314 | check "ready, but it doesn't merge before its time" sh -c "! git --git-dir='$WORK/gitroot/acme/shop.git' merge-base --is-ancestor refs/heads/onset/$TIMED main" |
| 315 | check "it merges after, and comes into main" wait_main "Timed review (#2)" |
| 316 | stand_in rules/acme/shop '{}' |
| 317 |
| 318 | # Landing together: changes in two projects land at once, or neither does. |
| 319 | mkdir web && printf '<h1>web</h1>\n' > web/index.html |
| 320 | (cd web && "$VCS" init >/dev/null && "$VCS" land -m "First web" >/dev/null && "$VCS" publish "$HOST/acme/web" >/dev/null) |
| 321 | check "a second project" "$VCS" -C web status |
| 322 | printf 'shop part\n' > hosted/together.txt |
| 323 | printf 'web part\n' > web/together.txt |
| 324 | "$VCS" -C web describe "Web part" >/dev/null |
| 325 | check "land together" "$VCS" -C hosted land -m "Shop part" --with ../web |
| 326 | check "shop has its part" sh -c "'$VCS' -C hosted log -n 1 | grep -q 'Shop part'" |
| 327 | check "web has its part" sh -c "'$VCS' -C web log -n 1 | grep -q 'Web part'" |
| 328 | check "get a second web workspace" "$VCS" get "$HOST/acme/web" web2 |
| 329 | printf '<h1>web two</h1>\n' > web2/index.html |
| 330 | check "web2 lands a change to index.html" "$VCS" -C web2 land -m "Web2 heading" |
| 331 | printf '<h1>web one</h1>\n' > web/index.html && "$VCS" -C web describe "Web heading" >/dev/null |
| 332 | printf 'shop again\n' > hosted/together2.txt |
| 333 | check "a conflict in one part: nothing lands (exit 3)" code 3 "$VCS" -C hosted land -m "Shop again" --with ../web |
| 334 | check "shop's part didn't land either" sh -c "! '$VCS' -C hosted log -n 1 | grep -q 'Shop again'" |
| 335 | (cd web && git checkout -q 2>/dev/null; "$VCS" sync >/dev/null 2>&1; printf '<h1>web one and two</h1>\n' > index.html) |
| 336 | # Under rules in one project: its part is reviewed, the other's held, and they land together once ready. |
| 337 | stand_in rules/acme/shop '{"minimumApprovals": 1}' |
| 338 | check "together under rules (exit 5)" code 5 "$VCS" -C hosted land -m "Shop with review" --with ../web |
| 339 | check "web's part is held" sh -c "! '$VCS' -C web log -n 1 | grep -q 'Web heading'" |
| 340 | stand_in ready/acme/shop/3 '{}' |
| 341 | check "shop's part merges" wait_main "Shop with review (#3)" |
| 342 | wait_web() { |
| 343 | for _ in $(seq 60); do "$VCS" -C web2 sync >/dev/null 2>&1; "$VCS" -C web2 log -n 1 | grep -q "$1" && return 0; sleep 0.5; done |
| 344 | return 1 |
| 345 | } |
| 346 | check "and web's lands with it" wait_web "Web heading" |
| 347 | stand_in rules/acme/shop '{}' |
| 348 |
| 349 | # Release lines go through the same rules: a land on 1.x under rules is a |
| 350 | # change request into branch 1.x, whose merge comes back as the line's next |
| 351 | # version. Lines take --at and --with too. |
| 352 | wait_line() { # title of 1.x's latest version to wait for |
| 353 | for _ in $(seq 60); do "$VCS" -C hosted-1x sync >/dev/null 2>&1; "$VCS" -C hosted-1x log --line 1.x | head -1 | grep -qF "$1" && return 0; sleep 0.5; done |
| 354 | return 1 |
| 355 | } |
| 356 | "$VCS" -C hosted-1x sync >/dev/null 2>&1 |
| 357 | stand_in rules/acme/shop '{"minimumApprovals": 1}' |
| 358 | printf 'reviewed on the line\n' > hosted-1x/line-reviewed.txt |
| 359 | check "a land on the line under rules opens a change request (exit 5)" code 5 "$VCS" -C hosted-1x land -m "Line review" |
| 360 | LR=$("$VCS" --json -C hosted-1x status | jq_ 'd["change"]["id"]') |
| 361 | check "its branch is on the line's branch" test "$(gv rev-parse "refs/heads/onset/$LR~1")" = "$(gv rev-parse 1.x)" |
| 362 | stand_in ready/acme/shop/4 '{}' |
| 363 | check "Upstream merges it into 1.x" gv merge-base --is-ancestor "refs/heads/onset/$LR" 1.x |
| 364 | check "and it comes into the line" wait_line "Line review (#4)" |
| 365 | check "as the change, landed" sh -c "'$VCS' --json -C hosted-1x status $LR | grep -q '\"landed\"'" |
| 366 | check "main didn't get it" sh -c "! git --git-dir='$WORK/gitroot/acme/shop.git' show main:line-reviewed.txt >/dev/null 2>&1" |
| 367 | stand_in rules/acme/shop '{}' |
| 368 | printf 'later on the line\n' > hosted-1x/line-later.txt |
| 369 | check "a line land at a set time" "$VCS" -C hosted-1x land -m "Line later" --at "in 3s" |
| 370 | check "lands on the line at its time" wait_line "Line later" |
| 371 | "$VCS" -C hosted-1x sync >/dev/null 2>&1 |
| 372 | printf 'line part\n' > hosted-1x/line-together.txt |
| 373 | printf 'web with a line\n' > web/with-line.txt |
| 374 | "$VCS" -C web describe "Web with a line" >/dev/null |
| 375 | check "a line's change lands together with another project's" "$VCS" -C hosted-1x land -m "Line part" --with ../web |
| 376 | check "on the line" wait_line "Line part" |
| 377 | check "and on web's main" wait_web "Web with a line" |
| 378 |
| 379 | # Your own git URL: main with the private folders you can see. A push to it |
| 380 | # lands like vcs land; the shared git view still leaves them out. |
| 381 | MINE=$("$VCS" --json -C hosted git | jq_ 'd.get("mine") or ""') |
| 382 | check "alice has a git URL of her own" test -n "$MINE" |
| 383 | AUTHED=$(printf '%s' "$MINE" | sed "s#http://#http://alice:$ALICE@#") |
| 384 | check "clone it" g clone -q "$AUTHED" mine |
| 385 | check "it has the private folder" test -f mine/secret/key.txt |
| 386 | check "and the rest of main" test -f mine/fromvcs.txt |
| 387 | (cd mine && printf 'rotated\n' > secret/key.txt && g add -A && g commit -qm "Rotate the key" -q) |
| 388 | check "push to it" sh -c "cd mine && git push -q origin main" |
| 389 | check "the push lands" wait_main "Rotate the key" |
| 390 | check "with the private file" sh -c "'$VCS' -C hosted sync >/dev/null 2>&1; grep -q rotated hosted/secret/key.txt" |
| 391 | check "the shared git view still leaves it out" sh -c "! git --git-dir='$WORK/gitroot/acme/shop.git' cat-file -e main:secret/key.txt" |
| 392 | printf 'after mine\n' > hosted/after-mine.txt |
| 393 | check "vcs lands on top" "$VCS" -C hosted land -m "After my push" |
| 394 | check "a pull is a fast-forward" sh -c "cd mine && git pull -q --ff-only && test -f after-mine.txt" |
| 395 | check "someone without a token is asked to sign in" sh -c "! GIT_TERMINAL_PROMPT=0 git clone -q '$MINE' anon" |
| 396 | check "only main and lines can be pushed" sh -c "cd mine && git checkout -qb side && ! git push -q origin side 2>/dev/null" |
| 397 | # Release lines are branches of your own git URL too, and a push to one lands on the line. |
| 398 | check "the line is a branch of it" sh -c "cd mine && git ls-remote origin | grep -q 'refs/heads/1.x'" |
| 399 | check "fetching it" sh -c "cd mine && git fetch -q origin 1.x && git show FETCH_HEAD:linefix.txt | grep -q 'line fix'" |
| 400 | check "a line's branch is a fast-forward of what you have" sh -c "cd mine && git fetch -q origin && git fsck --no-dangling" |
| 401 | (cd mine && git checkout -q -b line-work origin/1.x && printf 'pushed to the line\n' > line-push.txt && g add -A && g commit -qm "Push to 1.x" -q) |
| 402 | check "push to the line's branch" sh -c "cd mine && git push -q origin HEAD:1.x" |
| 403 | check "it lands on the line" wait_line "Push to 1.x" |
| 404 | check "a later fetch has it" sh -c "cd mine && git fetch -q origin && git show origin/1.x:line-push.txt | grep -q 'pushed to the line'" |
| 405 | # A sealed folder stays out of git (only vcs opens it): your git URL leaves |
| 406 | # it out, a push from git keeps it as it is, and a push into it is refused. |
| 407 | check "alice seals vault/" "$VCS" -C hosted access vault @alice --sealed |
| 408 | mkdir -p hosted/vault && printf 'sealed\n' > hosted/vault/s.txt |
| 409 | check "it lands" "$VCS" -C hosted land -m "Sealed vault" |
| 410 | g clone -q "$AUTHED" sealedclone 2>/dev/null |
| 411 | check "a clone leaves it out" sh -c "test ! -e sealedclone/vault && test -f sealedclone/fromvcs.txt" |
| 412 | check "so does the shared git view" sh -c "! git --git-dir='$WORK/gitroot/acme/shop.git' cat-file -e main:vault/s.txt" |
| 413 | (cd sealedclone && printf 'from git\n' > beside-vault.txt && g add -A && g commit -qm "Beside the vault" -q) |
| 414 | check "a push from git" sh -c "cd sealedclone && git push -q origin main" |
| 415 | check "lands" wait_main "Beside the vault" |
| 416 | check "and the vault is as it was" sh -c "'$VCS' -C hosted sync >/dev/null 2>&1; grep -q sealed hosted/vault/s.txt && test -f hosted/beside-vault.txt" |
| 417 | (cd sealedclone && mkdir -p vault && printf 'x\n' > vault/new.txt && g add -A && g commit -qm "Into the vault" -q) |
| 418 | check "a push into it is refused" sh -c "cd sealedclone && ! git push -q origin main 2>/dev/null" |
| 419 |
| 420 | # The repository's "Use vcs" switch (what Upstream's settings page calls): |
| 421 | # on links a project to an existing git repository, whose history comes in |
| 422 | # with the first vcs get; off leaves the repository to git. |
| 423 | g init -q --bare gitroot/acme/old.git |
| 424 | g clone -q "http://127.0.0.1:$GPORT/acme/old.git" oldgit 2>/dev/null |
| 425 | (cd oldgit && printf 'old\n' > old.txt && g add -A && g commit -qm "Old one" && printf 'older\n' > old2.txt && g add -A && g commit -qm "Old two" && g push -q origin main) |
| 426 | internal() { curl -s -X "$1" -H 'x-vcs-internal: test-internal' -H 'content-type: application/json' ${3:+-d "$3"} "http://$HOST/internal/projects/acme/old$2"; } |
| 427 | OWNER_VIEW='"viewer": {"handle": "alice", "role": "owner", "public": false}' |
| 428 | check "turn vcs on" sh -c "$(declare -f internal); HOST=$HOST; internal POST /on '{$OWNER_VIEW, \"repo\": {\"git_url\": \"http://127.0.0.1:$GPORT/acme/old.git\", \"history\": true}}' | grep -q import_pending" |
| 429 | check "a wrong secret is refused" sh -c "curl -s -o /dev/null -w '%{http_code}' -H 'x-vcs-internal: nope' http://$HOST/internal/projects/acme/old | grep -q 403" |
| 430 | check "the first vcs get brings the history in" "$VCS" get "$HOST/acme/old" oldws |
| 431 | check "with its files" test -f oldws/old2.txt |
| 432 | check "a version per git commit" test "$("$VCS" --json -C oldws log | jq_ 'len(d["versions"])')" = 2 |
| 433 | printf 'new\n' > oldws/new.txt |
| 434 | check "vcs lands on it" "$VCS" -C oldws land -m "From vcs" |
| 435 | check "and git gets it" sh -c "for i in \$(seq 50); do git --git-dir='$WORK/gitroot/acme/old.git' log -1 --format=%s main | grep -q 'From vcs' && exit 0; sleep 0.2; done; exit 1" |
| 436 | check "turn vcs off" sh -c "$(declare -f internal); HOST=$HOST; internal POST /off '{$OWNER_VIEW}' | grep -q null" |
| 437 | printf 'off\n' > oldws/off.txt |
| 438 | check "vcs lands are refused while it's off" sh -c "! '$VCS' -C oldws land -m 'While off' 2>&1 | grep -q 'landed'" |
| 439 | check "a member can't turn it on" sh -c "$(declare -f internal); HOST=$HOST; internal POST /on '{\"viewer\": {\"handle\": \"bob\", \"role\": \"member\", \"public\": false}}' | grep -q not_allowed" |
| 440 | check "turn it on again" sh -c "$(declare -f internal); HOST=$HOST; internal POST /on '{$OWNER_VIEW}' | grep -q '\"main\"'" |
| 441 | check "and vcs lands again" "$VCS" -C oldws land -m "While on again" |
| 442 |
| 443 | # Git and vcs both change the same lines: the view pauses until someone |
| 444 | # resolves it. (The view is off while they diverge, so the order is sure.) |
| 445 | "$VCS" -C hosted settings git=off >/dev/null |
| 446 | printf 'vcs line\n' > hosted/clash.txt |
| 447 | check "vcs lands its line" "$VCS" -C hosted land -m "Vcs clash" |
| 448 | (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) |
| 449 | "$VCS" -C hosted settings git=on >/dev/null |
| 450 | check "the view pauses" wait_paused |
| 451 | check "and names the file" sh -c "'$VCS' -C hosted git | grep -q 'both changed clash.txt'" |
| 452 | check "vcs main keeps its line" grep -q 'vcs line' hosted/clash.txt |
| 453 |
| 454 | echo "git: $pass passed, $fail failed" |
| 455 | [ "$fail" = 0 ] |
| 456 |