refactor: Split large cmd/bd files to meet 800-line limit (bd-xtf5)
Split 6 files exceeding 800 lines by extracting cohesive function groups: - show.go (1592→578): extracted show_thread.go, close.go, edit.go, update.go - doctor.go (1295→690): extracted doctor_fix.go, doctor_health.go, doctor_pollution.go - sync.go (1201→749): extracted sync_git.go - compact.go (1199→775): extracted compact_tombstone.go, compact_rpc.go - linear.go (1190→641): extracted linear_sync.go, linear_conflict.go - main.go (1148→800): extracted main_help.go, main_errors.go, main_daemon.go All files now under 800-line acceptance criteria. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
190
cmd/bd/linear_conflict.go
Normal file
190
cmd/bd/linear_conflict.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/steveyegge/beads/internal/linear"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
)
|
||||
|
||||
// detectLinearConflicts finds issues that have been modified both locally and in Linear
|
||||
// since the last sync. This is a more expensive operation as it fetches individual
|
||||
// issue timestamps from Linear.
|
||||
func detectLinearConflicts(ctx context.Context) ([]linear.Conflict, error) {
|
||||
lastSyncStr, _ := store.GetConfig(ctx, "linear.last_sync")
|
||||
if lastSyncStr == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
lastSync, err := time.Parse(time.RFC3339, lastSyncStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid last_sync timestamp: %w", err)
|
||||
}
|
||||
|
||||
config := loadLinearMappingConfig(ctx)
|
||||
|
||||
client, err := getLinearClient(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create Linear client: %w", err)
|
||||
}
|
||||
|
||||
// Get all local issues with Linear external refs
|
||||
allIssues, err := store.SearchIssues(ctx, "", types.IssueFilter{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var conflicts []linear.Conflict
|
||||
|
||||
for _, issue := range allIssues {
|
||||
if issue.ExternalRef == nil || !linear.IsLinearExternalRef(*issue.ExternalRef) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !issue.UpdatedAt.After(lastSync) {
|
||||
continue
|
||||
}
|
||||
|
||||
linearIdentifier := linear.ExtractLinearIdentifier(*issue.ExternalRef)
|
||||
if linearIdentifier == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
linearIssue, err := client.FetchIssueByIdentifier(ctx, linearIdentifier)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to fetch Linear issue %s for conflict check: %v\n",
|
||||
linearIdentifier, err)
|
||||
continue
|
||||
}
|
||||
if linearIssue == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
linearUpdatedAt, err := time.Parse(time.RFC3339, linearIssue.UpdatedAt)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to parse Linear UpdatedAt for %s: %v\n",
|
||||
linearIdentifier, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if !linearUpdatedAt.After(lastSync) {
|
||||
continue
|
||||
}
|
||||
|
||||
localComparable := linear.NormalizeIssueForLinearHash(issue)
|
||||
linearComparable := linear.IssueToBeads(linearIssue, config).Issue.(*types.Issue)
|
||||
if localComparable.ComputeContentHash() == linearComparable.ComputeContentHash() {
|
||||
continue
|
||||
}
|
||||
|
||||
conflicts = append(conflicts, linear.Conflict{
|
||||
IssueID: issue.ID,
|
||||
LocalUpdated: issue.UpdatedAt,
|
||||
LinearUpdated: linearUpdatedAt,
|
||||
LinearExternalRef: *issue.ExternalRef,
|
||||
LinearIdentifier: linearIdentifier,
|
||||
LinearInternalID: linearIssue.ID,
|
||||
})
|
||||
}
|
||||
|
||||
return conflicts, nil
|
||||
}
|
||||
|
||||
// reimportLinearConflicts re-imports conflicting issues from Linear (Linear wins).
|
||||
// For each conflict, fetches the current state from Linear and updates the local copy.
|
||||
func reimportLinearConflicts(ctx context.Context, conflicts []linear.Conflict) error {
|
||||
if len(conflicts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
client, err := getLinearClient(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create Linear client: %w", err)
|
||||
}
|
||||
|
||||
config := loadLinearMappingConfig(ctx)
|
||||
resolved := 0
|
||||
failed := 0
|
||||
|
||||
for _, conflict := range conflicts {
|
||||
linearIssue, err := client.FetchIssueByIdentifier(ctx, conflict.LinearIdentifier)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, " Warning: failed to fetch %s for resolution: %v\n",
|
||||
conflict.LinearIdentifier, err)
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
if linearIssue == nil {
|
||||
fmt.Fprintf(os.Stderr, " Warning: Linear issue %s not found, skipping\n",
|
||||
conflict.LinearIdentifier)
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
|
||||
updates := linear.BuildLinearToLocalUpdates(linearIssue, config)
|
||||
|
||||
err = store.UpdateIssue(ctx, conflict.IssueID, updates, actor)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, " Warning: failed to update local issue %s: %v\n",
|
||||
conflict.IssueID, err)
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf(" Resolved: %s <- %s (Linear wins)\n", conflict.IssueID, conflict.LinearIdentifier)
|
||||
resolved++
|
||||
}
|
||||
|
||||
if failed > 0 {
|
||||
return fmt.Errorf("%d conflict(s) failed to resolve", failed)
|
||||
}
|
||||
|
||||
fmt.Printf(" Resolved %d conflict(s) by keeping Linear version\n", resolved)
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveLinearConflictsByTimestamp resolves conflicts by keeping the newer version.
|
||||
// For each conflict, compares local and Linear UpdatedAt timestamps.
|
||||
// If Linear is newer, re-imports from Linear. If local is newer, push will overwrite.
|
||||
func resolveLinearConflictsByTimestamp(ctx context.Context, conflicts []linear.Conflict) error {
|
||||
if len(conflicts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var linearWins []linear.Conflict
|
||||
var localWins []linear.Conflict
|
||||
|
||||
for _, conflict := range conflicts {
|
||||
if conflict.LinearUpdated.After(conflict.LocalUpdated) {
|
||||
linearWins = append(linearWins, conflict)
|
||||
} else {
|
||||
localWins = append(localWins, conflict)
|
||||
}
|
||||
}
|
||||
|
||||
if len(linearWins) > 0 {
|
||||
fmt.Printf(" %d conflict(s): Linear is newer, will re-import\n", len(linearWins))
|
||||
}
|
||||
if len(localWins) > 0 {
|
||||
fmt.Printf(" %d conflict(s): Local is newer, will push to Linear\n", len(localWins))
|
||||
}
|
||||
|
||||
if len(linearWins) > 0 {
|
||||
err := reimportLinearConflicts(ctx, linearWins)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to re-import Linear-wins conflicts: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(localWins) > 0 {
|
||||
for _, conflict := range localWins {
|
||||
fmt.Printf(" Resolved: %s -> %s (local wins, will push)\n",
|
||||
conflict.IssueID, conflict.LinearIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user