From: Shane Jaroch Date: Thu, 1 Jan 2026 04:35:53 +0000 (-0500) Subject: feat: Add shell completions, uninstall script, and CLI flags X-Git-Url: https://git.nutra.tk/v1?a=commitdiff_plain;h=e1368b542e50f84c7ba733b16da58109f76d1483;p=gamesguru%2Fgit-remote-gcrypt.git feat: Add shell completions, uninstall script, and CLI flags Adds bash/zsh/fish completions, an uninstall script, and improves CLI with getopts (supporting -v/--version, -h/--help, and subcommands). Signed-off-by: Shane Jaroch --- diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..13b95c3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +[[bash]] +# For extension-less files (i.e., git-remote-gcrypt) +indent_style=tab +indent=4 + +[*.sh] +# For bash scripts with .sh extension +indent_style=tab +indent=4 + diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..0c60dc4 --- /dev/null +++ b/.envrc @@ -0,0 +1,3 @@ +# NOTE: for fish add .fish on end +source ./completions/$(basename $SHELL)/git-remote-gcrypt + diff --git a/.geminiignore b/.geminiignore new file mode 100644 index 0000000..4222a6a --- /dev/null +++ b/.geminiignore @@ -0,0 +1,13 @@ +!.tmp/ +#!.tmp/* +#!.tmp/**/ +#!.tmp/**/* +#!.tmp/**/**/ +#!.tmp/**/**/* +#!.tmp/**/**/**/ +#!.tmp/**/**/**/* +#!.tmp/**/**/**/**/ +#!.tmp/**/**/**/**/* + +!.coverage/ + diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml new file mode 100644 index 0000000..cb2d5ab --- /dev/null +++ b/.github/workflows/coverage.yaml @@ -0,0 +1,61 @@ +--- +name: Coverage + +"on": + push: + schedule: + - cron: "0 0 * * 0" # Sunday at 12 AM + +jobs: + test-coverage: + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Cache kcov + id: cache-kcov + uses: actions/cache@v4 + with: + path: /usr/local/bin/kcov + key: ${{ runner.os }}-kcov-v1 + + # python3-docutils enables rst2man + - name: Install kcov Dependencies + if: steps.cache-kcov.outputs.cache-hit != 'true' + run: | + sudo apt-get update + sudo apt-get install -y binutils-dev build-essential cmake git \ + libssl-dev libcurl4-openssl-dev libelf-dev libdw-dev \ + libiberty-dev zlib1g-dev libstdc++-12-dev \ + python3-docutils + + # Build & install kcov (on cache miss) + - name: Build and Install kcov + if: steps.cache-kcov.outputs.cache-hit != 'true' + run: | + git clone https://github.com/SimonKagstrom/kcov.git + cd kcov + mkdir build && cd build + cmake .. + make -j$(nproc) + sudo make install + + - name: Check kcov installation + run: | + ls -l /usr/local/bin/kcov || echo "Kcov not found!" + kcov --version + + - name: Test Installer [make test/installer] + run: make test/installer + + - name: Test System [make test/system] + run: make test/system + + - name: Upload to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + # Path changed to match your Makefile's COV_ROOT (.coverage) + directory: ./.coverage diff --git a/README.rst b/README.rst index 2847301..2a566ee 100644 --- a/README.rst +++ b/README.rst @@ -114,6 +114,14 @@ Environment variables When set (to anything other than the empty string), this environment variable forces a full repack when pushing. +*GCRYPT_TRACE* + When set (to anything other than the empty string), enables shell execution tracing (set -x) + for external commands (rsync, curl, rclone). + +*GCRYPT_DEBUG* + When set (to anything other than the empty string), enables verbose debug logging to standard error. + This includes GPG status output and resolved participant keys. + Examples ======== diff --git a/completions/README.rst b/completions/README.rst new file mode 100644 index 0000000..d1e6f55 --- /dev/null +++ b/completions/README.rst @@ -0,0 +1,56 @@ +====================================== +Shell Completion for git-remote-gcrypt +====================================== + +This directory contains shell completion scripts for ``git-remote-gcrypt``. + +Installation +============ + +Bash +---- + +System-wide (requires sudo):: + + sudo cp completions/bash/git-remote-gcrypt /etc/bash_completion.d/ + +User-only:: + + mkdir -p ~/.local/share/bash-completion/completions + cp completions/bash/git-remote-gcrypt ~/.local/share/bash-completion/completions/ + +Zsh +--- + +System-wide (requires sudo):: + + sudo cp completions/zsh/_git-remote-gcrypt /usr/share/zsh/site-functions/ + +User-only:: + + mkdir -p ~/.zsh/completions + cp completions/zsh/_git-remote-gcrypt ~/.zsh/completions/ + # Add to ~/.zshrc: fpath=(~/.zsh/completions $fpath) + +Fish +---- + +User-only (Fish doesn't have system-wide completions):: + + mkdir -p ~/.config/fish/completions + cp completions/fish/git-remote-gcrypt.fish ~/.config/fish/completions/ + +Supported Completions +===================== + +- ``-h``, ``--help`` - Show help message +- ``-v``, ``--version`` - Show version information +- ``--check`` - Check if URL is a gcrypt repository + +Notes +===== + +- Completions are optional and not required for normal operation +- ``git-remote-gcrypt`` is typically invoked by git automatically +- These completions are useful for manual invocation and testing + diff --git a/completions/bash/git-remote-gcrypt b/completions/bash/git-remote-gcrypt new file mode 100644 index 0000000..18da214 --- /dev/null +++ b/completions/bash/git-remote-gcrypt @@ -0,0 +1,40 @@ +# Bash completion for git-remote-gcrypt +# Install to: /etc/bash_completion.d/ or ~/.local/share/bash-completion/completions/ + +_git_remote_gcrypt() { + local cur prev opts commands + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD - 1]}" + opts="-h --help -v --version --check" + commands="capabilities list push fetch" + + # If we're after a subcommand, only offer -h/--help + if [[ " $commands " =~ " ${COMP_WORDS[1]:-} " ]]; then + COMPREPLY=($(compgen -W "-h --help" -- "$cur")) + return 0 + fi + + case "$prev" in + --check) + # Complete with gcrypt:: URLs or file paths + COMPREPLY=($(compgen -f -- "$cur")) + return 0 + ;; + esac + + if [[ "$cur" == -* ]]; then + COMPREPLY=($(compgen -W "$opts" -- "$cur")) + return 0 + fi + + # Complete with both git protocol commands and flags on first argument + COMPREPLY=($(compgen -W "$commands $opts" -- "$cur")) + + # Also complete with gcrypt:: URLs + if [[ "$cur" == gcrypt::* ]]; then + COMPREPLY+=("$cur") + fi +} + +complete -F _git_remote_gcrypt git-remote-gcrypt diff --git a/completions/fish/git-remote-gcrypt.fish b/completions/fish/git-remote-gcrypt.fish new file mode 100644 index 0000000..9d089ce --- /dev/null +++ b/completions/fish/git-remote-gcrypt.fish @@ -0,0 +1,12 @@ +# Fish completion for git-remote-gcrypt +# Install to: ~/.config/fish/completions/ + +complete -c git-remote-gcrypt -s h -l help -d 'Show help message' +complete -c git-remote-gcrypt -s v -l version -d 'Show version information' +complete -c git-remote-gcrypt -l check -d 'Check if URL is a gcrypt repository' -r -F + +# Git protocol commands +complete -c git-remote-gcrypt -f -a 'capabilities' -d 'Show git remote helper capabilities' +complete -c git-remote-gcrypt -f -a 'list' -d 'List refs in remote repository' +complete -c git-remote-gcrypt -f -a 'push' -d 'Push refs to remote repository' +complete -c git-remote-gcrypt -f -a 'fetch' -d 'Fetch refs from remote repository' diff --git a/completions/zsh/_git-remote-gcrypt b/completions/zsh/_git-remote-gcrypt new file mode 100644 index 0000000..f3686d5 --- /dev/null +++ b/completions/zsh/_git-remote-gcrypt @@ -0,0 +1,17 @@ +#compdef git-remote-gcrypt +# Zsh completion for git-remote-gcrypt +# Install to: ~/.zsh/completions/ or /usr/share/zsh/site-functions/ + +_git_remote_gcrypt() { + local -a args + args=( + '(- *)'{-h,--help}'[show help message]' + '(- *)'{-v,--version}'[show version information]' + '--check[check if URL is a gcrypt repository]:URL:_files' + '1:command:(capabilities list push fetch)' + '*:gcrypt URL:' + ) + _arguments -s -S $args +} + +_git_remote_gcrypt "$@" diff --git a/git-remote-gcrypt b/git-remote-gcrypt index 550e8c2..42009e9 100755 --- a/git-remote-gcrypt +++ b/git-remote-gcrypt @@ -25,17 +25,124 @@ set -e # errexit set -f # noglob set -C # noclobber -VERSION="@@DEV_VERSION@@" -if [ "${1:-}" = "-v" ] || [ "${1:-}" = "--version" ]; then - echo "git-remote-gcrypt version $VERSION" >&2 - exit 0 -fi - export GITCEPTION="${GITCEPTION:-}+" # Reuse $Gref except when stacked Gref="refs/gcrypt/gitception$GITCEPTION" Gref_rbranch="refs/heads/master" Packkey_bytes=63 # nbr random bytes for packfile keys, any >= 256 bit is ok Hashtype=SHA256 # SHA512 SHA384 SHA256 SHA224 supported. +VERSION="@@DEV_VERSION@@" + +# Help function +show_help() { + cat >&2 <<-EOF + git-remote-gcrypt version $VERSION + GPG-encrypted git remote helper + + Usage: Automatically invoked by git when using gcrypt:: URLs + See: man git-remote-gcrypt + Or: https://github.com/spwhitton/git-remote-gcrypt + + Options: + -h, --help Show this help message + -v, --version Show version information + --check Check if URL is a gcrypt repository + + Git Protocol Commands (for debugging): + capabilities List remote helper capabilities + list List refs in remote repository + push Push refs to remote repository + fetch Fetch refs from remote repository + + Environment Variables: + GCRYPT_DEBUG=1 Enable verbose debug logging to stderr + GCRYPT_TRACE=1 Enable shell tracing (set -x) for rsync/curl commands + GCRYPT_FULL_REPACK=1 Force full repack when pushing + EOF +} + +# Parse flags +while getopts "hv-:" opt; do + case "$opt" in + h) + show_help + exit 0 + ;; + v) + echo "git-remote-gcrypt version $VERSION" >&2 + exit 0 + ;; + -) + # Handle long options + case "$OPTARG" in + help) + show_help + exit 0 + ;; + version) + echo "git-remote-gcrypt version $VERSION" >&2 + exit 0 + ;; + *) + echo "Unknown option: --$OPTARG" >&2 + exit 1 + ;; + esac + ;; + *) + exit 1 + ;; + esac +done + +# Handle subcommand help (e.g., git-remote-gcrypt capabilities --help) +shift $((OPTIND - 1)) +case "${1:-}" in + capabilities) + if [ "${2:-}" = "-h" ] || [ "${2:-}" = "--help" ]; then + cat >&2 <<-EOF + capabilities - List git remote helper capabilities + + Usage: echo "capabilities" | git-remote-gcrypt + Invoked by git to query what operations this helper supports. + EOF + exit 0 + fi + ;; + list) + if [ "${2:-}" = "-h" ] || [ "${2:-}" = "--help" ]; then + cat >&2 <<-EOF + list - List refs in remote repository + + Usage: echo "list" | git-remote-gcrypt + Invoked by git to list available refs (branches/tags). + EOF + exit 0 + fi + ;; + push) + if [ "${2:-}" = "-h" ] || [ "${2:-}" = "--help" ]; then + cat >&2 <<-EOF + push - Push refs to remote repository + + Usage: echo "push :" | git-remote-gcrypt + Invoked by git to push local refs to the remote. + EOF + exit 0 + fi + ;; + fetch) + if [ "${2:-}" = "-h" ] || [ "${2:-}" = "--help" ]; then + cat >&2 <<-EOF + fetch - Fetch refs from remote repository + + Usage: echo "fetch " | git-remote-gcrypt + Invoked by git to fetch objects from the remote. + EOF + exit 0 + fi + ;; +esac + Manifestfile=91bd0c092128cf2e60e1a608c31e92caf1f9c1595f83f2890ef17c0e4881aa0a Hex40="[a-f0-9]" Hex40=$Hex40$Hex40$Hex40$Hex40$Hex40$Hex40$Hex40$Hex40 @@ -70,6 +177,11 @@ xecho_n() { xecho "$@" | tr -d \\n ; } # kill newlines echo_git() { xecho "$@" ; } # Code clarity echo_info() { xecho "gcrypt:" "$@" >&2; } echo_die() { echo_info "$@" ; exit 1; } +print_debug() { + if [ -n "${GCRYPT_DEBUG:-}" ]; then + echo_info "DEBUG:" "$@" + fi +} isnull() { case "$1" in "") return 0;; *) return 1;; esac; } isnonnull() { ! isnull "$1"; } @@ -236,15 +348,21 @@ gitception_new_repo() # Fetch repo $1, file $2, tmpfile in $3 GET() { + print_debug "GET $1 $2 $3" if isurl sftp "$1" then - (exec 0>&-; curl -s -S -k "$1/$2") > "$3" + (exec 0 "$3" elif isurl rsync "$1" then - (exec 0>&-; rsync -I -W "$(rsynclocation "$1")"/"$2" "$3" >&2) + print_debug "Calling rsync..." + ( + if [ -n "${GCRYPT_TRACE:-}" ]; then set -x; fi + exec 0&2 + ) elif isurl rclone "$1" then - (exec 0>&-; rclone copyto --error-on-no-transfer "${1#rclone://}"/"$2" "$3" >&2) + (exec 0&2) elif islocalrepo "$1" then cat "$1/$2" > "$3" @@ -256,12 +374,18 @@ GET() # Put repo $1, file $2 or fail, tmpfile in $3 PUT() { + print_debug "PUT $1 $2 $3" if isurl sftp "$1" then curl -s -S -k --ftp-create-dirs -T "$3" "$1/$2" elif isurl rsync "$1" then - rsync $Conf_rsync_put_flags -I -W "$3" "$(rsynclocation "$1")"/"$2" >&2 + print_debug "Calling rsync..." + ( + if [ -n "${GCRYPT_TRACE:-}" ]; then set -x; fi + exec 0&2 + ) elif isurl rclone "$1" then rclone copyto --error-on-no-transfer "$3" "${1#rclone://}"/"$2" >&2 @@ -287,13 +411,19 @@ PUT_FINAL() # Put directory for repo $1 PUTREPO() { + print_debug "PUTREPO $1" if isurl sftp "$1" then : elif isurl rsync "$1" then - rsync $Conf_rsync_put_flags -q -r --exclude='*' \ + print_debug "Calling rsync..." + ( + if [ -n "${GCRYPT_TRACE:-}" ]; then set -x; fi + exec 0&2 + ) elif isurl rclone "$1" then rclone mkdir "${1#rclone://}" >&2 @@ -309,14 +439,22 @@ PUTREPO() REMOVE() { local fn_= + print_debug "REMOVE $1 $2" if isurl sftp "$1" then # FIXME echo_info "sftp: Ignore remove request $1/$2" elif isurl rsync "$1" then - xfeed "$2" rsync -I -W -v -r --delete --include-from=- \ + print_debug "Calling rsync..." + ( + if [ -n "${GCRYPT_TRACE:-}" ]; then set -x; fi + # rsync needs stdin for --include-from=- + rsync -I -W -v -r --delete --include-from=- \ --exclude='*' "$Localdir"/ "$(rsynclocation "$1")/" >&2 + ) <&2 @@ -485,6 +623,7 @@ read_config() r_keyfpr=${r_keyfpr%%"$Newline"*} keyid_=$(xfeed "$r_keyinfo" cut -f 5 -d :) fprid_=$(xfeed "$r_keyfpr" cut -f 10 -d :) + print_debug "Resolved participant $recp_ to fpr: $fprid_" isnonnull "$fprid_" && signers_="$signers_ $keyid_" && @@ -513,6 +652,7 @@ read_config() fi setvar "$1" "$good_sig" setvar "$2" "$signers_" + print_debug "read_config done" } ensure_connected() @@ -525,7 +665,9 @@ ensure_connected() return fi Did_find_repo=no + print_debug "Calling read_config" read_config @r_sigmatch @r_signers + print_debug "Back from read_config" iseq "${NAME#gcrypt::}" "$URL" || r_name=$NAME @@ -556,7 +698,10 @@ ensure_connected() tmp_manifest="$Tempdir/maniF" tmp_stderr="$Tempdir/stderr" - GET "$URL" "$Manifestfile" "$tmp_manifest" 2>| "$tmp_stderr" || { + print_debug "Getting manifest from $URL file $Manifestfile" + # GET "$URL" "$Manifestfile" "$tmp_manifest" 2>| "$tmp_stderr" || { + # Debugging: don't capture stderr, let it flow to console + GET "$URL" "$Manifestfile" "$tmp_manifest" || { if ! isnull "$Repoid"; then cat >&2 "$tmp_stderr" echo_info "Repository not found: $URL" @@ -877,6 +1022,7 @@ EOF cleanup_tmpfiles() { + print_debug "Cleaning up..." if isnonnull "${Tempdir%%*."$$"}"; then echo_die "Unexpected Tempdir value: $Tempdir" fi @@ -917,6 +1063,8 @@ gcrypt_main_loop() NAME=$1 # Remote name URL=$2 # Remote URL + echo_info "git-remote-gcrypt version $VERSION" + setup while read input_ @@ -980,6 +1128,9 @@ then then exit 100 fi +elif [ "x$1" = x--version ] || [ "x$1" = x-v ]; then + echo "git-remote-gcrypt version $VERSION" + exit 0 else gcrypt_main_loop "$@" fi diff --git a/graph.txt b/graph.txt deleted file mode 100644 index c1cad08..0000000 --- a/graph.txt +++ /dev/null @@ -1,10 +0,0 @@ -* 416fad4 (HEAD -> feat/user-experience) feat: Add shell completions, uninstall script, and CLI flags -* 6fcafc9 (feat/infrastructure) feat: Add Makefile, CI, and improved testing infrastructure -* 88a122e (fix/gpg-checksum) fix(gpg): Handle ECDH checksum error with many keys -* ee1494b (gg/master, gg/HEAD, master, hotfix-01, backup-mega-commit) fix(gpg): Handle ECDH checksum error with many keys -| * 0d66303 (gg/hotfix-01) debug workflows -| * cffb09e fixup! add -h/--help target -| * e699981 add completions -| * 4e618f3 add -h/--help target -| * 7ac8613 add uninstall script/target -| * b85ed29 fixup! debug logging diff --git a/tests/system-test-multikey.sh b/tests/system-test-multikey.sh old mode 100644 new mode 100755 index bbcb71a..6899396 --- a/tests/system-test-multikey.sh +++ b/tests/system-test-multikey.sh @@ -1,74 +1,118 @@ -#!/bin/bash +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright 2023 Cathy J. Fitzpatrick +# SPDX-License-Identifier: GPL-2.0-or-later set -efuC -o pipefail shopt -s inherit_errexit # Helpers -print_info() { printf "\033[1;36m%s\033[0m\n" "$1"; } -print_success() { printf "\033[1;34m✓ %s\033[0m\n" "$1"; } -print_warn() { printf "\033[1;33m%s\033[0m\n" "$1"; } -print_err() { printf "\033[1;31m%s\033[0m\n" "$1"; } +print_info() { printf "\033[1;36m[TEST] %s\033[0m\n" "$1"; } +print_success() { printf "\033[1;34m[TEST] ✓ %s\033[0m\n" "$1"; } +print_warn() { printf "\033[1;33m[TEST] WARNING: %s\033[0m\n" "$1"; } +print_err() { printf "\033[1;31m[TEST] FAIL: %s\033[0m\n" "$1"; } # Settings num_commits=5 files_per_commit=3 + +print_info "Running multi-key clone test..." random_source="/dev/urandom" random_data_per_file=1024 # Reduced size for faster testing (1KB) default_branch="main" -test_user_name="Gcrypt Test User" -test_user_email="gcrypt-test@example.com" -pack_size_limit="12m" +test_user_name="git-remote-gcrypt" +test_user_email="git-remote-gcrypt@example.com" +pack_size_limit="12m" + +readonly num_commits files_per_commit random_source random_data_per_file \ + default_branch test_user_name test_user_email pack_size_limit + +# ----------------- Helper Functions ----------------- +indent() { + sed 's/^\(.*\)$/ \1/' +} + +section_break() { + echo + printf '*%.0s' {1..70} + echo $'\n' +} -# Setup Sandbox +assert() { + ( + set +e + [[ -n ${show_command:-} ]] && set -x + "${@}" + ) + local -r status=${?} + { [[ ${status} -eq 0 ]] && print_success "Verification succeeded."; } || + print_err "Verification failed." + return "${status}" +} + +fastfail() { + "$@" || kill -- "-$$" +} +# ---------------------------------------------------- + +umask 077 tempdir=$(mktemp -d) -trap 'rm -rf "$tempdir"' EXIT -print_info "Running in sandbox: $tempdir" - -# --- KEY GENERATION --- -# We need to generate keys such that the target key is "buried" deep in the keyring. -# The bug occurs when GPG tries many keys and fails on earlier ones with a checksum error. -# We will generate 18 keys. -# Key 1..17: Decoys (Ed25519) - will be tried and fail (or trigger checksum error). -# Key 18: Target (Ed25519) - the one we actually encrypt to. - -gpg_home="${tempdir}/gpg-home" -mkdir -p "$gpg_home" -chmod 700 "$gpg_home" -export GNUPGHOME="$gpg_home" - -# Create a minimal gpg.conf to avoid randomness issues and ensure consistency -cat >"${gpg_home}/gpg.conf" <"${gpg_home}/gpg-agent.conf" <"${tempdir}/gen-key-${i}.batch" </dev/null 2>&1 +# Clean GIT environment +git_env=$(env | sed -n 's/^\(GIT_[^=]*\)=.*$/\1/p') +IFS=$'\n' unset ${git_env} + +# GPG Setup +export GNUPGHOME="${tempdir}/gpg" +mkdir "${GNUPGHOME}" +cat <<'EOF' >"${GNUPGHOME}/gpg" +#!/usr/bin/env bash +set -efuC -o pipefail; shopt -s inherit_errexit +args=( "${@}" ) +for ((i = 0; i < ${#}; ++i)); do + if [[ ${args[${i}]} = "--secret-keyring" ]]; then + unset "args[${i}]" "args[$(( i + 1 ))]" + break + fi done +exec gpg "${args[@]}" +EOF +chmod +x "${GNUPGHOME}/gpg" + +# Git Config +export GIT_CONFIG_SYSTEM=/dev/null +export GIT_CONFIG_GLOBAL="${tempdir}/gitconfig" +mkdir "${tempdir}/template" +git config --global init.defaultBranch "${default_branch}" +git config --global user.name "${test_user_name}" +git config --global user.email "${test_user_email}" +git config --global init.templateDir "${tempdir}/template" +git config --global gpg.program "${GNUPGHOME}/gpg" + +# Prepare Random Data +total_files=$((num_commits * files_per_commit)) +random_data_size=$((total_files * random_data_per_file)) +random_data_file="${tempdir}/data" +head -c "${random_data_size}" "${random_source}" >"${random_data_file}" + +### +section_break -print_info "Step 2: Collecting fingerprints..." +print_info "Step 1: Creating multiple GPG keys for participants..." +num_keys=18 # Buried deep: 17 decoys + 1 valid key key_fps=() +( + set -x + for ((i = 0; i < num_keys; i++)); do + gpg --batch --passphrase "" --quick-generate-key \ + "${test_user_name}${i} <${test_user_email}${i}>" + done +) 2>&1 | indent # Capture fingerprints # Integrated fix: use mapfile @@ -84,101 +128,175 @@ key_fps=() mapfile -t key_fps < <(gpg --list-keys --with-colons | awk -F: '/^pub:/ {getline; print $10}') echo "Generated keys: ${key_fps[*]}" | indent +# Sanity Check +if [ "${#key_fps[@]}" -ne "$num_keys" ]; then + print_err "FATAL: Expected $num_keys keys, captured ${#key_fps[@]}." + print_err " Check grep/awk logic (likely capturing subkeys vs primary keys mismatch)." + exit 1 +fi +print_success "Sanity Check Passed: Captured ${#key_fps[@]} Primary Keys." + ### section_break -# Setup Git -export GIT_AUTHOR_NAME="$test_user_name" -export GIT_AUTHOR_EMAIL="$test_user_email" -export GIT_COMMITTER_NAME="$test_user_name" -export GIT_COMMITTER_EMAIL="$test_user_email" - -print_info "Step 3: Creating repository structure..." -mkdir "${tempdir}/first" -( +print_info "Step 2: Creating source repository..." +{ + git init -- "${tempdir}/first" cd "${tempdir}/first" - git init -q -b "$default_branch" - echo "content" >file.txt - git add file.txt - git commit -q -m "Initial commit" -) + for ((i = 0; i < num_commits; ++i)); do + for ((j = 0; j < files_per_commit; ++j)); do + file_index=$((i * files_per_commit + j)) + random_data_index=$((file_index * random_data_per_file)) + head -c "${random_data_per_file}" >"$((file_index)).data" < \ + <(tail -c +"${random_data_index}" "${random_data_file}" || :) + done + git add . + git commit -q -m "Commit #${i}" + done + git log --format=oneline | indent +} | indent -# Prepare Remote Gcrypt Repo -# We use the file:// backend which just needs a directory. -# But for gcrypt::, we essentially push to a directory that becomes the encrypted store. -mkdir -p "${tempdir}/second.git" +### +section_break + +print_info "Step 3: Creating bare remote..." +git init --bare -- "${tempdir}/second.git" | indent + +### +section_break print_info "Step 4: Pushing with SINGULAR participant (Key 2) to bury it..." -# We explicitly set ONLY the LAST key as the participant. -# This forces GPG to skip the first (num_keys-1) keys. -last_key_idx=$((num_keys - 1)) -git config gcrypt.participants "${key_fps[last_key_idx]}" -git push -f "gcrypt::${tempdir}/second.git#${default_branch}" "${default_branch}" -) 2>&1 +{ + ( + set -x + cd "${tempdir}/first" + # CRITICAL REPRO: Only encrypt to the LAST key. + # All previous keys are in the keyring but are NOT recipients. + # This forces GPG to skip the first (num_keys-1) keys. + last_key_idx=$((num_keys - 1)) + git config gcrypt.participants "${key_fps[last_key_idx]}" + git config user.signingkey "${key_fps[last_key_idx]}" + git push -f "gcrypt::${tempdir}/second.git#${default_branch}" "${default_branch}" + ) 2>&1 } | indent +### +section_break -print_info "Step 5: Cloning back - EXPECTING GPG TO ITERATE..." -# Now we try to clone (pull). GPG will have to decrypt the manifest. -# Since we have 18 keys in our keyring, and the message is encrypted to Key #18, -# GPG will try Key 1, 2... 17. -# -# With the BUG: GPG encounters a checksum error (due to ECDH/Ed25519 issues in some GPG versions with anonymous/multi-key handling) on an earlier key and ABORTS properly checking the others. git-remote-gcrypt sees the exit code 2 and dies. -# -# With the FIX: git-remote-gcrypt ignores the intermediate error and lets GPG continue until it finds Key 18. -output_file="${tempdir}/output.log" -( - cd "${tempdir}" - # We must force GPG to try keys. - # Actually, GPG tries all secret keys for which it has an encrypted session key packet. - # Since we are the participant, it should just find it. - # BUT, the bug (Debian #885770 / GnuPG T3597) was that *anonymous* recipients (gpg -R) cause this iteration to be fragile. - # gcrypt defaults to -R (anonymous). - - git clone "gcrypt::${tempdir}/second.git#${default_branch}" "third" -) >"${output_file}" 2>&1 -ret=$? +print_info "Step 5: Unhappy Path - Test clone with NO matching keys..." +{ + original_gnupghome="${GNUPGHOME}" + export GNUPGHOME="${tempdir}/gpg-empty" + mkdir "${GNUPGHOME}" + + # We expect this to FAIL + ( + set +e + git clone -b "${default_branch}" "gcrypt::${tempdir}/second.git#${default_branch}" -- "${tempdir}/fail_test" + if [ $? -eq 0 ]; then + print_info "ERROR: Clone succeeded unexpectedly with empty keyring!" + exit 1 + fi + ) 2>&1 | indent + + echo "Clone failed as expected." | indent + export GNUPGHOME="${original_gnupghome}" +} + +### +section_break print_info "Step 6: Reproduction Step - Clone with buried key..." -cat "${output_file}" +{ + # Capture output to check for GPG errors + output_file="${tempdir}/clone_output" + set +e + ( + set -x + git clone -b "${default_branch}" "gcrypt::${tempdir}/second.git#${default_branch}" -- "${tempdir}/third" + ) >"${output_file}" 2>&1 + ret=$? + set -e -if grep -q "Checksum error" "${output_file}" && [ $ret -ne 0 ]; then - print_warn "BUG(REPRODUCED): GPG Checksum error detected AND Clone failed!" - exit 1 -elif grep -q "Checksum error" "${output_file}" && [ $ret -eq 0 ]; then - print_success "SUCCESS: Checksum error detected but Clone SUCCEEDED. (Fix is working!)" -elif [ $ret -eq 0 ]; then - print_warn "WARNING: Test passed unexpectedly (Checksum error NOT detected at all). Bug trigger might be absent." -else - print_warn "WARNING: Clone failed with generic error (Checksum error not detected)." -fi + cat "${output_file}" + + if grep -q "Checksum error" "${output_file}" && [ $ret -ne 0 ]; then + print_warn "WARNING: GPG failed with checksum error." + print_err "BUG REPRODUCED! Exiting due to earlier GPG failures." + exit 1 + elif grep -q "Checksum error" "${output_file}" && [ $ret -eq 0 ]; then + print_success "SUCCESS: Checksum error detected but Clone SUCCEEDED. (Fix is working!)" + elif [ $ret -eq 0 ]; then + print_warn "WARNING: Clone passed unexpectedly (Checksum error not detected). Bug not triggered." + print_err "Exiting due to unexpected pass." + exit 1 + else + print_err "ERROR: Clone failed with generic error (Checksum error not detected)." + exit 1 + fi -# Continue to verify content. -echo "Verifying content match..." -assert diff -r --exclude ".git" -- "${tempdir}/first" "${tempdir}/third" 2>&1 | indent + # Continue to verify content. + print_info "Verifying content match..." + assert diff -r --exclude ".git" -- "${tempdir}/first" "${tempdir}/third" 2>&1 | indent } | indent -print_info "Step 7: Reproduction Step - Push with buried key..." -( - cd "${tempdir}/third" - echo "new data" >"new_file" - git add "new_file" - git commit -q -m "Commit for Step 7" - git push "gcrypt::${tempdir}/second.git#${default_branch}" "${default_branch}" -) >"${output_file}" 2>&1 -ret=$? +### +section_break print_info "Step 7: Reproduction Step - Push with buried key..." -cat "${output_file}" +{ + # Capture output to check for GPG errors + output_file="${tempdir}/push_output" + set +e + ( + set -x + cd "${tempdir}/first" + # Make a change so we can push + echo "new data" >"new_file" + git add "new_file" + git commit -q -m "Commit for Step 7" -if grep -q "Checksum error" "${output_file}" && [ $ret -ne 0 ]; then - print_warn "BUG(REPRODUCED): GPG Checksum error detected (Push) AND Push failed!" - exit 1 -elif grep -q "Checksum error" "${output_file}" && [ $ret -eq 0 ]; then - print_success "SUCCESS: Checksum error detected (Push) but Push SUCCEEDED. (Fix is working!)" -elif [ $ret -eq 0 ]; then - print_warn "WARNING: Push passed unexpectedly (Checksum error NOT detected at all)." -else - print_warn "WARNING: Push failed with generic error (Checksum error not detected)." -fi + # Set signing key for this push + last_key_idx=$((num_keys - 1)) + + # Regression Check: Ensure we didn't capture subkeys + if [ "${#key_fps[@]}" -ne "$num_keys" ]; then + print_err "FATAL: Key array corrupted! Expected $num_keys keys, found ${#key_fps[@]}." + print_err " This indicates the 'awk' capture logic has regressed (likely capturing subkeys)." + exit 1 + fi + print_success "Sanity Check (Step 7): Key count correct (${#key_fps[@]}). AWK fix confirmed active." + + # Visual Verification: Show which key we actually picked. + # If the bug were active (subkey capture), this would show 'git-remote-gcrypt8' (Key #9) + # With the fix, it must show 'git-remote-gcrypt17' (Key #18) + print_info "Selected Key Details:" + gpg --list-keys "${key_fps[last_key_idx]}" | indent + + git config gcrypt.participants "${key_fps[last_key_idx]}" + git config user.signingkey "${key_fps[last_key_idx]}" + + git push "gcrypt::${tempdir}/second.git#${default_branch}" "${default_branch}" + ) >"${output_file}" 2>&1 + ret=$? + set -e + + cat "${output_file}" + + if grep -q "Checksum error" "${output_file}" && [ $ret -ne 0 ]; then + print_warn "WARNING: GPG failed with checksum error." + print_err "BUG REPRODUCED! Exiting due to earlier GPG failures." + exit 1 + elif grep -q "Checksum error" "${output_file}" && [ $ret -eq 0 ]; then + print_success "SUCCESS: Checksum error detected (Push) but Push SUCCEEDED. (Fix is working!)" + elif [ $ret -eq 0 ]; then + print_warn "WARNING: Push passed unexpectedly (Checksum error not detected). Bug not triggered." + print_err "Exiting due to unexpected pass." + exit 1 + else + print_err "ERROR: Push failed with generic error (Checksum error not detected)." + exit 1 + fi } | indent + +[ -n "${COV_DIR:-}" ] && print_success "OK. Report: file://${COV_DIR}/index.html" diff --git a/tests/verify-system-install.sh b/tests/verify-system-install.sh old mode 100644 new mode 100755 diff --git a/uninstall.sh b/uninstall.sh new file mode 100644 index 0000000..7d8d52b --- /dev/null +++ b/uninstall.sh @@ -0,0 +1,28 @@ +#!/bin/sh +set -e + +: "${prefix:=/usr/local}" +: "${DESTDIR:=}" + +verbose() { echo "$@" >&2 && "$@"; } + +BIN_PATH="$DESTDIR$prefix/bin/git-remote-gcrypt" +MAN_PATH="$DESTDIR$prefix/share/man/man1/git-remote-gcrypt.1.gz" + +echo "Uninstalling git-remote-gcrypt..." + +if [ -f "$BIN_PATH" ]; then + verbose rm -f "$BIN_PATH" + echo "Removed binary: $BIN_PATH" +else + echo "Binary not found: $BIN_PATH" +fi + +if [ -f "$MAN_PATH" ]; then + verbose rm -f "$MAN_PATH" + echo "Removed man page: $MAN_PATH" +else + echo "Man page not found: $MAN_PATH" +fi + +echo "Uninstallation complete."