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
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -16,6 +18,8 @@ import (
|
||||
"github.com/steveyegge/beads/internal/configfile"
|
||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||
"github.com/steveyegge/beads/internal/syncbranch"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/utils"
|
||||
)
|
||||
|
||||
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 == "" {
|
||||
// Try to get from config file
|
||||
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 == "" {
|
||||
// Auto-detect from directory name
|
||||
cwd, err := os.Getwd()
|
||||
@@ -969,3 +985,39 @@ func createConfigYaml(beadsDir string, noDbMode bool) error {
|
||||
|
||||
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