Merge branch 'main' into subtle-ux-improvements
Resolved modify/delete conflict: cmd/bd/mail.go was deleted on main (mail functionality moved to Gas Town), accepting deletion.
This commit is contained in:
@@ -12,18 +12,16 @@ import (
|
||||
|
||||
// Event types
|
||||
const (
|
||||
EventCreate = "create"
|
||||
EventUpdate = "update"
|
||||
EventClose = "close"
|
||||
EventMessage = "message"
|
||||
EventCreate = "create"
|
||||
EventUpdate = "update"
|
||||
EventClose = "close"
|
||||
)
|
||||
|
||||
// Hook file names
|
||||
const (
|
||||
HookOnCreate = "on_create"
|
||||
HookOnUpdate = "on_update"
|
||||
HookOnClose = "on_close"
|
||||
HookOnMessage = "on_message"
|
||||
HookOnCreate = "on_create"
|
||||
HookOnUpdate = "on_update"
|
||||
HookOnClose = "on_close"
|
||||
)
|
||||
|
||||
// Runner handles hook execution
|
||||
@@ -120,8 +118,6 @@ func eventToHook(event string) string {
|
||||
return HookOnUpdate
|
||||
case EventClose:
|
||||
return HookOnClose
|
||||
case EventMessage:
|
||||
return HookOnMessage
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -44,7 +44,6 @@ func TestEventToHook(t *testing.T) {
|
||||
{EventCreate, HookOnCreate},
|
||||
{EventUpdate, HookOnUpdate},
|
||||
{EventClose, HookOnClose},
|
||||
{EventMessage, HookOnMessage},
|
||||
{"unknown", ""},
|
||||
{"", ""},
|
||||
}
|
||||
@@ -182,7 +181,7 @@ echo "$1 $2" > ` + outputFile
|
||||
|
||||
func TestRunSync_ReceivesJSON(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
hookPath := filepath.Join(tmpDir, HookOnMessage)
|
||||
hookPath := filepath.Join(tmpDir, HookOnCreate)
|
||||
outputFile := filepath.Join(tmpDir, "stdin.txt")
|
||||
|
||||
// Create a hook that captures stdin
|
||||
@@ -194,13 +193,12 @@ cat > ` + outputFile
|
||||
|
||||
runner := NewRunner(tmpDir)
|
||||
issue := &types.Issue{
|
||||
ID: "bd-msg",
|
||||
Title: "Test Message",
|
||||
Sender: "alice",
|
||||
ID: "bd-test",
|
||||
Title: "Test Issue",
|
||||
Assignee: "bob",
|
||||
}
|
||||
|
||||
err := runner.RunSync(EventMessage, issue)
|
||||
err := runner.RunSync(EventCreate, issue)
|
||||
if err != nil {
|
||||
t.Errorf("RunSync returned error: %v", err)
|
||||
}
|
||||
@@ -380,7 +378,6 @@ func TestAllHookEvents(t *testing.T) {
|
||||
{EventCreate, HookOnCreate},
|
||||
{EventUpdate, HookOnUpdate},
|
||||
{EventClose, HookOnClose},
|
||||
{EventMessage, HookOnMessage},
|
||||
}
|
||||
|
||||
for _, e := range events {
|
||||
|
||||
@@ -554,6 +554,8 @@ func upsertIssues(ctx context.Context, sqliteStore *sqlite.SQLiteStorage, issues
|
||||
updates["acceptance_criteria"] = incoming.AcceptanceCriteria
|
||||
updates["notes"] = incoming.Notes
|
||||
updates["closed_at"] = incoming.ClosedAt
|
||||
// Pinned field (bd-7h5)
|
||||
updates["pinned"] = incoming.Pinned
|
||||
|
||||
if incoming.Assignee != "" {
|
||||
updates["assignee"] = incoming.Assignee
|
||||
@@ -647,6 +649,8 @@ func upsertIssues(ctx context.Context, sqliteStore *sqlite.SQLiteStorage, issues
|
||||
updates["acceptance_criteria"] = incoming.AcceptanceCriteria
|
||||
updates["notes"] = incoming.Notes
|
||||
updates["closed_at"] = incoming.ClosedAt
|
||||
// Pinned field (bd-7h5)
|
||||
updates["pinned"] = incoming.Pinned
|
||||
|
||||
if incoming.Assignee != "" {
|
||||
updates["assignee"] = incoming.Assignee
|
||||
|
||||
@@ -112,6 +112,15 @@ func (fc *fieldComparator) equalPriority(existing int, newVal interface{}) bool
|
||||
return ok && int64(existing) == newPriority
|
||||
}
|
||||
|
||||
func (fc *fieldComparator) equalBool(existingVal bool, newVal interface{}) bool {
|
||||
switch t := newVal.(type) {
|
||||
case bool:
|
||||
return existingVal == t
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (fc *fieldComparator) checkFieldChanged(key string, existing *types.Issue, newVal interface{}) bool {
|
||||
switch key {
|
||||
case "title":
|
||||
@@ -134,6 +143,8 @@ func (fc *fieldComparator) checkFieldChanged(key string, existing *types.Issue,
|
||||
return !fc.equalStr(existing.Assignee, newVal)
|
||||
case "external_ref":
|
||||
return !fc.equalPtrStr(existing.ExternalRef, newVal)
|
||||
case "pinned":
|
||||
return !fc.equalBool(existing.Pinned, newVal)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ func (s *SQLiteStorage) GetStatistics(ctx context.Context) (*types.Statistics, e
|
||||
COALESCE(SUM(CASE WHEN status = 'closed' THEN 1 ELSE 0 END), 0) as closed,
|
||||
COALESCE(SUM(CASE WHEN status = 'deferred' THEN 1 ELSE 0 END), 0) as deferred,
|
||||
COALESCE(SUM(CASE WHEN status = 'tombstone' THEN 1 ELSE 0 END), 0) as tombstone,
|
||||
COALESCE(SUM(CASE WHEN status = 'pinned' THEN 1 ELSE 0 END), 0) as pinned
|
||||
COALESCE(SUM(CASE WHEN pinned = 1 THEN 1 ELSE 0 END), 0) as pinned
|
||||
FROM issues
|
||||
`).Scan(&stats.TotalIssues, &stats.OpenIssues, &stats.InProgressIssues, &stats.ClosedIssues, &stats.DeferredIssues, &stats.TombstoneIssues, &stats.PinnedIssues)
|
||||
if err != nil {
|
||||
|
||||
@@ -558,6 +558,8 @@ var allowedUpdateFields = map[string]bool{
|
||||
// Messaging fields (bd-kwro)
|
||||
"sender": true,
|
||||
"ephemeral": true,
|
||||
// Pinned field (bd-7h5)
|
||||
"pinned": true,
|
||||
// NOTE: replies_to, relates_to, duplicate_of, superseded_by removed per Decision 004
|
||||
// Use AddDependency() to create graph edges instead
|
||||
}
|
||||
|
||||
@@ -69,6 +69,23 @@ func FindMoleculesJSONLInDir(dbDir string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// ResolveForWrite returns the path to write to, resolving symlinks.
|
||||
// If path is a symlink, returns the resolved target path.
|
||||
// If path doesn't exist, returns path unchanged (new file).
|
||||
func ResolveForWrite(path string) (string, error) {
|
||||
info, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return path, nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
return filepath.EvalSymlinks(path)
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// CanonicalizePath converts a path to its canonical form by:
|
||||
// 1. Converting to absolute path
|
||||
// 2. Resolving symlinks
|
||||
|
||||
@@ -179,3 +179,56 @@ func TestCanonicalizePathSymlink(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveForWrite(t *testing.T) {
|
||||
t.Run("regular file", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
file := filepath.Join(tmpDir, "regular.txt")
|
||||
if err := os.WriteFile(file, []byte("test"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := ResolveForWrite(file)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got != file {
|
||||
t.Errorf("got %q, want %q", got, file)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("symlink", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
target := filepath.Join(tmpDir, "target.txt")
|
||||
if err := os.WriteFile(target, []byte("test"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
link := filepath.Join(tmpDir, "link.txt")
|
||||
if err := os.Symlink(target, link); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := ResolveForWrite(link)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
// Resolve target too - on macOS, /var is symlink to /private/var
|
||||
wantTarget, _ := filepath.EvalSymlinks(target)
|
||||
if got != wantTarget {
|
||||
t.Errorf("got %q, want %q", got, wantTarget)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-existent", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
newFile := filepath.Join(tmpDir, "new.txt")
|
||||
|
||||
got, err := ResolveForWrite(newFile)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got != newFile {
|
||||
t.Errorf("got %q, want %q", got, newFile)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user