.PHONY: clean
clean: ##H Clean up
- rm -rf .coverage
+ rm -rf .coverage .build_tmp
+
+# --- Autogeneration ---
+.PHONY: generate
+generate: ##H Autogenerate README usage & completions
+ @$(call print_info,Generating documentation and completions...)
+ @python3 tests/sync_docs.py
+ @# Update README.rst Usage section (simple version for now)
+ @sed -i '/Detecting gcrypt repos/,/Exit status is 0/c\Detecting gcrypt repos\n======================\n\nTo detect if a git url is a gcrypt repo, use::\n\n git-remote-gcrypt check url\n\n(Legacy syntax ``--check`` is also supported).\n\nExit status is 0' README.rst
+ @sed -i '/Cleaning gcrypt repos/,/Known issues/c\Cleaning gcrypt repos\n=====================\n\nTo scan for unencrypted files in a remote gcrypt repo, use::\n\n git-remote-gcrypt clean [url|remote]\n\nIf no URL or remote is specified, ``git-remote-gcrypt`` will list all\navailable ``gcrypt::`` remotes.\n\nBy default, this command only performs a scan. To actually remove the\nunencrypted files, you must use the ``--force`` (or ``-f``) flag::\n\n git-remote-gcrypt clean url --force\n\nKnown issues' README.rst
+ @$(call print_success,Generated.)
(Legacy syntax ``--check`` is also supported).
-Exit status is 0 if the repo exists and can be decrypted, 1 if the repo
+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).
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD - 1]}"
opts="-h --help -v --version --check"
- commands="capabilities list push fetch check clean"
+ commands="capabilities check clean fetch list push"
# If we're after a subcommand, only offer -h/--help
if [[ " $commands " =~ " ${COMP_WORDS[1]:-} " ]]; then
complete -c git-remote-gcrypt -l check -d '(Legacy) Check if URL is a gcrypt repository' -r -F
# Subcommands
-complete -c git-remote-gcrypt -f -n "not __fish_seen_subcommand_from capabilities list push fetch check clean" -a 'check' -d 'Check if URL is a gcrypt repository'
-complete -c git-remote-gcrypt -f -n "not __fish_seen_subcommand_from capabilities list push fetch check clean" -a 'clean' -d 'Scan/Clean unencrypted files from remote'
+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'
# Clean flags
-complete -c git-remote-gcrypt -f -n "__fish_seen_subcommand_from clean" -s f -l force -d 'Actually delete files during clean'
+complete -c git-remote-gcrypt -f -n "__fish_seen_subcommand_from capabilities check clean fetch list push" -s f -l force -d 'Actually delete files during clean'
# Git protocol commands
-complete -c git-remote-gcrypt -f -n "not __fish_seen_subcommand_from capabilities list push fetch check clean" -a 'capabilities' -d 'Show git remote helper capabilities'
-complete -c git-remote-gcrypt -f -n "not __fish_seen_subcommand_from capabilities list push fetch check clean" -a 'list' -d 'List refs in remote repository'
-complete -c git-remote-gcrypt -f -n "not __fish_seen_subcommand_from capabilities list push fetch check clean" -a 'push' -d 'Push refs to remote repository'
-complete -c git-remote-gcrypt -f -n "not __fish_seen_subcommand_from capabilities list push fetch check clean" -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'
'(- *)'{-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 check clean)'
+ '1:command:(capabilities check clean fetch list push)'
'*::subcommand arguments:->args'
)
_arguments -s -S $args
# 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
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
-case "$1" in
- check|--check)
- NAME=gcrypt-check
- URL=$2
- ;;
- clean)
- NAME=gcrypt-clean
- shift
- FORCE_CLEAN=
- URL=
- while [ $# -gt 0 ]; do
- case "$1" in
- --force|-f) FORCE_CLEAN=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
- ;;
- help|--help|-h)
- show_help
- exit 0
- ;;
- version|--version|-v)
- echo "git-remote-gcrypt version $VERSION" >&2
- exit 0
- ;;
-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|--check)
+ NAME=gcrypt-check
+ URL="$2"
+ shift
;;
- *)
+ clean)
+ NAME=gcrypt-clean
+ shift
+ while [ $# -gt 0 ]; do
+ case "$1" in
+ --force|-f) FORCE_CLEAN=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
;;
+ *)
+ # Assume it's the remote name if not already set
+ if [ -z "$NAME" ]; then
+ NAME="$1"
+ elif [ -z "$URL" ]; then
+ URL="$1"
+ fi
+ ;;
esac
+ shift || :
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
+ show_help
+ exit 1
+fi
case "${1:-}" in
capabilities)
if [ "${2:-}" = "-h" ] || [ "${2:-}" = "--help" ]; then
# Resolve URL or remote name, or list remotes if empty
resolve_url() {
local cmd="$1"
- if [ -z "$URL" ]; then
+ if [ -z "$URL" ] || [ "$URL" = "$cmd" ]; then
+ # Handle cases where shift might have messed up or URL was empty
+ if [ "$URL" = "$cmd" ]; then URL=""; fi
+
local remotes
remotes=$(git remote -v | grep 'gcrypt::' | awk '{print $1}' | sort -u || :)
echo "Usage: git-remote-gcrypt $cmd [URL|REMOTE]" >&2
fi
exit 1
fi
- if ! echo "$URL" | grep -q -E '://|::'; then
+
+ # 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
+ print_debug "Resolved remote '$URL' to '$potential_url'"
URL="$potential_url"
fi
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" != "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
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 --force $URL' to remove these files,"
- echo_info "or set 'git config gcrypt.allow-unencrypted-remote true' to ignore."
+ echo_info "Found unexpected files: $(echo "$early_bad_files" | head -n 3 | tr '\n' ' ')"
+ echo_info "To see full list of unexpected files, use: git-remote-gcrypt clean --force $URL"
+ echo_info "To fix and remove these files, use: git-remote-gcrypt clean --force $URL"
exit 1
fi
fi
echo "git-remote-gcrypt version $VERSION"
exit 0
else
- gcrypt_main_loop "$@"
+ gcrypt_main_loop "$NAME" "$URL"
fi
--- /dev/null
+#!/usr/bin/env python3
+import re
+import os
+import sys
+
+def extract_help_text(script_path):
+ with open(script_path, 'r') as f:
+ content = f.read()
+
+ match = re.search(r'HELP_TEXT="(.*?)"', content, re.DOTALL)
+ if not match:
+ print("Error: Could not find HELP_TEXT in git-remote-gcrypt", file=sys.stderr)
+ sys.exit(1)
+ return match.group(1)
+
+def parse_commands(help_text):
+ commands = []
+ # Look for lines starting with lowercase words in the Options: or Git Protocol Commands sections
+ lines = help_text.split('\n')
+ capture = False
+ for line in lines:
+ line = line.strip()
+ if line.startswith('Options:') or line.startswith('Git Protocol Commands'):
+ capture = True
+ continue
+ if line.startswith('Environment Variables:'):
+ capture = False
+ continue
+
+ if capture and line:
+ # Match lines like "check [URL] Description" or "capabilities Description"
+ match = re.match(r'^([a-z-]+)(\s+.*)?$', line)
+ if match:
+ cmd = match.group(1)
+ if cmd not in ['help', 'version']:
+ commands.append(cmd)
+ return sorted(list(set(commands)))
+def update_bash_completion(path, commands):
+ if not os.path.exists(path): return
+ with open(path, 'r') as f: content = f.read()
+ cmd_str = ' '.join(commands)
+ new_content = re.sub(r'commands="[^"]+"', f'commands="{cmd_str}"', content)
+ with open(path, 'w') as f: f.write(new_content)
+
+def update_zsh_completion(path, commands):
+ if not os.path.exists(path): return
+ with open(path, 'r') as f: content = f.read()
+ cmd_str = ' '.join(commands)
+ # Match 1:command:(capabilities list push fetch check clean)
+ new_content = re.sub(r'1:command:\([^)]+\)', f'1:command:({cmd_str})', content)
+ with open(path, 'w') as f: f.write(new_content)
+
+def update_fish_completion(path, commands):
+ if not os.path.exists(path): return
+ with open(path, 'r') as f: content = f.read()
+ # Replace the list in "not __fish_seen_subcommand_from ..."
+ cmd_str = ' '.join(commands)
+ new_content = re.sub(r'not __fish_seen_subcommand_from [^"]+', f'not __fish_seen_subcommand_from {cmd_str}', content)
+ with open(path, 'w') as f: f.write(new_content)
+
+def main():
+ script_dir = os.path.dirname(os.path.abspath(__file__))
+ root_dir = os.path.dirname(script_dir)
+ script_path = os.path.join(root_dir, 'git-remote-gcrypt')
+
+ help_text = extract_help_text(script_path)
+ commands = parse_commands(help_text)
+
+ # We always want protocol commands in completions too
+ comp_commands = sorted(list(set(commands + ['capabilities', 'list', 'push', 'fetch'])))
+
+ print(f"Detected commands: {' '.join(comp_commands)}")
+
+ update_bash_completion(os.path.join(root_dir, 'completions/bash/git-remote-gcrypt'), comp_commands)
+ update_zsh_completion(os.path.join(root_dir, 'completions/zsh/_git-remote-gcrypt'), comp_commands)
+ update_fish_completion(os.path.join(root_dir, 'completions/fish/git-remote-gcrypt.fish'), comp_commands)
+
+ print("Completions updated.")
+
+if __name__ == "__main__":
+ main()