wip
authorShane Jaroch <chown_tee@proton.me>
Sat, 17 Jan 2026 02:56:06 +0000 (21:56 -0500)
committerShane Jaroch <chown_tee@proton.me>
Sat, 17 Jan 2026 03:03:09 +0000 (22:03 -0500)
Makefile
README.rst
completions/bash/git-remote-gcrypt
completions/fish/git-remote-gcrypt.fish
completions/gen_docs.sh
completions/templates/bash.in
completions/templates/zsh.in
completions/zsh/_git-remote-gcrypt
git-remote-gcrypt
tests/system-test-clean-command.sh

index 0ba4d0a8590adb76ff65c4857519d6f29936dd58..02c51283e521bb9183bf7ced792e69b7e52f918b 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -111,11 +111,15 @@ test/installer:   ##H Test installer logic
        @mkdir -p $(COV_INSTALL)
        @export COV_DIR=$(COV_INSTALL); \
         for test_script in tests/installer-test*.sh; do \
-            kcov --bash-dont-parse-binary-dir \
-                --include-pattern=install.sh \
-                --exclude-path=$(PWD)/.git,$(PWD)/tests \
-                $(COV_INSTALL) \
-                "$$test_script" 2>&1 | tee -a .tmp/kcov.log; \
+            if [ "$$test_script" = "tests/installer-test-logic.sh" ]; then \
+                kcov --bash-dont-parse-binary-dir \
+                    --include-pattern=install.sh \
+                    --exclude-path=$(PWD)/.git,$(PWD)/tests \
+                    $(COV_INSTALL) \
+                    "$$test_script" 2>&1 | tee -a .tmp/kcov.log; \
+            else \
+                bash "$$test_script" 2>&1 | tee -a .tmp/kcov.log; \
+            fi; \
         done; \
         if grep -q 'kcov: error:' .tmp/kcov.log; then \
             echo "FAIL: kcov errors detected (see above)"; exit 1; \
index 134d4a466ec995a1d39e9ed788313deed9bca47b..b34465db245f1bd0e4da710629fe9123f38c4df4 100644 (file)
@@ -60,8 +60,9 @@ Command Reference
       version          Show version information
       check [URL]      Check if URL is a gcrypt repository
       clean [URL|REMOTE] Scan/Clean unencrypted files from remote
-        clean -f, --force    Actually delete files (default is scan only)
-        clean -i, --init     Scan even if no manifest found (DANGEROUS with --force)
+        clean --force        Actually delete files (default is scan only)
+        clean --init         Allow cleaning valid files (requires --force)
+        clean --hard         Override safety checks (requires --force)
       stat [URL|REMOTE] Show diagnostics (file counts, tracked vs untracked)
     Git Protocol Commands (for debugging):
       capabilities     List remote helper capabilities
index a9fe8f1803685083d1c0f1ba128184aa61cd605f..6415eaf5e077d726ac004a08c58245db8a34f9c2 100644 (file)
@@ -20,20 +20,20 @@ _git_remote_gcrypt() {
 
        # 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 -i --init $remotes" -- "$cur" ) )
-                       return 0
-                       ;;
-               check|stat)
-                       local remotes=$( git remote 2>/dev/null || : )
-                       COMPREPLY=( $( compgen -W "$remotes" -- "$cur" ) )
-                       return 0
-                       ;;
-               capabilities|fetch|list|push)
-                       COMPREPLY=($(compgen -W "-h --help" -- "$cur"))
-                       return 0
-                       ;;
+       clean)
+               local remotes=$(git remote -v 2>/dev/null | grep 'gcrypt::' | awk '{print $1}' | sort -u || :)
+               COMPREPLY=($(compgen -W "--force Actually --init Allow --hard Override $remotes" -- "$cur"))
+               return 0
+               ;;
+       check | stat)
+               local remotes=$(git remote 2>/dev/null || :)
+               COMPREPLY=($(compgen -W "$remotes" -- "$cur"))
+               return 0
+               ;;
+       capabilities | fetch | list | push)
+               COMPREPLY=($(compgen -W "-h --help" -- "$cur"))
+               return 0
+               ;;
        esac
 
        # 3. Fallback (global flags if not in a known subcommand?)
index 583101a1ba3fe7e91a1d86673ce4c917ef68489c..52496c172a597777785d1a2ed4a6a990b5c925f6 100644 (file)
@@ -13,6 +13,7 @@ complete -c git-remote-gcrypt -n "__fish_seen_subcommand_from check" -a "(git re
 complete -c git-remote-gcrypt -n "__fish_seen_subcommand_from stat" -a "(git remote 2>/dev/null)" -d 'Git Remote'
 
 # Clean flags
-complete -c git-remote-gcrypt -f -n "__fish_seen_subcommand_from clean" -s f -l force -d 'Flag';
-complete -c git-remote-gcrypt -f -n "__fish_seen_subcommand_from clean" -s i -l init -d 'Flag';
+complete -c git-remote-gcrypt -f -n "__fish_seen_subcommand_from clean" -s -force -l Actually -d 'Flag';
+complete -c git-remote-gcrypt -f -n "__fish_seen_subcommand_from clean" -s -init -l Allow -d 'Flag';
+complete -c git-remote-gcrypt -f -n "__fish_seen_subcommand_from clean" -s -hard -l Override -d 'Flag';
 
