URL resolution logic, clean/check, shell completions
authorShane Jaroch <chown_tee@proton.me>
Thu, 8 Jan 2026 20:32:44 +0000 (15:32 -0500)
committerShane Jaroch <chown_tee@proton.me>
Sat, 10 Jan 2026 18:11:43 +0000 (13:11 -0500)
generate script generates from scratch
gen docs with shell not python

show all files (not just top-level dirs) in clean
update install.sh (automate completion installs)

fix installer logic test
remove `--check` flag in favor of `check` command
remove redundant `--help` argument on subcommands

more helpful warning (not error) message; clean msg
strip gcrypt:: & report URLs like git.
don't clean non-gcrypt URLs
clean URLs filtered specially for rsync:// protocol

update/fix clean command for sft/rclone

19 files changed:
Makefile
README.rst
completions/README.rst [deleted file]
completions/bash/git-remote-gcrypt
completions/fish/git-remote-gcrypt.fish
completions/gen_docs.sh [new file with mode: 0755]
completions/templates/README.rst.in [copied from README.rst with 91% similarity]
completions/templates/bash.in [new file with mode: 0644]
completions/templates/fish.in [new file with mode: 0644]
completions/templates/zsh.in [new file with mode: 0644]
completions/zsh/_git-remote-gcrypt
git-remote-gcrypt
install.sh
tests/system-test-multikey.sh
tests/system-test.sh
tests/test-clean-command.sh
tests/test-install-logic.sh
tests/verify-system-install.sh
utils/gen_docs.sh [new file with mode: 0755]

index c1c823fc1e93bb8642ca3728e31a8f19717d749e..8d2a012196aca2fa681085b095591e27857beeca 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -20,10 +20,6 @@ _help:
                        print ""; \
                }' $(MAKEFILE_LIST)
 
-.PHONY: _all
-_all: lint test/installer test/system
-       @$(call print_success,All checks passed!)
-
 .PHONY: vars
 vars:  ##H Display all Makefile variables (simple)
        $(info === Makefile Variables (file/command/line origin) ===)
@@ -33,6 +29,7 @@ vars: ##H Display all Makefile variables (simple)
                ) \
        )
 
+
 define print_err
 printf "\033[1;31m%s\033[0m\n" "$(1)"
 endef
@@ -52,37 +49,40 @@ endef
 
 .PHONY: check/deps
 check/deps:    ##H Verify kcov & shellcheck
-       @command -v shellcheck >/dev/null 2>&1 || { $(call print_err,Error: 'shellcheck' not installed.); exit 1; }
-       @$(call print_info,  --- shellcheck version ---) && shellcheck --version
-       @command -v kcov >/dev/null 2>&1 || { $(call print_err,Error: 'kcov' not installed.); exit 1; }
-       @$(call print_info,  --- kcov version ---) && kcov --version
-       @$(call print_success,Dependencies OK.)
+       @$(call print_info,  --- shellcheck version ---)
+       @shellcheck --version
+       @$(call print_info,  --- kcov version ---)
+       @kcov --version
 
 
 
 LINT_LOCS_PY ?= $(shell git ls-files '*.py')
-LINT_LOCS_SH ?=
+LINT_LOCS_SH ?= $(shell git ls-files '*.sh' ':!tests/system-test.sh')
 
 .PHONY: format
 format:        ##H Format scripts
        @$(call print_target,format)
+       @$(call print_info,Formatting shell scripts...)
+       shfmt -ci -bn -s -w $(LINT_LOCS_SH)
+       @$(call print_success,OK.)
        @$(call print_info,Formatting Python scripts...)
-       $(if $(LINT_LOCS_SH),shfmt -i 4 -ci -bn -s -w $(LINT_LOCS_SH))
        -black $(LINT_LOCS_PY)
        -isort $(LINT_LOCS_PY)
        @$(call print_success,OK.)
 
 .PHONY: lint
 lint:  ##H Run shellcheck
-       # lint install script
+       @$(call print_target,lint)
+       @$(call print_info,Running shellcheck...)
+       shellcheck --version
        shellcheck install.sh
