Add auto-detection of issue prefix from git history (#277)
- Check existing JSONL issues before falling back to directory name on initialization - Implement readFirstIssueFromJSONL() to extract prefix from first issue - Added tests for readFirstIssueFromJSONL
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -16,6 +18,8 @@ import (
|
|||||||
"github.com/steveyegge/beads/internal/configfile"
|
"github.com/steveyegge/beads/internal/configfile"
|
||||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||||
"github.com/steveyegge/beads/internal/syncbranch"
|
"github.com/steveyegge/beads/internal/syncbranch"
|
||||||
|
"github.com/steveyegge/beads/internal/types"
|
||||||
|
"github.com/steveyegge/beads/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var initCmd = &cobra.Command{
|
var initCmd = &cobra.Command{
|
||||||
@@ -47,12 +51,24 @@ With --no-db: creates .beads/ directory and issues.jsonl file instead of SQLite
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine prefix with precedence: flag > config > auto-detect
|
// Determine prefix with precedence: flag > config > auto-detect from git > auto-detect from directory name
|
||||||
if prefix == "" {
|
if prefix == "" {
|
||||||
// Try to get from config file
|
// Try to get from config file
|
||||||
prefix = config.GetString("issue-prefix")
|
prefix = config.GetString("issue-prefix")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// auto-detect prefix from first issue in JSONL file
|
||||||
|
if prefix == "" {
|
||||||
|
issueCount, jsonlPath := checkGitForIssues()
|
||||||
|
if issueCount > 0 {
|
||||||
|
firstIssue, err := readFirstIssueFromJSONL(jsonlPath)
|
||||||
|
if firstIssue != nil && err == nil {
|
||||||
|
prefix = utils.ExtractIssuePrefix(firstIssue.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// auto-detect prefix from directory name
|
||||||
if prefix == "" {
|
if prefix == "" {
|
||||||
// Auto-detect from directory name
|
// Auto-detect from directory name
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
@@ -969,3 +985,39 @@ func createConfigYaml(beadsDir string, noDbMode bool) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// readFirstIssueFromJSONL reads the first issue from a JSONL file
|
||||||
|
func readFirstIssueFromJSONL(path string) (*types.Issue, error) {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open JSONL file: %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
lineNum := 0
|
||||||
|
for scanner.Scan() {
|
||||||
|
lineNum++
|
||||||
|
line := scanner.Text()
|
||||||
|
|
||||||
|
// skip empty lines
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var issue types.Issue
|
||||||
|
if err := json.Unmarshal([]byte(line), &issue); err == nil {
|
||||||
|
return &issue, nil
|
||||||
|
} else {
|
||||||
|
// Skip malformed lines with warning
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: skipping malformed JSONL line %d: %v\n", lineNum, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading JSONL file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -788,3 +788,90 @@ func TestInitMergeDriverAutoConfiguration(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestReadFirstIssueFromJSONL_ValidFile verifies reading first issue from valid JSONL
|
||||||
|
func TestReadFirstIssueFromJSONL_ValidFile(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
jsonlPath := filepath.Join(tempDir, "test.jsonl")
|
||||||
|
|
||||||
|
// Create test JSONL file with multiple issues
|
||||||
|
content := `{"id":"bd-1","title":"First Issue","description":"First test"}
|
||||||
|
{"id":"bd-2","title":"Second Issue","description":"Second test"}
|
||||||
|
{"id":"bd-3","title":"Third Issue","description":"Third test"}
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(jsonlPath, []byte(content), 0o600); err != nil {
|
||||||
|
t.Fatalf("Failed to write test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
issue, err := readFirstIssueFromJSONL(jsonlPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("readFirstIssueFromJSONL failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if issue == nil {
|
||||||
|
t.Fatal("Expected non-nil issue, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify we got the FIRST issue
|
||||||
|
if issue.ID != "bd-1" {
|
||||||
|
t.Errorf("Expected ID 'bd-1', got '%s'", issue.ID)
|
||||||
|
}
|
||||||
|
if issue.Title != "First Issue" {
|
||||||
|
t.Errorf("Expected title 'First Issue', got '%s'", issue.Title)
|
||||||
|
}
|
||||||
|
if issue.Description != "First test" {
|
||||||
|
t.Errorf("Expected description 'First test', got '%s'", issue.Description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestReadFirstIssueFromJSONL_EmptyLines verifies skipping empty lines
|
||||||
|
func TestReadFirstIssueFromJSONL_EmptyLines(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
jsonlPath := filepath.Join(tempDir, "test.jsonl")
|
||||||
|
|
||||||
|
// Create JSONL with empty lines before first valid issue
|
||||||
|
content := `
|
||||||
|
|
||||||
|
{"id":"bd-1","title":"First Valid Issue"}
|
||||||
|
{"id":"bd-2","title":"Second Issue"}
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(jsonlPath, []byte(content), 0o600); err != nil {
|
||||||
|
t.Fatalf("Failed to write test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
issue, err := readFirstIssueFromJSONL(jsonlPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("readFirstIssueFromJSONL failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if issue == nil {
|
||||||
|
t.Fatal("Expected non-nil issue, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if issue.ID != "bd-1" {
|
||||||
|
t.Errorf("Expected ID 'bd-1', got '%s'", issue.ID)
|
||||||
|
}
|
||||||
|
if issue.Title != "First Valid Issue" {
|
||||||
|
t.Errorf("Expected title 'First Valid Issue', got '%s'", issue.Title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestReadFirstIssueFromJSONL_EmptyFile verifies handling of empty file
|
||||||
|
func TestReadFirstIssueFromJSONL_EmptyFile(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
jsonlPath := filepath.Join(tempDir, "empty.jsonl")
|
||||||
|
|
||||||
|
// Create empty file
|
||||||
|
if err := os.WriteFile(jsonlPath, []byte(""), 0o600); err != nil {
|
||||||
|
t.Fatalf("Failed to write test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
issue, err := readFirstIssueFromJSONL(jsonlPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("readFirstIssueFromJSONL should not error on empty file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if issue != nil {
|
||||||
|
t.Errorf("Expected nil issue for empty file, got %+v", issue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user