]> Nutra Git (v1) - gamesguru/git-remote-gcrypt.git/commitdiff
wip
authorShane Jaroch <chown_tee@proton.me>
Thu, 8 Jan 2026 22:04:27 +0000 (17:04 -0500)
committerShane Jaroch <chown_tee@proton.me>
Thu, 8 Jan 2026 22:14:58 +0000 (17:14 -0500)
Makefile
completions/gen_docs.py [new file with mode: 0755]
completions/templates/README.rst.in [new file with mode: 0644]
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]
install.sh
tests/sync_docs.py [deleted file]
tests/system-test-multikey.sh
tests/test-install-logic.sh
tests/verify-system-install.sh

index db9d41a7fa96a6ffb5bfb4e3f753a83b7233690b..79f42ba4c47e23f96e13f75349dc39bd69d41c72 100644 (file)
--- 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 (executable)
index 0000000..bc46bce
--- /dev/null
@@ -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 (file)
index 0000000..1753636
--- /dev/null
@@ -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 `<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
diff --git a/completions/templates/bash.in b/completions/templates/bash.in
new file mode 100644 (file)
index 0000000..5b761d8
--- /dev/null
@@ -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 (file)
index 0000000..f08a59e
--- /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'
+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 (file)
index 0000000..227920b
--- /dev/null
@@ -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 "$@"
index 4fa90a25c30bcc304269369e5a26800a2b092be7..0d8bba30b9afe87898eb6a5a18c16d4236769b74 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 ---
diff --git a/tests/sync_docs.py b/tests/sync_docs.py
deleted file mode 100755 (executable)
index b03214a..0000000
+++ /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()
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 a9f9bf830e7050b9d79edbced69b3edf2b8563a3..e9aa643127d7f25f9d3c9c9fdfefe08e8c938451 100755 (executable)
@@ -45,7 +45,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