# BST-TIMEOUT: 900 # ~110 git invocations; git is the largest binary on this image # # git 2.44.4 -- the distributed version control system. # # THE EVIDENCE THIS SUITE IS BUILT ON IS THE OBJECT IDS THEMSELVES, and that is # a stronger kind of evidence than any other suite in this package has available. # Everywhere else a port is checked by comparing an output against a reference we # had to measure and carry. git needs no carried reference: a blob id is the # SHA-1 of "blob \0", a tree id the SHA-1 of a byte-exact listing # of blob ids, and a commit id the SHA-1 of a byte-exact serialisation of the # tree id, the parents, the author and committer lines and the message. Fix the # content, the identity and the dates and every id in the graph is DETERMINED -- # by the format, not by the build, the version or the architecture. # # So the six ids asserted below are not "what our x86_64 build happened to # print". They were produced by src/git_mknative.sh and then re-derived # independently from the raw object format with a hashlib script that shares no # code with git at all; both agree. A wasm32 git that returns ccf7aeaf for the # second commit has a working index, zlib deflate, SHA-1, tree serialisation and # commit format, all at once, byte for byte -- and there is no way to be # approximately right about a SHA-1. That is the single most informative # assertion available for this package and it costs one commit. # # WHAT THIS PORT CANNOT DO, asserted rather than omitted. curl is not on this # image, so git is built NO_CURL/NO_EXPAT and the http/https/ftp/ftps remote # helpers do not exist (git_%.bbappend has the reasoning). The `boundary-*` # cases below assert that absence and assert that an https:// URL fails with # git's OWN diagnostic rather than a trap or a silent hang -- a port whose # missing half is undocumented is how a user finds it the hard way. # # THE THREE PLATFORM QUESTIONS IT ANSWERS, each in a named block: # pack-* git maps packfiles MAP_PRIVATE|PROT_READ. brintos/linux#30 is # about PROT_WRITE, so it should not reach git -- these cases pack # the fixture and read every object back OUT of the packfile, so # that is measured on the mmap read path instead of assumed. If #30 # ever widened to PROT_READ this block is where it would land. # clone-* a local-path clone runs a real upload-pack in a real child over a # real pipe. That is fork()+exec, the half of brintos/linux#14 that # works, and `clone-file-proto` forces the daemon-ish path that # cannot take the hardlink shortcut. # hook-* a pre-commit hook is the same fork()+exec surface reached from # the other direction -- git spawning a script the user wrote. # # NOT COVERED, deliberately: git's own test suite (t/). It is ~1000 shell scripts # that fork thousands of processes and expect perl for a good part of its # machinery; this image ships no perl, and the run would be hours. The ids above # are a better use of the budget for a format-defined tool. . "$BST_LIB" require git git/present W=${TMPDIR:-/tmp}/bst-git.$$ mkdir -p "$W" 2>/dev/null || { bst_notok git/workdir "cannot mkdir $W"; exit 0; } cd "$W" || { bst_notok git/workdir "cannot cd $W"; exit 0; } trap 'cd /; rm -rf "$W"' EXIT # Hermetic and deterministic. HOME is redirected because git reads ~/.gitconfig # even with an explicit --git-dir, and the two GIT_CONFIG_* pins mean a machine # that grows an /etc/gitconfig later cannot change what these cases assert. HOME=$W; export HOME GIT_CONFIG_GLOBAL=/dev/null; export GIT_CONFIG_GLOBAL GIT_CONFIG_SYSTEM=/dev/null; export GIT_CONFIG_SYSTEM GIT_AUTHOR_NAME="brintOS Selftest"; export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL="selftest@brintos.invalid"; export GIT_AUTHOR_EMAIL GIT_COMMITTER_NAME="brintOS Selftest"; export GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL="selftest@brintos.invalid"; export GIT_COMMITTER_EMAIL GIT_TERMINAL_PROMPT=0; export GIT_TERMINAL_PROMPT # The reference ids. src/git_mknative.sh regenerates them; a hashlib re-derivation # from the raw object format confirms them. Do not hand-edit: change the fixture # and re-run the script. G_BLOB1=7d8ff7073fa6c2bdb5b9fef84db702c37d74711f G_TREE1=d0001707f7e501f4315475ba1f1ef246bc0ee954 G_COMMIT1=0a97c90acd144fd371577ed90cb61a8e9fe00905 G_BLOB2=8d2f8d7026c48958fede739188a774d96e401d08 G_TREE2=ab7e4a63e1728de4171f42a8125aa1a8e15ebafc G_COMMIT2=ccf7aeafe69c79a6c1cd42c83fa52298c0f345a8 G_BLOB1_256=65814d1ac35d1b41372fef62b93a0832011d935442a1a65cc398b4f2f6c26295 G_COMMIT1_256=775dade79c3d0800dc55c21f0f35e839feb0dac13fbdb46f946699dc37b572f9 # ---------------------------------------------------------------- inventory -- # First, so a red further down is attributable to behaviour rather than to a # package that shipped the wrong files. check_out git/version "git version 2." git --version check git/exec-path git --exec-path GITEXEC=$(git --exec-path 2>/dev/null) [ -d "$GITEXEC" ] && bst_ok git/exec-path-exists \ || bst_notok git/exec-path-exists "git --exec-path '$GITEXEC' is not a directory" # The templates decide what `git init` produces; a port that lost them still # inits a usable repo, so nothing else here would notice. check git/templates-present test -d /usr/share/git-core/templates # ------------------------------------------------------------- the identity -- # The core of the suite. Everything below this point is downstream of it. git init -q -b main repo 2>/dev/null check git/init-repo test -d repo/.git cd repo || { bst_notok git/init-cd "cannot cd repo"; exit 0; } check_out git/init-head "refs/heads/main" git symbolic-ref HEAD printf 'hello brintOS\n' > a.txt check_eq git/hash-object-blob1 "$G_BLOB1" "$(git hash-object a.txt 2>&1)" check git/add git add a.txt check_out git/status-staged "new file: a.txt" git status GIT_AUTHOR_DATE="2000-01-01T00:00:00Z" GIT_COMMITTER_DATE="2000-01-01T00:00:00Z" \ git commit -q -m "first commit" 2>/dev/null check_eq git/commit1-id "$G_COMMIT1" "$(git rev-parse HEAD 2>&1)" check_eq git/commit1-tree "$G_TREE1" "$(git rev-parse 'HEAD^{tree}' 2>&1)" check_eq git/commit1-blob "$G_BLOB1" "$(git rev-parse 'HEAD:a.txt' 2>&1)" printf 'hello brintOS\nsecond line\n' > a.txt git add a.txt GIT_AUTHOR_DATE="2000-01-02T00:00:00Z" GIT_COMMITTER_DATE="2000-01-02T00:00:00Z" \ git commit -q -m "second commit" 2>/dev/null check_eq git/commit2-id "$G_COMMIT2" "$(git rev-parse HEAD 2>&1)" check_eq git/commit2-tree "$G_TREE2" "$(git rev-parse 'HEAD^{tree}' 2>&1)" check_eq git/commit2-blob "$G_BLOB2" "$(git rev-parse 'HEAD:a.txt' 2>&1)" check_eq git/commit2-parent "$G_COMMIT1" "$(git rev-parse 'HEAD^' 2>&1)" # The whole graph in one string. A single id could in principle be right while # the DAG around it was not; this is the shape assertion the ids cannot make. check_eq git/log-graph "$G_COMMIT2 $G_COMMIT1" \ "$(git log --format=%H 2>/dev/null | tr '\n' ' ' | sed 's/ $//')" # ------------------------------------------------------------- object access -- check_eq git/cat-file-type "commit" "$(git cat-file -t $G_COMMIT2 2>&1)" check_eq git/cat-file-blob "hello brintOS" "$(git cat-file -p $G_BLOB1 2>&1)" check_eq git/cat-file-size "26" "$(git cat-file -s $G_BLOB2 2>&1)" check_out git/cat-file-commit "second commit" git cat-file -p $G_COMMIT2 # The stored timestamp is what makes the id reproducible -- assert it directly so # a timezone leak is named rather than surfacing as an unexplained id mismatch. check_out git/commit-date-utc "946684800 +0000" git cat-file -p $G_COMMIT1 check_out git/ls-tree "a.txt" git ls-tree --name-only HEAD check_eq git/rev-parse-short "$(printf %s "$G_COMMIT2" | cut -c1-7)" \ "$(git rev-parse --short=7 HEAD 2>&1)" # ---------------------------------------------------------------- read-back -- check_out git/show "second line" git show HEAD check_out git/diff "+second line" git diff HEAD~1 HEAD check_out git/diff-stat "1 insertion" git diff --stat HEAD~1 HEAD check_out git/diff-name "a.txt" git diff --name-only HEAD~1 HEAD check_out git/log-oneline "second commit" git log --oneline check_out git/log-patch "+second line" git log -p -1 # --grep and -G are the two regex consumers. This build keeps the host libc's # regex rather than git's bundled compat one (see git_%.bbappend), so these are # the cases that cover that choice. check_out git/log-grep "$G_COMMIT1" git log --format=%H --grep=first check_out git/log-pickaxe "$G_COMMIT2" git log --format=%H -G'second line' check_out git/grep "hello" git grep -h hello HEAD check_out git/blame "brintOS Selftest" git blame a.txt check_out git/shortlog "brintOS Selftest" git shortlog -s HEAD # --------------------------------------------------------------- worktree ops -- check_out git/status-clean "nothing to commit" git status printf 'dirty\n' >> a.txt check_out git/status-dirty "modified: a.txt" git status check git/checkout-restore git checkout -- a.txt check_eq git/restore-content "hello brintOS second line" "$(cat a.txt)" check git/branch-create git branch topic check_out git/branch-list "topic" git branch check git/tag-annotated env GIT_AUTHOR_DATE=2000-01-03T00:00:00Z \ GIT_COMMITTER_DATE=2000-01-03T00:00:00Z git tag -a v1 -m "release one" check_out git/tag-list "v1" git tag check_eq git/tag-target "$G_COMMIT2" "$(git rev-parse 'v1^{commit}' 2>&1)" check_out git/describe "v1" git describe # A real 3-way merge, not a fast-forward: both sides add a distinct file, so the # merge has to build a new tree from two parents rather than move a ref. git checkout -q topic 2>/dev/null printf 'from topic\n' > t.txt git add t.txt GIT_AUTHOR_DATE="2000-01-04T00:00:00Z" GIT_COMMITTER_DATE="2000-01-04T00:00:00Z" \ git commit -q -m "topic commit" 2>/dev/null git checkout -q main 2>/dev/null printf 'from main\n' > m.txt git add m.txt GIT_AUTHOR_DATE="2000-01-05T00:00:00Z" GIT_COMMITTER_DATE="2000-01-05T00:00:00Z" \ git commit -q -m "main commit" 2>/dev/null check git/merge-3way env GIT_AUTHOR_DATE=2000-01-06T00:00:00Z \ GIT_COMMITTER_DATE=2000-01-06T00:00:00Z git merge --no-ff -m "merge topic" topic check_eq git/merge-parents "2" "$(git rev-list --parents -n1 HEAD 2>/dev/null | wc -w | tr -d ' ' | awk '{print $1-1}')" check git/merge-both-files test -f t.txt -a -f m.txt # A conflicting merge must be REFUSED and must leave the markers -- a VCS that # resolves a real conflict silently is worse than one that cannot merge at all. git checkout -q -b conflict "$G_COMMIT2" 2>/dev/null printf 'conflicting\n' > m.txt git add m.txt GIT_AUTHOR_DATE="2000-01-07T00:00:00Z" GIT_COMMITTER_DATE="2000-01-07T00:00:00Z" \ git commit -q -m "conflicting commit" 2>/dev/null check_rc git/merge-conflict-refused 1 git merge --no-edit main check_out git/merge-conflict-markers "<<<<<<<" cat m.txt git merge --abort 2>/dev/null check_out git/merge-abort "conflicting" cat m.txt git checkout -q main 2>/dev/null check git/stash-push sh -c 'printf stashed >> a.txt && git stash' check_out git/stash-list "stash@{0}" git stash list check git/stash-pop git stash pop git checkout -q -- a.txt 2>/dev/null check_out git/cherry-pick "topic commit" sh -c \ 'git checkout -q -b cp '"$G_COMMIT2"' && git cherry-pick topic >/dev/null 2>&1; git log --format=%s -1' git checkout -q main 2>/dev/null # ------------------------------------------------------------------- hooks -- # fork()+exec from git's side. The hook must RUN (marker file) and its non-zero # exit must ABORT the commit -- a hook whose failure is ignored is the dangerous # half of this working only partly. mkdir -p .git/hooks printf '#!/bin/sh\ntouch "$PWD/hook-ran"\nexit 0\n' > .git/hooks/pre-commit chmod 0755 .git/hooks/pre-commit printf 'hooked\n' > h.txt git add h.txt GIT_AUTHOR_DATE="2000-01-08T00:00:00Z" GIT_COMMITTER_DATE="2000-01-08T00:00:00Z" \ git commit -q -m "hooked commit" 2>/dev/null check git/hook-ran test -f hook-ran printf '#!/bin/sh\nexit 1\n' > .git/hooks/pre-commit printf 'rejected\n' > r.txt git add r.txt check_rc git/hook-rejects 1 git commit -m "should be rejected" check_out git/hook-rejects-nocommit "hooked commit" git log --format=%s -1 rm -f .git/hooks/pre-commit git reset -q HEAD r.txt 2>/dev/null; rm -f r.txt # -------------------------------------------------------------------- pack -- # The mmap read path, exercised rather than trusted (see the header note). # repack -ad leaves NO loose object, so every read below must come out of the # packfile through git's mmap window. check git/pack-repack git repack -ad PACKDIR=.git/objects/pack check git/pack-exists sh -c 'ls '"$PACKDIR"'/pack-*.pack >/dev/null 2>&1' # The claim that makes every read below an mmap-path read: the four objects those # cases ask for are no longer available as loose files, so git has nowhere to get # them but the packfile. It is NOT "no loose objects exist" -- `repack -ad` packs # what is REACHABLE and leaves unreachable loose objects alone, and this repo has # some by design (the aborted merge, the popped stash). Asserting zero would be # asserting something false about git rather than something true about the pack. STILL_LOOSE=0 for _oid in $G_BLOB1 $G_TREE1 $G_COMMIT1 $G_COMMIT2; do _d=$(printf %s "$_oid" | cut -c1-2); _f=$(printf %s "$_oid" | cut -c3-) [ -e ".git/objects/$_d/$_f" ] && STILL_LOOSE=$((STILL_LOOSE+1)) done check_eq git/pack-asserted-objects-not-loose "0" "$STILL_LOOSE" check git/pack-verify sh -c 'git verify-pack -v '"$PACKDIR"'/pack-*.idx >/dev/null' # Every object read back out of the pack, by id, with its content checked. check_eq git/pack-read-blob1 "hello brintOS" "$(git cat-file -p $G_BLOB1 2>&1)" check_eq git/pack-read-commit1 "$G_COMMIT1" "$(git rev-parse $G_COMMIT1^{commit} 2>&1)" check_out git/pack-read-tree1 "a.txt" git ls-tree --name-only $G_TREE1 check_out git/pack-read-log "first commit" git log --format=%s $G_COMMIT1 check git/pack-cat-batch sh -c 'printf "%s\n%s\n" '"$G_COMMIT1 $G_BLOB1"' | git cat-file --batch-check >/dev/null' # fsck walks and re-hashes every object in the pack: the strongest single # statement that the packed round trip preserved content exactly. check git/fsck git fsck --full check_out git/count-objects "in-pack" git count-objects -v # ------------------------------------------------------------------- clone -- # The ticket's pass criterion. Three transports over a local path, because they # take three different code paths. cd "$W" || exit 0 # 1. plain local clone -- the hardlink/copy shortcut, no upload-pack at all check git/clone-local git clone -q repo clone-local check_eq git/clone-local-head "$(git -C repo rev-parse HEAD 2>&1)" \ "$(git -C clone-local rev-parse HEAD 2>&1)" check_eq git/clone-local-log "$(git -C repo log --format=%H 2>/dev/null | tr '\n' ' ')" \ "$(git -C clone-local log --format=%H 2>/dev/null | tr '\n' ' ')" check git/clone-local-worktree test -f clone-local/a.txt check git/clone-local-fsck sh -c 'git -C clone-local fsck --full' # 2. --no-hardlinks -- forces real object copies check git/clone-nohardlinks git clone -q --no-hardlinks repo clone-nohl check_eq git/clone-nohardlinks-head "$(git -C repo rev-parse HEAD 2>&1)" \ "$(git -C clone-nohl rev-parse HEAD 2>&1)" # 3. file:// -- the real protocol: git spawns upload-pack in a child and the two # talk pack protocol over a pipe. This is the fork()+exec case, and the only # one of the three that actually runs index-pack on the receiving side. check git/clone-file-proto git clone -q "file://$W/repo" clone-file check_eq git/clone-file-head "$(git -C repo rev-parse HEAD 2>&1)" \ "$(git -C clone-file rev-parse HEAD 2>&1)" check git/clone-file-pack sh -c 'ls clone-file/.git/objects/pack/pack-*.pack >/dev/null 2>&1' check git/clone-file-fsck sh -c 'git -C clone-file fsck --full' check_eq git/clone-file-blob "hello brintOS" "$(git -C clone-file cat-file -p $G_BLOB1 2>&1)" # A clone is only half a round trip. Push back over file:// and require the # origin to have moved -- that is receive-pack, the other daemon. git -C clone-file config user.name "brintOS Selftest" 2>/dev/null git -C clone-file config user.email "selftest@brintos.invalid" 2>/dev/null ( cd clone-file && printf 'pushed\n' > p.txt && git add p.txt && GIT_AUTHOR_DATE="2000-01-09T00:00:00Z" GIT_COMMITTER_DATE="2000-01-09T00:00:00Z" \ git commit -q -m "pushed commit" ) 2>/dev/null git -C repo config receive.denyCurrentBranch ignore 2>/dev/null check git/push-file-proto sh -c 'git -C clone-file push -q origin main' check_eq git/push-file-landed "$(git -C clone-file rev-parse HEAD 2>&1)" \ "$(git -C repo rev-parse refs/heads/main 2>&1)" check git/fetch-file-proto sh -c 'git -C clone-local fetch -q origin' # ------------------------------------------------------------------ archive -- check git/archive-tar sh -c 'git -C repo archive --format=tar HEAD > '"$W"'/ar.tar' check git/archive-nonempty test -s "$W/ar.tar" check_out git/archive-contents "a.txt" tar -tf "$W/ar.tar" # ------------------------------------------------------------------ sha256 -- # A second, independent hash pipeline over the same serialisations. cd "$W" || exit 0 git init -q -b main --object-format=sha256 repo256 2>/dev/null if [ -d repo256/.git ]; then cd repo256 || exit 0 printf 'hello brintOS\n' > a.txt check_eq git/sha256-blob "$G_BLOB1_256" "$(git hash-object a.txt 2>&1)" git add a.txt GIT_AUTHOR_DATE="2000-01-01T00:00:00Z" GIT_COMMITTER_DATE="2000-01-01T00:00:00Z" \ git commit -q -m "first commit" 2>/dev/null check_eq git/sha256-commit "$G_COMMIT1_256" "$(git rev-parse HEAD 2>&1)" check git/sha256-fsck git fsck --full cd "$W" || exit 0 else bst_notok git/sha256-init "git init --object-format=sha256 produced no repository" fi # ---------------------------------------------------------------- boundary -- # What this port deliberately does not carry. Asserted so a user meets it as a # documented limit rather than as a surprise -- and so the day curl is ported, # these three go red and say exactly what changed. check git/boundary-no-remote-http sh -c '! test -e "$(git --exec-path)/git-remote-http"' check git/boundary-no-remote-https sh -c '! test -e "$(git --exec-path)/git-remote-https"' # The failure has to be git's own, legible one. A trap, a hang or a segfault # here would be a platform finding; "Unable to find remote helper" is the port. OUT=$(git clone -q https://example.invalid/x.git "$W/nope" 2>&1); RC=$? if [ "$RC" -eq 0 ]; then bst_notok git/boundary-https-refused "https:// clone unexpectedly succeeded" else case "$OUT" in *"remote helper"*|*"Unable to find"*) bst_ok git/boundary-https-refused ;; *) bst_notok git/boundary-https-refused "rc=$RC not git's own helper diagnostic: $(_bst_1line "$OUT")" ;; esac fi # The `file://` and local transports ARE present -- the boundary is the network # half only, and this is what says so. check_out git/boundary-has-upload-pack "upload-pack" sh -c 'command -v git-upload-pack || ls "$(git --exec-path)/git-upload-pack"' check_out git/boundary-has-receive-pack "receive-pack" sh -c 'command -v git-receive-pack || ls "$(git --exec-path)/git-receive-pack"'