index 93b2a921e01bbafc939a891d34db25c32fbb8824..23277e8bcbc782dd2193f1606aaffb4f5b270f1c 100755 (executable)
@@ -86,14 +86,13 @@ SAFE_CMDS=$(echo "$COMMANDS_LIST" | sed 's/ / /g') # just space separated
 # We'll read the template line by line? No, sed is standard.
 # We use a temp file for the replacement string to avoid sed escaping hell for large blocks?
 # Or just keep it simple.
-sed "s/{commands}/$COMMANDS_LIST/" "$ZSH_TMPL" \
-       sed "s|{clean_flags_zsh}|$CLEAN_FLAGS_ZSH|" >"$ZSH_OUT"
+sed "s/{commands}/$COMMANDS_LIST/" "$ZSH_TMPL" |
+       sed "s|{clean_flags_zsh}|$CLEAN_FLAGS_ZSH|" >"$ZSH_OUT"
 
 # 6. Generate Fish
 echo "Generating Fish completions..."
 # Fish needs {not_sc_list} which matches {commands} (space separated)
-sed "s/{not_sc_list}/$COMMANDS_LIST/g" "$FISH_TMPL" \
-       |
+sed "s/{not_sc_list}/$COMMANDS_LIST/g" "$FISH_TMPL" |
        # Multi-line replacement in sed is hard. Use awk?
        # Or just injecting the string with escaped newlines.
        sed "s|{clean_flags_fish}|$CLEAN_FLAGS_FISH|" >"$FISH_OUT"
index dcf66f8179243b5ae926bba2468f8cae4fcef6a1..a6b6985b5f9e8faee083cdbdc1df82dd14eed590 100644 (file)
@@ -20,20 +20,20 @@ _git_remote_gcrypt() {
 
        # 2. Handle subcommands
        case "${COMP_WORDS[1]}" in
-               clean)
-                       local remotes=$( git remote -v 2>/dev/null | grep 'gcrypt::' | awk '{print $1}' | sort -u || : )
-                       COMPREPLY=( $( compgen -W "{clean_flags_bash} $remotes" -- "$cur" ) )
-                       return 0
-                       ;;
-               check|stat)
-                       local remotes=$( git remote 2>/dev/null || : )
-                       COMPREPLY=( $( compgen -W "$remotes" -- "$cur" ) )
-                       return 0
-                       ;;
-               capabilities|fetch|list|push)
-                       COMPREPLY=($(compgen -W "-h --help" -- "$cur"))
-                       return 0
-                       ;;
+       clean)
+               local remotes=$(git remote -v 2>/dev/null | grep 'gcrypt::' | awk '{print $1}' | sort -u || :)
+               COMPREPLY=($(compgen -W "{clean_flags_bash} $remotes" -- "$cur"))
+               return 0
+               ;;
+       check | stat)
+               local remotes=$(git remote 2>/dev/null || :)
+               COMPREPLY=($(compgen -W "$remotes" -- "$cur"))
+               return 0
+               ;;
+       capabilities | fetch | list | push)
+               COMPREPLY=($(compgen -W "-h --help" -- "$cur"))
+               return 0
+               ;;
        esac
 
        # 3. Fallback (global flags if not in a known subcommand?)
index 69bac26ef73e206205a61772511d6284b680fdb1..0a44fe188a723be7754248bb43451c2c45bf9cf8 100644 (file)
@@ -17,7 +17,7 @@ _git_remote_gcrypt() {
                _arguments {clean_flags_zsh} \
                        '*:gcrypt URL: _alternative "remotes:gcrypt remote:($(git remote -v 2>/dev/null | grep "gcrypt::" | awk "{print \$1}" | sort -u))" "files:file:_files"'
                ;;
-       check|stat)
+       check | stat)
                _arguments \
                        '*:gcrypt URL: _alternative "remotes:git remote:($(git remote 2>/dev/null))" "files:file:_files"'
                ;;
index acda347826bb8383305f74cf25eb513f4951355f..415c3d1b5eb6c2fa62f41ea19aa4a34e8fed677c 100644 (file)
@@ -14,10 +14,10 @@ _git_remote_gcrypt() {
 
        case $line[1] in
        clean)
-               _arguments '(-f --force -i --init)'{-f,--force,-i,--init}'[flag]' \
+               _arguments '(--force Actually --init Allow --hard Override)'{--force,Actually,--init,Allow,--hard,Override}'[flag]' \
                        '*:gcrypt URL: _alternative "remotes:gcrypt remote:($(git remote -v 2>/dev/null | grep "gcrypt::" | awk "{print \$1}" | sort -u))" "files:file:_files"'
                ;;
-       check|stat)
+       check | stat)
                _arguments \
                        '*:gcrypt URL: _alternative "remotes:git remote:($(git remote 2>/dev/null))" "files:file:_files"'
                ;;
