From: Shane Jaroch Date: Thu, 8 Jan 2026 22:04:27 +0000 (-0500) Subject: wip X-Git-Url: https://git.nutra.tk/v2?a=commitdiff_plain;h=d2d67a075318ef2c577455caee531e8ae6e3c4b7;p=gamesguru%2Fgit-remote-gcrypt.git wip --- diff --git a/Makefile b/Makefile index db9d41a..79f42ba 100644 --- a/Makefile +++ b/Makefile @@ -61,28 +61,30 @@ check/deps: ##H Verify kcov & shellcheck 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 install.sh - @$(call print_success,OK.) - # lint system/binary script shellcheck -e SC3043,SC2001 git-remote-gcrypt - @$(call print_success,OK.) - # lint test scripts 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) @@ -202,8 +204,5 @@ clean: ##H Clean up .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 + python3 completions/gen_docs.py @$(call print_success,Generated.) diff --git a/completions/gen_docs.py b/completions/gen_docs.py new file mode 100755 index 0000000..bc46bce --- /dev/null +++ b/completions/gen_docs.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +import os +import re +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_readme(path, template_path): + if not os.path.exists(template_path): + print(f"Error: Template not found at {template_path}", file=sys.stderr) + sys.exit(1) + + with open(template_path, "r") as f: + template_content = f.read() + + # If the destination exists, check if it matches + if os.path.exists(path): + with open(path, "r") as f: + content = f.read() + else: + content = "" + + if content != template_content: + print(f"Updating README at: {path}") + with open(path, "w") as f: + f.write(template_content) + else: + print(f"README at {path} is up to date.") + + +def update_bash_completion(path, template_path, commands): + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(template_path, "r") as f: + template = f.read() + + cmd_str = " ".join(commands) + content = template.replace("{commands}", cmd_str) + + with open(path, "w") as f: + f.write(content) + + +def update_zsh_completion(path, template_path, commands): + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(template_path, "r") as f: + template = f.read() + + cmd_str = " ".join(commands) + content = template.replace("{commands}", cmd_str) + + with open(path, "w") as f: + f.write(content) + + +def update_fish_completion(path, template_path, commands): + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(template_path, "r") as f: + template = f.read() + + cmd_str = " ".join(commands) + content = template.replace("{not_sc_list}", cmd_str) + + with open(path, "w") as f: + f.write(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") + templates_dir = os.path.join(script_dir, "templates") + + 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)}") + + # Bash + bash_path = os.path.join(root_dir, "completions/bash/git-remote-gcrypt") + bash_tmpl = os.path.join(templates_dir, "bash.in") + print(f"Updating Bash completions at: {bash_path}") + update_bash_completion(bash_path, bash_tmpl, comp_commands) + + # Zsh + zsh_path = os.path.join(root_dir, "completions/zsh/_git-remote-gcrypt") + zsh_tmpl = os.path.join(templates_dir, "zsh.in") + print(f"Updating Zsh completions at: {zsh_path}") + update_zsh_completion(zsh_path, zsh_tmpl, comp_commands) + + # Fish + fish_path = os.path.join(root_dir, "completions/fish/git-remote-gcrypt.fish") + fish_tmpl = os.path.join(templates_dir, "fish.in") + print(f"Updating Fish completions at: {fish_path}") + update_fish_completion(fish_path, fish_tmpl, comp_commands) + + readme_path = os.path.join(root_dir, "README.rst") + readme_tmpl = os.path.join(templates_dir, "README.rst.in") + update_readme(readme_path, readme_tmpl) + + print("Completions and Documentation updated.") + + +if __name__ == "__main__": + main() diff --git a/completions/templates/README.rst.in b/completions/templates/README.rst.in new file mode 100644 index 0000000..1753636 --- /dev/null +++ b/completions/templates/README.rst.in @@ -0,0 +1,324 @@ +================= +git-remote-gcrypt +================= + +-------------------------------------- +GNU Privacy Guard-encrypted git remote +-------------------------------------- + +:Manual section: 1 + +Description +=========== + +git-remote-gcrypt is a git remote helper to push and pull from +repositories encrypted with GnuPG, using a custom format. This remote +helper handles URIs prefixed with `gcrypt::`. + +Supported backends are `local`, `rsync://` and `sftp://`, where the +repository is stored as a set of files, or instead any `` +where gcrypt will store the same representation in a git repository, +bridged over arbitrary git transport. Prefer `local` or `rsync://` if +you can use one of those; see "Performance" below for discussion. + +There is also an experimental `rclone://` backend for early adoptors +only (you have been warned). + +The aim is to provide confidential, authenticated git storage and +collaboration using typical untrusted file hosts or services. + +Installation +............ + +* use your GNU/Linux distribution's package manager -- Debian, Ubuntu, + Fedora, Arch and some smaller distros are known to have packages + +* run the supplied ``install.sh`` script on other systems + +Quickstart +.......... + +Create an encrypted remote by pushing to it:: + + git remote add cryptremote gcrypt::rsync://example.com/repo + git push cryptremote master + > gcrypt: Setting up new repository + > gcrypt: Remote ID is :id:7VigUnLVYVtZx8oir34R + > [ more lines .. ] + > To gcrypt::[...] + > * [new branch] master -> master + +Configuration +============= + +The following ``git-config(1)`` variables are supported: + +``remote..gcrypt-participants`` + .. +``gcrypt.participants`` + Space-separated list of GPG key identifiers. The remote is encrypted + to these participants and only signatures from these are accepted. + ``gpg -k`` lists all public keys you know. + + If this option is not set, we encrypt to your default key and accept + any valid signature. This behavior can also be requested explicitly + by setting participants to ``simple``. + + The ``gcrypt-participants`` setting on the remote takes precedence + over the repository variable ``gcrypt.participants``. + +``remote..gcrypt-publish-participants`` + .. +``gcrypt.publish-participants`` + By default, the gpg key ids of the participants are obscured by + encrypting using ``gpg -R``. Setting this option to ``true`` disables + that security measure. + + The problem with using ``gpg -R`` is that to decrypt, gpg tries each + available secret key in turn until it finds a usable key. + This can result in unnecessary passphrase prompts. + +``gcrypt.gpg-args`` + The contents of this setting are passed as arguments to gpg. + E.g. ``--use-agent``. + +``remote..gcrypt-signingkey`` + .. +``user.signingkey`` + (The latter from regular git configuration) The key to use for signing. + You should set ``user.signingkey`` if your default signing key is not + part of the participant list. You may use the per-remote version + to sign different remotes using different keys. + +``remote..gcrypt-rsync-put-flags`` + .. +``gcrypt.rsync-put-flags`` + Flags to be passed to ``rsync`` when uploading to a remote using the + ``rsync://`` backend. If the flags are set to a specific remote, the + global flags, if also set, will not be applied for that remote. + +``remote..gcrypt-require-explicit-force-push`` + .. +``gcrypt.require-explicit-force-push`` + A longstanding bug is that every git push effectively has a ``--force``. + + If this flag is set to ``true``, git-remote-gcrypt will refuse to push, + unless ``--force`` is passed, or refspecs are prefixed with ``+``. + + There is a potential solution here: https://bugs.debian.org/877464#32 + +Environment variables +===================== + +*GCRYPT_FULL_REPACK* + 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 +======== + +How to set up a remote for two participants:: + + git remote add cryptremote gcrypt::rsync://example.com/repo + git config remote.cryptremote.gcrypt-participants "KEY1 KEY2" + git push cryptremote master + +How to use a git backend:: + + # notice that the target git repo must already exist and its + # `next` branch will be overwritten! + git remote add gitcrypt gcrypt::git@example.com:repo#next + git push gitcrypt master + +The URL fragment (``#next`` here) indicates which backend branch is used. + +Notes +===== + +Collaboration + The encryption of the manifest is updated for each push to match the + participant configuration. Each pushing user must have the public + keys of all collaborators and correct participant config. + +Dependencies + ``rsync``, ``curl`` and ``rclone`` for remotes ``rsync:``, ``sftp:`` and + ``rclone:`` respectively. The main executable requires a POSIX-compliant + shell that supports ``local``. + +GNU Privacy Guard + Both GPG 1.4 and 2 are supported. You need a personal GPG key. GPG + configuration applies to algorithm choices for public-key + encryption, symmetric encryption, and signing. See ``man gpg`` for + more information. + +Remote ID + The Remote ID is not secret; it only ensures that two repositories + signed by the same user can be distinguished. You will see + a warning if the Remote ID changes, which should only happen if the + remote was re-created. + +Performance + Using an arbitrary `` or an `sftp://` URI requires + uploading the entire repository history with each push. This + means that pushes of your repository become slower over time, as + your git history becomes longer, and it can easily get to the + point that continued usage of git-remote-gcrypt is impractical. + + Thus, you should use these backends only when you know that your + repository will not ever grow very large, not just that it's not + large now. This means that these backends are inappropriate for + most repositories, and likely suitable only for unusual cases, + such as small credential stores. Even then, use `rsync://` if you + can. Note, however, that `rsync://` won't work with a repository + hosting service like Gitolite, GitHub or GitLab. + +rsync URIs + The URI format for the rsync backend is ``rsync://user@host/path``, + which translates to the rsync location ``user@host:/path``, + accessed over ssh. Note that the path is absolute, not relative to the + home directory. An earlier non-standard URI format is also supported: + ``rsync://user@host:path``, which translates to the rsync location + ``user@host:path`` + +rclone backend + In addition to adding the rclone backend as a remote with URI like + ``gcrypt::rclone://remote:subdir``, you must add the remote to the + rclone configuration too. This is typically done by executing + ``rclone config``. See rclone(1). + + The rclone backend is considered experimental and is for early + adoptors only. You have been warned. + +Repository format +................. + +| `EncSign(X):` Sign and Encrypt to GPG key holder +| `Encrypt(K,X):` Encrypt using symmetric-key algorithm +| `Hash(X):` SHA-2/256 +| +| `B:` branch list +| `L:` list of the hash (`Hi`) and key (`Ki`) for each packfile +| `R:` Remote ID +| +| To write the repository: +| +| Store each packfile `P` as `Encrypt(Ki, P)` → `P'` in filename `Hi` +| where `Ki` is a new random string and `Hash(P')` → `Hi` +| Store `EncSign(B || L || R)` in the manifest +| +| To read the repository: +| +| Get manifest, decrypt and verify using GPG keyring → `(B, L, R)` +| Warn if `R` does not match previously seen Remote ID +| for each `Hi, Ki` in `L`: +| Get file `Hi` from the server → `P'` +| Verify `Hash(P')` matches `Hi` +| Decrypt `P'` using `Ki` → `P` then open `P` with git + +Manifest file +............. + +Example manifest file (with ellipsis for brevity):: + + $ gpg -d 91bd0c092128cf2e60e1a608c31e92caf1f9c1595f83f2890ef17c0e4881aa0a + 542051c7cd152644e4995bda63cc3ddffd635958 refs/heads/next + 3c9e76484c7596eff70b21cbe58408b2774bedad refs/heads/master + pack :SHA256:f2ad50316...cd4ba67092dc4 z8YoAnFpMlW...3PkI2mND49P1qm + pack :SHA256:a6e17bb4c...426492f379584 82+k2cbiUn7...dgXfyX6wXGpvVa + keep :SHA256:f2ad50316...cd4ba67092dc4 1 + repo :id:OYiSleGirtLubEVqJpFF + +Each item extends until newline, and matches one of the following: + +`` `` + Git object id and its ref + +``pack :: `` + Packfile hash (`Hi`) and corresponding symmetric key (`Ki`). + +``keep :: `` + Packfile hash and its repack generation + +``repo `` + The remote id + +``extn ...`` + Extension field, preserved but unused. + +Detecting gcrypt repos +====================== + +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] + +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 +============ + +Every git push effectively has ``--force``. Be sure to pull before +pushing. + +git-remote-gcrypt can decide to repack the remote without warning, +which means that your push can suddenly take significantly longer than +you were expecting, as your whole history has to be reuploaded. +This push might fail over a poor link. + +git-remote-gcrypt might report a repository as "not found" when the +repository does in fact exist, but git-remote-gcrypt is having +authentication, port, or network connectivity issues. + +See also +======== + +git-remote-helpers(1), gpg(1) + +Credits +======= + +The original author of git-remote-gcrypt was GitHub user bluss. + +The de facto maintainer in 2013 and 2014 was Joey Hess. + +The current maintainer, since 2016, is Sean Whitton +. + +License +======= + +This document and git-remote-gcrypt are licensed under identical terms, +GPL-3 (or 2+); see the git-remote-gcrypt file. + +.. this document generates a man page with rst2man +.. vim: ft=rst tw=72 sts=4 diff --git a/completions/templates/bash.in b/completions/templates/bash.in new file mode 100644 index 0000000..5b761d8 --- /dev/null +++ b/completions/templates/bash.in @@ -0,0 +1,45 @@ +# 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="{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 "-f --force -h --help $remotes" -- "$cur")) + return 0 + ;; + check|--check) + COMPREPLY=($(compgen -f -- "$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 index 0000000..f08a59e --- /dev/null +++ b/completions/templates/fish.in @@ -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' +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 {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 check" -a "(git remote -v 2>/dev/null | grep 'gcrypt::' | awk '{print \$1}' | sort -u)" -d 'Gcrypt Remote' + +# Clean flags +complete -c git-remote-gcrypt -f -n "__fish_seen_subcommand_from {not_sc_list}" -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 {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 index 0000000..227920b --- /dev/null +++ b/completions/templates/zsh.in @@ -0,0 +1,33 @@ +#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:({commands})' + '*::subcommand arguments:->args' + ) + _arguments -s -S $args + + case $words[1] in + clean) + _arguments \ + '(-f --force)'{-f,--force}'[actually delete files]' \ + '*:gcrypt URL: _alternative "remotes:gcrypt remote:($(git remote -v 2>/dev/null | grep "gcrypt::" | awk "{print \$1}" | sort -u))" "files:file:_files"' + ;; + check) + _arguments \ + '1:gcrypt URL:_files' + ;; + *) + _arguments \ + '*:gcrypt URL:' + ;; + esac +} + +_git_remote_gcrypt "$@" diff --git a/install.sh b/install.sh index 4fa90a2..0d8bba3 100755 --- a/install.sh +++ b/install.sh @@ -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 --- diff --git a/tests/sync_docs.py b/tests/sync_docs.py deleted file mode 100755 index b03214a..0000000 --- a/tests/sync_docs.py +++ /dev/null @@ -1,254 +0,0 @@ -#!/usr/bin/env python3 -import os -import re -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))) - - -BASH_TEMPLATE = r'''# 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="{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 "-f --force -h --help $remotes" -- "$cur")) - return 0 - ;; - check|--check) - COMPREPLY=($(compgen -f -- "$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 -''' - -ZSH_TEMPLATE = r'''#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:({commands})' - '*::subcommand arguments:->args' - ) - _arguments -s -S $args - - case $words[1] in - clean) - _arguments \ - '(-f --force)'{{-f,--force}}'[actually delete files]' \ - '*:gcrypt URL: _alternative "remotes:gcrypt remote:($(git remote -v 2>/dev/null | grep "gcrypt::" | awk "{{print \$1}}" | sort -u))" "files:file:_files"' - ;; - check) - _arguments \ - '1:gcrypt URL:_files' - ;; - *) - _arguments \ - '*:gcrypt URL:' - ;; - esac -}} - -_git_remote_gcrypt "$@" -''' - -FISH_TEMPLATE = r'''# 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 '(Legacy) Check if URL is a gcrypt repository' -r -F - -# 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 check" -a "(git remote -v 2>/dev/null | grep 'gcrypt::' | awk '{{print \$1}}' | sort -u)" -d 'Gcrypt Remote' - -# Clean flags -complete -c git-remote-gcrypt -f -n "__fish_seen_subcommand_from {not_sc_list}" -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 {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' -''' - -DETECT_SECTION = r'''Detecting gcrypt repos -====================== - -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''' - -CLEAN_SECTION = r'''Cleaning gcrypt repos -===================== - -To scan for unencrypted files in a remote gcrypt repo, use:: - - git-remote-gcrypt clean [url|remote] - -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 - -''' - - -def update_readme(path): - if not os.path.exists(path): - return - with open(path, "r") as f: - content = f.read() - - # robustly replace sections - pattern1 = r"(Detecting gcrypt repos\n======================.*?Exit status is 0)" - new_content = re.sub(pattern1, DETECT_SECTION, content, flags=re.DOTALL) - - pattern2 = r"(Cleaning gcrypt repos\n=====================.*?)(?=\nKnown issues)" - new_content = re.sub(pattern2, CLEAN_SECTION, new_content, flags=re.DOTALL) - - if content != new_content: - print(f"Updating README sections at: {path}") - with open(path, "w") as f: - f.write(new_content) - else: - print(f"README at {path} is up to date.") - - -def update_bash_completion(path, commands): - os.makedirs(os.path.dirname(path), exist_ok=True) - cmd_str = " ".join(commands) - content = BASH_TEMPLATE.format(commands=cmd_str) - with open(path, "w") as f: - f.write(content) - - -def update_zsh_completion(path, commands): - os.makedirs(os.path.dirname(path), exist_ok=True) - cmd_str = " ".join(commands) - content = ZSH_TEMPLATE.format(commands=cmd_str) - with open(path, "w") as f: - f.write(content) - - -def update_fish_completion(path, commands): - os.makedirs(os.path.dirname(path), exist_ok=True) - cmd_str = " ".join(commands) - content = FISH_TEMPLATE.format(not_sc_list=cmd_str) - with open(path, "w") as f: - f.write(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)}") - - bash_path = os.path.join(root_dir, "completions/bash/git-remote-gcrypt") - print(f"Updating Bash completions at: {bash_path}") - update_bash_completion(bash_path, comp_commands) - - zsh_path = os.path.join(root_dir, "completions/zsh/_git-remote-gcrypt") - print(f"Updating Zsh completions at: {zsh_path}") - update_zsh_completion(zsh_path, comp_commands) - - fish_path = os.path.join(root_dir, "completions/fish/git-remote-gcrypt.fish") - print(f"Updating Fish completions at: {fish_path}") - update_fish_completion(fish_path, comp_commands) - - readme_path = os.path.join(root_dir, "README.rst") - update_readme(readme_path) - - print("Completions and Documentation updated.") - - -if __name__ == "__main__": - main() diff --git a/tests/system-test-multikey.sh b/tests/system-test-multikey.sh index d1892d3..2024a13 100755 --- a/tests/system-test-multikey.sh +++ b/tests/system-test-multikey.sh @@ -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}" } diff --git a/tests/test-install-logic.sh b/tests/test-install-logic.sh index a9f9bf8..e9aa643 100755 --- a/tests/test-install-logic.sh +++ b/tests/test-install-logic.sh @@ -45,7 +45,7 @@ assert_version() { OUTPUT=$("$INSTALLED_BIN" --version 2>&1