repack on init option
authorShane Jaroch <chown_tee@proton.me>
Sat, 17 Jan 2026 04:03:33 +0000 (23:03 -0500)
committerShane Jaroch <chown_tee@proton.me>
Sat, 17 Jan 2026 04:03:33 +0000 (23:03 -0500)
git-remote-gcrypt
tests/system-test-clean-repack.sh [new file with mode: 0755]

index 74c2a584fd0aaf5ab9f5d434c03928ccd166e9d8..3e8bb9fd1139c70506aa5b6f99f86427753b1668 100755 (executable)
@@ -89,6 +89,7 @@ while [ $# -gt 0 ]; do
                                case "$1" in
                                        --force) FORCE_CLEAN=yes ;;
                                        --hard) HARD_FORCE=yes ;;
+                                       --repack) DO_REPACK=yes ;;
                                        --init) FORCE_INIT=yes ;;
                                        -*) echo "Unknown option: $1" >&2; exit 1 ;;
                                        *)
@@ -1128,6 +1129,77 @@ do_fetch()
 }
 
 # do_push PUSHARGS (multiple lines like +src:dst, with both + and src opt.)
+# Perform repack, manifest generation, and upload
+# Requires: r_revlist, Tempdir, Packkey_bytes, Hashtype, Packlist, Keeplist, Recipients, Refslist, Repoid, Extnlist, URL, NAME, VERSION
+perform_repack()
+{
+       local tmp_encrypted tmp_objlist tmp_manifest pack_id key_ r_pack_delete=""
+
+       tmp_encrypted="$Tempdir/packP"
+       tmp_objlist="$Tempdir/objlP"
+
+       {
+               xfeed "$r_revlist" git rev-list --objects --stdin --
+               repack_if_needed @r_pack_delete
+       } > "$tmp_objlist"
+
+       # Only send pack if we have any objects to send
+       if [ -s "$tmp_objlist" ]
+       then
+               key_=$(genkey "$Packkey_bytes")
+               pack_id=$(export GIT_ALTERNATE_OBJECT_DIRECTORIES="$Tempdir";
+                       pipefail git pack-objects --stdout < "$tmp_objlist" |
+                       pipefail ENCRYPT "$key_" |
+                       tee "$tmp_encrypted" | gpg_hash "$Hashtype")
+
+               append_to @Packlist "pack :${Hashtype}:$pack_id $key_"
+               if isnonnull "$r_pack_delete"
+               then
+                       append_to @Keeplist "keep :${Hashtype}:$pack_id 1"
+               fi
+       fi
+
+       # Generate manifest
+       # Update the gcrypt version in extensions (remove old, add current)
+       filter_to ! @Extnlist "extn gcrypt-version *" "$Extnlist"
+       append_to @Extnlist "extn gcrypt-version $VERSION"
+
+       echo_info "Encrypting to: $Recipients"
+       echo_info "Requesting manifest signature"
+
+       tmp_manifest="$Tempdir/maniP"
+       PRIVENCRYPT "$Recipients" > "$tmp_manifest" <<EOF
+$Refslist
+$Packlist
+$Keeplist
+repo $Repoid
+$Extnlist
+EOF
+
+       # Upload pack
+       if [ -s "$tmp_objlist" ]
+       then
+               PUT "$URL" "$pack_id" "$tmp_encrypted"
+       fi
+
+       # Upload manifest
+       PUT "$URL" "$Manifestfile" "$tmp_manifest"
+
+       rm -f "$tmp_encrypted"
+       rm -f "$tmp_objlist"
+       rm -f "$tmp_manifest"
+
+       # Delete packs
+       if isnonnull "$r_pack_delete"; then
+               REMOVE "$URL" "$(xecho "$r_pack_delete" | \
+                       while IFS=': ' read -r _ _ pack_
+                       do
+                               isnonnull "$pack_" || continue
+                               xecho "$pack_"
+                       done)"
+       fi
+}
+
 do_push()
 {
        # Security protocol:
@@ -1279,69 +1351,7 @@ EOF
                fi
        fi
 
-       tmp_encrypted="$Tempdir/packP"
-       tmp_objlist="$Tempdir/objlP"
-
-       {
-               xfeed "$r_revlist" git rev-list --objects --stdin --
-               repack_if_needed @r_pack_delete
-       } > "$tmp_objlist"
-
-       # Only send pack if we have any objects to send
-       if [ -s "$tmp_objlist" ]
-       then
-               key_=$(genkey "$Packkey_bytes")
-               pack_id=$(export GIT_ALTERNATE_OBJECT_DIRECTORIES="$Tempdir";
-                       pipefail git pack-objects --stdout < "$tmp_objlist" |
-                       pipefail ENCRYPT "$key_" |
-                       tee "$tmp_encrypted" | gpg_hash "$Hashtype")
-
-               append_to @Packlist "pack :${Hashtype}:$pack_id $key_"
-               if isnonnull "$r_pack_delete"
-               then
-                       append_to @Keeplist "keep :${Hashtype}:$pack_id 1"
-               fi
-       fi
-
-       # Generate manifest
-       # Update the gcrypt version in extensions (remove old, add current)
-       filter_to ! @Extnlist "extn gcrypt-version *" "$Extnlist"
-       append_to @Extnlist "extn gcrypt-version $VERSION"
-
-       echo_info "Encrypting to: $Recipients"
-       echo_info "Requesting manifest signature"
-
-       tmp_manifest="$Tempdir/maniP"
-       PRIVENCRYPT "$Recipients" > "$tmp_manifest" <<EOF
-$Refslist
-$Packlist
-$Keeplist
-repo $Repoid
-$Extnlist
-EOF
-
-       # Upload pack
-       if [ -s "$tmp_objlist" ]
-       then
-               PUT "$URL" "$pack_id" "$tmp_encrypted"
-       fi
-
-       # Upload manifest
-       PUT "$URL" "$Manifestfile" "$tmp_manifest"
-
-       rm -f "$tmp_encrypted"
-       rm -f "$tmp_objlist"
-       rm -f "$tmp_manifest"
-
-       # Delete packs
-       if isnonnull "$r_pack_delete"; then
-               REMOVE "$URL" "$(xecho "$r_pack_delete" | \
-                       while IFS=': ' read -r _ _ pack_
-                       do
-                               isnonnull "$pack_" || continue
-                               xecho "$pack_"
-                       done)"
-       fi
+       perform_repack
 
        PUT_FINAL "$URL"
 
