Onset: version control where nobody waits and nothing gets lost. The on command, its core and onset-server.
| 6 | VCS=${1:-$(cd "$(dirname "$0")/.." && pwd)/target/debug/vcs} |
| 7 | WORK=$(mktemp -d) |
| 8 | export VCS_HOME="$WORK/home" VCS_NAME="Test Person" VCS_EMAIL="test@example.com" VCS_SAVER=0 |
| 9 | trap 'rm -rf "$WORK"' EXIT |
| 10 | cd "$WORK" |
| 11 |
| 12 | pass=0 |
| 13 | fail=0 |
| 14 | check() { # name, command... |
| 15 | local name=$1 |
| 16 | shift |
| 17 | if "$@" >/dev/null 2>&1; then pass=$((pass + 1)); else fail=$((fail + 1)); echo "FAIL: $name"; fi |
| 18 | } |
| 19 | code() { # expected exit code, command... |
| 20 | local want=$1 |
| 21 | shift |
| 22 | "$@" >/dev/null 2>&1 |
| 23 | [ "$?" = "$want" ] |
| 24 | } |
| 25 | json() { "$VCS" --json "$@" 2>/dev/null; } |
| 26 |
| 27 | # A new project with a first version. |
| 28 | mkdir app && cd app |
| 29 | printf 'hello\n' > README.md |
| 30 | mkdir src && printf 'a\nb\nc\nd\ne\nf\ng\n' > src/lib.txt |
| 31 | printf '{\n "name": "app"\n}\n' > package.json |
| 32 | printf 'SECRET=1\n' > .env |
| 33 | check "init" "$VCS" init |
| 34 | check "land first version" "$VCS" land -m "First version" |
| 35 | check "main is version 1" test "$(json log | python3 -c 'import json,sys; print(json.load(sys.stdin)["versions"][0]["n"])')" = 1 |
| 36 | check ".env never landed" test -z "$(json log 1 | python3 -c 'import json,sys; print([f for f in json.load(sys.stdin)["files"] if ".env" in f] or "")')" |
| 37 | check "empty land refused (exit 1)" code 1 "$VCS" land -m "Nothing" |
| 38 |
| 39 | # A second workspace gets the files and the .env. |
| 40 | cd "$WORK" |
| 41 | check "get second workspace" "$VCS" get ./app app2 |
| 42 | check "files arrived" test "$(cat app2/src/lib.txt | head -1)" = a |
| 43 | check ".env came along" test "$(cat app2/.env)" = "SECRET=1" |
| 44 |
| 45 | # Parallel edits to different files both land, in any order. |
| 46 | printf 'hello there\n' > app/README.md |
| 47 | printf 'a\nb\nc\nd\ne\nf\nG\n' > app2/src/lib.txt |
| 48 | check "land from app" "$VCS" -C app land -m "Greet better" |
| 49 | check "land from app2 without syncing" "$VCS" -C app2 land -m "Capital G" |
| 50 | check "main is version 3" test "$(json -C app log | python3 -c 'import json,sys; print(json.load(sys.stdin)["versions"][0]["n"])')" = 3 |
| 51 |
| 52 | # JSON files merge by key. |
| 53 | cd "$WORK" && "$VCS" -C app sync >/dev/null 2>&1 && "$VCS" -C app2 sync >/dev/null 2>&1 |
| 54 | printf '{\n "name": "app",\n "version": "1.0.0"\n}\n' > app/package.json |
| 55 | printf '{\n "name": "app",\n "license": "MIT"\n}\n' > app2/package.json |
| 56 | check "land json key A" "$VCS" -C app land -m "Add version" |
| 57 | check "land json key B (merged by key)" "$VCS" -C app2 land -m "Add license" |
| 58 | "$VCS" -C app sync >/dev/null 2>&1 |
| 59 | check "both keys on main" python3 -c 'import json; d=json.load(open("app/package.json")); assert d["version"]=="1.0.0" and d["license"]=="MIT"' |
| 60 |
| 61 | # A real conflict: land refuses, sync marks it, fixing it lands. |
| 62 | "$VCS" -C app2 sync >/dev/null 2>&1 |
| 63 | perl -pi -e 's/^b$/B from app/' app/src/lib.txt |
| 64 | perl -pi -e 's/^b$/B from app2/' app2/src/lib.txt |
| 65 | check "land A" "$VCS" -C app land -m "B from app" |
| 66 | check "land B conflicts (exit 3)" code 3 "$VCS" -C app2 land -m "B from app2" |
| 67 | check "sync leaves markers (exit 3)" code 3 "$VCS" -C app2 sync |
| 68 | check "markers in file" grep -q '<<<<<<< main' app2/src/lib.txt |
| 69 | check "land refused while markers remain" code 3 "$VCS" -C app2 land |
| 70 | python3 - "$WORK/app2/src/lib.txt" <<'EOF' |
| 71 | import sys, re |
| 72 | p = sys.argv[1] |
| 73 | t = open(p).read() |
| 74 | t = re.sub(r"<<<<<<< [^\n]*\n(.*?)=======\n(.*?)>>>>>>> [^\n]*\n", lambda m: "B from both\n", t, flags=re.S) |
| 75 | open(p, "w", newline="\n").write(t) |
| 76 | EOF |
| 77 | check "land after fixing (reuses description)" "$VCS" -C app2 land |
| 78 | check "status clean after land" test "$(json -C app2 status | python3 -c 'import json,sys; print(len(json.load(sys.stdin)["files"]))')" = 0 |
| 79 |
| 80 | # A file's history, and the version behind each line. |
| 81 | check "log of a file" test "$(json -C app log src/lib.txt | python3 -c 'import json,sys; print(",".join(str(v["n"]) for v in json.load(sys.stdin)["versions"]))')" = 7,6,3,1 |
| 82 | check "log of a folder" sh -c "\"$VCS\" -C app log src | grep -q 'Capital G'" |
| 83 | check "log of a file from inside its folder" sh -c "cd app/src && \"$VCS\" log lib.txt | grep -q 'First version'" |
| 84 | check "blame" test "$(json -C app2 blame src/lib.txt | python3 -c 'import json,sys; print(",".join(str(l["version"]) for l in json.load(sys.stdin)["lines"]))')" = 1,7,1,1,1,1,3 |
| 85 | check "blame of a missing file fails" sh -c "! \"$VCS\" -C app blame nope.txt" |
| 86 | check "show a file in main" sh -c "\"$VCS\" -C app show src/lib.txt | grep -q 'B from both'" |
| 87 | check "show a file at a version" test "$("$VCS" -C app show src/lib.txt --from 1 | tail -1)" = g |
| 88 | check "show a missing file fails" sh -c "! \"$VCS\" -C app show nope.txt" |
| 89 | check "completions" sh -c "\"$VCS\" completions bash | grep -q _vcs" |
| 90 |
| 91 | # Tags: names for versions. |
| 92 | check "tag main's latest" "$VCS" -C app tag v1.0 |
| 93 | check "tag an older version" "$VCS" -C app tag first 1 |
| 94 | check "the list has them" sh -c "\"$VCS\" -C app tag | grep -q 'v1.0' && \"$VCS\" -C app tag | grep -q first" |
| 95 | check "show at a tag" test "$("$VCS" -C app show src/lib.txt --from first | tail -1)" = g |
| 96 | check "log of a tag" sh -c "\"$VCS\" -C app log first | grep -q 'First version'" |
| 97 | check "a tag stays on its version (exit 3)" code 3 "$VCS" -C app tag first 2 |
| 98 | check "a number can't be a tag (exit 2)" code 2 "$VCS" -C app tag 12 |
| 99 | check "remove a tag" "$VCS" -C app tag first --remove |
| 100 | check "it's gone" sh -c "! \"$VCS\" -C app tag | grep -q first" |
| 101 |
| 102 | # Cleaning up: a dropped change's saves and objects go; what's kept still works. |
| 103 | cd "$WORK/app" |
| 104 | "$VCS" sync >/dev/null 2>&1 |
| 105 | head -c 200000 /dev/urandom > scratch.bin |
| 106 | "$VCS" status >/dev/null 2>&1 |
| 107 | "$VCS" drop >/dev/null 2>&1 |
| 108 | check "gc --dry-run says what would go" sh -c "VCS_GC_FRESH=0 '$VCS' gc --older 0s --dry-run | grep -q 'would remove'" |
| 109 | check "gc removes the dropped change's objects" test "$(VCS_GC_FRESH=0 json gc --older 0s | python3 -c 'import json,sys; print(json.load(sys.stdin)["objects"] > 0)')" = True |
| 110 | check "main still reads" sh -c "\"$VCS\" show src/lib.txt | grep -q 'B from both'" |
| 111 | check "history still reads" sh -c "\"$VCS\" log src/lib.txt | grep -q 'First version'" |
| 112 | check "nothing is damaged" "$VCS" check |
| 113 | check "a second gc has nothing left" test "$(VCS_GC_FRESH=0 json gc --older 0s | python3 -c 'import json,sys; print(json.load(sys.stdin)["objects"])')" = 0 |
| 114 | cd "$WORK" |
| 115 |
| 116 | # Undo a land: main gets a version that takes it back out. |
| 117 | cd "$WORK/app" |
| 118 | "$VCS" sync >/dev/null 2>&1 |
| 119 | printf 'oops\n' > oops.txt |
| 120 | check "land oops" "$VCS" land -m "Oops" |
| 121 | check "undo the land" "$VCS" undo |
| 122 | check "oops is open again" test "$(json status | python3 -c 'import json,sys; print(json.load(sys.stdin)["change"]["state"])')" = open |
| 123 | check "main took it back" test "$(json log -n 1 | python3 -c 'import json,sys; print(json.load(sys.stdin)["versions"][0]["title"][:13])')" = "Take back mai" |
| 124 | check "drop it" "$VCS" drop |
| 125 | check "undo the drop" "$VCS" undo |
| 126 | check "drop again" "$VCS" drop |
| 127 |
| 128 | # Restore a file from an earlier save. |
| 129 | printf 'v1\n' > notes.txt |
| 130 | "$VCS" status >/dev/null 2>&1 |
| 131 | sleep 3 |
| 132 | printf 'broken\n' > notes.txt |
| 133 | "$VCS" status >/dev/null 2>&1 |
| 134 | check "restore --at" "$VCS" restore notes.txt --at "2s ago" |
| 135 | check "notes is v1" test "$(cat notes.txt)" = v1 |
| 136 | check "undo the restore" "$VCS" undo |
| 137 | check "notes is broken again" test "$(cat notes.txt)" = broken |
| 138 | check "restore to base (removes the new file)" "$VCS" restore notes.txt |
| 139 | check "notes gone" test ! -e notes.txt |
| 140 |
| 141 | # New, switch, stacks. |
| 142 | printf 'feature a\n' > a.txt |
| 143 | check "describe" "$VCS" describe "Feature A" |
| 144 | A=$(json status | python3 -c 'import json,sys; print(json.load(sys.stdin)["change"]["id"])') |
| 145 | check "new stacked on A" "$VCS" new "Feature B" --on "$A" |
| 146 | printf 'feature b\n' > b.txt |
| 147 | check "a.txt visible in B" test -e a.txt |
| 148 | check "switch back to A" "$VCS" switch "${A:0:6}" |
| 149 | check "b.txt gone in A" test ! -e b.txt |
| 150 | check "undo switch" "$VCS" undo |
| 151 | check "b.txt back" test -e b.txt |
| 152 | check "land B lands A then B" "$VCS" land |
| 153 | check "main has both" test "$(json log -n 2 | python3 -c 'import json,sys; print(",".join(v["title"] for v in json.load(sys.stdin)["versions"]))')" = "Feature B,Feature A" |
| 154 |
| 155 | # Secrets don't land. |
| 156 | printf 'const key = "sk_live_%s";\n' "$(printf 'x%.0s' {1..24})" > pay.js |
| 157 | check "secret refused (exit 8)" code 8 "$VCS" land -m "Payments" |
| 158 | rm pay.js |
| 159 | printf 'const key = process.env.STRIPE_KEY;\n' > pay.js |
| 160 | check "lands without the secret" "$VCS" land -m "Payments" |
| 161 |
| 162 | # vcs.toml [land] run: the workspace's own checks before it lands. |
| 163 | printf '[land]\nrun = ["exit 1"]\n' > vcs.toml |
| 164 | check "a failing [land] run stops the land (exit 4)" code 4 "$VCS" land -m "Checks" |
| 165 | check "--no-run lands anyway" "$VCS" land -m "Checks" --no-run |
| 166 | printf '[land]\nrun = ["exit 0"]\n' > vcs.toml |
| 167 | check "a passing [land] run lands" "$VCS" land -m "Checks pass" |
| 168 |
| 169 | # No description. |
| 170 | printf 'x\n' > x.txt |
| 171 | check "no description (exit 2)" code 2 "$VCS" land |
| 172 | check "describe then land" "$VCS" land -m "Add x" |
| 173 |
| 174 | # Diff and log output. |
| 175 | printf 'y\n' > x.txt |
| 176 | check "diff shows the edit" sh -c "\"$VCS\" diff | grep -q '^+y'" |
| 177 | check "log --saves" "$VCS" log --saves |
| 178 | check "log --ops" "$VCS" log --ops |
| 179 | check "log --changes" "$VCS" log --changes |
| 180 | check "guide" sh -c "\"$VCS\" guide | grep -q 'vcs land'" |
| 181 |
| 182 | # Deleting a workspace is deleting its folder; a new one can take its place. |
| 183 | cd "$WORK" |
| 184 | check "get a third workspace" "$VCS" get ./app app3 |
| 185 | rm -rf app3 |
| 186 | check "get again at the same path" "$VCS" get ./app app3 |
| 187 | cd "$WORK/app" |
| 188 |
| 189 | # Mine: vcs access <path> me. |
| 190 | printf 'token\n' > secrets.local |
| 191 | check "access me" "$VCS" access secrets.local me |
| 192 | check "mine stays out of the change" sh -c "! \"$VCS\" --json status | grep -q secrets.local" |
| 193 |
| 194 | # An agent each: workspaces added next to this one, and cleaned up once done. |
| 195 | check "add a workspace" "$VCS" workspaces add ../agent1 |
| 196 | check "it has main's files" test -f ../agent1/README.md |
| 197 | printf 'parent work\n' > parent.txt |
| 198 | check "add one on this change" "$VCS" workspaces add ../sub1 --on . |
| 199 | check "it starts from this change's work" test -f ../sub1/parent.txt |
| 200 | check "the list has them" sh -c "'$VCS' workspaces | grep -q agent1 && '$VCS' workspaces | grep -q sub1" |
| 201 | printf 'agent work\n' > ../agent1/agent.txt && printf 'sub work\n' > ../sub1/sub.txt |
| 202 | check "clean (nothing idle long enough)" "$VCS" workspaces clean |
| 203 | check "keeps them all" test -d ../agent1 -a -d ../sub1 -a -d ../app2 |
| 204 | check "clean, no idle time" "$VCS" workspaces clean --idle 0s |
| 205 | check "keeps work that hasn't landed" test -f ../agent1/agent.txt -a -f ../sub1/sub.txt |
| 206 | check "removes a workspace whose change was empty" test ! -e ../app3 |
| 207 | check "never this one" test -d .vcs |
| 208 | check "agent1 lands" "$VCS" -C ../agent1 land -m "Agent work" |
| 209 | check "clean again" "$VCS" workspaces clean --idle 0s |
| 210 | check "removes it once its work landed" test ! -e ../agent1 |
| 211 | check "and forgets it" sh -c "! '$VCS' workspaces | grep -q agent1" |
| 212 | check "main has its work" sh -c "'$VCS' sync >/dev/null && test -f agent.txt" |
| 213 |
| 214 | # Renames: a move shows as one, an edit made elsewhere follows the file to |
| 215 | # its new name when it lands, and history follows it back. |
| 216 | cd "$WORK" |
| 217 | "$VCS" get ./app ren1 >/dev/null 2>&1 |
| 218 | mkdir -p ren1/notes && for i in $(seq 1 30); do printf 'note line %s\n' "$i"; done > ren1/notes/long.txt |
| 219 | check "land a long file" "$VCS" -C ren1 land -m "Long notes" |
| 220 | "$VCS" get ./app ren2 >/dev/null 2>&1 |
| 221 | mkdir -p ren1/docs && mv ren1/notes/long.txt ren1/docs/long.md |
| 222 | check "status shows the move" sh -c "'$VCS' -C ren1 status | grep -q 'R notes/long.txt -> docs/long.md'" |
| 223 | check "land the move" "$VCS" -C ren1 land -m "Move the notes" |
| 224 | perl -pi -e 's/^note line 20$/note line twenty/' ren2/notes/long.txt |
| 225 | check "an edit to the old name lands" "$VCS" -C ren2 land -m "Edit the notes" |
| 226 | "$VCS" -C ren1 sync >/dev/null 2>&1 |
| 227 | check "the edit followed the file" grep -q 'note line twenty' ren1/docs/long.md |
| 228 | check "and the old name is gone" test ! -e ren1/notes/long.txt |
| 229 | check "log follows the move" sh -c "'$VCS' -C ren1 log docs/long.md | grep -q 'from notes/long.txt' && '$VCS' -C ren1 log docs/long.md | grep -q 'Long notes'" |
| 230 | LONG=$(json -C ren1 log docs/long.md | python3 -c 'import json,sys; print([v["n"] for v in json.load(sys.stdin)["versions"] if v["title"] == "Long notes"][0])') |
| 231 | check "blame goes back past the move" test "$(json -C ren1 blame docs/long.md | python3 -c 'import json,sys; print(json.load(sys.stdin)["lines"][0]["version"])')" = "$LONG" |
| 232 |
| 233 | # Bisect: the first version where a check fails, tried in a folder of its own. |
| 234 | mkdir "$WORK/bis" && cd "$WORK/bis" |
| 235 | printf 'fine\n' > state.txt |
| 236 | "$VCS" init >/dev/null 2>&1 && "$VCS" land -m "v1" >/dev/null 2>&1 |
| 237 | for i in 2 3 4 5 6; do printf 'fine %s\n' "$i" > state.txt; "$VCS" land -m "v$i" >/dev/null 2>&1; done |
| 238 | printf 'broken\n' > state.txt && "$VCS" land -m "Breaks it" >/dev/null 2>&1 |
| 239 | for i in 8 9 10; do printf 'broken %s\n' "$i" > state.txt; "$VCS" land -m "v$i" >/dev/null 2>&1; done |
| 240 | check "bisect finds the version that broke it" test "$(json bisect 1 10 --run 'grep -q fine state.txt' | python3 -c 'import json,sys; print(json.load(sys.stdin)["first_bad"])')" = 7 |
| 241 | check "the workspace's files didn't change" grep -q 'broken 10' state.txt |
| 242 | check "with --in it keeps the folder" sh -c "'$VCS' bisect 1 main --run 'grep -q fine state.txt' --in ../bis-try >/dev/null && test -f ../bis-try/state.txt" |
| 243 | printf 'grep -qx "fine 6" state.txt && exit 125\ngrep -q fine state.txt\n' > "$WORK/skip-check.sh" |
| 244 | check "exit 125 skips a version" test "$(json bisect 1 10 --run "sh $WORK/skip-check.sh" | python3 -c 'import json,sys; print(json.load(sys.stdin)["first_bad"])')" = 7 |
| 245 | check "a good version after the bad one is refused (exit 2)" code 2 "$VCS" bisect 10 1 --run true |
| 246 |
| 247 | echo "local: $pass passed, $fail failed" |
| 248 | [ "$fail" = 0 ] |
| 249 |