#!/bin/bash

## Copyright (C) 2012 - 2026 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
## See the file COPYING for copying conditions.

## Single-source git verification tool. Verifies signatures, tag status, and
## working-tree cleanliness for the main repo and recursively for all
## submodules. Used by 'derivative-update' and 'build-steps.d/1100_sanity-tests',
## and runnable standalone by developers.

#set -x
set -o errexit
set -o nounset
set -o pipefail
set -o errtrace
shopt -s inherit_errexit
shopt -s shift_verbose

MYDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

export HELPER_SCRIPTS_PATH="${MYDIR}/../packages/kicksecure/helper-scripts"
## log_run_die.sh enforces PATH-presence of stecho + sanitize-string
## (helper-scripts /usr/bin); prepend the submodule's bin so the
## bootstrap check finds them when the package is not installed
## system-wide (CI dry-run, fresh dev checkouts). sanitize-string
## is a Python script that imports sanitize_string.* from
## /usr/lib/python3/dist-packages/, so PYTHONPATH needs the
## submodule equivalent too.
PATH="${HELPER_SCRIPTS_PATH}/usr/bin:${PATH}"
if [ -n "${PYTHONPATH:-}" ]; then
   PYTHONPATH="${HELPER_SCRIPTS_PATH}/usr/lib/python3/dist-packages:${PYTHONPATH}"
else
   PYTHONPATH="${HELPER_SCRIPTS_PATH}/usr/lib/python3/dist-packages"
fi
export PYTHONPATH
source "${HELPER_SCRIPTS_PATH}/usr/libexec/helper-scripts/get_colors.sh"
source "${HELPER_SCRIPTS_PATH}/usr/libexec/helper-scripts/log_run_die.sh"

## This function is defined up here to avoid redundancy between comments and
## documentation.
print_help() {
   printf '%s\n' "
Usage:
  help-steps/git_sanity_test [--mode MODE] [options]

  With no arguments, defaults to '--mode all --context \"main repo\"',
  which verifies the current repository (main repo + all submodules)
  with the standard set of checks (signature, tag status, uncommitted).

Modes:

  --mode all --context LABEL
    'working-tree' on the current directory, then 'submodules'.
    This is the default when no '--mode' is given.

  --mode working-tree --context LABEL
    Verify the current working directory's git repo:
      1. Authenticate HEAD commit against sq_git_trust_root via
         'sq-git log' (walks the commit chain, validates each
         commit signature against openpgp-policy.toml). This is
         the cryptographic trust gate.
      2. Tag policy. The only valid tag states for this repo are:
           A) no tag at HEAD        (allowed under --allow-untagged)
           B) exactly one annotated SIGNED tag at HEAD
         Everything else is a hard error, regardless of
         --allow-untagged:
           - lightweight tag at HEAD
           - annotated but unsigned tag at HEAD
           - multiple tags at HEAD
         --allow-untagged is a narrow escape hatch for case A only;
         it means \"it is OK that there is NO tag here\" and not \"it
         is OK that tag state is broken\".
      3. Uncommitted changes check.
    Exit codes: 0=ok, 1=signature, 2=tag/untagged, 3=uncommitted

  --mode ref --ref REF --ref-type {tag|commit}
    Authenticate a specific ref's target commit.
    - Resolves REF to a commit (tag -> its target commit).
    - Authenticates that commit via 'sq-git log' from the trust
      root, the same as --mode working-tree step 1.
    - For --ref-type tag: REF must exist under refs/tags/, and
      the tag must be annotated-and-signed per the three-way
      classify_tag check. --allow-untagged is NOT honoured here -
      when the caller explicitly names a specific ref and declares
      its type, they mean it, and the declaration is enforced
      strictly. A caller who wants to build from a commit without
      tag enforcement should use --mode working-tree with
      --allow-untagged instead.
    Exit codes: 0=ok, 1=verification failed

  --mode submodules
    Recursively run 'working-tree' on every submodule via
    'git submodule foreach --recursive'. Submodules use --trust-root HEAD
    because the parent repo (already verified) pins each submodule commit.
    Exit codes: 0=ok, non-zero=at least one submodule failed

  --help, -h
    Print this usage information and exit.

