]> Nutra Git (v1) - gamesguru/git-remote-gcrypt.git/commitdiff
feat: Add shell completions, uninstall script, and CLI flags
authorShane Jaroch <chown_tee@proton.me>
Thu, 1 Jan 2026 04:35:53 +0000 (23:35 -0500)
committerShane Jaroch <chown_tee@proton.me>
Thu, 1 Jan 2026 07:41:00 +0000 (02:41 -0500)
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 <chown_tee@proton.me>
14 files changed:
.editorconfig [new file with mode: 0644]
.envrc [new file with mode: 0644]
.geminiignore [new file with mode: 0644]
.github/workflows/coverage.yaml [new file with mode: 0644]
README.rst
completions/README.rst [new file with mode: 0644]
completions/bash/git-remote-gcrypt [new file with mode: 0644]
completions/fish/git-remote-gcrypt.fish [new file with mode: 0644]
completions/zsh/_git-remote-gcrypt [new file with mode: 0644]
git-remote-gcrypt
graph.txt [deleted file]
tests/system-test-multikey.sh [changed mode: 0644->0755]
tests/verify-system-install.sh [changed mode: 0644->0755]
uninstall.sh [new file with mode: 0644]

diff --git a/.editorconfig b/.editorconfig
new file mode 100644 (file)
index 0000000..13b95c3
--- /dev/null
@@ -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 (file)
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 (file)
index 0000000..4222a6a
--- /dev/null
@@ -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 (file)
index 0000000..cb2d5ab
--- /dev/null
@@ -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
index 28473011b8a61f868a5b39e919c7c6c5aadcd922..2a566ee76f0a5c7c96e62edba4f129720071e6ee 100644 (file)
@@ -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 (file)
index 0000000..d1e6f55
--- /dev/null
@@ -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 (file)
index 0000000..18da214
--- /dev/null
@@ -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 (file)
index 0000000..9d089ce
--- /dev/null
@@ -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 (file)
index 0000000..f3686d5
--- /dev/null
@@ -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 "$@"
index 550e8c290eab8b720cd8f3d896233bb40e36561b..42009e98a3dd26be3337e7b11007864477b436e5 100755 (executable)
@@ -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 <URL>    Check if URL is a gcrypt repository
+
+       Git Protocol Commands (for debugging):
+         capabilities     List remote helper capabilities
+         list             List refs in remote repository
+         push <refspec>   Push refs to remote repository
+         fetch <sha> <ref> 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 <remote-url>
+                              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 <remote-url>
+                              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 <src>:<dst>" | git-remote-gcrypt <remote-url>
+                              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 <sha> <ref>" | git-remote-gcrypt <remote-url>
+                              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</dev/null; curl -s -S -k "$1/$2") > "$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</dev/null
+                       rsync -I -W "$(rsynclocation "$1")"/"$2" "$3" >&2
+               )
        elif isurl rclone "$1"
        then
-               (exec 0>&-; rclone copyto --error-on-no-transfer "${1#rclone://}"/"$2" "$3" >&2)
+               (exec 0</dev/null; rclone copyto --error-on-no-transfer "${1#rclone://}"/"$2" "$3" >&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</dev/null
+                       rsync $Conf_rsync_put_flags -I -W "$3" "$(rsynclocation "$1")"/"$2" >&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</dev/null
+                       rsync $Conf_rsync_put_flags -q -r --exclude='*' \
                        "$Localdir/" "$(rsynclocation "$1")" >&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
+               ) <<EOF
+$2
+EOF
        elif isurl rclone "$1"
        then
                xfeed "$2" rclone delete -v --include-from=/dev/stdin "${1#rclone://}/" >&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 (file)
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
old mode 100644 (file)
new mode 100755 (executable)
index bbcb71a..6899396
-#!/bin/bash
+#!/usr/bin/env bash
+# SPDX-FileCopyrightText: Copyright 2023 Cathy J. Fitzpatrick <cathy@cathyjf.com>
+# 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" <<EOF
-use-agent
-pinentry-mode loopback
-no-tty
-EOF
+readonly tempdir
+trap 'rm -Rf -- "${tempdir}"' EXIT
 
-cat >"${gpg_home}/gpg-agent.conf" <<EOF
-allow-loopback-pinentry
-EOF
+# Setup PATH to use local git-remote-gcrypt
+PATH=$(git rev-parse --show-toplevel):${PATH}
+readonly PATH
+export PATH
 
-print_info "Step 1: Generating 18 Ed25519 keys (this may take a moment)..."
-num_keys=18
-for i in $(seq 1 $num_keys); do
-       # Generate simple Ed25519 key (fast, no expiration)
-       # We use a batch file for speed and non-interactivity
-       cat >"${tempdir}/gen-key-${i}.batch" <<EOF
-%echo Generating key $i...
-Key-Type: EDDSA
-Key-Curve: ed25519
-Key-Usage: sign
-Subkey-Type: ECDH
-Subkey-Curve: cv25519
-Name-Real: git-remote-gcrypt${i}
-Name-Email: gcrypt${i}@example.com
-Expire-Date: 0
-%no-protection
-%commit
-EOF
-       gpg --batch --generate-key "${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"
old mode 100644 (file)
new mode 100755 (executable)
diff --git a/uninstall.sh b/uninstall.sh
new file mode 100644 (file)
index 0000000..7d8d52b
--- /dev/null
@@ -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."