index 91e526bcfdd2d721763869ef0982cbe7361c0390..97bcc8bc4f69d6da327504795855b97aaa3765a1 100755 (executable)
@@ -46,8 +46,9 @@ Options:
   version          Show version information
   check [URL]      Check if URL is a gcrypt repository
   clean [URL|REMOTE] Scan/Clean unencrypted files from remote
-    clean -f, --force    Actually delete files (default is scan only)
-    clean -i, --init     Scan even if no manifest found (DANGEROUS with --force)
+    clean --force        Actually delete files (default is scan only)
+    clean --init         Allow cleaning valid files (requires --force)
+    clean --hard         Override safety checks (requires --force)
   stat [URL|REMOTE] Show diagnostics (file counts, tracked vs untracked)
 Git Protocol Commands (for debugging):
   capabilities     List remote helper capabilities
@@ -86,8 +87,9 @@ while [ $# -gt 0 ]; do
                        shift
                        while [ $# -gt 0 ]; do
                                case "$1" in
-                                       --force|-f) FORCE_CLEAN=yes ;;
-                                       --init|-i) FORCE_INIT=yes ;;
+                                       --force) FORCE_CLEAN=yes ;;
+                                       --hard) HARD_FORCE=yes ;;
+                                       --init) FORCE_INIT=yes ;;
                                        -*) echo "Unknown option: $1" >&2; exit 1 ;;
                                        *)
                                                if [ -z "$URL" ]; then
@@ -100,6 +102,15 @@ while [ $# -gt 0 ]; do
                                esac
                                shift
                        done
+                       if [ -n "$HARD_FORCE" ] && [ -z "$FORCE_CLEAN" ]; then
+                               echo "Error: --hard requires --force" >&2
+                               exit 1
+                       fi
+                       if [ "${FORCE_INIT:-}" = "yes" ] && [ -z "$FORCE_CLEAN" ]; then
+                               echo "Error: --init requires --force" >&2
+                               echo "(To scan a repository, use 'clean' without arguments)" >&2
+                               exit 1
+                       fi
                        break # Stop parsing outer loop
                        ;;
                stat)
@@ -415,7 +426,24 @@ gitception_remove()
        (
                export GIT_INDEX_FILE="$temp_index"
                git read-tree "$Gref"
-               git rm --cached --ignore-unmatch -q "$2"
+               if [ -n "$HARD_FORCE" ]; then
+                       git rm --cached --ignore-unmatch -q -f "$2"
+               else
+                       if ! git rm --cached --ignore-unmatch -q "$2"; then
+                               echo_info "Error: Failed to remove '$2' because it has local modifications."
+                               echo_info "Manual removal command: git rm --cached -f '$2'"
+                               
+                               suggest_args="--force --hard"
+                               # If we are in init mode (FORCE_INIT set), the user MUST pass --init again
+                               if [ "${FORCE_INIT:-}" = "yes" ]; then
+                                       suggest_args="--init $suggest_args"
+                               fi
+                               
+                               echo_info "To force remove via tool, run:"
+                               echo_info "  git-remote-gcrypt clean $suggest_args $URL"
+                               exit 1
+                       fi
+               fi
                tree_id=$(git write-tree)
                if [ "$tree_id" != "$(git rev-parse "$Gref^{tree}")" ]; then
                        commit_id=$(anon_commit "$tree_id") &&
index 7b486de14dbbcf823347b07beb179c0ebeddd73d..b5d9e786f7d28657912fba88569553587c46c265 100755 (executable)
@@ -218,11 +218,28 @@ if ! $GIT ls-tree -r "$GREF" | grep -q "garbage_file"; then
 fi
 print_info "Injected garbage_file into remote $GREF"
 
+# 2.5 Inject a DOTFILE garbage file
+DOT_GARBAGE_BLOB=$(echo "HIDDEN GARBAGE" | $GIT hash-object -w --stdin)
+export GIT_INDEX_FILE=index.dotgarbage
+$GIT read-tree "$NEW_TREE"
+$GIT update-index --add --cacheinfo 100644 "$DOT_GARBAGE_BLOB" ".garbage_file"
+NEW_TREE_DOT=$($GIT write-tree)
+rm index.dotgarbage
+NEW_COMMIT_DOT=$(echo "Inject dot garbage" | $GIT commit-tree "$NEW_TREE_DOT" -p "$NEW_COMMIT")
+$GIT update-ref "$GREF" "$NEW_COMMIT_DOT"
+
+if ! $GIT ls-tree -r "$GREF" | grep -q "\.garbage_file"; then
+       print_err "Failed to inject .garbage_file into $GREF"
+       exit 1
+fi
+print_info "Injected .garbage_file into remote $GREF"
+
 # 3. Scan (expect to find garbage_file)
 set -x
 output=$("$SCRIPT_DIR/git-remote-gcrypt" clean "gcrypt::$tempdir/valid.git" 2>&1)
 set +x
 assert_grep "garbage_file" "$output" "clean identified unencrypted file in valid repo"
+assert_grep "\.garbage_file" "$output" "clean identified unencrypted DOTFILE in valid repo"
 assert_grep "NOTE: This is a scan" "$output" "clean scan-only mode confirmed"
 
 # 4. Clean Force