@@ -1551,9 +1561,20 @@ cmd_clean()
        done
        IFS="$OIFS"
        bad_files="${bad_files#"$Newline"}"
-       
+
        if isnull "$bad_files"; then
                echo_info "No unencrypted files found. Remote is clean."
+               if [ "${DO_REPACK:-}" = "yes" ]; then
+                       echo_info "Repacking remote..."
+                       # Prepare r_revlist for repack (all refs)
+                       r_revlist=""
+                       if isnonnull "$Refslist"; then
+                               r_revlist=$(xecho "$Refslist" | cut -d' ' -f1)
+                       fi
+                       GCRYPT_FULL_REPACK=1
+                       perform_repack
+                       PUT_FINAL "$URL"
+               fi
                CLEAN_FINAL "$URL"
                git remote remove "$NAME" 2>/dev/null || true
                exit 0
@@ -1570,6 +1591,12 @@ cmd_clean()
                else
                        echo_info "   git-remote-gcrypt clean --force $URL"
                fi
+
+               # If user requested repack but found bad files and no force, abort (safety first)
+               if [ "${DO_REPACK:-}" = "yes" ]; then
+                       echo_info "NOTE: Repack requested but pending file deletions require --force."
+               fi
+
                CLEAN_FINAL "$URL"
                git remote remove "$NAME" 2>/dev/null || true
                exit 0
@@ -1577,6 +1604,20 @@ cmd_clean()
 
        echo_info "Removing files..."
        REMOVE "$URL" "$bad_files"
+
+       if [ "${DO_REPACK:-}" = "yes" ]; then
+               echo_info "Repacking remote..."
+               # Prepare r_revlist from all current refs for full repack
+               r_revlist=""
+               if isnonnull "$Refslist"; then
+                       r_revlist=$(xecho "$Refslist" | cut -d' ' -f1)
+               fi
+
+               # Set flag to force repack_if_needed to act
+               GCRYPT_FULL_REPACK=1
+               perform_repack
+       fi
+
        PUT_FINAL "$URL"
        CLEAN_FINAL "$URL"
        git remote remove "$NAME" 2>/dev/null || true
