Files
beads/cmd/bd/linear_conflict.go
Steve Yegge 6c14fd2225 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>
2025-12-28 18:43:09 -08:00

191 lines
5.1 KiB
Go

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
}