-       @$(call print_success,OK.)
-       # lint system/binary script
-       shellcheck git-remote-gcrypt
-       @$(call print_success,OK.)
-       # lint test scripts
+       shellcheck -s sh -e SC3043,SC2001 git-remote-gcrypt
        shellcheck tests/*.sh
        @$(call print_success,OK.)
+       @$(call print_info,Linting Python scripts...)
+       -ruff check $(LINT_LOCS_PY)
+       @$(call print_success,OK.)
+
 
 # --- Test Config ---
 PWD := $(shell pwd)
@@ -91,11 +91,17 @@ COV_SYSTEM  := $(COV_ROOT)/system
 COV_INSTALL := $(COV_ROOT)/installer
 
 .PHONY: test/, test
-test/: test
-test: test/installer test/system test/cov      ##H All tests & coverage
+test: test/
+test/: ##H Run tests (purity checks only if kcov missing)
+       @if command -v kcov >/dev/null 2>&1; then \
+               $(MAKE) test/installer test/system test/cov; \
+       else \
+               $(call print_warn,kcov not found: skipping coverage/bash tests.); \
+               $(MAKE) test/purity; \
+       fi
 
 .PHONY: test/installer
-test/installer: check/deps     ##H Test installer logic
+test/installer:        ##H Test installer logic
        @rm -rf $(COV_INSTALL)
        @mkdir -p $(COV_INSTALL)
        @export COV_DIR=$(COV_INSTALL); \
@@ -107,7 +113,7 @@ test/installer: check/deps  ##H Test installer logic
 
 
 .PHONY: test/purity
-test/purity: check/deps        ##H Run logic tests with native shell (As Shipped Integrity Check)
+test/purity: check/deps/shellcheck     ##H Run logic tests (with native /bin/sh)
        @echo "running system tests (native /bin/sh)..."
        @export GPG_TTY=$$(tty); \
         [ -n "$(DEBUG)$(V)" ] && export GCRYPT_DEBUG=1; \
@@ -117,7 +123,7 @@ test/purity: check/deps     ##H Run logic tests with native shell (As Shipped Integr
         done
 
 .PHONY: test/system
-test/system: check/deps        ##H Run coverage tests (Dynamic Bash)
+test/system:   ##H Run logic tests (with bash & coverage)
        @echo "running system tests (coverage/bash)..."
        @rm -rf $(COV_SYSTEM)
        @mkdir -p $(COV_SYSTEM)
@@ -151,20 +157,30 @@ CHECK_COVERAGE = $(if $(call find_coverage_xml,$(1)), \
        echo "Error: No coverage report found for $(2) in $(1)" ; \
        exit 1)
 
+
 .PHONY: test/cov _test_cov_internal
 test/cov:      ##H Show coverage gaps
        $(MAKE) _test_cov_internal
 
 _test_cov_internal:
        @err=0; \
-       $(call CHECK_COVERAGE,$(COV_SYSTEM),git-remote-gcrypt,60) || err=1; \
-       $(call CHECK_COVERAGE,$(COV_INSTALL),install.sh,80) || err=1; \
+       $(call CHECK_COVERAGE,$(COV_SYSTEM),git-remote-gcrypt,59) || err=1; \
+       $(call CHECK_COVERAGE,$(COV_INSTALL),install.sh,78) || err=1; \
        exit $$err
 
 
+
 # Version from git describe (or fallback)
 __VERSION__ := $(shell git describe --tags --always --dirty 2>/dev/null || echo "@@DEV_VERSION@@")
 
+
+.PHONY: generate
+generate:      ##H Autogen man docs & shell completions
+       @$(call print_info,Generating documentation and completions...)
+       ./utils/gen_docs.sh
+       @$(call print_success,Generated.)
+
+
 .PHONY: install/, install
 install/: install
 install:       ##H Install system-wide
@@ -177,10 +193,12 @@ install:  ##H Install system-wide
 install/user:  ##H make install prefix=~/.local
        $(MAKE) install prefix=~/.local
 
+
 .PHONY: check/install
 check/install: ##H Verify installation works
        bash ./tests/verify-system-install.sh
 
+
 .PHONY: uninstall/, uninstall
 uninstall/: uninstall
 uninstall:     ##H Uninstall
@@ -196,4 +214,4 @@ uninstall/user:     ##H make uninstall prefix=~/.local
 
 .PHONY: clean
 clean: ##H Clean up
-       rm -rf .coverage
+       rm -rf .coverage .build_tmp
index 2a566ee76f0a5c7c96e62edba4f129720071e6ee..736564f85ddeb2aa5ada111bcea6d513c3c131f8 100644 (file)
@@ -48,6 +48,32 @@ Create an encrypted remote by pushing to it::
     > To gcrypt::[...]
     > * [new branch]      master -> master
 
+    > * [new branch]      master -> master
+
+Command Reference
+=================
+
+::
+
+    Options:
+      help             Show this help message
+      version          Show version information
+      check [URL]      Check if URL is a gcrypt repository
+      clean [URL|REMOTE] Scan/Clean unencrypted files from remote
+        clean -f, --force    Actually delete files (default is scan only)
+        clean -i, --init     Scan even if no manifest found (DANGEROUS with --force)
+    
+    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
+
 Configuration
 =============
 
@@ -78,6 +104,13 @@ The following ``git-config(1)`` variables are supported:
     available secret key in turn until it finds a usable key.
     This can result in unnecessary passphrase prompts.
 
+``gcrypt.allow-unencrypted-remote``
+    Fail safe: by default, git-remote-gcrypt refuses to push to a remote
+    that appears to contain unencrypted files, to avoid exposing your data
+    or overwriting a non-gcrypt repository.
+
+    Setting this to ``true`` disables this safety check.
+
 ``gcrypt.gpg-args``
     The contents of this setting are passed as arguments to gpg.
     E.g. ``--use-agent``.
@@ -256,14 +289,40 @@ Each item extends until newline, and matches one of the following:
 Detecting gcrypt repos
 ======================
 
-To detect if a git url is a gcrypt repo, use: ``git-remote-gcrypt --check url``
-Exit status is 0 if the repo exists and can be decrypted, 1 if the repo
+To detect if a git url is a gcrypt repo, use::
+
+    git-remote-gcrypt check url
+
+(Legacy syntax ``--check`` is also supported).
+
+Exit status is 0
 uses gcrypt but could not be decrypted, and 100 if the repo is not
 encrypted with gcrypt (or could not be accessed).
 
 Note that this has to fetch the repo contents into the local git
 repository, the same as is done when using a gcrypt repo.
 
+Cleaning gcrypt repos
+=====================
+
+To scan for unencrypted files in a remote gcrypt repo, use::
+
+    git-remote-gcrypt clean [url|remote]
+
+.. warning::
+   The clean command is unstable and subject to deprecation or renaming and should not be used in scripts.
+
+Supported backends for the clean command are ``rsync://``, ``rclone://``,
+``sftp://``, and git-based remotes.
+
+If no URL or remote is specified, ``git-remote-gcrypt`` will list all
+available ``gcrypt::`` remotes.
+
+By default, this command only performs a scan. To actually remove the
+unencrypted files, you must use the ``--force`` (or ``-f``) flag::
+
+    git-remote-gcrypt clean url --force
+
 Known issues
 ============
 
diff --git a/completions/README.rst b/completions/README.rst
deleted file mode 100644 (file)
index d1e6f55..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-======================================
-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
-
index 18da214b27c65a8b241b4a43d14a1addda18024d..baf07157aef4412a4fe2a23c7bc73826cc94a92a 100644 (file)
@@ -6,35 +6,41 @@ _git_remote_gcrypt() {
        COMPREPLY=()
        cur="${COMP_WORDS[COMP_CWORD]}"
        prev="${COMP_WORDS[COMP_CWORD - 1]}"
-       opts="-h --help -v --version --check"
-       commands="capabilities list push fetch"
+       opts="-h --help -v --version"
+       commands="capabilities check clean fetch list push"
 
-       # If we're after a subcommand, only offer -h/--help
-       if [[ " $commands " =~ " ${COMP_WORDS[1]:-} " ]]; then
-               COMPREPLY=($(compgen -W "-h --help" -- "$cur"))
+       # 1. First argument: complete commands and global options
+       if [[ $COMP_CWORD -eq 1 ]]; then
+               COMPREPLY=($(compgen -W "$commands $opts" -- "$cur"))
+               if [[ "$cur" == gcrypt::* ]]; then
+                       COMPREPLY+=("$cur")
+               fi
                return 0
        fi
 
-       case "$prev" in
-       --check)
-               # Complete with gcrypt:: URLs or file paths
-               COMPREPLY=($(compgen -f -- "$cur"))
-               return 0
-               ;;
+       # 2. Handle subcommands
+       case "${COMP_WORDS[1]}" in
+               clean)
+                       local remotes=$(git remote -v 2>/dev/null | grep 'gcrypt::' | awk '{print $1}' | sort -u || :)
+                       COMPREPLY=($(compgen -W " $remotes" -- "$cur"))
+                       return 0
+                       ;;
+               check)
+                       local remotes=$(git remote 2>/dev/null || :)
+                       COMPREPLY=($(compgen -W "$remotes" -- "$cur"))
+                       return 0
+                       ;;
+               capabilities|fetch|list|push)
+                       COMPREPLY=($(compgen -W "-h --help" -- "$cur"))
+                       return 0
+                       ;;
        esac
 
+       # 3. Fallback (global flags if not in a known subcommand?)
        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
index 9d089ce62235e82ff8f804a06827bc0082bdf9a5..c5441aab5c1fb9cb905971db3b1fb62d7cb14236 100644 (file)
@@ -3,10 +3,18 @@
 
 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
+
+# Subcommands
+complete -c git-remote-gcrypt -f -n "not __fish_seen_subcommand_from capabilities check clean fetch list push" -a 'check' -d 'Check if URL is a gcrypt repository'
+complete -c git-remote-gcrypt -f -n "not __fish_seen_subcommand_from capabilities check clean fetch list push" -a 'clean' -d 'Scan/Clean unencrypted files from remote'
+complete -c git-remote-gcrypt -n "__fish_seen_subcommand_from clean" -a "(git remote -v 2>/dev/null | grep 'gcrypt::' | awk '{print \$1}' | sort -u)" -d 'Gcrypt Remote'
+complete -c git-remote-gcrypt -n "__fish_seen_subcommand_from check" -a "(git remote 2>/dev/null)" -d 'Git Remote'
+
+# Clean flags
+
 
 # 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'
+complete -c git-remote-gcrypt -f -n "not __fish_seen_subcommand_from capabilities check clean fetch list push" -a 'capabilities' -d 'Show git remote helper capabilities'
+complete -c git-remote-gcrypt -f -n "not __fish_seen_subcommand_from capabilities check clean fetch list push" -a 'list' -d 'List refs in remote repository'
+complete -c git-remote-gcrypt -f -n "not __fish_seen_subcommand_from capabilities check clean fetch list push" -a 'push' -d 'Push refs to remote repository'
+complete -c git-remote-gcrypt -f -n "not __fish_seen_subcommand_from capabilities check clean fetch list push" -a 'fetch' -d 'Fetch refs from remote repository'
diff --git a/completions/gen_docs.sh b/completions/gen_docs.sh
new file mode 100755 (executable)
index 0000000..85691ff
--- /dev/null
@@ -0,0 +1,101 @@
+#!/bin/sh
+set -e
+
+# gen_docs.sh
+# Generates documentation and shell completions from git-remote-gcrypt source.
+# Strictly POSIX sh compliant.
+
+SCRIPT_KEY="HELP_TEXT"
+SRC="git-remote-gcrypt"
+README_TMPL="completions/templates/README.rst.in"
+README_OUT="README.rst"
+BASH_TMPL="completions/templates/bash.in"
+BASH_OUT="completions/bash/git-remote-gcrypt"
+ZSH_TMPL="completions/templates/zsh.in"
+ZSH_OUT="completions/zsh/_git-remote-gcrypt"
+FISH_TMPL="completions/templates/fish.in"
+FISH_OUT="completions/fish/git-remote-gcrypt.fish"
+
+# Ensure we're in the project root
+if [ ! -f "$SRC" ]; then
+       echo "Error: Must be run from project root" >&2
+       exit 1
+fi
+
+# Extract HELP_TEXT variable content
+# Using sed to capture lines between double quotes of HELP_TEXT="..."
+# Assumes HELP_TEXT="..." is a single block.
+RAW_HELP=$(sed -n "/^$SCRIPT_KEY=\"/,/\"$/p" "$SRC" | sed "s/^$SCRIPT_KEY=\"//;s/\"$//")
+
+# 1. Prepare {commands_help} for README (Indented for RST)
+# We want the Options and Git Protocol Commands sections
+COMMANDS_HELP=$(echo "$RAW_HELP" | sed -n '/^Options:/,$p' | sed 's/^/    /')
+
+# 2. Parse Commands and Flags for Completions
+# Extract command names (first word after 2 spaces)
+COMMANDS_LIST=$(echo "$RAW_HELP" | awk '/^  [a-z]+ / {print $1}' | grep -vE "^(help|version)$" | sort | tr '\n' ' ' | sed 's/ $//')
+
+# Extract clean flags
+# Text: "    clean -f, --force    Actually delete files..."
+# We want: "-f --force -i --init" for Bash
+CLEAN_FLAGS_RAW=$(echo "$RAW_HELP" | grep "^    clean -" | awk -F'  ' '{print $2}' | sed 's/,//g')
+CLEAN_FLAGS_BASH=$(echo "$CLEAN_FLAGS_RAW" | tr '\n' ' ' | sed 's/ $//')
+
+# For Zsh: we want simple list for now as per plan, user asked for dynamic but safe.
+# Constructing a simple list of flags requires parsing.
+# The previous python script just injected them.
+CLEAN_FLAGS_ZSH=""
+# We'll just provide the flags as a list for _arguments
+# ZSH format roughly: '(-f --force)'{-f,--force}'[desc]'
+# Let's simplify and just pass the flags for now to match the user's "native completion" request without over-engineering the parsing in shell.
+# We will just list them.
+COMMA_FLAGS=$(echo "$CLEAN_FLAGS_BASH" | tr ' ' ',')
+CLEAN_FLAGS_ZSH="'(${CLEAN_FLAGS_BASH})' {${COMMA_FLAGS}} '[flag]'"
+
+# For Fish
+# We need to turn "-f, --force" into:
+# complete ... -s f -l force ...
+CLEAN_FLAGS_FISH=""
+# Use a loop over the raw lines
+IFS="
+"
+for line in $CLEAN_FLAGS_RAW; do
+       # line is like "-f --force"
+       short=$(echo "$line" | awk '{print $1}' | sed 's/-//')
+       long=$(echo "$line" | awk '{print $2}' | sed 's/--//')
+       # Escape quotes if needed (none usually)
+       CLEAN_FLAGS_FISH="${CLEAN_FLAGS_FISH}complete -c git-remote-gcrypt -f -n \"__fish_seen_subcommand_from clean\" -s $short -l $long -d 'Flag';\n"
+done
+unset IFS
+
+# 3. Generate README
+echo "Generating $README_OUT..."
+sed "s/{commands_help}/$(echo "$COMMANDS_HELP" | sed 's/[\/&]/\\&/g' | sed ':a;N;$!ba;s/\n/\\n/g')/" "$README_TMPL" >"$README_OUT"
+
+# 4. Generate Bash
+echo "Generating Bash completions..."
+sed "s/{commands}/$COMMANDS_LIST/; s/{clean_flags_bash}/$CLEAN_FLAGS_BASH/" "$BASH_TMPL" >"$BASH_OUT"
+
+# 5. Generate Zsh
+echo "Generating Zsh completions..."
+# Zsh substitution is tricky with the complex string.
+# We'll stick to replacing {commands} and {clean_flags_zsh}
+# Need to escape special chars for sed
+SAFE_CMDS=$(echo "$COMMANDS_LIST" | sed 's/ / /g') # just space separated
+# For clean_flags_zsh, since it contains quotes and braces, we need care.
+# We'll read the template line by line? No, sed is standard.
+# We use a temp file for the replacement string to avoid sed escaping hell for large blocks?
+# Or just keep it simple.
+sed "s/{commands}/$COMMANDS_LIST/" "$ZSH_TMPL" \
+       | sed "s|{clean_flags_zsh}|$CLEAN_FLAGS_ZSH|" >"$ZSH_OUT"
+
+# 6. Generate Fish
+echo "Generating Fish completions..."
+# Fish needs {not_sc_list} which matches {commands} (space separated)
+sed "s/{not_sc_list}/$COMMANDS_LIST/g" "$FISH_TMPL" \
+       |
+       # Multi-line replacement in sed is hard. Use awk?
+       # Or just injecting the string with escaped newlines.
+       sed "s|{clean_flags_fish}|$CLEAN_FLAGS_FISH|" >"$FISH_OUT"
+
+echo "Done."
similarity index 91%
copy from README.rst
copy to completions/templates/README.rst.in
index 2a566ee76f0a5c7c96e62edba4f129720071e6ee..a31a7fb9c31cd9321dc12493c410df5f744f2bdf 100644 (file)
@@ -48,6 +48,15 @@ Create an encrypted remote by pushing to it::
     > To gcrypt::[...]
     > * [new branch]      master -> master
 
+    > * [new branch]      master -> master
+
+Command Reference
+=================
+
+::
+
+{commands_help}
+
 Configuration
 =============
 
@@ -256,14 +265,40 @@ Each item extends until newline, and matches one of the following:
 Detecting gcrypt repos
 ======================
 
-To detect if a git url is a gcrypt repo, use: ``git-remote-gcrypt --check url``
-Exit status is 0 if the repo exists and can be decrypted, 1 if the repo
+To detect if a git url is a gcrypt repo, use::
+
+    git-remote-gcrypt check url
+
+(Legacy syntax ``--check`` is also supported).
+
+Exit status is 0
 uses gcrypt but could not be decrypted, and 100 if the repo is not
 encrypted with gcrypt (or could not be accessed).
 
 Note that this has to fetch the repo contents into the local git
 repository, the same as is done when using a gcrypt repo.
 
+Cleaning gcrypt repos
+=====================
+
+To scan for unencrypted files in a remote gcrypt repo, use::
+
+    git-remote-gcrypt clean [url|remote]
+
+.. warning::
+   The clean command is unstable and subject to deprecation or renaming and should not be used in scripts.
+
+Supported backends for the clean command are ``rsync://``, ``rclone://``,
+``sftp://``, and git-based remotes.
+
+If no URL or remote is specified, ``git-remote-gcrypt`` will list all
+available ``gcrypt::`` remotes.
+
+By default, this command only performs a scan. To actually remove the
+unencrypted files, you must use the ``--force`` (or ``-f``) flag::
+
+    git-remote-gcrypt clean url --force
+
 Known issues
 ============
 
diff --git a/completions/templates/bash.in b/completions/templates/bash.in
new file mode 100644 (file)
index 0000000..32956ad
--- /dev/null
@@ -0,0 +1,46 @@
+# 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"
+       commands="{commands}"
+
+       # 1. First argument: complete commands and global options
+       if [[ $COMP_CWORD -eq 1 ]]; then
+               COMPREPLY=($(compgen -W "$commands $opts" -- "$cur"))
+               if [[ "$cur" == gcrypt::* ]]; then
+                       COMPREPLY+=("$cur")
+               fi
+               return 0
+       fi
+
+       # 2. Handle subcommands
+       case "${COMP_WORDS[1]}" in
+               clean)
+                       local remotes=$(git remote -v 2>/dev/null | grep 'gcrypt::' | awk '{print $1}' | sort -u || :)
+                       COMPREPLY=($(compgen -W "{clean_flags_bash} $remotes" -- "$cur"))
+                       return 0
+                       ;;
+               check)
+                       local remotes=$(git remote 2>/dev/null || :)
+                       COMPREPLY=($(compgen -W "$remotes" -- "$cur"))
+                       return 0
+                       ;;
+               capabilities|fetch|list|push)
+                       COMPREPLY=($(compgen -W "-h --help" -- "$cur"))
+                       return 0
+                       ;;
+       esac
+
+       # 3. Fallback (global flags if not in a known subcommand?)
+       if [[ "$cur" == -* ]]; then
+               COMPREPLY=($(compgen -W "$opts" -- "$cur"))
+               return 0
+       fi
+}
+
+complete -F _git_remote_gcrypt git-remote-gcrypt
diff --git a/completions/templates/fish.in b/completions/templates/fish.in
new file mode 100644 (file)
index 0000000..f8f187c
--- /dev/null
@@ -0,0 +1,20 @@
+# 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'
+
+# Subcommands
+complete -c git-remote-gcrypt -f -n "not __fish_seen_subcommand_from {not_sc_list}" -a 'check' -d 'Check if URL is a gcrypt repository'
+complete -c git-remote-gcrypt -f -n "not __fish_seen_subcommand_from {not_sc_list}" -a 'clean' -d 'Scan/Clean unencrypted files from remote'
+complete -c git-remote-gcrypt -n "__fish_seen_subcommand_from clean" -a "(git remote -v 2>/dev/null | grep 'gcrypt::' | awk '{print \$1}' | sort -u)" -d 'Gcrypt Remote'
+complete -c git-remote-gcrypt -n "__fish_seen_subcommand_from check" -a "(git remote 2>/dev/null)" -d 'Git Remote'
+
+# Clean flags
+{clean_flags_fish}
+
+# Git protocol commands
+complete -c git-remote-gcrypt -f -n "not __fish_seen_subcommand_from {not_sc_list}" -a 'capabilities' -d 'Show git remote helper capabilities'
+complete -c git-remote-gcrypt -f -n "not __fish_seen_subcommand_from {not_sc_list}" -a 'list' -d 'List refs in remote repository'
+complete -c git-remote-gcrypt -f -n "not __fish_seen_subcommand_from {not_sc_list}" -a 'push' -d 'Push refs to remote repository'
+complete -c git-remote-gcrypt -f -n "not __fish_seen_subcommand_from {not_sc_list}" -a 'fetch' -d 'Fetch refs from remote repository'
diff --git a/completions/templates/zsh.in b/completions/templates/zsh.in
new file mode 100644 (file)
index 0000000..7d2794d
--- /dev/null
@@ -0,0 +1,32 @@
+#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]'
+               '1:command:({commands})'
+               '*::subcommand arguments:->args'
+       )
+       _arguments -s -S $args
+
+       case $words[1] in
+       clean)
+               _arguments \
+                       {clean_flags_zsh} \
+                       '*:gcrypt URL: _alternative "remotes:gcrypt remote:($(git remote -v 2>/dev/null | grep "gcrypt::" | awk "{print \$1}" | sort -u))" "files:file:_files"'
+               ;;
+       check)
+               _arguments \
+                       '*:gcrypt URL: _alternative "remotes:git remote:($(git remote 2>/dev/null))" "files:file:_files"'
+               ;;
+       *)
+               _arguments \
+                       '*:gcrypt URL:'
+               ;;
+       esac
+}
+
+_git_remote_gcrypt "$@"
index f3686d573b93ac84c7117978b2d3b0caff8036c8..46b09af281c1eb002bcbfe3fafbe9aa5290c0a2f 100644 (file)
@@ -7,11 +7,26 @@ _git_remote_gcrypt() {
        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:'
+               '1:command:(capabilities check clean fetch list push)'
+               '*::subcommand arguments:->args'
        )
        _arguments -s -S $args
+
+       case $words[1] in
+       clean)
+               _arguments \
+                       '()' {} '[flag]' \
+                       '*:gcrypt URL: _alternative "remotes:gcrypt remote:($(git remote -v 2>/dev/null | grep "gcrypt::" | awk "{print \$1}" | sort -u))" "files:file:_files"'
+               ;;
+       check)
+               _arguments \
+                       '*:gcrypt URL: _alternative "remotes:git remote:($(git remote 2>/dev/null))" "files:file:_files"'
+               ;;
+       *)
+               _arguments \
+                       '*:gcrypt URL:'
+               ;;
+       esac
 }
 
 _git_remote_gcrypt "$@"
index c54de9852eb70a025373c02c01ae4c51f0f1ad02..be58d0ce0cb6d7f2c533a18bbc62abcd2d2399c5 100755 (executable)
@@ -32,10 +32,9 @@ 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
+HELP_TEXT="git-remote-gcrypt version $VERSION
 GPG-encrypted git remote helper
 
 Usage: Automatically invoked by git when using gcrypt:: URLs
@@ -43,12 +42,12 @@ Usage: Automatically invoked by git when using gcrypt:: URLs
        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
-  --clean <URL>    Remove unencrypted files from remote (with confirmation)
-    --clean <URL> --dry-run  Show what would be deleted without deleting
-    --clean <URL> --force    Delete without confirmation
+  help             Show this help message
+  version          Show version information
+  check [URL]      Check if URL is a gcrypt repository
+  clean [URL|REMOTE] Scan/Clean unencrypted files from remote
+    clean -f, --force    Actually delete files (default is scan only)
+    clean -i, --init     Scan even if no manifest found (DANGEROUS with --force)
 
 Git Protocol Commands (for debugging):
   capabilities     List remote helper capabilities
@@ -59,75 +58,66 @@ Git Protocol Commands (for debugging):
 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
-}
+  GCRYPT_FULL_REPACK=1  Force full repack when pushing"
 
-# Handle subcommands early (before getopts consumes them)
-# These are not options but subcommands that need their own argument handling
-case "$1" in
-       --check)
-               NAME=dummy-gcrypt-check
-               URL=$2
-               # Will be handled at the end of the script
-               ;;
-       --clean)
-               NAME=dummy-gcrypt-clean
-               URL=$2
-               FORCE_CLEAN=
-               DRY_RUN=
-               if [ "$3" = "--force" ] || [ "$3" = "-f" ]; then
-                       FORCE_CLEAN=yes
-               fi
-               if [ "$3" = "--dry-run" ] || [ "$3" = "-n" ]; then
-                       DRY_RUN=yes
-               fi
-               # Will be handled at the end of the script
-               ;;
-esac
+# Help function
+show_help() {
+       echo "$HELP_TEXT" >&2
+}
 
-# Parse flags
-while getopts "hv-:" opt; do
-       case "$opt" in
-               h)
+# Parse arguments
+while [ $# -gt 0 ]; do
+       case "$1" in
+               help|--help|-h)
                        show_help
                        exit 0
                        ;;
-               v)
+               version|--version|-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
-                                       ;;
-                               check)
-                                       # Allow --check to pass through to the main logic at the bottom
-                                       ;;
-                               clean)
-                                       # Allow --clean to pass through to the main logic at the bottom
-                                       ;;
-                               *)
-                                       echo "Unknown option: --$OPTARG" >&2
-                                       exit 1
-                                       ;;
-                       esac
+               check)
+                       NAME=gcrypt-check
+                       URL="$2"
+                       shift
                        ;;
-               *)
+               clean)
+                       NAME=gcrypt-clean
+                       shift
+                       while [ $# -gt 0 ]; do
+                               case "$1" in
+                                       --force|-f) FORCE_CLEAN=yes ;;
+                                       --init|-i) FORCE_INIT=yes ;;
+                                       -*) echo "Unknown option: $1" >&2; exit 1 ;;
+                                       *)
+                                               if [ -z "$URL" ]; then
+                                                       URL="$1"
+                                               else
+                                                       echo "Error: Multiple URLs/remotes provided to clean" >&2
+                                                       exit 1
+                                               fi
+                                               ;;
+                               esac
+                               shift
+                       done
+                       break # Stop parsing outer loop
+                       ;;
+               -*)
+                       echo "Unknown option: $1" >&2
                        exit 1
                        ;;
+               *)
+                       break
+                       ;;
        esac
 done
 
-# Handle subcommand help (e.g., git-remote-gcrypt capabilities --help)
-shift $((OPTIND - 1))
+# If NAME is not set, we might be invoked as a remote helper
+if [ -z "$NAME" ]; then
+       # We are likely running as "git-remote-gcrypt <remote> <url>"
+       # This case is handled by gcrypt_main_loop "$@" at the bottom if flags/commands were not matched
+       :
+fi
 case "${1:-}" in
        capabilities)
                if [ "${2:-}" = "-h" ] || [ "${2:-}" = "--help" ]; then
@@ -198,7 +188,6 @@ Recipients=
 # xfeed: The most basic output function puts $1 into the stdin of $2..$#
 xfeed()
 {
-       # shellcheck disable=SC3043
        local input_=""
        input_=$1; shift
        "$@" <<EOF
@@ -233,6 +222,41 @@ islocalrepo() { isnull "${1##/*}" && [ ! -e "$1/HEAD" ]; }
 
 xgrep() { command grep "$@" || : ; }
 
+
+
+# Resolve URL or remote name, or list remotes if empty
+resolve_url() {
+       local cmd="$1"
+       if [ -z "$URL" ]; then
+               local remotes
+               remotes=$(git remote -v | awk '{print $1 "  " $2}' | sort -u || :)
+               echo "Usage: git-remote-gcrypt $cmd [URL|REMOTE]" >&2
+               if [ -n "$remotes" ]; then
+                       echo "No URL or remote specified. Available remotes:" >&2
+                       echo "$remotes" | sed 's/^/  /' >&2
+                       exit 0
+               else
+                       echo "Error: No remotes found and no URL/remote specified." >&2
+                       exit 1
+               fi
+       fi
+
+       # If it's not a URL, try to resolve as a remote name
+       if ! echo "$URL" | grep -q -E '://|::' || [ -n "${URL##*/*}" ]; then
+               local potential_url
+               potential_url=$(git config --get "remote.$URL.url" || :)
+               if [ -n "$potential_url" ]; then
+                       # Don't clean non-gcrypt remotes!
+                       if ! echo "$potential_url" | grep -q '^gcrypt::'; then
+                               echo_die "Error: Remote '$URL' is not a gcrypt:: remote."
+                       fi
+                       print_debug "Resolved remote '$URL' to '$potential_url'"
+                       URL="$potential_url"
+               fi
+       fi
+       URL="${URL#gcrypt::}"
+}
+
 # setvar is used for named return variables
 # $1 *must* be a valid variable name, $2 is any value
 #
