Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## 0.9.4 - Unreleased

- Report the actual reset failure when portable-store initialization cannot recover from a dirty merge. Thanks @SebTardif.

## 0.9.3 - 2026-08-29

- Fix repeated gzip-only portable initialization and add a locked, bounded, preservation-first `portable refresh` subscriber command with automatic Git maintenance disabled across portable operations.
Expand Down
3 changes: 3 additions & 0 deletions docs/portable-stores.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ Initialization validates portable arguments before invoking Git and validates
the artifact before saving configuration. Repeated initialization and a
publisher's raw-to-gzip transition do not require a raw `.db` in the checkout.
Use `init` for setup; it still regenerates configuration on success.
If a dirty merge triggers legacy reset recovery and that reset fails, `init`
reports the reset failure with the same credential-safe Git diagnostics used
elsewhere, so the error identifies the recovery step that needs attention.

## Routine subscriber refresh

Expand Down
2 changes: 1 addition & 1 deletion internal/cli/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -3756,7 +3756,7 @@ func syncPortableStore(ctx context.Context, remoteURL, dir string) (string, erro
return "", err
}
if resetErr := runGit(ctx, "", "-C", dir, "reset", "--hard", "HEAD"); resetErr != nil {
return "", err
return "", resetErr
}
if retryErr := fastForwardGitCheckout(ctx, dir, false); retryErr != nil {
return "", retryErr
Expand Down
51 changes: 51 additions & 0 deletions internal/cli/portable_safety_unix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
"os/exec"
Expand Down Expand Up @@ -135,6 +136,56 @@ func TestPortableGitFailureDiagnostics(t *testing.T) {
}
}

func TestSyncPortableStoreReturnsResetFailure(t *testing.T) {
for _, dirtyBeforePull := range []bool{false, true} {
t.Run(fmt.Sprintf("dirty-before-pull=%t", dirtyBeforePull), func(t *testing.T) {
fixture := newPortableRefreshFixture(t, false)
if err := os.WriteFile(filepath.Join(fixture.remote, "incoming.txt"), []byte("remote data\n"), 0o600); err != nil {
t.Fatal(err)
}
portableTestCommit(t, fixture.remote)
localPath := filepath.Join(fixture.checkout, "incoming.txt")
if dirtyBeforePull {
localPath = filepath.Join(fixture.checkout, fixture.relative)
}
if err := os.WriteFile(localPath, []byte("local data\n"), 0o600); err != nil {
t.Fatal(err)
}
if clean := gitWorktreeClean(context.Background(), fixture.checkout); clean == dirtyBeforePull {
t.Fatalf("unexpected tracked worktree cleanliness: %t", clean)
}

realGit, err := exec.LookPath("git")
if err != nil {
t.Fatal(err)
}
wrapper := filepath.Join(t.TempDir(), "git")
script := `#!/bin/sh
for arg in "$@"; do
if [ "$arg" = reset ]; then
echo 'synthetic-private-path: No space left on device' >&2
exit 79
fi
done
exec "$GITCRAWL_TEST_REAL_GIT" "$@"
`
if err := os.WriteFile(wrapper, []byte(script), 0o700); err != nil {
t.Fatal(err)
}
t.Setenv("GITCRAWL_TEST_REAL_GIT", realGit)
t.Setenv("GITCRAWL_PORTABLE_GIT", wrapper)
_, err = syncPortableStore(context.Background(), fixture.remote, fixture.checkout)
var exit *exec.ExitError
if !errors.As(err, &exit) || exit.ExitCode() != 79 || !strings.Contains(err.Error(), "insufficient disk space for Git") {
t.Fatalf("expected reset failure with exit 79 and disk-space guidance, got %v", err)
}
if strings.Contains(err.Error(), "synthetic-private-path") || isDirtyPortablePullError(err) {
t.Fatalf("reset error exposed raw diagnostics or retained the earlier merge failure: %v", err)
}
})
}
}

func TestPortableGitCancellationAllowsOwnedCleanup(t *testing.T) {
dir := t.TempDir()
owned := filepath.Join(dir, "tmp_pack_owned")
Expand Down