diff --git a/lib/stage.sh b/lib/stage.sh index 75f7e2e..c37cdf4 100644 --- a/lib/stage.sh +++ b/lib/stage.sh @@ -1,15 +1,34 @@ #!/usr/bin/env bash # stage.sh — Git index mutations for readable note staging +# Restore one tracked path to the exact index entry present before staging. +restore_tracked_note_index() { + local repo="$1" readable_path="$2" mode="$3" blob="$4" + if ! git -C "$repo" update-index --cacheinfo \ + "$mode" "$blob" "$readable_path"; then + echo "Error: failed to restore index entry after staging refusal: $readable_path" >&2 + return 1 + fi +} + # Stage one readable note. Existing tracked plaintext paths are renormalized so # a newly-added clean filter replaces their unchanged plaintext index blobs. +# Restore the prior entry if post-staging ciphertext verification refuses it. stage_readable_note() { local repo="$1" notes_dir="$2" relpath="$3" local readable_path="$notes_dir/$relpath" local tracked_readable=false + local original_entry original_metadata original_mode original_blob original_stage if git -C "$repo" ls-files --error-unmatch -- "$readable_path" >/dev/null 2>&1; then tracked_readable=true + original_entry=$(git -C "$repo" ls-files --stage -- "$readable_path") || return 1 + original_metadata="${original_entry%%$'\t'*}" + read -r original_mode original_blob original_stage <<< "$original_metadata" + if [ "$original_stage" != "0" ] || [ -z "$original_blob" ]; then + echo "Error: tracked readable note has no ordinary index entry: $relpath" >&2 + return 1 + fi git -C "$repo" add -f --renormalize -- "$readable_path" else git -C "$repo" add -f -- "$readable_path" @@ -18,6 +37,10 @@ stage_readable_note() { local index_blob head_blob="" blob_state blob_status=0 if ! index_blob=$(git -C "$repo" rev-parse ":$readable_path" 2>/dev/null); then echo "Error: staging did not create an index entry for: $relpath" >&2 + if $tracked_readable; then + restore_tracked_note_index \ + "$repo" "$readable_path" "$original_mode" "$original_blob" || return 1 + fi return 1 fi if git -C "$repo" cat-file -e "HEAD:$readable_path" 2>/dev/null; then @@ -26,6 +49,8 @@ stage_readable_note() { if $tracked_readable && [ -n "$head_blob" ] && [ "$index_blob" = "$head_blob" ]; then echo "Error: staging produced no index change for tracked readable note: $relpath" >&2 echo "Ensure setup configured an active encryption filter, then retry." >&2 + restore_tracked_note_index \ + "$repo" "$readable_path" "$original_mode" "$original_blob" || return 1 return 1 fi @@ -34,11 +59,15 @@ stage_readable_note() { || blob_status=$? if [ "$blob_status" -ne 0 ]; then echo "Error: failed to inspect staged note blob: $relpath" >&2 + restore_tracked_note_index \ + "$repo" "$readable_path" "$original_mode" "$original_blob" || return 1 return "$blob_status" fi if [ "$blob_state" != "encrypted" ]; then echo "Error: staged tracked readable note is not git-crypt encrypted: $relpath" >&2 echo "Ensure setup configured an active encryption filter, then retry." >&2 + restore_tracked_note_index \ + "$repo" "$readable_path" "$original_mode" "$original_blob" || return 1 return 1 fi fi diff --git a/test/stage.bats b/test/stage.bats index 56982c7..213b3ef 100644 --- a/test/stage.bats +++ b/test/stage.bats @@ -41,11 +41,13 @@ setup() { [[ "$output" == *"legacy.md"* ]] } -@test "notes stage rejects a changed tracked blob that remains plaintext" { +@test "notes stage restores a changed tracked blob that remains plaintext" { echo "# Legacy plaintext" > "$NOTES_CALLER_PWD/notes/legacy.md" git -C "$NOTES_CALLER_PWD" add -f notes/legacy.md git -C "$NOTES_CALLER_PWD" commit -q --no-verify -m "legacy plaintext note" + local original_blob + original_blob=$(git -C "$NOTES_CALLER_PWD" rev-parse :notes/legacy.md) local clean_filter="$BATS_TEST_TMPDIR/plaintext-clean-filter" cat > "$clean_filter" <<'SH' #!/usr/bin/env bash @@ -64,9 +66,12 @@ SH [ "$status" -ne 0 ] [[ "$output" == *"not git-crypt encrypted"* ]] [[ "$output" == *"legacy.md"* ]] + run git -C "$NOTES_CALLER_PWD" rev-parse :notes/legacy.md + [ "$output" = "$original_blob" ] + run git -C "$NOTES_CALLER_PWD" diff --cached --name-only -- notes/legacy.md + [ -z "$output" ] run git -C "$NOTES_CALLER_PWD" show :notes/legacy.md - [[ "$output" == FILTERED:* ]] - [[ "$output" != *"GITCRYPT"* ]] + [ "$output" = "# Legacy plaintext" ] } @test "notes stage: no args requires explicit scope" {