diff --git a/tests/system-test-clean-repack.sh b/tests/system-test-clean-repack.sh
new file mode 100755 (executable)
index 0000000..29df415
--- /dev/null
@@ -0,0 +1,152 @@
+#!/bin/sh
+set -e
+
+# Setup test environment
+echo "Setting up repack test environment..."
+PROJECT_ROOT="$(pwd)"
+mkdir -p .tmp
+TEST_DIR="$PROJECT_ROOT/.tmp/repack_test"
+rm -rf "$TEST_DIR"
+mkdir -p "$TEST_DIR"
+
+# Repo paths
+REPO_DIR="$TEST_DIR/repo"
+REMOTE_DIR="$TEST_DIR/remote"
+
+mkdir -p "$REPO_DIR"
+mkdir -p "$REMOTE_DIR"
+
+# Tools
+GCRYPT_BIN="$PROJECT_ROOT/git-remote-gcrypt"
+if [ ! -x "$GCRYPT_BIN" ]; then
+       echo "Error: git-remote-gcrypt binary not found at $GCRYPT_BIN"
+       exit 1
+fi
+
+# GPG Setup
+export GNUPGHOME="$TEST_DIR/gpg"
+mkdir -p "$GNUPGHOME"
+chmod 700 "$GNUPGHOME"
+
+cat <<EOF >"${GNUPGHOME}/gpg"
+#!/usr/bin/env bash
+export GNUPGHOME="$GNUPGHOME"
+set -efuC -o pipefail; shopt -s inherit_errexit
+args=( "\${@}" )
+for ((i = 0; i < \${#}; ++i)); do
+    if [[ \${args[\${i}]} = "--secret-keyring" ]]; then
+        unset "args[\${i}]" "args[\$(( i + 1 ))]"
+        break
+    fi
+done
+exec gpg "\${args[@]}"
+EOF
+chmod +x "${GNUPGHOME}/gpg"
+
+echo "Generating GPG key..."
+gpg --batch --passphrase "" --quick-generate-key "Test <test@test.com>"
+
+# Initialize repo
+cd "$REPO_DIR"
+git init
+git config user.email "test@test.com"
+git config user.name "Test User"
+git config --global advice.defaultBranchName false
+
+# Initialize local remote
+git init --bare "$REMOTE_DIR"
+git remote add origin "gcrypt::$REMOTE_DIR"
+git config remote.origin.gcrypt-participants "test@test.com"
+git config remote.origin.gcrypt-signingkey "test@test.com"
+git config gpg.program "${GNUPGHOME}/gpg"
+git config user.signingkey "test@test.com"
+
+export PATH="$PROJECT_ROOT:$PATH"
+
+# Create fragmentation by pushing multiple times
+echo "Push 1"
+echo "data 1" >file1.txt
+git add file1.txt
+git commit -m "Commit 1" --no-gpg-sign
+# Initial push needs force to initialize remote gcrypt repo
+git push origin +master
+
+echo "Push 2"
+echo "data 2" >file2.txt
+git add file2.txt
+git commit -m "Commit 2" --no-gpg-sign
+git push origin master
+
+echo "Push 3"
+echo "data 3" >file3.txt
+git add file3.txt
+git commit -m "Commit 3" --no-gpg-sign
+git push origin master
+
+# Verify we have multiple pack files in remote
+# Note: gcrypt stores packs in 'pack' directory if using rsync-like backend?
+# For git backend (gitception), they are objects in the git repo.
+# We are using local file backend? No, gcrypt::$REMOTE_DIR where REMOTE_DIR is bare git repo.
+# This makes it a Git Backend (gitception).
+# The packs are stored as blobs in the backend repo.
+# But 'do_push' logic downloads packs using 'git rev-list'.
+# The 'Packlist' manifest file lists the active packs.
+# We can check the Manifest to count packs.
+
+# Clone the raw backend to inspect manifest
+cd "$TEST_DIR"
+git clone "$REMOTE_DIR" raw_backend
+cd raw_backend
+git checkout master
+# The manifest is a file with randomized name, but we can find it encrypt/decrypt?
+# No, easier: use git-remote-gcrypt to list packs via debug or inference.
+# Or just trust that multiple pushes created multiple packs (as gcrypt doesn't auto-repack on push unless configured).
+
+# Let's count lines in Packlist from the helper's debug output?
+# Or we can verify the backend git repo has multiple commits (one per push).
+HEAD_SHA=$(git rev-parse HEAD)
+echo "Backend SHA: $HEAD_SHA"
+# Start should have 3 commits (init, push1, push2, push3) -> wait, init is implicit.
+# Each push updates the manifested repo.
+
+# Run clean --repack
+cd "$REPO_DIR"
+echo "Running clean --repack..."
+git-remote-gcrypt clean --repack origin
+
+# Verify result
+# Clone backend again (pull) and check structure
+cd "$TEST_DIR/raw_backend"
+git pull origin master
+
+# Count commits? Repack might add a commit?
+# Repack reads all objects, creates 1 new pack, updates manifest.
+# This results in a NEW commit on the backend that has the new manifest.
+# The OLD packs are removed (deleted from backend).
+# So we should see a new commit.
+# Check if commit SHA changed. Repack force-pushes a new manifest state.
+NEW_HEAD=$(git rev-parse HEAD)
+echo "Old HEAD: $HEAD_SHA"
+echo "New HEAD: $NEW_HEAD"
+
+if [ "$NEW_HEAD" != "$HEAD_SHA" ]; then
+       echo "Repack successful (HEAD changed)."
+else
+       echo "Repack failed (HEAD did not change)."
+       exit 1
+fi
+
+# Verify data integrity
+cd "$REPO_DIR"
+# Force fresh clone to verified data
+cd "$TEST_DIR"
+git clone "gcrypt::$REMOTE_DIR" verified_repo
+cd verified_repo
+if [ -f file1.txt ] && [ -f file2.txt ] && [ -f file3.txt ]; then
+       echo "Data integrity verified."
+else
+       echo "Data integrity failed!"
+       exit 1
+fi
+
+echo "Test passed."