@@ -252,7 +276,6 @@ Newline="
 # $1 is return var, $2 is value appended with newline separator
 append_to()
 {
-       # shellcheck disable=SC3043
        local f_append_tmp_=""
        eval f_append_tmp_=\$"${1#@}"
        isnull "$f_append_tmp_" || f_append_tmp_=$f_append_tmp_$Newline
@@ -264,7 +287,6 @@ append_to()
 # $2 input value
 pick_fields_1_2()
 {
-       # shellcheck disable=SC3043
        local f_ret="" f_one="" f_two=""
        while read -r f_one f_two _ # from << here-document
        do
@@ -283,7 +305,6 @@ EOF
 #  we instead remove all lines matching
 filter_to()
 {
-       # shellcheck disable=SC3043
        local f_neg="" f_line="" f_ret="" IFS=""
        isnoteq "$1" "!" || { f_neg=negate; shift; }
        IFS=$Newline
@@ -298,7 +319,6 @@ filter_to()
 # Output the number of lines in $1
 line_count()
 {
-       # shellcheck disable=SC3043
        local IFS=""
        IFS=$Newline
        # shellcheck disable=SC2086
@@ -318,7 +338,6 @@ rsynclocation ()
 gitception_get()
 {
        # Take care to preserve FETCH_HEAD
-       # shellcheck disable=SC3043
        local ret_=: obj_id="" fet_head="$GIT_DIR/FETCH_HEAD"
        if [ -e "$fet_head" ]; then
                command mv -f "$fet_head" "$fet_head.$$~" || :
@@ -353,7 +372,6 @@ EOF
 # Get 'tree' from $1, change file $2 to obj id $3
 update_tree()
 {
-       # shellcheck disable=SC3043
        local tab_="    "
        # $2 is a filename from the repo format
        (set +e;
@@ -366,7 +384,6 @@ update_tree()
 # depends on previous GET to set $Gref and depends on PUT_FINAL later
 gitception_put()
 {
-       # shellcheck disable=SC3043
        local obj_id="" tree_id="" commit_id=""
        obj_id=$(git hash-object -w --stdin) &&
                tree_id=$(update_tree "$Gref" "$2" "$obj_id") &&
@@ -378,7 +395,6 @@ gitception_put()
 # depends on previous GET like put
 gitception_remove()
 {
-       # shellcheck disable=SC3043
        local tree_id="" commit_id="" tab_="    "
        # $2 is a filename from the repo format
        tree_id=$(git ls-tree "$Gref" | awk -F'\t' -v f="$2" '$2 != f' | git mktree) &&
@@ -388,7 +404,6 @@ gitception_remove()
 
 gitception_new_repo()
 {
-       # shellcheck disable=SC3043
        local commit_id="" empty_tree=4b825dc642cb6eb9a060e54bf8d69288fbee4904
        # get any file to update Gref, and if it's not updated we create empty
        git update-ref -d "$Gref" || :
@@ -494,7 +509,6 @@ PUTREPO()
 # For repo $1, delete all newline-separated files in $2
 REMOVE()
 {
-       # shellcheck disable=SC3043
        local fn_=""
        print_debug "REMOVE $1 $2"
        if isurl sftp "$1"
@@ -566,7 +580,6 @@ PRIVENCRYPT()
 # $1 is the match for good signature, $2 is the textual signers list
 PRIVDECRYPT()
 {
-       # shellcheck disable=SC3043
        local status_="" signer_=""
        exec 4>&1 &&
        status_=$(rungpg --status-fd 3 -q -d 3>&1 1>&4 || {
@@ -601,7 +614,6 @@ genkey()
 
 gpg_hash()
 {
-       # shellcheck disable=SC3043
        local hash_=""
        hash_=$(rungpg --with-colons --print-md "$1" | tr A-F a-f)
        hash_=${hash_#:*:}
@@ -647,7 +659,7 @@ make_new_repo()
 # $1 return var for goodsig match, $2 return var for signers text
 read_config()
 {
-       # shellcheck disable=SC3043,SC2034
+       # shellcheck disable=SC2034
        local recp_="" r_tail="" r_keyinfo="" r_keyfpr="" gpg_list="" cap_="" conf_part="" good_sig="" signers_=""
        Conf_signkey=$(git config --get "remote.$NAME.gcrypt-signingkey" '.+' ||
                git config --path user.signingkey || :)
@@ -722,9 +734,70 @@ read_config()
        print_debug "read_config done"
 }
 
+early_safety_check()
+{
+       local check_files="" early_bad_files=""
+
+       # EARLY SAFETY CHECK for gitception backends:
+       # Before GPG validation, check if the remote has unencrypted files.
+       if [ "$NAME" = "gcrypt-clean" ]; then
+               return 0
+       fi
+       # For dumb backends (rsync/sftp/rclone/local), check for ANY files.
+       if isurl sftp "$URL" || isurl rsync "$URL" || isurl rclone "$URL" || islocalrepo "$URL"; then
+               local dumb_files=""
+               get_remote_file_list @dumb_files
+               if isnull "$dumb_files"; then
+                       return 0
+               fi
+               
+               if [ "$(git config --bool gcrypt.allow-unencrypted-remote)" = "true" ]; then
+                       return 0
+               fi
+
+               echo_info "ERROR: Remote repository is not empty!"
+               echo_info "To protect your privacy, git-remote-gcrypt will NOT push to this remote"
+               echo_info "unless you force it or clean it."
+               echo_info "Found files: $(echo "$dumb_files" | head -n 3 | tr '\n' ' ')..."
+               echo_info "To see files: git-remote-gcrypt clean $URL"
+               echo_info "To init anyway (DANGEROUS if not empty): git push --force ..."
+               echo_info "OR set gcrypt.allow-unencrypted-remote to true."
+               exit 1
+       fi
+
+       git fetch --quiet "$URL" "refs/heads/master:refs/gcrypt/safety-check" 2>/dev/null ||
+       git fetch --quiet "$URL" "refs/heads/main:refs/gcrypt/safety-check" 2>/dev/null || true
+
+       if ! git rev-parse --verify "refs/gcrypt/safety-check" >/dev/null 2>&1; then
+               return 0
+       fi
+
+       check_files=$(git ls-tree --name-only "refs/gcrypt/safety-check" 2>/dev/null || :)
+       git update-ref -d "refs/gcrypt/safety-check" 2>/dev/null || true
+
+       if isnull "$check_files"; then
+               return 0
+       fi
+
+       early_bad_files=$(echo "$check_files" | grep -v -E '^[a-f0-9]{56}$|^[a-f0-9]{64}$|^[a-f0-9]{96}$|^[a-f0-9]{128}$' || :)
+       if isnull "$early_bad_files"; then
+               return 0
+       fi
+
+       if [ "$(git config --bool gcrypt.allow-unencrypted-remote)" = "true" ]; then
+               return 0
+       fi
+
+       echo_info "ERROR: Remote repository contains unencrypted or unknown files!"
+       echo_info "To protect your privacy, git-remote-gcrypt will NOT push to this remote."
+       echo_info "Found unexpected files: $(echo "$early_bad_files" | head -n 3 | tr '\n' ' ')"
+       echo_info "To see unencrypted files, use: git-remote-gcrypt clean $URL"
+       echo_info "To fix and remove these files, use: git-remote-gcrypt clean --force $URL"
+       exit 1
+}
+
 ensure_connected()
 {
-       # shellcheck disable=SC3043
        local manifest_="" r_repoid="" r_name="" url_frag="" r_sigmatch="" r_signers="" \
                tmp_manifest="" tmp_stderr="" early_bad_files=""
 
@@ -732,44 +805,9 @@ ensure_connected()
        then
                return
        fi
-       
-       # EARLY SAFETY CHECK for gitception backends:
-       # Before GPG validation, check if the remote has unencrypted files.
-       # This prevents the GPG error from masking the privacy leak warning.
-       # Skip this check if we are explicitly running the clean command.
-       if [ "$NAME" != "dummy-gcrypt-clean" ] && ! isurl sftp "$URL" && ! isurl rsync "$URL" && ! isurl rclone "$URL" && ! islocalrepo "$URL"; then
-               # It's a gitception backend - do early safety check
-               # Fetch the default branch to see what files exist
-               # shellcheck disable=SC3043
-               local check_files=""
-               git fetch --quiet "$URL" "refs/heads/master:refs/gcrypt/safety-check" 2>/dev/null ||
-               git fetch --quiet "$URL" "refs/heads/main:refs/gcrypt/safety-check" 2>/dev/null || true
 
-               if git rev-parse --verify "refs/gcrypt/safety-check" >/dev/null 2>&1; then
-                       check_files=$(git ls-tree --name-only "refs/gcrypt/safety-check" 2>/dev/null || :)
-                       # Clean up the temp ref
-                       git update-ref -d "refs/gcrypt/safety-check" 2>/dev/null || true
-
-                       if isnonnull "$check_files"; then
-                               # Check if ANY file doesn't match gcrypt pattern (hash filenames)
-                               early_bad_files=$(echo "$check_files" | grep -v -E '^[a-f0-9]{56}$|^[a-f0-9]{64}$|^[a-f0-9]{96}$|^[a-f0-9]{128}$' || :)
-                               if isnonnull "$early_bad_files"; then
-                                       # Check config to see if we should ignore
-                                       if [ "$(git config --bool gcrypt.allow-unencrypted-remote)" != "true" ]; then
-                                               echo_info "ERROR: Remote repository contains unencrypted or unknown files!"
-                                               echo_info "To protect your privacy, git-remote-gcrypt will NOT push to this remote."
-                                               echo_info "Found the following unexpected files:"
-                                               echo_info "$early_bad_files" | head -n 5 | sed 's/^/  /' >&2
-                                               echo_info ""
-                                               echo_info "To fix: use 'git-remote-gcrypt --clean $URL' to remove these files,"
-                                               echo_info "or set 'git config gcrypt.allow-unencrypted-remote true' to ignore."
-                                               exit 1
-                                       fi
-                               fi
-                       fi
-               fi
-       fi
-       
+       early_safety_check
+
        Did_find_repo=no
        print_debug "Calling read_config"
        read_config @r_sigmatch @r_signers
@@ -872,7 +910,6 @@ ensure_connected()
 # $3 the key
 get_verify_decrypt_pack()
 {
-       # shellcheck disable=SC3043
        local rcv_id="" tmp_encrypted=""
        tmp_encrypted="$Tempdir/packF"
        GET "$URL" "$2" "$tmp_encrypted" &&
@@ -887,7 +924,6 @@ get_verify_decrypt_pack()
 # $1 destdir (when repack, else "")
 get_pack_files()
 {
-       # shellcheck disable=SC3043
        local pack_id="" r_pack_key_line="" htype_="" pack_="" key_=""
        while IFS=': ' read -r _ htype_ pack_ # <<here-document
        do
@@ -922,7 +958,6 @@ get_pack_files()
 # $1 return var for list of packfiles to delete
 repack_if_needed()
 {
-       # shellcheck disable=SC3043
        local n_="" m_="" kline_="" r_line="" r_keep_packlist="" r_del_list=""
 
        isnonnull "$Packlist" || return 0
@@ -976,7 +1011,6 @@ do_capabilities()
 
 do_list()
 {
-       # shellcheck disable=SC3043
        local obj_id="" ref_name="" line_=""
        ensure_connected
 
@@ -999,7 +1033,6 @@ do_list()
 do_fetch()
 {
        # Download packs in the manifest that don't appear in have_packs
-       # shellcheck disable=SC3043
        local pneed_="" premote_=""
 
        ensure_connected
@@ -1025,7 +1058,6 @@ do_push()
        # Each git packfile is encrypted and then named for the encrypted
        # file's hash. The manifest is updated with the pack id.
        # The manifest is encrypted.
-       # shellcheck disable=SC3043
        local r_revlist="" pack_id="" key_="" obj_="" src_="" dst_="" \
                r_pack_delete="" tmp_encrypted="" tmp_objlist="" tmp_manifest="" \
                force_passed=true
@@ -1034,9 +1066,8 @@ do_push()
 # Check if the backend repo has unencrypted files (safety check)
 check_safety()
 {
-       # shellcheck disable=SC3043
        local bad_files="" remote_files="" valid_files="" f=""
-       
+
        # We want to check this for 'gitception' backends (git-over-git).
        # Logic mirrors GET/PUT logic: if not sftp, rsync, rclone, or a Dumb Local Repo (islocalrepo),
        # then it is a git repo (gitception or implicit).
@@ -1286,7 +1317,6 @@ setup()
 # handle git-remote-helpers protocol
 gcrypt_main_loop()
 {
-       # shellcheck disable=SC3043
        local input_="" input_inner="" r_args="" temp_key=""
 
        NAME=$1  # Remote name
@@ -1346,32 +1376,65 @@ gcrypt_main_loop()
        done
 }
 
-if [ "$NAME" = "dummy-gcrypt-check" ]; then
-       # NAME and URL were set at the top of the script
-       setup
-       ensure_connected
-       git remote remove "$NAME" 2>/dev/null || true
-       if iseq "$Did_find_repo" "no"
-       then
-               exit 100
+get_remote_file_list()
+{
+       local r_files="" err_code=0
+       # Get all files in the remote
+       # For rsync backends, list files directly via rsync --list-only (awk extracts filename).
+       # For rclone backends, list files via rclone lsf.
+       # For sftp backends, list files via curl directory listing.
+       # For local backends, list files with ls
+       # For git backends, list files from the gcrypt branch tree.
+       if isurl rsync "$URL"; then
+               r_files=$(rsync --no-motd --list-only "$(rsynclocation "$URL")/" | awk '{print $NF}' | grep -vE '^\.$|^\.\.$') || return 1
+       elif isurl rclone "$URL"; then
+               r_files=$(rclone lsf "$(rclonelocation "$URL")") || return 1
+       elif isurl sftp "$URL"; then
+               r_files=$(curl -s -S -k "$URL/" | grep -vE '^\.$|^\.\.$') || return 1
+       elif islocalrepo "$URL"; then
+               if [ -d "$URL" ]; then
+                       r_files=$(ls -1A "$URL") || return 1
+               else
+                       # If directory doesn't exist, it's "empty" (or will be created)
+                       r_files=""
+               fi
+       else
+               # Git backend: Check safety-check ref first (most reliable if early_safety_check ran)
+               # Or try to fetch master?
+               # If early_safety_check ran, it fetched to refs/gcrypt/safety-check.
+               if git rev-parse --verify "refs/gcrypt/safety-check" >/dev/null 2>&1; then
+                       r_files=$(git ls-tree -r --name-only "refs/gcrypt/safety-check") || return 1
+               else
+                       # Try fetching default branch?
+                       # If we can't verify emptiness, we should return error to prevent implicit init.
+                       # Using $Gref (refs/gcrypt/gitception...) might be empty if we haven't pushed yet.
+                       return 1
+               fi
        fi
-elif [ "$NAME" = "dummy-gcrypt-clean" ]; then
-       # Cleanup command: NAME, URL, FORCE_CLEAN, DRY_RUN were set at the top
-
-       if isnull "$URL"; then
-               echo_info "Usage: git-remote-gcrypt --clean <URL> [--force|--dry-run]"
-               echo_info "  Removes unencrypted files from the remote repository."
-               echo_info "  --force, -f     Don't ask for confirmation"
-               echo_info "  --dry-run, -n   Show what would be deleted without deleting"
-               exit 1
+       setvar "$1" "$r_files"
+       return $err_code
+}
+
+cmd_clean()
+{
+       local remote_files="" valid_files="" bad_files="" f=""
+
+       if ! ensure_connected; then
+               echo_die "Could not connect to $URL."
        fi
-       
-       setup
-       ensure_connected
-       
-       # Get all files in the remote
-       remote_files=$(git ls-tree --name-only "$Gref" 2>/dev/null || :)
-       
+
+       if [ "$Did_find_repo" != "yes" ]; then
+               if [ "${FORCE_INIT:-}" = "yes" ]; then
+                       echo_info "WARNING: No gcrypt manifest found, but --init specified."
+                       echo_info "WARNING: Proceeding to scan/clean potential unencrypted files."
+               else
+                       echo_die "Error: No gcrypt manifest found on remote '$URL'." \
+                               "Aborting clean to prevent accidental data loss."
+               fi
+       fi
+
+       get_remote_file_list @remote_files || echo_die "Failed to list remote files."
+
        if isnull "$remote_files"; then
                echo_info "Remote is empty. Nothing to clean."
                CLEAN_FINAL "$URL"
@@ -1390,11 +1453,14 @@ elif [ "$NAME" = "dummy-gcrypt-clean" ]; then
        
        # Find files to delete
        bad_files=""
+       OIFS="$IFS"
+       IFS="$Newline"
        for f in $remote_files; do
                if isnull "$valid_files" || ! xfeed "$valid_files" grep -qxF "$f"; then
                        bad_files="$bad_files$Newline$f"
                fi
        done
+       IFS="$OIFS"
        bad_files="${bad_files#"$Newline"}"
        
        if isnull "$bad_files"; then
@@ -1407,26 +1473,15 @@ elif [ "$NAME" = "dummy-gcrypt-clean" ]; then
        echo_info "Found the following files to remove:"
        xecho "$bad_files" | sed 's/^/  /' >&2
 
-       if isnonnull "$DRY_RUN"; then
-               echo_info "(Dry run - no files were deleted)"
+       if isnull "$FORCE_CLEAN"; then
+               echo_info "NOTE: This is a scan of unencrypted files on the remote."
+               echo_info "To actually delete these files, use:"
+               echo_info "   git-remote-gcrypt clean $URL --force"
                CLEAN_FINAL "$URL"
                git remote remove "$NAME" 2>/dev/null || true
                exit 0
        fi
 
-       if isnull "$FORCE_CLEAN"; then
-               echo_info ""
-               echo_info "WARNING: This will permanently delete these files from the remote!"
-               echo_info "Make sure you have a backup (e.g., git clone $URL backup-repo)"
-               echo_info ""
-               printf "Delete these files? [y/N] " >&2
-               read -r ans
-               case "$ans" in
-                       [Yy]*) ;;
-                       *) echo_info "Aborted."; exit 1 ;;
-               esac
-       fi
-
        echo_info "Removing files..."
        REMOVE "$URL" "$bad_files"
        PUT_FINAL "$URL"
@@ -1434,9 +1489,28 @@ elif [ "$NAME" = "dummy-gcrypt-clean" ]; then
        git remote remove "$NAME" 2>/dev/null || true
        echo_info "Done. Remote cleaned."
        exit 0
+}
+
+if [ "$NAME" = "gcrypt-check" ]; then
+       resolve_url check
+       echo_info "Checking remote: $URL"
+       setup
+       ensure_connected
+       CLEAN_FINAL "$URL"
+       git remote remove "$NAME" 2>/dev/null || true
+       if iseq "$Did_find_repo" "no"
+       then
+               exit 100
+       fi
+elif [ "$NAME" = "gcrypt-clean" ]; then
+       resolve_url clean
+       echo_info "Checking remote: $URL"
+       setup
+       cmd_clean
 elif [ "$1" = --version ] || [ "$1" = -v ]; then
        echo "git-remote-gcrypt version $VERSION"
        exit 0
 else
        gcrypt_main_loop "$@"
+       # gcrypt_main_loop "$NAME" "$URL"
 fi
index 4fa90a25c30bcc304269369e5a26800a2b092be7..f83d3534350003333d7b26400e9925b2ce61c124 100755 (executable)
@@ -8,8 +8,8 @@ verbose() { echo "$@" >&2 && "$@"; }
 
 install_v() {
        # Install $1 into $2/ with mode $3
-       verbose install -d "$2" &&
-               verbose install -m "$3" "$1" "$2"
+       verbose install -d "$2" \
+               && verbose install -m "$3" "$1" "$2"
 }
 
 # --- VERSION DETECTION ---
@@ -46,6 +46,9 @@ trap 'rm -rf "$BUILD_DIR"' EXIT
 # Placeholder injection
 sed "s|@@DEV_VERSION@@|$VERSION|g" git-remote-gcrypt >"$BUILD_DIR/git-remote-gcrypt"
 
+# --- GENERATION ---
+verbose ./utils/gen_docs.sh
+
 # --- INSTALLATION ---
 # This is where the 'Permission denied' happens if not sudo
 install_v "$BUILD_DIR/git-remote-gcrypt" "$DESTDIR$prefix/bin" 755
@@ -65,15 +68,13 @@ else
        echo "'rst2man' not found, man page not installed" >&2
 fi
 
-# Suggest installing shell completions
-cat >&2 <<EOF
-
-Installation complete!
-
-Optional: Install shell completions for tab completion:
-  Bash:  completions/bash/git-remote-gcrypt
-  Zsh:   completions/zsh/_git-remote-gcrypt
-  Fish:  completions/fish/git-remote-gcrypt.fish
+# Install shell completions
+# Bash
+install_v completions/bash/git-remote-gcrypt "$DESTDIR$prefix/share/bash-completion/completions" 644
+# Zsh
+install_v completions/zsh/_git-remote-gcrypt "$DESTDIR$prefix/share/zsh/site-functions" 644
+# Fish
+install_v completions/fish/git-remote-gcrypt.fish "$DESTDIR$prefix/share/fish/vendor_completions.d" 644
 
-See completions/README.rst for details.
-EOF
+echo "Installation complete!"
+echo "Completions installed to $DESTDIR$prefix/share/"
index d1892d3bf4655e5d3a2deb246351dba15af39fd0..2024a1378730c1c206369612b4f8ac04c3d34e96 100755 (executable)
@@ -42,8 +42,8 @@ assert() {
                "${@}"
        )
        local -r status=${?}
-       { [[ ${status} -eq 0 ]] && print_success "Verification succeeded."; } ||
-               print_err "Verification failed."
+       { [[ ${status} -eq 0 ]] && print_success "Verification succeeded."; } \
+               || print_err "Verification failed."
        return "${status}"
 }
 
index 1d329a4076450bf0cc564f734d586c25b6c1a4ad..ba476f9c45c4ccf3b315e8eeed9ff7560dff9db6 100755 (executable)
@@ -443,7 +443,7 @@ print_info "Step 9: Network Failure Guard Test (manifest unavailable):"
             print_success "Correct error message received."
         else
             print_err "Wrong error message!"
-            cat "${step9_output}" | indent
+            indent < "${step9_output}"
             exit 1
         fi
     else
@@ -482,7 +482,9 @@ print_info "Step 10: New Repo Safety Test (Require Force):"
     
     cd "${tempdir}/fresh_clone_test"
     
-    print_info "Attempting push to missing remote WITHOUT force..."
+
+
+    print_info "Attempting push to missing remote WITHOUT force (Should Fail)..."
     set +e
     (
         git push "gcrypt::${missing_remote_url}" "${default_branch}" 2>&1
@@ -491,14 +493,12 @@ print_info "Step 10: New Repo Safety Test (Require Force):"
     set -e
     
     if [ $rc -ne 0 ]; then
+        print_success "Push correctly failed without force."
         if grep -q "Use --force to create valid new repository" "step10.fail"; then
-            print_success "Push correctly BLOCKED without force."
-        else
-            cat "step10.fail" | indent
-            print_err "Push failed but with wrong error message!"
-            exit 1
+            print_success "Correct error message received."
         fi
     else
+        indent < "step10.fail"
         print_err "Push SHOULD have failed but SUCCEEDED!"
         exit 1
     fi
@@ -514,7 +514,7 @@ print_info "Step 10: New Repo Safety Test (Require Force):"
     if [ $rc -eq 0 ]; then
         print_success "Push succeeded with force."
     else
-        cat "step10.succ" | indent
+        indent < "step10.succ"
         print_err "Push failed even with force!"
         exit 1
     fi
index b9bce31e4db3c620e9f1f03907455bc7ac033b77..11dc50b33eefe3d9af18ae85c68df256bfa98198 100755 (executable)
@@ -1,6 +1,6 @@
 #!/bin/bash
-# Test: --clean command removes unencrypted files
-# This test verifies that git-remote-gcrypt --clean correctly identifies
+# Test: clean command removes unencrypted files
+# This test verifies that git-remote-gcrypt clean correctly identifies
 # and removes unencrypted files from a remote.
 
 set -e
@@ -19,71 +19,255 @@ print_err() { echo -e "${RED}✗ $*${NC}"; }
 SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
 export PATH="$SCRIPT_DIR:$PATH"
 
+# Isolate git config from user environment
+export GIT_CONFIG_SYSTEM=/dev/null
+export GIT_CONFIG_GLOBAL=/dev/null
+
 # Suppress git advice messages
-GIT="git -c advice.defaultBranchName=false"
+# Note: git-remote-gcrypt reads actual config files, not just CLI -c options
+GIT="git -c advice.defaultBranchName=false -c commit.gpgSign=false"
 
+# --------------------------------------------------
+# Set up test environment
+# --------------------------------------------------
 # Create temp directory
 tempdir=$(mktemp -d)
 trap 'rm -rf "$tempdir"' EXIT
 
 print_info "Setting up test environment..."
 
+# --------------------------------------------------
+# GPG Setup (Derived from system-test.sh)
+# --------------------------------------------------
+export GNUPGHOME="${tempdir}/gpg"
+mkdir "${GNUPGHOME}"
+
+# Wrapper to suppress obsolete warnings
+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"
+
+# Generate key
+(
+       gpg --batch --passphrase "" --quick-generate-key "Test <test@test.com>"
+)
+
+# --------------------------------------------------
+# Git Setup
+# --------------------------------------------------
+
 # Create a bare repo with dirty files
 $GIT init --bare "$tempdir/remote.git" >/dev/null
 cd "$tempdir/remote.git"
 $GIT config user.email "test@test.com"
 $GIT config user.name "Test"
+$GIT config gpg.program "${GNUPGHOME}/gpg"
+# Needed for encryption to work during setup
+$GIT config gcrypt.participants "test@test.com"
 
 # Add multiple unencrypted files
+# Add multiple unencrypted files including nested ones
 echo "SECRET=abc" >"$tempdir/secret1.txt"
 echo "PASSWORD=xyz" >"$tempdir/secret2.txt"
+# Nested file
+mkdir -p "$tempdir/subdir"
+echo "NESTED=123" >"$tempdir/subdir/nested.txt"
+
+echo "SPACE=789" >"$tempdir/Has Space.txt"
+
 BLOB1=$($GIT hash-object -w "$tempdir/secret1.txt")
 BLOB2=$($GIT hash-object -w "$tempdir/secret2.txt")
-TREE=$(echo -e "100644 blob $BLOB1\tsecret1.txt\n100644 blob $BLOB2\tsecret2.txt" | $GIT mktree)
-COMMIT=$(echo "Dirty commit" | $GIT commit-tree "$TREE")
+BLOB3=$($GIT hash-object -w "$tempdir/subdir/nested.txt")
+BLOB4=$($GIT hash-object -w "$tempdir/Has Space.txt")
+
+# Create root tree using index
+export GIT_INDEX_FILE=index.dirty
+$GIT update-index --add --cacheinfo 100644 "$BLOB1" "secret1.txt"
+$GIT update-index --add --cacheinfo 100644 "$BLOB2" "secret2.txt"
+$GIT update-index --add --cacheinfo 100644 "$BLOB3" "subdir/nested.txt"
+$GIT update-index --add --cacheinfo 100644 "$BLOB4" "Has Space.txt"
+TREE=$($GIT write-tree)
+rm index.dirty
+
+COMMIT=$(echo "Dirty commit with nested files" | $GIT commit-tree "$TREE")
 $GIT update-ref refs/heads/master "$COMMIT"
 
 print_info "Created dirty remote with 2 unencrypted files"
 
-# Test 1: --clean without URL shows usage
+# Test helper
+assert_grep() {
+       local pattern="$1"
+       local input="$2"
+       local msg="$3"
+       if echo "$input" | grep -q "$pattern"; then
+               print_success "$msg"
+       else
+               print_err "$msg - Pattern '$pattern' not found"
+               echo "Output: $input"
+               exit 1
+       fi
+}
+
+# --------------------------------------------------
+# Test 1: Usage message when no remotes found
+# --------------------------------------------------
 print_info "Test 1: Usage message..."
-if "$SCRIPT_DIR/git-remote-gcrypt" --clean 2>&1 | grep -q "Usage"; then
-       print_success "--clean shows usage when URL missing"
+mkdir "$tempdir/empty" && cd "$tempdir/empty" && $GIT init >/dev/null
+output=$("$SCRIPT_DIR/git-remote-gcrypt" clean 2>&1 || :)
+assert_grep "Usage: git-remote-gcrypt clean" "$output" "clean shows usage when no URL/remote found"
+
+# --------------------------------------------------
+# Test 2: Safety Check (Abort on non-gcrypt)
+# --------------------------------------------------
+print_info "Test 2: Safety Check (Abort on non-gcrypt)..."
+cd "$tempdir/remote.git"
+output=$("$SCRIPT_DIR/git-remote-gcrypt" clean "$tempdir/remote.git" 2>&1 || :)
+assert_grep "Error: No gcrypt manifest found" "$output" "clean aborts on non-gcrypt repo"
+
+if $GIT ls-tree HEAD | grep -q "secret1.txt"; then
+       print_success "Files preserved (Safety check passed)"
 else
-       print_err "--clean should show usage when URL missing"
+       print_err "Files deleted despite safety check!"
        exit 1
 fi
 
-# Test 2: --clean --dry-run shows files without deleting
-print_info "Test 2: Dry run mode..."
-output=$("$SCRIPT_DIR/git-remote-gcrypt" --clean "$tempdir/remote.git" --dry-run 2>&1)
-if echo "$output" | grep -q "secret1.txt" && echo "$output" | grep -q "Dry run"; then
-       print_success "--clean --dry-run shows files and doesn't delete"
-else
-       print_err "--clean --dry-run failed"
-       echo "$output"
+# --------------------------------------------------
+# Test 3: Remote resolution (Abort on non-gcrypt)
+# --------------------------------------------------
+print_info "Test 3: Remote resolution..."
+mkdir -p "$tempdir/client" && cd "$tempdir/client" && $GIT init >/dev/null
+$GIT config gpg.program "${GNUPGHOME}/gpg"
+$GIT remote add origin "$tempdir/remote.git"
+output=$("$SCRIPT_DIR/git-remote-gcrypt" clean origin 2>&1 || :)
+assert_grep "Error: Remote 'origin' is not a gcrypt:: remote" "$output" "clean aborts on resolved non-gcrypt remote"
+
+# --------------------------------------------------
+# Test 4: Remote listing
+# --------------------------------------------------
+print_info "Test 4: Remote listing..."
+$GIT remote add gcrypt-origin "gcrypt::$tempdir/remote.git"
+output=$("$SCRIPT_DIR/git-remote-gcrypt" clean 2>&1 || :)
+assert_grep "Available remotes:" "$output" "clean lists remotes"
+assert_grep "gcrypt-origin" "$output" "clean listed 'gcrypt-origin'"
+
+# --------------------------------------------------
+# Test 5: Clean Valid Gcrypt Repo
+# --------------------------------------------------
+print_info "Test 5: Clean Valid Gcrypt Repo..."
+
+# 1. Initialize a valid gcrypt repo
+mkdir "$tempdir/valid.git" && cd "$tempdir/valid.git" && $GIT init --bare >/dev/null
+$GIT config user.email "test@test.com"
+$GIT config user.name "Test"
+cd "$tempdir/client"
+$GIT config user.name "Test"
+$GIT config user.email "test@test.com"
+$GIT config user.signingkey "test@test.com"
+# Create content to push
+echo "valid content" >content.txt
+$GIT add content.txt
+$GIT commit -m "init valid"
+# Push to intialize
+set -x
+$GIT push -f "gcrypt::$tempdir/valid.git" master:master || {
+       set +x
+       print_err "Git push failed"
+       exit 1
+}
+set +x
+
+print_info "Initialized valid gcrypt repo"
+
+# 2. Inject garbage file into the remote git index/tree
+cd "$tempdir/valid.git"
+GREF="refs/heads/master"
+if ! $GIT rev-parse --verify "$GREF" >/dev/null 2>&1; then
+       print_err "Gref $GREF not found in remote!"
        exit 1
 fi
 
-# Verify files still exist
-if $GIT -C "$tempdir/remote.git" ls-tree HEAD | grep -q "secret1.txt"; then
-       print_success "Files still exist after dry run"
-else
-       print_err "Dry run incorrectly deleted files!"
+GARBAGE_BLOB=$(echo "GARBAGE DATA" | $GIT hash-object -w --stdin)
+CURRENT_TREE=$($GIT rev-parse "$GREF^{tree}")
+export GIT_INDEX_FILE=index.garbage
+$GIT read-tree "$CURRENT_TREE"
+$GIT update-index --add --cacheinfo 100644 "$GARBAGE_BLOB" "garbage_file"
+NEW_TREE=$($GIT write-tree)
+rm index.garbage
+PARENT=$($GIT rev-parse "$GREF")
+NEW_COMMIT=$(echo "Inject garbage" | $GIT commit-tree "$NEW_TREE" -p "$PARENT")
+$GIT update-ref "$GREF" "$NEW_COMMIT"
+
+# Verify injection
+if ! $GIT ls-tree -r "$GREF" | grep -q "garbage_file"; then
+       print_err "Failed to inject garbage_file into $GREF"
        exit 1
 fi
+print_info "Injected garbage_file into remote $GREF"
+
+# 3. Scan (expect to find garbage_file)
+set -x
+output=$("$SCRIPT_DIR/git-remote-gcrypt" clean "gcrypt::$tempdir/valid.git" 2>&1)
+set +x
+assert_grep "garbage_file" "$output" "clean identified unencrypted file in valid repo"
+assert_grep "NOTE: This is a scan" "$output" "clean scan-only mode confirmed"
 
-# Test 3: --clean --force deletes files
-print_info "Test 3: Force cleanup..."
-"$SCRIPT_DIR/git-remote-gcrypt" --clean "$tempdir/remote.git" --force 2>&1
+# 4. Clean Force
+"$SCRIPT_DIR/git-remote-gcrypt" clean "gcrypt::$tempdir/valid.git" --force >/dev/null 2>&1
 
-# Verify files are gone
-if $GIT -C "$tempdir/remote.git" ls-tree HEAD 2>/dev/null | grep -q "secret"; then
-       print_err "Files still exist after cleanup!"
-       $GIT -C "$tempdir/remote.git" ls-tree HEAD
+# Verify garbage_file is GONE from the GREF tree
+UPDATED_TREE=$($GIT rev-parse "$GREF^{tree}")
+if $GIT ls-tree -r "$UPDATED_TREE" | grep -q "garbage_file"; then
+       print_err "Garbage file still exists in remote git tree after CLEAN FORCE!"
        exit 1
 else
-       print_success "Files removed after --clean --force"
+       print_success "Garbage file removed successfully."
 fi
 
-print_success "All --clean command tests passed!"
+# --------------------------------------------------
+# Test 6: check command
+# --------------------------------------------------
+print_info "Test 6: check command..."
+output=$("$SCRIPT_DIR/git-remote-gcrypt" check "$tempdir/remote.git" 2>&1 || :)
+assert_grep "gcrypt: Checking remote:" "$output" "check command is recognized"
+
+print_success "All clean/check command tests passed!"
+
+# --------------------------------------------------
+# Test 7: clean --init (Bypass manifest check)
+# --------------------------------------------------
+print_info "Test 7: clean --init (Bypass manifest check)..."
+
+# Reuse the dirty remote from earlier ($tempdir/remote.git) which has secret1.txt and secret2.txt
+
+# 1. Standard clean should fail (as tested in Test 2)
+output=$("$SCRIPT_DIR/git-remote-gcrypt" clean "gcrypt::$tempdir/remote.git" 2>&1 || :)
+assert_grep "Error: No gcrypt manifest found" "$output" "standard clean fails on dirty remote"
+
+# 2. Clean with --init should succeed (scan only)
+output=$("$SCRIPT_DIR/git-remote-gcrypt" clean --init "gcrypt::$tempdir/remote.git" 2>&1 || :)
+assert_grep "WARNING: No gcrypt manifest found, but --init specified" "$output" "--init warns about missing manifest"
+assert_grep "Found the following files to remove" "$output" "--init scan found files"
+assert_grep "secret1.txt" "$output" "--init found secret1.txt"
+assert_grep "subdir/nested.txt" "$output" "--init found nested file in subdir"
+assert_grep "Has Space.txt" "$output" "--init found file with spaces"
+
+# 3. Clean with --init --force should remove files
+"$SCRIPT_DIR/git-remote-gcrypt" clean --init --force "gcrypt::$tempdir/remote.git" >/dev/null 2>&1
+
+cd "$tempdir/remote.git"
+if $GIT ls-tree HEAD | grep -q "secret1.txt"; then
+       print_err "--init --force FAILED to remove secret1.txt"
+       exit 1
+else
+       print_success "--init --force removed unencrypted files"
+fi
index a9f9bf830e7050b9d79edbced69b3edf2b8563a3..8b44cbf5b9d58d64441821a7665f372ea73d8b2f 100755 (executable)
@@ -15,6 +15,9 @@ print_info "Running install logic tests in $SANDBOX..."
 # 2. Copy artifacts
 cp git-remote-gcrypt "$SANDBOX"
 cp README.rst "$SANDBOX" 2>/dev/null || touch "$SANDBOX/README.rst"
+cp completions/templates/README.rst.in "$SANDBOX"
+cp -r completions/ "$SANDBOX"
+cp -r utils/ "$SANDBOX"
 cp install.sh "$SANDBOX"
 cd "$SANDBOX" || exit 2
 
@@ -45,7 +48,7 @@ assert_version() {
        OUTPUT=$("$INSTALLED_BIN" --version 2>&1 </dev/null)
 
        # CRITICAL: Use quotes around the variable to handle parentheses correctly
-       if [[ "$OUTPUT" != *"$EXPECTED_SUBSTRING"* ]]; then
+       if [[ $OUTPUT != *"$EXPECTED_SUBSTRING"* ]]; then
                print_err "FAILED: Expected '$EXPECTED_SUBSTRING' in output."
                print_err "        Got: '$OUTPUT'"
                exit 1
index c84575b05944cad355eb1120e154ac77779998da..f7bc3ee5a79211b43fd543b355f141357e8f1b9b 100755 (executable)
@@ -24,7 +24,7 @@ if [ $EXIT_CODE -ne 0 ]; then
 fi
 
 # 3. Verify the placeholder was replaced
-if [[ "$OUTPUT" == *"@@DEV_VERSION@@"* ]]; then
+if [[ $OUTPUT == *"@@DEV_VERSION@@"* ]]; then
        print_err "ERROR: Version placeholder @@DEV_VERSION@@ was not replaced!"
        exit 1
 fi
@@ -40,7 +40,7 @@ else
        EXPECTED_ID="unknown_OS"
 fi
 
-if [[ "$OUTPUT" != *"(deb running on $EXPECTED_ID)"* ]]; then
+if [[ $OUTPUT != *"(deb running on $EXPECTED_ID)"* ]]; then
        print_err "ERROR: Distro ID '$EXPECTED_ID' missing from version string! (Got: $OUTPUT)"
        exit 1
 fi
diff --git a/utils/gen_docs.sh b/utils/gen_docs.sh
new file mode 100755 (executable)
index 0000000..85691ff
--- /dev/null
@@ -0,0 +1,101 @@
+#!/bin/sh
+set -e
+
+# gen_docs.sh
+# Generates documentation and shell completions from git-remote-gcrypt source.
+# Strictly POSIX sh compliant.
+
+SCRIPT_KEY="HELP_TEXT"
+SRC="git-remote-gcrypt"
+README_TMPL="completions/templates/README.rst.in"
+README_OUT="README.rst"
+BASH_TMPL="completions/templates/bash.in"
+BASH_OUT="completions/bash/git-remote-gcrypt"
+ZSH_TMPL="completions/templates/zsh.in"
+ZSH_OUT="completions/zsh/_git-remote-gcrypt"
+FISH_TMPL="completions/templates/fish.in"
+FISH_OUT="completions/fish/git-remote-gcrypt.fish"
+
+# Ensure we're in the project root
+if [ ! -f "$SRC" ]; then
+       echo "Error: Must be run from project root" >&2
+       exit 1
+fi
+
+# Extract HELP_TEXT variable content
+# Using sed to capture lines between double quotes of HELP_TEXT="..."
+# Assumes HELP_TEXT="..." is a single block.
+RAW_HELP=$(sed -n "/^$SCRIPT_KEY=\"/,/\"$/p" "$SRC" | sed "s/^$SCRIPT_KEY=\"//;s/\"$//")
+
+# 1. Prepare {commands_help} for README (Indented for RST)
+# We want the Options and Git Protocol Commands sections
+COMMANDS_HELP=$(echo "$RAW_HELP" | sed -n '/^Options:/,$p' | sed 's/^/    /')
+
+# 2. Parse Commands and Flags for Completions
+# Extract command names (first word after 2 spaces)
+COMMANDS_LIST=$(echo "$RAW_HELP" | awk '/^  [a-z]+ / {print $1}' | grep -vE "^(help|version)$" | sort | tr '\n' ' ' | sed 's/ $//')
+
+# Extract clean flags
+# Text: "    clean -f, --force    Actually delete files..."
+# We want: "-f --force -i --init" for Bash
+CLEAN_FLAGS_RAW=$(echo "$RAW_HELP" | grep "^    clean -" | awk -F'  ' '{print $2}' | sed 's/,//g')
+CLEAN_FLAGS_BASH=$(echo "$CLEAN_FLAGS_RAW" | tr '\n' ' ' | sed 's/ $//')
+
+# For Zsh: we want simple list for now as per plan, user asked for dynamic but safe.
+# Constructing a simple list of flags requires parsing.
+# The previous python script just injected them.
+CLEAN_FLAGS_ZSH=""
+# We'll just provide the flags as a list for _arguments
+# ZSH format roughly: '(-f --force)'{-f,--force}'[desc]'
+# Let's simplify and just pass the flags for now to match the user's "native completion" request without over-engineering the parsing in shell.
+# We will just list them.
+COMMA_FLAGS=$(echo "$CLEAN_FLAGS_BASH" | tr ' ' ',')
+CLEAN_FLAGS_ZSH="'(${CLEAN_FLAGS_BASH})' {${COMMA_FLAGS}} '[flag]'"
+
+# For Fish
+# We need to turn "-f, --force" into:
+# complete ... -s f -l force ...
+CLEAN_FLAGS_FISH=""
+# Use a loop over the raw lines
+IFS="
+"
+for line in $CLEAN_FLAGS_RAW; do
+       # line is like "-f --force"
+       short=$(echo "$line" | awk '{print $1}' | sed 's/-//')
+       long=$(echo "$line" | awk '{print $2}' | sed 's/--//')
+       # Escape quotes if needed (none usually)
+       CLEAN_FLAGS_FISH="${CLEAN_FLAGS_FISH}complete -c git-remote-gcrypt -f -n \"__fish_seen_subcommand_from clean\" -s $short -l $long -d 'Flag';\n"
+done
+unset IFS
+
+# 3. Generate README
+echo "Generating $README_OUT..."
+sed "s/{commands_help}/$(echo "$COMMANDS_HELP" | sed 's/[\/&]/\\&/g' | sed ':a;N;$!ba;s/\n/\\n/g')/" "$README_TMPL" >"$README_OUT"
+
+# 4. Generate Bash
+echo "Generating Bash completions..."
+sed "s/{commands}/$COMMANDS_LIST/; s/{clean_flags_bash}/$CLEAN_FLAGS_BASH/" "$BASH_TMPL" >"$BASH_OUT"
+
+# 5. Generate Zsh
+echo "Generating Zsh completions..."
+# Zsh substitution is tricky with the complex string.
+# We'll stick to replacing {commands} and {clean_flags_zsh}
+# Need to escape special chars for sed
+SAFE_CMDS=$(echo "$COMMANDS_LIST" | sed 's/ / /g') # just space separated
+# For clean_flags_zsh, since it contains quotes and braces, we need care.
+# We'll read the template line by line? No, sed is standard.
+# We use a temp file for the replacement string to avoid sed escaping hell for large blocks?
+# Or just keep it simple.
+sed "s/{commands}/$COMMANDS_LIST/" "$ZSH_TMPL" \
+       | sed "s|{clean_flags_zsh}|$CLEAN_FLAGS_ZSH|" >"$ZSH_OUT"
+
+# 6. Generate Fish
+echo "Generating Fish completions..."
+# Fish needs {not_sc_list} which matches {commands} (space separated)
+sed "s/{not_sc_list}/$COMMANDS_LIST/g" "$FISH_TMPL" \
+       |
+       # Multi-line replacement in sed is hard. Use awk?
+       # Or just injecting the string with escaped newlines.
+       sed "s|{clean_flags_fish}|$CLEAN_FLAGS_FISH|" >"$FISH_OUT"
+
+echo "Done."