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: 1 addition & 1 deletion .issues/.counter
Original file line number Diff line number Diff line change
@@ -1 +1 @@
13
14
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
id: "013"
assignee: ""
labels:
- bug
- fixed
created: 2025-11-18T10:20:19.765626+09:00
updated: 2025-11-18T10:24:47.5259+09:00
---

# Fix filename preservation bug when closing/opening issues with modified titles

## Problem

When users modified issue titles after creation, the `close` and `open` commands created duplicate files with malformed filenames:

1. **Korean/Special Character Titles**: Generated malformed filenames like `001-.md`
2. **Modified English Titles**: Created new files with updated slugs, leaving originals behind

### Root Cause

Both `close` and `open` commands called `SaveIssue()` before `MoveIssue()`, which:
- Generated new slugs based on current `issue.Title` (which could be modified by users)
- Created new files instead of updating existing ones
- Left original files in place, resulting in duplicates


---


## Solution

1. **Modified `MoveIssue` in `pkg/storage.go`**:
- Update `updated` timestamp after successful file relocation
- Preserve original filename during move

2. **Removed redundant `SaveIssue` calls in `cmd/close.go` and `cmd/open.go`**:
- Eliminated duplicate file creation
- Simplified command logic

3. **Added regression tests**:
- `TestRunClosePreservesFilenameWithKoreanTitle`
- `TestRunClosePreservesFilenameWhenTitleModified`
- `TestRunOpenPreservesFilenameWithKoreanTitle`
- `TestRunOpenPreservesFilenameWhenTitleModified`
- Updated `TestMoveIssue` to verify timestamp updates

## Test Results

- ✅ All existing tests pass
- ✅ New regression tests pass
- ✅ Code coverage maintained: 86.7% (cmd), 76.0% (pkg)

## Files Changed

- `pkg/storage.go`: Add timestamp update in `MoveIssue`
- `cmd/close.go`: Remove `SaveIssue` call
- `cmd/open.go`: Remove `SaveIssue` call
- `pkg/storage_test.go`: Add timestamp verification
- `cmd/close_test.go`: Add filename preservation tests
- `cmd/open_test.go`: Add filename preservation tests
15 changes: 3 additions & 12 deletions cmd/close.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"fmt"
"os"
"os/exec"
"time"