Forwarded arguments:
  Any argument that is not in the script-specific list above
  (--mode/--context/--ref/--ref-type/--help) is forwarded to
  'help-steps/parse-cmd' via 'help-steps/variables' during the
  standalone bootstrap. This is the same parser the build-steps.d
  scripts use, so you can pass e.g.:

    ./help-steps/git_sanity_test --allow-untagged true
    ./help-steps/git_sanity_test --allow-uncommitted true
"
}

## Required env vars (set by help-steps/variables):
##   sq_git_policy_file             absolute path to openpgp-policy.toml
##   sq_git_trust_root              trust root for sq-git log
##   dist_build_ignore_untagged     "true" or "false"
##   dist_build_ignore_uncommitted  "true" or "false"
##   dist_build_ignore_unsigned     "true" or "false" (skip signature check;
##                                  defaults to inverse of dist_build_sign_and_tag)
##
## For detailed design docs (trust model, TOFU, submodule trust, sq-git
## version assumptions, tag verification analysis), see:
##   agents/git_sanity_test_design.md
##   agents/git_sanity_test_security.md

## ----------------------------------------------------------------------------
## Argument parsing
## ----------------------------------------------------------------------------
## Our args: --mode/--context/--ref/--ref-type/--help.
## Everything else is forwarded to parse-cmd via the standalone bootstrap.

mode=""
context=""
ref=""
ref_type=""
forwarded_args=()

while [ $# -gt 0 ]; do
   case "$1" in
      --mode)      mode="$2";     shift 2 ;;
      --context)   context="$2";  shift 2 ;;
      --ref)       ref="$2";      shift 2 ;;
      --ref-type)  ref_type="$2"; shift 2 ;;
      --help|-h)   print_help;    exit  0 ;;
      ## Forward unknown args to parse-cmd (one token at a time).
      *)
         forwarded_args+=("$1")
         shift
         ;;
   esac
done

## Default: --mode all (most thorough check).
[ -n "${mode:-}" ] || mode="all"
case "$mode" in
   all|working-tree)
      [ -n "${context:-}" ] || context="main repo"
      ;;
esac

## ----------------------------------------------------------------------------
## Standalone bootstrap (skip if build infrastructure already sourced).
## See agents/git_sanity_test_design.md for why this is conditional.
## ----------------------------------------------------------------------------
if [ "${dist_build_one_parsed:-}" != "true" ]; then
   orig_pwd="$PWD"
   set -- "${forwarded_args[@]}"
   dist_build_source_run="true"
   source "$MYDIR/pre"
   source "$MYDIR/variables"
   cd -- "$orig_pwd" || die 1 "cannot restore original cwd: $orig_pwd"
   unset orig_pwd
elif [ "${#forwarded_args[@]}" -gt 0 ]; then
   ## Not re-running parse-cmd in build context.
   printf '%s\n' "${0##*/}: ERROR: extra args not honoured in build context: ${forwarded_args[*]}" >&2
   printf '%s\n' "${0##*/}:        Pass these on the parent build invocation instead." >&2
   exit 1
fi

## Env var validation.
[ -n "${sq_git_policy_file:-}" ]            || die 1 "env var sq_git_policy_file must be set"
[ -n "${sq_git_trust_root:-}" ]             || die 1 "env var sq_git_trust_root must be set"
[ -n "${dist_build_ignore_untagged:-}" ]    || die 1 "env var dist_build_ignore_untagged must be set ('true' or 'false')"
[ -n "${dist_build_ignore_uncommitted:-}" ] || die 1 "env var dist_build_ignore_uncommitted must be set ('true' or 'false')"
[ -n "${dist_build_ignore_unsigned:-}" ]    || die 1 "env var dist_build_ignore_unsigned must be set ('true' or 'false')"
[[ "${sq_git_policy_file}" =~ ^/ ]]         || die 1 "sq_git_policy_file must be an absolute path, got: '${sq_git_policy_file}'"
[ -r "${sq_git_policy_file}" ]              || die 1 "sq_git_policy_file is not readable: '${sq_git_policy_file}'"
[[ "${dist_build_ignore_untagged}"    =~ ^(true|false)$ ]] || die 1 "dist_build_ignore_untagged must be 'true' or 'false', got: '${dist_build_ignore_untagged}'"
[[ "${dist_build_ignore_uncommitted}" =~ ^(true|false)$ ]] || die 1 "dist_build_ignore_uncommitted must be 'true' or 'false', got: '${dist_build_ignore_uncommitted}'"
[[ "${dist_build_ignore_unsigned}"    =~ ^(true|false)$ ]] || die 1 "dist_build_ignore_unsigned must be 'true' or 'false', got: '${dist_build_ignore_unsigned}'"

