2# Self-test for sync-forkgram.sh and export-patches.sh. Builds a fake Forkgram remote and a
3# fake MultiGram remote in a temporary folder (nothing touches GitHub or your clone) and
4# runs the sync through its cases: no new release, --force, a release that applies, a
5# release that conflicts, the hand fix (rerere, a multigram-next the sync must not replace,
6# --use-next and the check that it carries all of multigram), promote, the .github guard,
7# a missing multigram branch, local branches that must not move, merge commits, a full
8# clone that must stay full, and the patch export.
10# bash scripts/selftest-sync.sh [--keep] (--keep leaves the folder for a look)
13HERE=$(cd "$(dirname "$0")" && pwd)
14SYNC=$HERE/sync-forkgram.sh
15EXPORT=$HERE/export-patches.sh
16T=$(mktemp -d "${TMPDIR:-/tmp}/multigram-selftest.XXXXXX")
18[ "${1:-}" != --keep ] || KEEP=yes
19trap '[ $KEEP = yes ] && echo "left in $T" || rm -rf "$T"' EXIT
21# A clean git setup, whatever the user's own config says.
22export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=$T/gitconfig
23git config --global user.name "Self Test"
24git config --global user.email selftest@example.invalid
25git config --global init.defaultBranch main
26git config --global advice.detachedHead false
27unset GITHUB_ACTIONS GITHUB_OUTPUT GITHUB_STEP_SUMMARY RUNNER_TEMP FORKGRAM_URL FORKGRAM_BRANCH || true
28export FORKGRAM_URL=file://$T/forkgram.git FORKGRAM_BRANCH=dev
32ok() { PASS=$((PASS + 1)); printf ' ok %s\n' "$1"; }
33bad() { FAIL=$((FAIL + 1)); printf ' FAIL %s\n' "$1"; }
34eq() { if [ "$2" = "$3" ]; then ok "$1"; else bad "$1: got [$2], want [$3]"; fi; }
35has() { case $2 in *"$3"*) ok "$1" ;; *) bad "$1: [$3] not in [$2]" ;; esac; }
36title() { printf '\n== %s\n' "$*"; }
38# Runs the sync script; keeps its exit code in RC and its outputs in $T/out.
44 (cd "$d" && GITHUB_OUTPUT=$T/out bash "$SYNC" "$@") >"$T/log" 2>&1 || RC=$?
45 sed -e '/^---- outputs ----$/,$d' -e 's/^/ | /' "$T/log"
47o() { # key -> value from the last run's outputs
48 python3 - "$T/out" "$1" <<'PY'
50path, key = sys.argv[1], sys.argv[2]
51lines = open(path).read().split('\n')
56 k, d = line.split('<<', 1)
57 j = lines.index(d, i + 1)
58 vals[k] = '\n'.join(lines[i + 1:j])
62print(vals.get(key, ''), end='')
65rref() { git --git-dir="$T/multigram.git" rev-parse -q --verify "$1" || true; }
67# ------------------------------------------------------------------ fake Forkgram
69git init -q --bare "$T/forkgram.git"
70git --git-dir="$T/forkgram.git" config uploadpack.allowFilter true
71git --git-dir="$T/forkgram.git" config uploadpack.allowAnySHA1InWant true
75main_java() { # name line, tail line
76 printf 'class Main {\n String name = "%s";\n int a = 1;\n int b = 2;\n int c = 3;\n int d = 4;\n // %s\n}\n' "$1" "$2"
78props() { printf 'APP_VERSION_CODE=%s\nAPP_VERSION_NAME=%s\nAPP_PACKAGE=org.example\n' "$1" "$2"; }
79gitlink() { git update-index --add --cacheinfo "160000,$1,$2"; }
80subst() { sed "$1" "$2" >"$2.new" && mv "$2.new" "$2"; } # sed -i, on any sed
81setver() { subst "s/^APP_VERSION_NAME=.*/APP_VERSION_NAME=$1/" gradle.properties; }
82upstream() { # version code name tail sublink : a Telegram "update to" commit
84 props "$2" "$1" >gradle.properties
85 main_java "$3" "$4" >app/Main.java
86 printf 'class Util {}\n' >app/Util.java
87 printf '[submodule "lib/sub"]\n\tpath = lib/sub\n\turl = https://example.invalid/sub.git\n[submodule "topsub"]\n\tpath = topsub\n\turl = https://example.invalid/top.git\n' >.gitmodules
90 gitlink 2222222222222222222222222222222222222222 topsub
91 git commit -q -m "update to $1 ($2)"
93fork_commits() { # version : Forkgram's own commits, then the release commit
94 mkdir -p .github/workflows
95 printf 'on: push\n' >.github/workflows/ci.yml
96 printf 'class Fork {}\n' >app/Fork.java
98 git commit -q -m "Added fork settings."
100 printf '// release %s\n' "$1" >>app/Fork.java
101 git commit -q -am "[Release] $1."
104upstream 1.0.0 100 Fork "end" 1111111111111111111111111111111111111111
106git push -q "$T/forkgram.git" HEAD:refs/heads/dev
107R1=$(git rev-parse HEAD)
108U1=$(git rev-parse HEAD~2)
110# ------------------------------------------------------------------ fake MultiGram
112git init -q --bare "$T/multigram.git"
113git --git-dir="$T/multigram.git" config uploadpack.allowFilter true
114git --git-dir="$T/multigram.git" config uploadpack.allowAnySHA1InWant true
115git init -q "$T/mgsetup"
117git fetch -q "$T/forkgram.git" dev
118tree=$(git ls-tree FETCH_HEAD | grep -v " .github$" | git mktree)
119S1=$(printf 'Forkgram 1.0.1 snapshot\n\nSource: https://github.com/forkgram/TelegramAndroid/commit/%s\nForkgram-Commit: %s\nForkgram-Version: 1.0.1\nTelegram-Base: %s (update to 1.0.0)\n\nTree is Forkgram'"'"'s dev branch at that commit with the .github folder removed.\n' "$R1" "$R1" "$U1" | git commit-tree "$tree")
120git switch -q -c multigram "$S1"
122printf 'MultiGram notes\n' >multigram/README.md
123git add -A && git commit -q -m "Add MultiGram notes"
124subst 's/"Fork"/"MultiGram"/' app/Main.java
125git commit -q -am "Rename the app"
126printf 'class Util { /* MultiGram: hook */ }\n' >app/Util.java
127git commit -q -am "Add a MultiGram hook"
128git switch -q --orphan main
129printf 'main\n' >README.md
130git add README.md && git commit -q -m "Start main"
131git push -q "$T/multigram.git" main "$S1:refs/heads/forkgram" multigram
134# "CI": a blobless, depth-1 clone of main, like actions/checkout with filter: blob:none.
137 git clone -q --filter=blob:none --depth 1 --branch main "file://$T/multigram.git" "$T/ci"
138 git -C "$T/ci" config user.name "github-actions[bot]"
139 git -C "$T/ci" config user.email "41898282+github-actions[bot]@users.noreply.github.com"
142# ------------------------------------------------------------------ A: no new release
143title "A. No new Forkgram release"
145run "$T/ci" --remote origin --push
146eq "exit code" "$RC" 0
147eq "status" "$(o status)" up-to-date
148eq "new_snapshot" "$(o new_snapshot)" no
149eq "forkgram unchanged" "$(rref forkgram)" "$S1"
150eq "no multigram-next" "$(rref multigram-next)" ""
152# ------------------------------------------------------------------ B: --force
153title "B. --force with no new release, then promote"
155run "$T/ci" --remote origin --push --force
156eq "exit code" "$RC" 0
157eq "status" "$(o status)" candidate
158eq "new_snapshot" "$(o new_snapshot)" no
159eq "candidate is multigram (nothing to replay)" "$(o candidate)" "$MG0"
160eq "multigram-next pushed" "$(rref multigram-next)" "$MG0"
161eq "pushed" "$(o pushed)" multigram-next
162cand=$(o candidate) prev=$(o multigram) tv=$(o tag_version)
164run "$T/ci" promote --remote origin --candidate "$cand" --previous "$prev" --version "$tv" --shallow
165eq "promote exit code" "$RC" 0
166eq "promote status" "$(o status)" unchanged
167eq "multigram unchanged" "$(rref multigram)" "$MG0"
168eq "multigram-next deleted" "$(rref multigram-next)" ""
170# ------------------------------------------------------------------ C: clean release
171title "C. New Forkgram release 1.1.0 that applies cleanly"
173git reset -q --hard "$U1"
174printf 'class Other {}\n' >app/Other.java
176upstream 1.1.0 110 Fork "end, 1.1.0" 3333333333333333333333333333333333333333
177U2=$(git rev-parse HEAD)
179R2=$(git rev-parse HEAD)
180git push -q -f "$T/forkgram.git" HEAD:refs/heads/dev
182run "$T/ci" --remote origin --push
183eq "exit code" "$RC" 0
184eq "status" "$(o status)" candidate
185eq "new_snapshot" "$(o new_snapshot)" yes
186eq "forkgram_commit" "$(o forkgram_commit)" "$R2"
187eq "forkgram_version" "$(o forkgram_version)" 1.1.0
188eq "telegram_base" "$(o telegram_base)" "$U2 (update to 1.1.0)"
189eq "previous_version" "$(o previous_version)" 1.0.1
191eq "forkgram pushed" "$S2" "$(o snapshot)"
192eq "snapshot parent is the previous snapshot" "$(git --git-dir="$T/multigram.git" rev-parse "$S2^")" "$S1"
193eq "snapshot has no .github" "$(git --git-dir="$T/multigram.git" ls-tree --name-only "$S2" .github)" ""
194want=$(git -C "$T/fg" ls-tree "$R2" | grep -v " .github$")
195eq "snapshot tree = Forkgram tree minus .github" "$(git --git-dir="$T/multigram.git" ls-tree "$S2")" "$want"
196eq "gitlink lib/sub kept" "$(git --git-dir="$T/multigram.git" ls-tree "$S2" lib/sub | cut -f1)" "160000 commit 3333333333333333333333333333333333333333"
197eq "gitlink topsub kept" "$(git --git-dir="$T/multigram.git" ls-tree "$S2" topsub | cut -f1)" "160000 commit 2222222222222222222222222222222222222222"
198msg=$(git --git-dir="$T/multigram.git" log -1 --format=%B "$S2")
199has "message title" "$msg" "Forkgram 1.1.0 snapshot"
200has "message Source" "$msg" "Source: file://$T/forkgram.git (dev) at $R2"
201has "message Forkgram-Commit" "$msg" "Forkgram-Commit: $R2"
202has "message Forkgram-Version" "$msg" "Forkgram-Version: 1.1.0"
203has "message Telegram-Base" "$msg" "Telegram-Base: $U2 (update to 1.1.0)"
204has "message tail" "$msg" "Tree is Forkgram's dev branch at that commit with the .github folder removed."
205C1=$(rref multigram-next)
206eq "multigram-next = candidate" "$C1" "$(o candidate)"
207eq "candidate has 3 commits on the snapshot" "$(git --git-dir="$T/multigram.git" rev-list --count "$S2..$C1")" 3
208eq "candidate subjects" "$(git --git-dir="$T/multigram.git" log --reverse --format=%s "$S2..$C1" | tr '\n' '|')" "Add MultiGram notes|Rename the app|Add a MultiGram hook|"
209eq "candidate Main.java: stack change on the new upstream" "$(git --git-dir="$T/multigram.git" show "$C1:app/Main.java" | sed -n '2p;7p' | tr '\n' '|')" ' String name = "MultiGram";| // end, 1.1.0|'
210eq "candidate keeps upstream's new file" "$(git --git-dir="$T/multigram.git" show "$C1:app/Other.java")" "class Other {}"
211eq "candidate tree = snapshot + stack diff" \
212 "$(git --git-dir="$T/multigram.git" diff --stat "$S2" "$C1" | tail -n1)" \
213 "$(git --git-dir="$T/multigram.git" diff --stat "$S1" "$MG0" | tail -n1)"
214eq "multigram not moved yet" "$(rref multigram)" "$MG0"
215eq "the sync recorded the multigram-next it built" "$(rref refs/multigram-sync/next)" "$C1"
216cand=$(o candidate) prev=$(o multigram) tv=$(o tag_version)
218run "$T/ci" promote --remote origin --candidate "$cand" --previous "$prev" --version "$tv" --shallow
219eq "promote exit code" "$RC" 0
220eq "promote status" "$(o status)" promoted
221eq "multigram moved to the candidate" "$(rref multigram)" "$C1"
222eq "old multigram tagged" "$(rref refs/tags/multigram-before-1.1.0)" "$MG0"
223eq "multigram-next deleted" "$(rref multigram-next)" ""
225# ------------------------------------------------------------------ D: conflicting release
226title "D. New Forkgram release 1.2.0 that conflicts"
228git reset -q --hard "$U2"
229upstream 1.2.0 120 "Fork 2" "end, 1.1.0" 3333333333333333333333333333333333333333
231R3=$(git rev-parse HEAD)
232git push -q -f "$T/forkgram.git" HEAD:refs/heads/dev
235run "$T/ci" --remote origin --push
236eq "exit code" "$RC" 2
237eq "status" "$(o status)" conflict
239eq "snapshot pushed anyway" "$S3" "$(o snapshot)"
240eq "forkgram_version" "$(o forkgram_version)" 1.2.0
241eq "conflict commit is the rename" "$(o conflict_commit)" "$(git --git-dir="$T/multigram.git" rev-parse "$MG1~1")"
242eq "conflict subject" "$(o conflict_subject)" "Rename the app"
243eq "conflict position" "$(o conflict_position)" "2 of 3"
244eq "conflict files" "$(o conflict_files)" app/Main.java
245eq "conflict details" "$(o conflict_details)" "changed on both sides: app/Main.java"
246eq "multigram untouched" "$(rref multigram)" "$MG1"
247eq "no multigram-next" "$(rref multigram-next)" ""
248eq "no candidate" "$(o candidate)" ""
249eq "pushed" "$(o pushed)" forkgram
251# ------------------------------------------------------------------ E: behind
252title "E. Next run, no new release: multigram is behind"
254run "$T/ci" --remote origin --push
255eq "exit code" "$RC" 0
256eq "status" "$(o status)" behind
257eq "multigram_base is the old snapshot" "$(o multigram_base)" "$S2"
258eq "nothing pushed" "$(o pushed)" ""
260# ------------------------------------------------------------------ F: hand fix with rerere
261title "F. Hand fix in a local clone; rerere replays it on the next local run"
262git clone -q "file://$T/multigram.git" "$T/owner"
264git config rerere.enabled true
265git switch -q -C multigram-next origin/multigram
266if git rebase -q --onto "$S3" "$S2" >/dev/null 2>&1; then bad "hand rebase should conflict"; fi
267main_java "MultiGram 2" "end, 1.1.0" >app/Main.java
269GIT_EDITOR=true git rebase --continue >/dev/null 2>&1
270FIX=$(git rev-parse HEAD)
271eq "hand fix has 3 commits on the new snapshot" "$(git rev-list --count "$S3..$FIX")" 3
272git switch -q --detach
273git branch -q -D multigram-next
274# The owner's own branches, with rebase.updateRefs on: the replay must not move them.
275git config rebase.updateRefs true
276git branch -q -f multigram origin/multigram
277git branch -q -f wip origin/multigram~1
278run "$T/owner" --remote origin --force
279git config --unset rebase.updateRefs
280eq "exit code" "$RC" 0
281eq "status" "$(o status)" candidate
282eq "local multigram not moved (rebase.updateRefs)" "$(git rev-parse multigram)" "$MG1"
283eq "local wip not moved (rebase.updateRefs)" "$(git rev-parse wip)" "$(git rev-parse "$MG1~1")"
284eq "local record = local multigram-next" "$(git rev-parse refs/multigram-sync/next)" "$(git rev-parse multigram-next)"
285has "rerere_resolved names the file" "$(o rerere_resolved)" "app/Main.java"
286eq "local multigram-next has the recorded resolution" "$(git show multigram-next:app/Main.java | sed -n 2p)" ' String name = "MultiGram 2";'
287eq "same tree as the hand fix" "$(git rev-parse "multigram-next^{tree}")" "$(git rev-parse "$FIX^{tree}")"
288eq "local forkgram = latest snapshot" "$(git rev-parse forkgram)" "$S3"
289eq "nothing pushed without --push" "$(rref multigram-next)" ""
291# ------------------------------------------------------------------ G: --use-next
292title "G. The hand fix on multigram-next: never replaced; --use-next checks it, then promote"
293git push -q -f origin "$FIX:refs/heads/multigram-next"
295run "$T/ci" --remote origin --push --force
296eq "--force: exit code" "$RC" 0
297eq "--force: status" "$(o status)" next-edited
298eq "--force: existing_next" "$(o existing_next)" "$FIX"
299eq "--force: multigram-next kept" "$(rref multigram-next)" "$FIX"
300eq "--force: nothing pushed" "$(o pushed)" ""
303git commit -q -am "[Release] 1.2.1."
304git push -q -f "$T/forkgram.git" HEAD:refs/heads/dev
306run "$T/ci" --remote origin --push
307eq "new release: status" "$(o status)" next-edited
308eq "new release: forkgram_tip is the new commit" "$(o forkgram_tip)" "$(git -C "$T/fg" rev-parse HEAD)"
309eq "new release: no new snapshot" "$(rref forkgram)" "$S3"
310eq "new release: multigram-next kept" "$(rref multigram-next)" "$FIX"
311has "new release: message says why" "$(o message)" "has commits the sync did not make"
312git reset -q --hard "$R3"
313git push -q -f "$T/forkgram.git" HEAD:refs/heads/dev
316git switch -q -C late origin/multigram
317printf 'late\n' >app/Late.java
318git add -A && GIT_AUTHOR_DATE="2030-01-01T00:00:00Z" git commit -q -m "Late MultiGram commit"
319LATE=$(git rev-parse HEAD)
320git push -q origin late:multigram
321git switch -q --detach
324run "$T/ci" --remote origin --push --use-next
325eq "late commit: exit code" "$RC" 1
326eq "late commit: status" "$(o status)" error
327eq "late commit: missing" "$(o missing)" "$LATE Late MultiGram commit"
328has "late commit: error names it" "$(o error)" "Late MultiGram commit"
329has "late commit: error offers allow_drop" "$(o error)" "allow_drop"
331run "$T/ci" --remote origin --push --use-next --allow-drop
332eq "--allow-drop: status" "$(o status)" candidate
333eq "--allow-drop: dropped" "$(o dropped)" "$LATE Late MultiGram commit"
335run "$T/ci" --remote origin --push --allow-drop
336eq "--allow-drop without --use-next: exit code" "$RC" 1
337git -C "$T/owner" push -q -f origin "$MG1:refs/heads/multigram"
339run "$T/ci" --remote origin --push --use-next
340eq "exit code" "$RC" 0
341eq "status" "$(o status)" candidate
342eq "candidate is the hand fix" "$(o candidate)" "$FIX"
343eq "the hand fix carries the whole stack" "$(o missing)$(o dropped)" ""
344eq "forkgram_version" "$(o forkgram_version)" 1.2.0
345cand=$(o candidate) prev=$(o multigram) tv=$(o tag_version)
347run "$T/ci" promote --remote origin --candidate "$cand" --previous "$prev" --version "$tv" --shallow
348eq "promote status" "$(o status)" promoted
349eq "multigram = hand fix" "$(rref multigram)" "$FIX"
350eq "tag" "$(rref refs/tags/multigram-before-1.2.0)" "$MG1"
352run "$T/ci" --remote origin --push
353eq "and now up to date" "$(o status)" up-to-date
355# ------------------------------------------------------------------ H: dropped commit
356title "H. Release 1.3.0 already has one MultiGram change: that commit is dropped"
358git reset -q --hard "$R3"
359mkdir -p multigram && printf 'MultiGram notes\n' >multigram/README.md
360git add -A && git commit -q -m "Take the notes file"
362git commit -q -am "[Release] 1.3.0."
363git push -q -f "$T/forkgram.git" HEAD:refs/heads/dev
365run "$T/ci" --remote origin --push
366eq "status" "$(o status)" candidate
367eq "telegram_base still found" "$(o telegram_base)" "$(git -C "$T/fg" rev-parse "HEAD~4") (update to 1.2.0)"
368eq "stack_count" "$(o stack_count)" 3
369eq "candidate_count" "$(o candidate_count)" 2
370if [ -n "$(o dropped)" ]; then ok "dropped is reported"; else bad "dropped is empty"; fi
371cand=$(o candidate) prev=$(o multigram) tv=$(o tag_version)
373# ------------------------------------------------------------------ I: promote lease
374title "I. Promote refuses when multigram moved after the sync"
377git commit -q --allow-empty -m "Someone else's commit" && git push -q -f origin "HEAD:refs/heads/multigram"
379run "$T/ci" promote --remote origin --candidate "$cand" --previous "$prev" --version "$tv" --shallow
380eq "exit code" "$RC" 1
381eq "status" "$(o status)" error
382has "error says why" "$(o error)" "pushed to meanwhile"
383eq "multigram-next kept" "$(rref multigram-next)" "$cand"
384git push -q -f origin "$prev:refs/heads/multigram"
386run "$T/ci" promote --remote origin --candidate "$cand" --previous "$prev" --version "$tv" --shallow
387eq "promote after restoring multigram" "$(o status)" promoted
389# ------------------------------------------------------------------ J: .github guard
390title "J. A stack commit that adds .github is refused"
393git switch -q -C multigram origin/multigram
394mkdir -p .github && printf 'x\n' >.github/extra.yml
395git add -A && git commit -q -m "Add a workflow file"
396git push -q -f origin multigram
397BAD=$(git rev-parse HEAD)
398F_BEFORE=$(rref forkgram)
400run "$T/ci" --remote origin --push --force
401eq "exit code" "$RC" 1
402eq "status" "$(o status)" error
403has "error names .github" "$(o error)" ".github"
404eq "nothing pushed" "$(rref multigram-next)" ""
405eq "forkgram unchanged" "$(rref forkgram)" "$F_BEFORE"
406git push -q -f origin "$BAD~1:refs/heads/multigram"
407git switch -q --detach
409# ------------------------------------------------------------------ K: guards
410title "K. Guards: branch checked out, no multigram branch"
411git switch -q -C forkgram origin/forkgram
412run "$T/owner" --force
413eq "checked-out forkgram: exit code" "$RC" 1
414has "checked-out forkgram: error" "$(o error)" "checked out in a worktree"
415git switch -q --detach
416git init -q --bare "$T/empty.git"
417git -C "$T/owner" push -q "$T/empty.git" "origin/forkgram:refs/heads/forkgram"
418git -C "$T/owner" remote add empty "$T/empty.git"
419run "$T/owner" --remote empty --push
420eq "no multigram: exit code" "$RC" 1
421has "no multigram: error" "$(o error)" "has no multigram branch yet"
422run "$T/owner" --remote empty --use-next --force
423eq "--force with --use-next: exit code" "$RC" 1
425UNPUSHED=$(git commit-tree -p origin/multigram -m "Unpushed fix" "origin/multigram^{tree}")
426git branch -q -f multigram-next "$UNPUSHED"
427F_BEFORE=$(git rev-parse forkgram)
428run "$T/owner" --remote origin --force
429eq "unpushed local multigram-next: exit code" "$RC" 1
430has "unpushed local multigram-next: error" "$(o error)" "on no other branch or tag"
431eq "unpushed local multigram-next: not moved" "$(git rev-parse multigram-next)" "$UNPUSHED"
432eq "unpushed local multigram-next: local forkgram not moved" "$(git rev-parse forkgram)" "$F_BEFORE"
433git branch -q -D multigram-next
435# ------------------------------------------------------------------ L: local mode
436title "L. Local branches, no remote"
439git branch -q -f forkgram origin/forkgram
440git branch -q -f multigram origin/multigram
442eq "status" "$(o status)" up-to-date
443HAND=$(git commit-tree -p multigram -m "Hand fix" "multigram^{tree}")
444git branch -q -f multigram-next "$HAND"
445run "$T/owner" --force
446eq "hand-made local multigram-next: exit code" "$RC" 0
447eq "hand-made local multigram-next: status" "$(o status)" next-edited
448eq "hand-made local multigram-next: kept" "$(git rev-parse multigram-next)" "$HAND"
449git branch -q -D multigram-next
451# ------------------------------------------------------------------ M: export-patches
452title "M. export-patches.sh, applied onto a plain Forkgram checkout with git am -3"
455bash "$EXPORT" --base origin/forkgram --head origin/multigram "$T/patches" >"$T/log" 2>&1 || EXP_RC=$?
456sed 's/^/ | /' "$T/log" | head -n 8
457eq "export exit code" "$EXP_RC" 0
458eq "patch files" "$(cd "$T/patches" && ls ./*.patch | wc -l | tr -d ' ')" 2
459if [ -f "$T/patches/APPLY.txt" ]; then ok "APPLY.txt written"; else bad "APPLY.txt missing"; fi
460has "APPLY.txt names the Forkgram commit" "$(cat "$T/patches/APPLY.txt")" "$(git -C "$T/fg" rev-parse HEAD)"
461git clone -q -b dev "$T/forkgram.git" "$T/plain"
463git switch -q -c multigram
464if git am -q -3 "$T/patches"/*.patch >/dev/null 2>&1; then ok "git am -3 applies"; else bad "git am -3 failed"; fi
465eq "result = multigram + Forkgram's .github" \
466 "$(git ls-tree -r HEAD | grep -v ' .github/')" \
467 "$(git -C "$T/owner" ls-tree -r origin/multigram)"
469bash "$EXPORT" --base origin/forkgram --head origin/multigram "$T/patches" >/dev/null 2>&1 || EXP_RC=$?
470eq "refuses a non-empty folder without --force" "$EXP_RC" 1
472# ------------------------------------------------------------------ N: Forkgram already fetched
473title "N. Forkgram's tip already in the local clone; a history too short for Telegram-Base"
475printf 'class Later {}\n' >app/Later.java
476git add -A && git commit -q -m "Added a later feature."
478git commit -q -am "[Release] 1.5.0."
479git push -q -f "$T/forkgram.git" HEAD:refs/heads/dev
480R5=$(git rev-parse HEAD)
481git clone -q "file://$T/multigram.git" "$T/full"
482run "$T/full" --remote origin
483eq "full clone: status" "$(o status)" candidate
484has "full clone: fetched into a scratch repository" "$(cat "$T/log")" "into a scratch repository"
485eq "full clone: still not shallow" "$(git -C "$T/full" rev-parse --is-shallow-repository)" false
486eq "full clone: snapshot tree" "$(git -C "$T/full" ls-tree forkgram)" "$(git -C "$T/fg" ls-tree "$R5" | grep -v " .github$")"
487eq "full clone: version from gradle.properties" "$(o forkgram_version)" 1.5.0
488if git -C "$T/full" fsck --no-progress >"$T/fsck" 2>&1; then ok "full clone: fsck is clean"; else bad "full clone: fsck: $(head -n 3 "$T/fsck")"; fi
490git fetch -q "$T/forkgram.git" "+refs/heads/dev:refs/forkgram-full/dev"
492git branch -q -f forkgram origin/forkgram
493git branch -q -f multigram origin/multigram
494FORKGRAM_HISTORY_DEPTH=1 run "$T/owner" --remote origin
495eq "status" "$(o status)" candidate
496has "used the local copy" "$(cat "$T/log")" "already here"
497eq "no Telegram-Base when the history is too short" "$(o telegram_base)" ""
498if git log -1 --format=%B forkgram | grep -q '^Telegram-Base:'; then bad "Telegram-Base line written"; else ok "no Telegram-Base line"; fi
499eq "snapshot tree" "$(git ls-tree forkgram)" "$(git ls-tree "$R5" | grep -v " .github$")"
500eq "local forkgram is the new snapshot, origin untouched" "$(git rev-parse forkgram^)" "$(rref forkgram)"
501eq "not a shallow repository" "$(git rev-parse --is-shallow-repository)" false
503# ------------------------------------------------------------------ O: tag already taken
504title "O. A second promotion for the same Forkgram version gets its own tag"
507git switch -q -C multigram-next origin/multigram
508printf 'more\n' >>multigram/README.md 2>/dev/null || { mkdir -p multigram; printf 'more\n' >multigram/README.md; }
509git add -A && git commit -q -m "Another MultiGram change"
510git push -q -f origin multigram-next
511git switch -q --detach
513run "$T/ci" --remote origin --push --use-next
514cand=$(o candidate) prev=$(o multigram) tv=$(o tag_version)
515eq "same version as the last promotion" "$tv" 1.3.0
517run "$T/ci" promote --remote origin --candidate "$cand" --previous "$prev" --version "$tv" --shallow
518eq "status" "$(o status)" promoted
519eq "tag with a suffix" "$(o tag)" multigram-before-1.3.0-2
520eq "tag points at the old multigram" "$(rref refs/tags/multigram-before-1.3.0-2)" "$prev"
521eq "first tag untouched" "$(rref refs/tags/multigram-before-1.3.0)" "$FIX"
523# ------------------------------------------------------------------ Q: merge commits
524title "Q. Merge commits on multigram: a plain merge is flattened, one with its own changes stops the sync"
527MGQ=$(git rev-parse origin/multigram)
528git switch -q -C side origin/forkgram
529printf 'class Side {}\n' >app/Side.java
530git add -A && git commit -q -m "Side feature"
531git switch -q -C multigram "$MGQ"
532git merge -q --no-ff --no-edit side
533git push -q -f origin multigram
534run "$T/owner" --remote origin --force
535eq "plain merge: exit code" "$RC" 0
536eq "plain merge: status" "$(o status)" candidate
537has "plain merge: warned" "$(cat "$T/log")" "only join their parents"
538eq "plain merge: candidate = snapshot + everything multigram changes" \
539 "$(git diff "$(o snapshot)" multigram-next -- | grep -v '^index ')" "$(git diff origin/forkgram multigram -- | grep -v '^index ')"
540eq "plain merge: candidate is linear" "$(git rev-list --merges "origin/forkgram..multigram-next")" ""
541git reset -q --hard "$MGQ"
542git merge -q --no-ff --no-commit side
543printf 'class Glue {}\n' >app/Glue.java
544git add -A && git commit -q -m "Merge side with glue"
545git push -q -f origin multigram
546run "$T/owner" --remote origin --force
547eq "merge with changes: exit code" "$RC" 1
548has "merge with changes: error names it" "$(o error)" "Merge side with glue"
549git push -q -f origin "$MGQ:refs/heads/multigram"
550git switch -q --detach
551git branch -q -D side multigram
553# ------------------------------------------------------------------ P: sync-report.py
554title "P. sync-report.py against a fake gh"
556cat >"$T/bin/gh" <<'PY'
557#!/usr/bin/env python3
558# A tiny stand-in for the gh CLI: keeps issues in a JSON file.
560path = os.environ['FAKE_GH_STATE']
561st = json.load(open(path)) if os.path.exists(path) else {'issues': [], 'labels': []}
564 return a[a.index(name) + 1] if name in a else None
566 f = opt('--body-file')
567 return open(f).read() if f else ''
569 return next(i for i in st['issues'] if i['number'] == int(n))
570if a[:2] == ['label', 'create']:
571 st['labels'].append(a[2])
572elif a[:2] == ['issue', 'list']:
573 print(json.dumps([{'number': i['number'], 'title': i['title']} for i in st['issues']
574 if i['state'] == 'open' and opt('--label') in i['labels']]))
575elif a[:2] == ['issue', 'view']:
577 print(json.dumps({'body': i['body'], 'comments': [{'body': c} for c in i['comments']]}))
578elif a[:2] == ['issue', 'create']:
579 if opt('--label') not in st['labels']:
580 sys.exit('label missing')
581 n = len(st['issues']) + 1
582 st['issues'].append({'number': n, 'title': opt('--title'), 'body': body(), 'labels': [opt('--label')],
583 'state': 'open', 'comments': []})
584 print(f'https://example.invalid/issues/{n}')
585elif a[:2] == ['issue', 'comment']:
586 issue(a[2])['comments'].append(body())
587elif a[:2] == ['issue', 'edit']:
588 issue(a[2])['title'] = opt('--title')
589elif a[:2] == ['issue', 'close']:
590 i = issue(a[2]); i['state'] = 'closed'; i['comments'].append(opt('--comment'))
592 sys.exit('fake gh: unsupported ' + ' '.join(a))
593json.dump(st, open(path, 'w'))
596export REPORT_GH=$T/bin/gh FAKE_GH_STATE=$T/gh.json RUN_URL=https://example.invalid/runs/1
597report() { # VAR=value... : runs sync-report.py with only these job results and outputs
598 env -i PATH="$PATH" REPORT_GH="$REPORT_GH" FAKE_GH_STATE="$FAKE_GH_STATE" RUN_URL="$RUN_URL" "$@" \
599 python3 "$HERE/sync-report.py" 2>&1 | sed 's/^/ | /'
601gh_state() { python3 -c "import json,sys; st=json.load(open('$T/gh.json')); print(eval(sys.argv[1]))" "$1"; }
602A40=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa B40=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb C40=cccccccccccccccccccccccccccccccccccccccc
603conflict=(SYNC_RESULT=failure COMPILE_RESULT=skipped PROMOTE_RESULT=skipped SYNC_STATUS=conflict
604 SYNC_FORKGRAM_VERSION=1.2.0 SYNC_FORKGRAM_COMMIT=$A40 SYNC_SNAPSHOT=$B40 SYNC_PREVIOUS_SNAPSHOT=$C40
605 SYNC_CONFLICT_COMMIT=$C40 "SYNC_CONFLICT_SUBJECT=Rename the \`app\`" "SYNC_CONFLICT_POSITION=2 of 3"
606 SYNC_CONFLICT_FILES=app/Main.java "SYNC_CONFLICT_DETAILS=changed on both sides: app/Main.java")
607report "${conflict[@]}"
608eq "conflict opens an issue" "$(gh_state "len(st['issues'])")" 1
609eq "issue title" "$(gh_state "st['issues'][0]['title']")" "Forkgram sync needs a hand (Forkgram 1.2.0)"
610has "issue body has the rebase command" "$(gh_state "st['issues'][0]['body']")" "git rebase --onto $B40 $C40"
611has "issue body has the file" "$(gh_state "st['issues'][0]['body']")" "changed on both sides: app/Main.java"
612has "backticks in the subject are neutralised" "$(gh_state "st['issues'][0]['body']")" "Rename the 'app'"
613report "${conflict[@]}"
614eq "the same conflict is not reported twice" "$(gh_state "len(st['issues'][0]['comments'])")" 0
615report SYNC_RESULT=success COMPILE_RESULT=skipped PROMOTE_RESULT=skipped SYNC_STATUS=behind SYNC_FORKGRAM_VERSION=1.2.0
616eq "behind with an open issue adds nothing" "$(gh_state "len(st['issues'][0]['comments'])")" 0
617report SYNC_RESULT=success COMPILE_RESULT=failure PROMOTE_RESULT=skipped SYNC_STATUS=candidate \
618 SYNC_FORKGRAM_VERSION=1.2.0 SYNC_CANDIDATE=$A40 SYNC_SNAPSHOT=$B40
619eq "compile failure comments on the open issue" "$(gh_state "len(st['issues'][0]['comments'])")" 1
620has "comment has the autosquash fix" "$(gh_state "st['issues'][0]['comments'][0]")" "git rebase -i --autosquash $B40"
621next_edited=(SYNC_RESULT=success COMPILE_RESULT=skipped PROMOTE_RESULT=skipped SYNC_STATUS=next-edited
622 SYNC_FORKGRAM_VERSION=1.2.0 SYNC_FORKGRAM_COMMIT=$A40 SYNC_FORKGRAM_TIP=$B40 SYNC_EXISTING_NEXT=$C40)
623report "${next_edited[@]}"
624eq "next-edited comments on the open issue" "$(gh_state "len(st['issues'][0]['comments'])")" 2
625has "next-edited comment says what to do" "$(gh_state "st['issues'][0]['comments'][1]")" "use_next"
626has "next-edited comment names the new Forkgram commit" "$(gh_state "st['issues'][0]['comments'][1]")" "${B40:0:10}"
627report "${next_edited[@]}"
628eq "the same next-edited is not reported twice" "$(gh_state "len(st['issues'][0]['comments'])")" 2
629report SYNC_RESULT=success COMPILE_RESULT=success PROMOTE_RESULT=success SYNC_STATUS=candidate \
630 SYNC_FORKGRAM_VERSION=1.2.0 SYNC_CANDIDATE=$A40 PROMOTE_TAG=multigram-before-1.2.0 "SYNC_DROPPED=$C40 Late \`commit\`"
631eq "promotion closes the issue" "$(gh_state "st['issues'][0]['state']")" closed
632has "closing note names the tag" "$(gh_state "st['issues'][0]['comments'][-1]")" "multigram-before-1.2.0"
633has "closing note lists the dropped commits" "$(gh_state "st['issues'][0]['comments'][-1]")" "$C40 Late 'commit'"
634report SYNC_RESULT=success COMPILE_RESULT=skipped PROMOTE_RESULT=skipped SYNC_STATUS=up-to-date SYNC_FORKGRAM_VERSION=1.2.0
635eq "up to date with no open issue: nothing" "$(gh_state "len(st['issues'])")" 1
636report SYNC_RESULT=failure COMPILE_RESULT=skipped PROMOTE_RESULT=skipped SYNC_STATUS=error "SYNC_ERROR=origin has no multigram branch yet."
637eq "a sync error opens a new issue" "$(gh_state "len(st['issues'])")" 2
638has "error text in the issue" "$(gh_state "st['issues'][1]['body']")" "origin has no multigram branch yet."
639report SYNC_RESULT=cancelled COMPILE_RESULT=skipped PROMOTE_RESULT=skipped
640report SYNC_RESULT=success COMPILE_RESULT=skipped PROMOTE_RESULT=skipped SYNC_STATUS=up-to-date SYNC_FORKGRAM_VERSION=1.2.0
641eq "in step again closes it" "$(gh_state "st['issues'][1]['state']")" closed
642report SYNC_RESULT=success COMPILE_RESULT=skipped PROMOTE_RESULT=skipped SYNC_STATUS=behind SYNC_FORKGRAM_VERSION=1.3.0
643eq "behind with no open issue opens one" "$(gh_state "[i['state'] for i in st['issues']]")" "['closed', 'closed', 'open']"
645printf '\n%d passed, %d failed\n' "$PASS" "$FAIL"
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list