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
25 changes: 24 additions & 1 deletion internal/state/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package state

import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"syscall"

"github.com/Neokil/AutoPR/internal/domain/workflowstate"
"github.com/Neokil/AutoPR/internal/gitutil"
Expand Down Expand Up @@ -72,7 +74,10 @@ func (s *Store) SaveState(ticketNumber string, state workflowstate.State) error
return fmt.Errorf("write worktree state: %w", err)
}
// Remove the pre-worktree copy so there is only one source of truth.
_ = os.Remove(filepath.Join(s.TicketDir(ticketNumber), StateFileName))
err = s.cleanupLegacyTicketDir(ticketNumber)
if err != nil {
return err
}

return nil
}
Expand Down Expand Up @@ -160,6 +165,24 @@ func (s *Store) ensureTicketDir(ticketNumber string) (string, error) {
return dir, nil
}

func (s *Store) cleanupLegacyTicketDir(ticketNumber string) error {
statePath := filepath.Join(s.TicketDir(ticketNumber), StateFileName)
err := os.Remove(statePath)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("remove legacy state file: %w", err)
}

err = os.Remove(s.TicketDir(ticketNumber))
if err == nil || os.IsNotExist(err) {
return nil
}
if errors.Is(err, syscall.ENOTEMPTY) {
return nil
}

return fmt.Errorf("remove legacy ticket dir: %w", err)
}

func parseStateJSON(_ string, data []byte) (workflowstate.State, error) {
var state workflowstate.State
err := json.Unmarshal(data, &state)
Expand Down
123 changes: 123 additions & 0 deletions internal/state/store_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package state_test

import (
"os"
"path/filepath"
"slices"
"testing"

workflowstate "github.com/Neokil/AutoPR/internal/domain/workflowstate"
"github.com/Neokil/AutoPR/internal/state"
)

func TestSaveStateCreatesPreWorktreeStateFile(t *testing.T) {
t.Parallel()

repoRoot := t.TempDir()
store := state.NewStore(repoRoot, ".auto-pr-state")
ticketState := workflowstate.New("GH-12")

err := store.SaveState(ticketState.TicketNumber, ticketState)
if err != nil {
t.Fatalf("SaveState() error = %v", err)
}

statePath := filepath.Join(store.TicketDir(ticketState.TicketNumber), state.StateFileName)
_, statErr := os.Stat(statePath)
if statErr != nil {
t.Fatalf("expected pre-worktree state file at %s: %v", statePath, statErr)
}
}

func TestSaveStateMigratesStateAndRemovesEmptyLegacyDir(t *testing.T) {
t.Parallel()

repoRoot := t.TempDir()
store := state.NewStore(repoRoot, ".auto-pr-state")
ticketState := workflowstate.New("GH-12")

err := store.SaveState(ticketState.TicketNumber, ticketState)
if err != nil {
t.Fatalf("initial SaveState() error = %v", err)
}

ticketState.WorktreePath = filepath.Join(repoRoot, ".auto-pr-state", "worktrees", ticketState.TicketNumber)
err = store.SaveState(ticketState.TicketNumber, ticketState)
if err != nil {
t.Fatalf("migrating SaveState() error = %v", err)
}

worktreeStatePath := filepath.Join(ticketState.WorktreePath, ".auto-pr", state.StateFileName)
_, worktreeStateErr := os.Stat(worktreeStatePath)
if worktreeStateErr != nil {
t.Fatalf("expected worktree state file at %s: %v", worktreeStatePath, worktreeStateErr)
}

legacyStatePath := filepath.Join(store.TicketDir(ticketState.TicketNumber), state.StateFileName)
_, legacyStateErr := os.Stat(legacyStatePath)
if !os.IsNotExist(legacyStateErr) {
t.Fatalf("expected legacy state file to be removed, got err=%v", legacyStateErr)
}

_, legacyDirErr := os.Stat(store.TicketDir(ticketState.TicketNumber))
if !os.IsNotExist(legacyDirErr) {
t.Fatalf("expected empty legacy ticket dir to be removed, got err=%v", legacyDirErr)
}
}

func TestSaveStateKeepsLegacyDirWhenItContainsOtherFiles(t *testing.T) {
t.Parallel()

repoRoot := t.TempDir()
store := state.NewStore(repoRoot, ".auto-pr-state")
ticketState := workflowstate.New("GH-12")

err := store.SaveState(ticketState.TicketNumber, ticketState)
if err != nil {
t.Fatalf("initial SaveState() error = %v", err)
}

notePath := filepath.Join(store.TicketDir(ticketState.TicketNumber), "note.txt")
err = os.WriteFile(notePath, []byte("keep me\n"), 0o644)
if err != nil {
t.Fatalf("WriteFile() error = %v", err)
}

ticketState.WorktreePath = filepath.Join(repoRoot, ".auto-pr-state", "worktrees", ticketState.TicketNumber)
err = store.SaveState(ticketState.TicketNumber, ticketState)
if err != nil {
t.Fatalf("migrating SaveState() error = %v", err)
}

_, noteStatErr := os.Stat(notePath)
if noteStatErr != nil {
t.Fatalf("expected extra file to remain in legacy dir: %v", noteStatErr)
}
}

func TestListTicketDirsIncludesMigratedWorktreeState(t *testing.T) {
t.Parallel()

repoRoot := t.TempDir()
store := state.NewStore(repoRoot, ".auto-pr-state")
ticketState := workflowstate.New("GH-12")

err := store.SaveState(ticketState.TicketNumber, ticketState)
if err != nil {
t.Fatalf("initial SaveState() error = %v", err)
}

ticketState.WorktreePath = filepath.Join(repoRoot, ".auto-pr-state", "worktrees", ticketState.TicketNumber)
err = store.SaveState(ticketState.TicketNumber, ticketState)
if err != nil {
t.Fatalf("migrating SaveState() error = %v", err)
}

tickets, err := store.ListTicketDirs()
if err != nil {
t.Fatalf("ListTicketDirs() error = %v", err)
}
if !slices.Contains(tickets, ticketState.TicketNumber) {
t.Fatalf("expected migrated ticket %q in %v", ticketState.TicketNumber, tickets)
}
}
Loading