## Pre-flight dependency checks. Explicit guards here are necessary because
## help-steps/pre does 'set +e' and functions called from '|| cls=$?' have
## errexit AND ERR trap suppressed for all commands inside them.
die_if_not_has git sq sqop sq-git safe-rm

## ----------------------------------------------------------------------------
## Verification primitives
## ----------------------------------------------------------------------------
sq_git_verify() {
   local target_ref="$1"
   local label="$2"
   ## --allow-unsigned escape hatch (dist_build_ignore_unsigned=true): the
   ## caller has opted out of signing (e.g. dist_build_sign_and_tag=false for
   ## AI-driven / local builds), so skip the cryptographic signature
   ## authentication.
   if [ "${dist_build_ignore_unsigned}" = "true" ]; then
      if [ "${dist_build_redistributable}" = 'true' ]; then
         printf '%s\n' "${bold}${red}ERROR: --allow-unsigned set but 'dist_build_redistributable' is set to 'true'! These settings are mutually exclusive.${reset}" >&2
         return 1
      fi

      printf '%s\n' "${cyan}INFO: --allow-unsigned set: skipping sq-git signature verification for ${label}.${reset}" >&2
      return 0
   fi
   ## sq-git log --trust-root HEAD -- HEAD is unsafe when used without
   ## --policy-file, even if an openpgp-policy.toml file exists in the
   ## repository root directory. Because the trust root and target ref are the
   ## same, it will assume the target ref is authenticated even if it is
   ## unsigned. However, when a policy file is explicitly passed, sq-git will
   ## verify the trust root's commit and exit non-zero if validation fails.
   if log_run notice sq-git log --policy-file "${sq_git_policy_file}" --trust-root "${sq_git_trust_root}" -- "${target_ref}"; then
      true "INFO: sq-git verification ok: ${label}"
      return 0
   fi
   printf '%s\n' "${bold}${red}ERROR: sq-git verification failed: ${label}${reset}" >&2
   printf '%s\n' "${cyan}INFO: If this is intentional, configure your own sq-git policy file." >&2
   printf '%s\n' "${cyan}      See 'buildconfig.d/30_signing_key.conf'.${reset}" >&2
   return 1
}

print_allow_hint() {
   printf '%s\n' "${cyan}INFO: As a developer or advanced user you might want to use:" >&2
   printf '%s\n' "${under}WARNING:${eunder} This can be ${under}insecure${eunder} if you cannot audit the changes." >&2
   printf '%s\n' "${bold}${under}--allow-untagged true${eunder} ${under}--allow-uncommitted true${eunder}${reset}" >&2
}

split_tag() {
  local tag_contents output_dir
  local -a tag_lines
  local tag_line_idx sig_start_idx tag_count

  tag_contents="${1:-}"
  [ -z "${tag_contents}" ] && return 1

  output_dir="${2:-}"
  [ -z "${output_dir}" ] && return 1

  readarray -t tag_lines <<< "${tag_contents}"
  tag_count="${#tag_lines[@]}"

  [ "${tag_count}" -gt 0 ] || return 1

  if [ "${tag_lines[tag_count - 1]}" != '-----END PGP SIGNATURE-----' ]; then
    ## Tag is or appears unsigned
    return 1
  fi

  ## Scan backwards for the '-----BEGIN PGP SIGNATURE-----' line, to reduce
  ## the risk of spoofing.
  sig_start_idx=-1
  for (( tag_line_idx=tag_count - 1; tag_line_idx >= 0; tag_line_idx-- )); do
    if [ "${tag_lines[tag_line_idx]}" = '-----BEGIN PGP SIGNATURE-----' ]; then
      sig_start_idx="${tag_line_idx}"
      break
    fi
  done

  if [ "${sig_start_idx}" = '-1' ]; then
    return 1
  fi

  printf '%s\n' "${tag_lines[@]:0:sig_start_idx}" > "${output_dir}/tag-text"
  printf '%s\n' "${tag_lines[@]:sig_start_idx}" > "${output_dir}/tag-sig"
}