"github.com/Allra-Fintech/git-issue/pkg"
"github.com/spf13/cobra"
Expand All @@ -28,8 +27,8 @@ func init() {
func runClose(cmd *cobra.Command, args []string) error {
issueID := args[0]

// Load the issue
issue, currentDir, err := pkg.LoadIssue(issueID)
// Load the issue to check its status
_, currentDir, err := pkg.LoadIssue(issueID)
if err != nil {
return fmt.Errorf("failed to load issue: %w", err)
}
Expand All @@ -39,15 +38,7 @@ func runClose(cmd *cobra.Command, args []string) error {
return fmt.Errorf("issue #%s is already closed", issueID)
}

// Update timestamp
issue.Updated = time.Now()

// Save the issue with updated timestamp
if err := pkg.SaveIssue(issue, pkg.OpenDir); err != nil {
return fmt.Errorf("failed to update issue: %w", err)
}

// Move issue from open to closed
// Move issue from open to closed (timestamp update included)
if err := pkg.MoveIssue(issueID, pkg.OpenDir, pkg.ClosedDir); err != nil {
return fmt.Errorf("failed to move issue: %w", err)
}
Expand Down
124 changes: 124 additions & 0 deletions cmd/close_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"os"
"strings"
"testing"

Expand Down Expand Up @@ -117,3 +118,126 @@ func TestRunCloseCommitCreatesGitCommit(t *testing.T) {
t.Fatalf("unexpected commit message %q", lastMessage)
}
}

func TestRunClosePreservesFilenameWithKoreanTitle(t *testing.T) {
_, cleanup := setupCommandTestRepo(t)
defer cleanup()

// Create issue with English title
if err := runCreate(nil, []string{"Initial English Title"}); err != nil {
t.Fatalf("runCreate() failed: %v", err)
}

// Get original filename
originalPath, _, err := pkg.FindIssueFile("001")
if err != nil {
t.Fatalf("failed to find issue: %v", err)
}

// Edit issue to have Korean title
issue, _, err := pkg.LoadIssue("001")
if err != nil {
t.Fatalf("failed to load issue: %v", err)
}
issue.Title = "한글 제목으로 변경"
content, err := pkg.SerializeIssue(issue)
if err != nil {
t.Fatalf("failed to serialize issue: %v", err)
}
if err := os.WriteFile(originalPath, []byte(content), 0644); err != nil {
t.Fatalf("failed to write issue: %v", err)
}

// Close the issue
if err := runClose(nil, []string{"001"}); err != nil {
t.Fatalf("runClose() failed: %v", err)
}

// Verify issue is in closed directory with original filename
closedPath, dir, err := pkg.FindIssueFile("001")
if err != nil {
t.Fatalf("failed to find closed issue: %v", err)
}
if dir != pkg.ClosedDir {
t.Fatalf("issue should be in closed dir, got %s", dir)
}

// Verify filename was preserved (contains "initial-english-title")
if !strings.Contains(closedPath, "initial-english-title") {
t.Errorf("filename should be preserved, got %s", closedPath)
}

// Verify no malformed files like "001-.md" were created
openFiles, _ := pkg.ListIssues(pkg.OpenDir)
closedFiles, _ := pkg.ListIssues(pkg.ClosedDir)

if len(openFiles) != 0 {
t.Errorf("open directory should be empty, found %d files", len(openFiles))
}
if len(closedFiles) != 1 {
t.Errorf("closed directory should have exactly 1 file, found %d", len(closedFiles))
}
}

func TestRunClosePreservesFilenameWhenTitleModified(t *testing.T) {
_, cleanup := setupCommandTestRepo(t)
defer cleanup()

// Create issue
if err := runCreate(nil, []string{"Original Title"}); err != nil {
t.Fatalf("runCreate() failed: %v", err)
}

// Get original filename
originalPath, _, err := pkg.FindIssueFile("001")
if err != nil {
t.Fatalf("failed to find issue: %v", err)
}

// Edit issue to have completely different title
issue, _, err := pkg.LoadIssue("001")
if err != nil {
t.Fatalf("failed to load issue: %v", err)
}
issue.Title = "Completely Different Modified Title"
content, err := pkg.SerializeIssue(issue)
if err != nil {
t.Fatalf("failed to serialize issue: %v", err)
}
if err := os.WriteFile(originalPath, []byte(content), 0644); err != nil {
t.Fatalf("failed to write issue: %v", err)
}

// Close the issue
if err := runClose(nil, []string{"001"}); err != nil {
t.Fatalf("runClose() failed: %v", err)
}

// Verify issue is in closed directory with original filename
closedPath, dir, err := pkg.FindIssueFile("001")
if err != nil {
t.Fatalf("failed to find closed issue: %v", err)
}
if dir != pkg.ClosedDir {
t.Fatalf("issue should be in closed dir, got %s", dir)
}

// Verify filename was preserved (contains "original-title", not "completely-different")
if !strings.Contains(closedPath, "original-title") {
t.Errorf("original filename should be preserved, got %s", closedPath)
}
if strings.Contains(closedPath, "completely-different") {
t.Errorf("filename should not change to modified title, got %s", closedPath)
}

// Verify no duplicate files were created
openFiles, _ := pkg.ListIssues(pkg.OpenDir)
closedFiles, _ := pkg.ListIssues(pkg.ClosedDir)

if len(openFiles) != 0 {
t.Errorf("open directory should be empty, found %d files", len(openFiles))
}
if len(closedFiles) != 1 {
t.Errorf("closed directory should have exactly 1 file, found %d", len(closedFiles))
}
}
15 changes: 3 additions & 12 deletions cmd/open.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package cmd

import (
"fmt"
"time"

"github.com/Allra-Fintech/git-issue/pkg"
"github.com/spf13/cobra"
Expand All @@ -26,8 +25,8 @@ func init() {
func runOpen(cmd *cobra.Command, args []string) error {
issueID := args[0]

// Load the issue
issue, currentDir, err := pkg.LoadIssue(issueID)
// Load the issue to check its status
_, currentDir, err := pkg.LoadIssue(issueID)
if err != nil {
return fmt.Errorf("failed to load issue: %w", err)
}
Expand All @@ -37,15 +36,7 @@ func runOpen(cmd *cobra.Command, args []string) error {
return fmt.Errorf("issue #%s is already open", issueID)
}

// Update timestamp
issue.Updated = time.Now()

// Save the issue with updated timestamp
if err := pkg.SaveIssue(issue, pkg.ClosedDir); err != nil {
return fmt.Errorf("failed to update issue: %w", err)
}

// Move issue from closed to open
// Move issue from closed to open (timestamp update included)
if err := pkg.MoveIssue(issueID, pkg.ClosedDir, pkg.OpenDir); err != nil {
return fmt.Errorf("failed to move issue: %w", err)
}
Expand Down
Loading