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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,5 @@ go.work.sum

# Editor/IDE
.claude/

test/
2 changes: 1 addition & 1 deletion .issues/.counter
Original file line number Diff line number Diff line change
@@ -1 +1 @@
18
19
Empty file added .issues/closed/.keep
Empty file.
Empty file added .issues/open/.keep
Empty file.
56 changes: 56 additions & 0 deletions .issues/open/018-increment-counter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
id: "018"
assignee: ""
labels: []
created: 2025-11-27T10:01:37.532453+09:00
updated: 2025-11-27T10:01:37.532453+09:00
---

# increment counter

## Description

When creating a new issue, if the counter points to an ID that already exists in the closed directory, the `gi create` command fails with an error instead of automatically finding the next available ID.

**Current behavior:**
```
$ gi create aa
Error: failed to save issue: issue 003 exists in closed directory, cannot save to open
```

This happens because `GetNextID()` in `pkg/storage.go` simply reads the counter value and increments it, without checking if that ID is already occupied by a closed issue.

**Expected behavior:**
The system should automatically skip occupied IDs and use the next available one, updating the counter accordingly.

## Requirements

- Modify `GetNextID()` function in `pkg/storage.go:98` to check if the current counter ID exists
- If ID exists in either open or closed directory, increment and check again until an available ID is found
- Update the counter file to reflect the next available ID
- Maintain backward compatibility with existing counter behavior
- Handle edge cases (e.g., large gaps in ID sequence)

## Success Criteria

- [x] `gi create` successfully creates issues even when counter points to closed issue ID
- [x] Counter automatically increments to next available ID
- [x] System handles multiple sequential occupied IDs correctly
- [x] No breaking changes to existing functionality
- [x] Unit tests cover the auto-increment logic
- [x] Works correctly in both scenarios:
- Counter = 3, issue 003 in closed → creates issue 004
- Counter = 5, issues 005-007 in closed → creates issue 008

## Implementation Notes

Fixed in `pkg/storage.go:97-132`. The `GetNextID()` function now:
1. Checks if the current counter value points to an existing issue
2. Loops through IDs until it finds an available one
3. Updates the counter to the next available ID

Added comprehensive unit tests in `pkg/storage_test.go`:
- `TestGetNextIDSkipsOccupiedIDs` - verifies skipping sequential occupied IDs
- `TestGetNextIDWithGapsInSequence` - verifies finding gaps in ID sequence

All tests passing with 77.5% code coverage.
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ golangci-lint run
### Cross-Platform Builds

The Makefile should support:

```bash
make build # Current platform
make build-all # macOS (ARM64/AMD64), Linux (AMD64)
Expand Down Expand Up @@ -136,6 +137,7 @@ make lint # Run golangci-lint
All commands use Cobra. Global flag: `-h, --help`

**Command-specific flags:**

- `create`: `--assignee <name>`, `--label <label>` (repeatable)
- `list`: `--all`, `--assignee <name>`, `--label <label>`, `--status <status>`
- `close/open`: `--commit` (auto-commit to git)
Expand All @@ -148,6 +150,7 @@ All commands use Cobra. Global flag: `-h, --help`
- Integration tests for command workflows
- Error cases: missing files, invalid IDs, non-git directories, malformed YAML
- Target: >80% code coverage
- run manual test by running the command in the `test/` directory which already gitignored

## Cross-Platform Considerations

Expand Down
21 changes: 17 additions & 4 deletions pkg/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ Describe the issue here...
return nil
}

// GetNextID reads and increments the counter
// GetNextID reads and increments the counter, skipping any IDs that already exist
func GetNextID() (int, error) {
counterPath := filepath.Join(IssuesDir, CounterFile)

Expand All @@ -109,13 +109,26 @@ func GetNextID() (int, error) {
return 0, fmt.Errorf("invalid counter value: %w", err)
}

// Write incremented value
nextID := currentID + 1
// Find the next available ID by checking if current ID exists
availableID := currentID
for {
formattedID := FormatID(availableID)
_, _, err := FindIssueFile(formattedID)
if err != nil {
// ID not found, so it's available
break
}
// ID exists (in either open or closed), try next one
availableID++
}

// Write the next ID after the one we're returning
nextID := availableID + 1
if err := os.WriteFile(counterPath, []byte(fmt.Sprintf("%d\n", nextID)), 0644); err != nil {
return 0, fmt.Errorf("failed to write counter: %w", err)
}

return currentID, nil
return availableID, nil
}

// SaveIssue writes an issue to the specified directory (open or closed)
Expand Down
105 changes: 105 additions & 0 deletions pkg/storage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,111 @@ func TestGetNextID(t *testing.T) {
}
}

func TestGetNextIDSkipsOccupiedIDs(t *testing.T) {
cleanup := setupTestRepo(t)
defer cleanup()

if err := InitializeRepo(); err != nil {
t.Fatal(err)
}

// Create and save issues with IDs 1, 2, 3
issue1 := NewIssue(1, "First Issue", "", nil)
issue2 := NewIssue(2, "Second Issue", "", nil)
issue3 := NewIssue(3, "Third Issue", "", nil)

if err := SaveIssue(issue1, OpenDir); err != nil {
t.Fatal(err)
}
if err := SaveIssue(issue2, ClosedDir); err != nil {
t.Fatal(err)
}
if err := SaveIssue(issue3, ClosedDir); err != nil {
t.Fatal(err)
}

// Set counter to 1 (which is occupied in open directory)
counterPath := filepath.Join(IssuesDir, CounterFile)
if err := os.WriteFile(counterPath, []byte("1\n"), 0644); err != nil {
t.Fatal(err)
}

// GetNextID should skip 1, 2, 3 and return 4
id, err := GetNextID()
if err != nil {
t.Fatalf("GetNextID() error = %v", err)
}
if id != 4 {
t.Errorf("GetNextID() = %d, want 4 (should skip occupied IDs 1-3)", id)
}

// Verify counter was updated to 5
data, err := os.ReadFile(counterPath)
if err != nil {
t.Fatal(err)
}
if strings.TrimSpace(string(data)) != "5" {
t.Errorf("Counter = %q, want %q", strings.TrimSpace(string(data)), "5")
}
}

func TestGetNextIDWithGapsInSequence(t *testing.T) {
cleanup := setupTestRepo(t)
defer cleanup()

if err := InitializeRepo(); err != nil {
t.Fatal(err)
}

// Create issues with gaps: 1, 3, 5 (missing 2, 4)
issue1 := NewIssue(1, "First Issue", "", nil)
issue3 := NewIssue(3, "Third Issue", "", nil)
issue5 := NewIssue(5, "Fifth Issue", "", nil)

if err := SaveIssue(issue1, ClosedDir); err != nil {
t.Fatal(err)
}
if err := SaveIssue(issue3, OpenDir); err != nil {
t.Fatal(err)
}
if err := SaveIssue(issue5, ClosedDir); err != nil {
t.Fatal(err)
}

// Set counter to 1
counterPath := filepath.Join(IssuesDir, CounterFile)
if err := os.WriteFile(counterPath, []byte("1\n"), 0644); err != nil {
t.Fatal(err)
}

// GetNextID should find the first gap at ID 2
id, err := GetNextID()
if err != nil {
t.Fatalf("GetNextID() error = %v", err)
}
if id != 2 {
t.Errorf("GetNextID() = %d, want 2 (should find first gap)", id)
}

// Next call should find ID 4
id, err = GetNextID()
if err != nil {
t.Fatalf("GetNextID() error = %v", err)
}
if id != 4 {
t.Errorf("GetNextID() = %d, want 4 (should find second gap)", id)
}

// Next call should skip 5 and return 6
id, err = GetNextID()
if err != nil {
t.Fatalf("GetNextID() error = %v", err)
}
if id != 6 {
t.Errorf("GetNextID() = %d, want 6 (should skip occupied ID 5)", id)
}
}

func TestSaveAndLoadIssue(t *testing.T) {
cleanup := setupTestRepo(t)
defer cleanup()
Expand Down