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)
.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.)
--- /dev/null
+#!/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()
--- /dev/null
+=================
+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 `<giturl>`
+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.<name>.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.<name>.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.<name>.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.<name>.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.<name>.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 `<giturl>` 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:
+
+``<sha-1> <gitref>``
+ Git object id and its ref
+
+``pack :<hashtype>:<hash> <key>``
+ Packfile hash (`Hi`) and corresponding symmetric key (`Ki`).
+
+``keep :<hashtype>:<hash> <generation>``
+ Packfile hash and its repack generation
+
+``repo <id>``
+ The remote id
+
+``extn <name> ...``
+ 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
+<spwhitton@spwhitton.name>.
+
+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
--- /dev/null
+# 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
--- /dev/null
+# 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'
--- /dev/null
+#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 "$@"
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 ---
+++ /dev/null
-#!/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()
"${@}"
)
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}"
}
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
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
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