#!/bin/bash # Oracle: the system git. Builds a fixture under a fresh directory ROOT and # records, for each case, what git rev-parse --path-format=absolute prints # from a directory, with GIT_DIR set or not, for --git-common-dir, # --show-toplevel and --is-bare-repository: the repository mq resolves there # (doc/commands.md, mq -C PATH), the work tree it was found from, and whether # it has one; and the main worktree git worktree list names first, the # checkout mq opens the queue at. # # The fixture, which test.ml builds again with nox-git-eio: # main/ a repository on branch main, one commit of README and lib/ttl.ml # main/lib/ # wt/ a linked worktree of main, on branch topic # wt/lib/ # other/ a second repository, with no commit # outside/ a directory in no repository # link a symbolic link to main # bare.git/ a bare repository, with no commit # GIT_CEILING_DIRECTORIES is ROOT's parent, so no repository above ROOT is # found, wherever the temporary directory lies. # # discover.csv has one row per case: its name; its directory, relative to # ROOT, or @F for the Git directory the gitfile ROOT/F names; the GIT_DIR it # sets; the common directory git printed, relative to # ROOT, or nothing when git refused; the top of the work tree, relative to # ROOT, or nothing outside a work tree; whether the repository is bare, # or nothing when git refused; and its main worktree, relative to ROOT, or # nothing when git refused or the repository is bare. A GIT_DIR is empty for none, @F for the # absolute Git directory that the gitfile ROOT/F names (as Git exports it to a # hook run in that worktree), and otherwise a path set as written, relative to # the case's directory. Output goes to $1 (default ../traces). set -euo pipefail command -v git >/dev/null || { echo "git not on PATH" >&2; exit 1; } out="$(cd "${1:-$(dirname "$0")/../traces}" && pwd)" work="$(mktemp -d "${TMPDIR:-/tmp}/mq-discover.XXXXXX")" trap 'rm -rf "$work"' EXIT root="$(cd "$work" && pwd -P)" unset $(env | awk -F= '/^GIT_[A-Za-z0-9_]*=/ { print $1 }') export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 export GIT_CEILING_DIRECTORIES="$(dirname "$root")" export GIT_AUTHOR_NAME=Alice GIT_AUTHOR_EMAIL=alice@example.org export GIT_COMMITTER_NAME=Alice GIT_COMMITTER_EMAIL=alice@example.org export GIT_AUTHOR_DATE=2026-01-01T12:00:00+00:00 export GIT_COMMITTER_DATE=2026-01-01T12:00:00+00:00 git init -q --template= -b main "$root/main" mkdir "$root/main/lib" printf 'ttl: the time to live of a DNS record\n' > "$root/main/README" printf 'let of_string = int_of_string\n' > "$root/main/lib/ttl.ml" git -C "$root/main" add README lib/ttl.ml git -C "$root/main" commit -q -m "ttl: read a TTL" git -C "$root/main" worktree add -q -b topic "$root/wt" git init -q --template= -b main "$root/other" mkdir "$root/outside" ln -s main "$root/link" git init -q --template= --bare -b main "$root/bare.git" csv="$out/discover.csv" echo "name,dir,git_dir,common_dir,top,bare,checkout" > "$csv" # gitfile F: the Git directory the gitfile ROOT/F names. gitfile() { sed -n 's/^gitdir: //p' "$root/$1"; } # ask NAME DIR VALUE ARG: what git rev-parse --path-format=absolute ARG prints # from the directory DIR, with GIT_DIR=VALUE if VALUE is not empty, relative # to ROOT when it is a path; nothing when git refuses. ask() { local name=$1 dir=$2 value=$3 arg=$4 answer rc if [ -n "$value" ]; then answer=$(GIT_DIR=$value git -C "$dir" rev-parse \ --path-format=absolute "$arg" 2>/dev/null) && rc=0 || rc=$? else answer=$(git -C "$dir" rev-parse \ --path-format=absolute "$arg" 2>/dev/null) && rc=0 || rc=$? fi case $rc in 0) case $answer in true | false) echo "$answer" ;; "$root"/*) echo "${answer#"$root"/}" ;; *) echo "$name: git answered $answer, outside the fixture" >&2; exit 1 ;; esac ;; 128) ;; *) echo "$name: git exited $rc" >&2; exit 1 ;; esac } # main NAME DIR VALUE: the main worktree git worktree list names first from # the directory DIR, with GIT_DIR=VALUE if VALUE is not empty, relative to # ROOT; nothing when git refuses or the repository is bare. main() { local name=$1 dir=$2 value=$3 list if [ -n "$value" ]; then list=$(GIT_DIR=$value git -C "$dir" worktree list --porcelain 2>/dev/null) || return 0 else list=$(git -C "$dir" worktree list --porcelain 2>/dev/null) || return 0 fi case $(printf '%s\n' "$list" | sed -n 2p) in bare) return 0 ;; esac list=$(printf '%s\n' "$list" | sed -n '1s/^worktree //p') case $list in "$root"/*) echo "${list#"$root"/}" ;; *) echo "$name: git listed $list, outside the fixture" >&2; exit 1 ;; esac } # row NAME DIR [GIT_DIR]: git's answers from ROOT/DIR, with GIT_DIR if given. row() { local name=$1 dir=$2 git_dir=${3-} at value common top bare checkout case $dir in @*) at=$(gitfile "${dir#@}") ;; *) at=$root/$dir ;; esac case $git_dir in "") value= ;; @*) value=$(gitfile "${git_dir#@}") ;; *) value=$git_dir ;; esac common=$(ask "$name" "$at" "$value" --git-common-dir) || exit 1 top=$(ask "$name" "$at" "$value" --show-toplevel) || exit 1 bare=$(ask "$name" "$at" "$value" --is-bare-repository) || exit 1 checkout= [ -n "$common" ] && { checkout=$(main "$name" "$at" "$value") || exit 1; } case "$name$dir$git_dir$common$top$bare$checkout" in *[,\"]*) echo "$name: a field holds a comma or a quote" >&2; exit 1 ;; esac echo "$name,$dir,$git_dir,$common,$top,$bare,$checkout" >> "$csv" } row checkout main row checkout-subdir main/lib row worktree-subdir wt/lib # GIT_DIR names the worktree's own Git directory, absolute, as Git exports it # to a hook run in that worktree; the common directory is read from its # commondir file. row worktree-subdir-git-dir wt/lib @wt/.git # GIT_DIR names the worktree's gitfile, relative: it is resolved against the # directory, as git -C does, and followed to the worktree's Git directory. row worktree-subdir-gitfile wt/lib ../.git # GIT_DIR names another repository: nothing is found by walking up. row worktree-subdir-git-dir-other wt/lib ../../other/.git # A hook Git runs from a linked worktree: GIT_DIR names the worktree's own Git # directory, absolute, and the directory is in no repository. row hook-elsewhere outside @wt/.git # Git resolves the directory physically, through the link: the answer names # main, never link. row link link row link-subdir link/lib row outside outside # Inside a Git directory there is no work tree, and git still names the # repository: the main checkout's Git directory, a directory within it, and a # linked worktree's own Git directory. row git-dir main/.git row git-dir-subdir main/.git/refs row worktree-git-dir @wt/.git # A bare repository has no work tree at all. row bare bare.git # hooks.csv has one row per value of core.hooksPath in main: its name; the # value, empty for none, @P for the absolute path ROOT/P, and otherwise as # written; and what git rev-parse --path-format=absolute --git-path # hooks/pre-commit prints from the linked worktree wt, where a check runs, # and from main, relative to ROOT (doc/configuration.md, "A check named # after a Git hook is that hook"). hooks_csv="$out/hooks.csv" echo "name,hooks_path,from_worktree,from_checkout" > "$hooks_csv" # hook_row NAME VALUE: git's answers with core.hooksPath set to VALUE. hook_row() { local name=$1 value=$2 wt main case $value in "") git -C "$root/main" config --unset core.hooksPath || true ;; @*) git -C "$root/main" config core.hooksPath "$root/${value#@}" ;; *) git -C "$root/main" config core.hooksPath "$value" ;; esac wt=$(git -C "$root/wt" rev-parse --path-format=absolute \ --git-path hooks/pre-commit) main=$(git -C "$root/main" rev-parse --path-format=absolute \ --git-path hooks/pre-commit) echo "$name,$value,${wt#"$root"/},${main#"$root"/}" >> "$hooks_csv" } hook_row hooks-of-the-git-directory "" hook_row relative-in-the-work-tree .githooks hook_row absolute-outside @hooks hook_row absolute-in-the-git-directory @main/.git/custom hook_row absolute-in-the-work-tree @main/.githooks git -C "$root/main" config --unset core.hooksPath git --version > "$out/git-version"