## Classify a tag: 0=annotated-signed, 1=lightweight, 2=annotated-unsigned.
## See agents/git_sanity_test_security.md for the full tag verification analysis.
classify_tag() {
   local tag_ref="$1"
   local context_label="$2"
   local tag_objtype tag_body

   if ! tag_objtype="$(git cat-file -t "${tag_ref}")"; then
      die 1 "cannot inspect tag ref '${tag_ref}' in ${context_label}"
   fi

   case "$tag_objtype" in
      commit)
         ## lightweight
         return 1
         ;;
      tag)
         ;;
      *)
         die 1 "tag ref '${tag_ref}' resolves to unrecognized object type '${tag_objtype}' in ${context_label}"
         ;;
   esac

   if ! tag_body="$(git cat-file tag "${tag_ref}")"; then
      die 1 "cannot read tag object for '${tag_ref}' in ${context_label}"
   fi
   ## Only OpenPGP signatures are supported (not SSH, not PGP MESSAGE).
   case "$tag_body" in
      *'-----BEGIN PGP SIGNATURE-----'*)
         ## annotated and signed, but not yet verified
         ;;
      *)
         ## annotated-unsigned
         return 2
         ;;
   esac

   ## Extract tag signing keys from sq_git_policy_file and verify.
   ##
   ## Every command below needs an explicit '|| die' guard because
   ## classify_tag is called from a '|| cls=$?' context, which
   ## suppresses both errexit and the ERR trap for all commands inside
   ## the function. Without guards, failures are silently ignored.
   local verify_tag_dir
   verify_tag_dir="${binary_build_folder_dist}/verify_tag_temp"
   safe-rm --recursive --force -- "${verify_tag_dir}" \
      || die 1 "failed to clean verify_tag_dir: ${verify_tag_dir}"
   mkdir --parents -- "${verify_tag_dir}/certs" \
      || die 1 "failed to create directory: ${verify_tag_dir}/certs"
   chmod 0700 -- "${verify_tag_dir}" \
      || die 1 "failed to set permissions on: ${verify_tag_dir}"
   "${HELPER_SCRIPTS_PATH}/usr/libexec/helper-scripts/extract-openpgp-policy-trusted-certs" "${sq_git_policy_file}" 'sign_tag' "${verify_tag_dir}/certs" \
      || die 1 "failed to extract trusted certificates from policy file: ${sq_git_policy_file}"

   local tag_contents
   tag_contents="$(git cat-file tag "${tag_ref}")" \
      || die 1 "cannot read tag object for '${tag_ref}' in ${context_label}"
   mkdir --parents -- "${verify_tag_dir}/tag" \
      || die 1 "failed to create directory: ${verify_tag_dir}/tag"
   split_tag "${tag_contents}" "${verify_tag_dir}/tag" \
      || die 1 "failed to extract signature from tag '${tag_ref}' in ${context_label}"

   ## Verify the tag against any one of the extracted certs
   local verify_tag_cert
   for verify_tag_cert in "${verify_tag_dir}/certs/"/*; do
     if ! [ -r "${verify_tag_cert}" ]; then
       continue
     fi
     if sqop verify "${verify_tag_dir}/tag/tag-sig" "${verify_tag_cert}" < "${verify_tag_dir}/tag/tag-text" >/dev/null 2>&1; then
       safe-rm --recursive --force -- "${verify_tag_dir}" || true
       ## annotated and signed by a trusted developer
       return 0
     fi
   done

   safe-rm --recursive --force -- "${verify_tag_dir}" || true
   ## annotated, signed but not by a trusted developer
   return 2
}

## ----------------------------------------------------------------------------
## Modes
## ----------------------------------------------------------------------------

mode_working_tree() {
   [ -n "${context:-}" ] || die 1 "--context is required for --mode working-tree"

   local head git_status git_tags_output
   local -a tags_at_head git_tags_cmd

   head="$(git rev-parse HEAD)" || die 1 "Failed to get HEAD for: ${context}"

   ## Commit authentication via sq-git log.
   log_run notice sq_git_verify "${head}^{commit}" "${context} (commit ${head})" || exit 1

   ## Tag policy check.
   ##
   ## We use %(refname) not %(refname:short) to avoid branch/tag
   ## naming collisions when the value is fed back into git commands.
   git_tags_cmd=( git for-each-ref --points-at=HEAD --format='%(refname)' refs/tags )
   if ! git_tags_output="$( "${git_tags_cmd[@]}" )"; then
      die 1 "'${git_tags_cmd[*]}' failed in ${context}"
   fi
   if [ -n "${git_tags_output:-}" ]; then
      readarray -t tags_at_head <<< "$git_tags_output"
   else
      tags_at_head=()
   fi

   case "${#tags_at_head[@]}" in
      0)
         ## No tag at HEAD. This is the ONLY case --allow-untagged
         ## covers.
         if [ "${dist_build_ignore_untagged}" = "true" ]; then
            printf '%s\n' "${cyan}INFO: Untagged commit${reset} in: ${under}${context}${eunder}, --allow-untagged set, continuing.${reset}"
         else
            printf '%s\n' "${bold}${red}ERROR: Untagged commit${reset} in: ${under}${context}${eunder}" >&2
            print_allow_hint
            printf '%s\n' "${cyan}INFO: See build documentation on how to verify and checkout git tags.${reset}" >&2
            exit 2
         fi
         ;;
      1)
         ## Exactly one tag at HEAD. Must be annotated+signed.
         ## --allow-untagged is NOT honoured here (see block comment
         ## above).
         local cls=0
         classify_tag "${tags_at_head[0]}" "${context}" || cls=$?
         case "$cls" in
            0)
               ## annotated-signed: accept
               ;;
            1)
               printf '%s\n' "${bold}${red}ERROR: Tag '${tags_at_head[0]}' in ${context} is a lightweight tag${reset}" >&2
               printf '%s\n' "${cyan}INFO: Lightweight tags have no tag object and cannot carry a signature. Policy: all tags in this repo must be annotated signed tags. Remove the tag ('git tag -d ...') or recreate it as annotated and signed ('git tag -a -s ...'). If you need a local bookmark, use a branch instead.${reset}" >&2
               printf '%s\n' "${cyan}INFO: --allow-untagged does NOT bypass this check - it only covers the 'no tag at all' case.${reset}" >&2
               exit 2
               ;;
            2)
               printf '%s\n' "${bold}${red}ERROR: Tag '${tags_at_head[0]}' in ${context} is annotated but is not signed by a trusted developer, possible downgrade attack?${reset}" >&2
               printf '%s\n' "${cyan}INFO: Policy: all tags in this repo must be signed by a trusted developer.${reset}" >&2
               printf '%s\n' "${cyan}INFO: If you are a developer, you might have forgotten to use 'git tag -a -s ...'.${reset}" >&2
               printf '%s\n' "${cyan}INFO: --allow-untagged does NOT bypass this check - it only covers the 'no tag at all' case.${reset}" >&2
               exit 2
               ;;
         esac
         ;;
      *)
         ## Multiple tags at HEAD. --allow-untagged is NOT honoured.
         printf '%s\n' "${bold}${red}ERROR: Multiple tags point at HEAD in ${context}: ${tags_at_head[*]}${reset}" >&2
         printf '%s\n' "${cyan}INFO: Policy: at most one tag per commit so release identity is unambiguous. Remove the extras with 'git tag -d ...'.${reset}" >&2
         printf '%s\n' "${cyan}INFO: --allow-untagged does NOT bypass this check - it only covers the 'no tag at all' case.${reset}" >&2
         exit 2
         ;;
   esac

   ## Uncommitted changes check.
   ##
   ## NOTE: No '2>&1' here. Merging git's stderr into the dirty-tree
   ## signal would let unrelated warnings (e.g. advice messages, index
   ## version warnings) be misinterpreted as uncommitted changes.
   git_status="$(git status --porcelain=v1)"
   if [ -n "${git_status:-}" ]; then
      if [ "${dist_build_ignore_uncommitted}" = "true" ]; then
         printf '%s\n' "${cyan}INFO: Uncommitted changes in ${context}, --allow-uncommitted set, continuing.${reset}"
         ## Debugging.
         git status
      else
         printf '%s\n' "${bold}${red}ERROR: Uncommitted changes in: ${context}${reset}" >&2
         git status >&2
         print_allow_hint
         ## Show untracked files.
         printf '%s\n' "${cyan}INFO: Listing untracked files with 'git clean -d --force --force --dry-run':${reset}" >&2
         git clean -d --force --force --dry-run >&2 || true
         ## Show uncommitted changes to tracked files.
         printf '%s\n' "${cyan}INFO: Displaying uncommitted changes with 'git diff --compact-summary ${head}':${reset}" >&2
         git diff --compact-summary "${head}" >&2 || true
         if [ -n "${dist_source_help_steps_folder:-}" ]; then
            printf '%s\n' "${cyan}You most likely like to run:${reset}" >&2
            printf '%s\n' "${under}${dist_source_help_steps_folder}/cleanup-files${eunder}" >&2
            printf '%s\n' "${cyan}or if you know what you are doing:${reset}" >&2
            printf '%s\n' "${under}git clean -d --force --force${eunder}" >&2
            printf '%s\n' "${under}git reset --hard${eunder}" >&2
         fi
         exit 3
      fi
   fi
}

mode_ref() {
   [ -n "${ref:-}" ]      || die 1 "--ref is required for --mode ref"
   [ -n "${ref_type:-}" ] || die 1 "--ref-type is required for --mode ref"
   case "$ref_type" in
      tag|commit) ;;
      *) die 1 "--ref-type must be 'tag' or 'commit'" ;;
   esac

   local resolve_ref ref_commit

   ## For --ref-type tag: normalize to refs/tags/..., verify exists, classify.
   ## For --ref-type commit: accept any commit-ish, skip tag classification.
   if [ "${ref_type:-}" = 'tag' ]; then
      case "$ref" in
         refs/tags/*)  resolve_ref="$ref" ;;
         refs/*)       die 1 "ref '${ref}' is declared as --ref-type tag but is not under refs/tags/" ;;
         *)            resolve_ref="refs/tags/${ref}" ;;
      esac
      git show-ref --verify --quiet "${resolve_ref}" \
         || die 1 "tag ref not found: '${resolve_ref}'"
      local cls=0
      classify_tag "${resolve_ref}" "ref '${ref}'" || cls=$?
      case "$cls" in
         0) : ;;
         1) die 1 "ref '${resolve_ref}' is a lightweight tag - not an annotated signed tag" ;;
         2) die 1 "ref '${resolve_ref}' is annotated but not signed by a trusted developer - release tags must be signed" ;;
      esac
   else
      resolve_ref="${ref}"
   fi

   ## Resolve to the target commit SHA.
   if ! ref_commit="$(git rev-parse --verify "${resolve_ref}^{commit}" 2>/dev/null)"; then
      die 1 "could not resolve ref '${ref}' to a commit"
   fi

   ## Authenticate the target commit via sq-git log. When
   ## sq_git_trust_root == ref_commit, sq-git log authenticates the
   ## single-commit 'chain' trivially; see 'mode_working_tree'.
   sq_git_verify "${ref_commit}" "ref '${ref}' (commit ${ref_commit})" || exit 1
}

mode_submodules() {
   ## Run working-tree checks on every submodule via
   ## 'git submodule foreach --recursive'.
   ## Uses $MYDIR (not git rev-parse --show-toplevel) to find this script.
   ## foreach runs under /bin/sh, so we re-invoke via bash.
   [ -r "${MYDIR}/git_sanity_test" ] || die 1 "helper script not found or not readable: '${MYDIR}/git_sanity_test'"

   export MYDIR
   ## sq_git_trust_root=HEAD per-command:
   ## the parent's sq_git_trust_root value is a SHA from the parent's
   ## git db that does not exist in any submodule, so 'sq-git log
   ## --trust-root <parent-SHA>' inside a submodule fails to resolve
   ## the trust root. Forcing trust_root=HEAD inside the submodule
   ## context collapses sq-git verification to single-commit verify
   ## (see sq_git_verify and the 'trivial chain' note in mode_ref):
   ## the policy file's signing keys must validate the submodule's
   ## HEAD commit signature directly. The chain-walk security
   ## property is delegated to the parent - the parent commit
   ## (already chain-verified) records the submodule's pinned SHA.
   ## $MYDIR / $displaypath expanded by foreach's child shell.
   # shellcheck disable=SC2016
   sq_git_trust_root=HEAD \
      git submodule foreach --recursive \
         'bash "$MYDIR/git_sanity_test" --mode working-tree --context "$displaypath"'
}

mode_all() {
   [ -n "${context:-}" ] || die 1 "--context is required for --mode all"
   mode_working_tree
   mode_submodules
}

case "$mode" in
   working-tree) mode_working_tree ;;
   ref)          mode_ref ;;
   submodules)   mode_submodules ;;
   all)          mode_all ;;
   *) die 1 "unknown mode '$mode' (expected one of working-tree, ref, submodules, all)" ;;
esac
