Fix trail command behavior · Entire

Fix trail command behavior

a5e8ef6→main·

dipree·1mo ago·9 files·+520 added/-105 removed

Sessions

f2ba03f1bc65View transcript

Changes

9

27 unmodified lines

28
29
30
31
32
33
34
19 unmodified lines

54
55
56
57
58
59
60
20 unmodified lines

81
82
83
84
85
86
87
20 unmodified lines

108
109
110
111
112
113
114

27 unmodified lines

Title           string           `json:"title"`
    Body            string           `json:"body"`
    Status          string           `json:"status"`
    Phase           string           `json:"phase,omitempty"`
    Author          *trail.Author    `json:"author"`
    Assignees       []string         `json:"assignees"`
    Labels          []string         `json:"labels"`
19 unmodified lines

Title:     r.Title,
        Body:      r.Body,
        Status:    trail.Status(r.Status),
        Phase:     r.Phase,
        Author:    r.Author,
        Assignees: r.Assignees,
        Labels:    r.Labels,
20 unmodified lines

BranchName string   `json:"branch_name"`
    Base       string   `json:"base,omitempty"`
    Status     string   `json:"status,omitempty"`
    Phase      string   `json:"phase,omitempty"`
    Assignees  []string `json:"assignees,omitempty"`
    Labels     []string `json:"labels,omitempty"`
    Priority   string   `json:"priority,omitempty"`
20 unmodified lines

Branch    *string   `json:"branch,omitempty"`
    Base      *string   `json:"base,omitempty"`
    Status    *string   `json:"status,omitempty"`
    Phase     *string   `json:"phase,omitempty"`
    Title     *string   `json:"title,omitempty"`
    Body      *string   `json:"body,omitempty"`
    Assignees *[]string `json:"assignees,omitempty"`

Mcmd/entire/cli/api/trail_types.go+4

4 unmodified lines

5
6
7
8
8
9
10
11
12
13
14
15

4 unmodified lines

func TestTrailResourceToMetadataUsesID(t *testing.T) {
    t.Parallel()

metadata := (&TrailResource{ID: "trail-db-id", Branch: "feature/x"}).ToMetadata()
    metadata := (&TrailResource{ID: "trail-db-id", Branch: "feature/x", Phase: "has_code"}).ToMetadata()
    if got := metadata.TrailID.String(); got != "trail-db-id" {
        t.Fatalf("metadata TrailID = %q, want stable API id", got)
    }
    if metadata.Phase != "has_code" {
        t.Fatalf("metadata Phase = %q, want has_code", metadata.Phase)
    }
}

Mcmd/entire/cli/api/trail_types_test.go+4/-1

157 unmodified lines

158
159
160
161
162
163
164

157 unmodified lines

Title     string     `json:"title"`
    Body      string     `json:"body"`
    Status    Status     `json:"status"`
    Phase     string     `json:"phase,omitempty"`
    Author    *Author    `json:"author"`
    Assignees []string   `json:"assignees"`
    Labels    []string   `json:"labels"`

Mcmd/entire/cli/trail/trail.go+1

44 unmodified lines

45
46
47
48
49
50
51
48
49
53
50
51
52
53
3 unmodified lines

57
58
59
60
61
62
63
27 unmodified lines

91
92
93
96
97
98
99
100
101
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
105
106
107
108
109
117
118
119
120
121
115
116
117
118
119
120
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
13 unmodified lines

160
161
162
163
164
165
166
167
168
133 unmodified lines

302
303
304
305
306
307
308
309
310
311
9 unmodified lines

321
322
323
324
325
326
327
328
329
118 unmodified lines

448
449
450
451
452
453
454
455
456
457
458
459
460
461
12 unmodified lines

474
475
476
477
478
479
480
481
482
3 unmodified lines

486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
31 unmodified lines

540
541
542
486
543
544
545
546
547
548
549
550
493
551
552
553
554
2 unmodified lines

557
558
559
560
561
562
563
564
565
566
508
567
568
569
570
571
513
514
572
573
574
575
576
577
521
578
579
580
581
5 unmodified lines

587
588
589
533
590
591
592
593
594
538
595
596
597
598
5 unmodified lines

604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
554
625
556
557
558
559
560
561
562
563
626
565
566
567
568
569
570
571
572
573
574
575
627
628
629
630
631
7 unmodified lines

639
640
641
642
643
644
645
15 unmodified lines

661
662
663
610
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
22 unmodified lines

707
708
709
639
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
1 unmodified line

742
743
744
660
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
667
766
767
768
769
770
1 unmodified line

772
773
774
675
775
776
777
778
2 unmodified lines

781
782
783
684
685
784
785
786
787
788
789
790
791
792
11 unmodified lines

804
805
806
807
808
809
810
7 unmodified lines

818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
723
724
725
726
727
728
835
836
837
838
839
840
841
842
843
844
845
846
731
732
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
19 unmodified lines

882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
759
899
900
901
762
763
902
903
904
765
766
905
906
907
768
769
908
909
910
911
912
913
914
772
773
774
915
916
917
918
776
919
920
921
922
5 unmodified lines

928
929
930
788
931
932
933
934
101 unmodified lines

1036
1037
1038
896
897
898
899
900
901
902
903
904
905
906
907
1039
1040
1041
1042
1043
1044
1045
1046
1047
909
910
911
912
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
914
915
916
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078

44 unmodified lines

Hidden: true,
        Args:   cobra.NoArgs,
        Long: `Trails are branch-centric work tracking abstractions. They describe the
"why" and "what" of your work, while checkpoints capture the "how" and "when".

Running 'entire trail' without a subcommand shows the trail for the current
branch, or lists recent trails if no trail exists for the current branch.`,
"why" and "what" of your work, while checkpoints capture the "how" and "when".`,
        RunE: func(cmd *cobra.Command, _ []string) error {
            return runTrailShow(cmd.Context(), cmd.OutOrStdout(), cmd.ErrOrStderr(), insecureHTTPAuth)
            return cmd.Help()
        },
    }

3 unmodified lines

panic(fmt.Sprintf("hide insecure-http-auth flag: %v", err))
    }

cmd.AddCommand(newTrailShowCmd())
    cmd.AddCommand(newTrailListCmd())
    cmd.AddCommand(newTrailCreateCmd())
    cmd.AddCommand(newTrailUpdateCmd())
27 unmodified lines

}

// runTrailShow shows the trail for the current branch, or falls through to list.
func runTrailShow(ctx context.Context, w, errW io.Writer, insecureHTTP bool) error {
    listOpts := defaultTrailListOptions(insecureHTTP)
    listStatusFilters, err := validateTrailListOptions(listOpts)
    if err != nil {
        return err
}
func newTrailShowCmd() *cobra.Command {
    cmd := &cobra.Command{
        Use:   "show [<trail>]",
        Short: "Show a trail",
        Long: `Show a trail.

If <trail> is omitted, shows the trail for the current branch. Otherwise,
<trail> may be a trail number, id, or branch in the current repo.`,
        Args: cobra.MaximumNArgs(1),
        RunE: func(cmd *cobra.Command, args []string) error {
            selector := ""
            if len(args) == 1 {
                selector = args[0]
            }
            return runTrailShow(cmd.Context(), cmd.OutOrStdout(), cmd.ErrOrStderr(), trailInsecureHTTP(cmd), selector)
        },
    }
    return cmd
}

// runTrailShow shows one trail, defaulting to the current branch's trail.
func runTrailShow(ctx context.Context, w, errW io.Writer, insecureHTTP bool, selector string) error {
    return runAuthenticatedDataAPI(ctx, errW, insecureHTTP, func(ctx context.Context, client *api.Client) error {
        branch, err := GetCurrentBranch(ctx)
        if err != nil {
            return runTrailListAllWithClient(ctx, w, client, listOpts, listStatusFilters)
        }

forge, owner, repo, err := resolveTrailRemote(ctx)
        if err != nil {
            return err
        }

found, err := findTrailByBranch(ctx, client, forge, owner, repo, branch)
        if err != nil {
            return err
        }
        if found == nil {
            return runTrailListAllWithClient(ctx, w, client, listOpts, listStatusFilters)
        }
        selector = strings.TrimSpace(selector)
        var found *api.TrailResource
        if selector == "" {
            branch, err := GetCurrentBranch(ctx)
            if err != nil {
                return fmt.Errorf("no trail selector given and current branch is unknown: %w\nhint: run 'entire trail list --status any' or pass a trail number, id, or branch", err)
            }
            found, err = findTrailByBranch(ctx, client, forge, owner, repo, branch)
            if err != nil {
                return err
            }
            if found == nil {
                return fmt.Errorf("no trail found for current branch %q\nhint: run 'entire trail create' or 'entire trail list --status any'", branch)
            }
        } else {
            found, err = findTrailBySelector(ctx, client, forge, owner, repo, selector)
            if err != nil {
                return err
            }
            if found == nil {
                return fmt.Errorf("no trail %q found in %s/%s/%s (run 'entire trail list --status any')", selector, forge, owner, repo)
            }
        }
        
        printTrailDetails(w, found.ToMetadata())
13 unmodified lines

fmt.Fprintf(w, "  Base:    %s\n", m.Base)
        fmt.Fprintf(w, "  Status:  %s\n", m.Status)
        fmt.Fprintf(w, "  Author:  %s\n", m.AuthorLogin())
        if m.Phase != "" {
            fmt.Fprintf(w, "  Phase:   %s\n", trailPhaseDisplay(m.Phase))
        }
        if m.Body != "" {
            fmt.Fprintf(w, "  Body:    %s\n", m.Body)
        }
133 unmodified lines

// endpoint. Empty statusFilters (--status any) omits the status param so the
// server returns all statuses; the limit is capped at the server maximum.
func trailListQuery(statusFilters []trail.Status, author string, limit int) string {
    return trailListQueryWithOffset(statusFilters, author, limit, 0)
}

func trailListQueryWithOffset(statusFilters []trail.Status, author string, limit, offset int) string {
    q := url.Values{}
    if len(statusFilters) > 0 {
        parts := make([]string, len(statusFilters))
9 unmodified lines

limit = trailListServerMaxLimit
    }
    q.Set("limit", strconv.Itoa(limit))
    if offset > 0 {
        q.Set("offset", strconv.Itoa(offset))
    }
    return "?" + q.Encode()
}

118 unmodified lines

// tabwriter aligns by display columns instead of bytes, so multi-byte
// branch names or logins don't throw off the table.
tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0)
showPhase := trailListHasPhase(trails)
columns := []string{"NUM", "BRANCH", "TITLE"}
if showStatus {
    columns = append(columns, "STATUS")
}
if showPhase {
    columns = append(columns, "PHASE")
}
if showAuthor {
    columns = append(columns, "AUTHOR")
}
12 unmodified lines

if showStatus {
        fields = append(fields, trailStatusDisplay(t.Status))
    }
    if showPhase {
        fields = append(fields, trailPhaseDisplay(t.Phase))
    }
    if showAuthor {
        fields = append(fields, t.AuthorLogin())
    }
3 unmodified lines

_ = tw.Flush()
}

func trailListHasPhase(trails []*trail.Metadata) bool {
    for _, t := range trails {
        if t != nil && strings.TrimSpace(t.Phase) != "" {
            return true
        }
    }
    return false
}

func trailPhaseDisplay(phase string) string {
    phase = strings.TrimSpace(phase)
    if phase == "" {
        return "-"
    }
    return strings.ReplaceAll(phase, "_", " ")
}

func trailStatusListDisplay(statuses []trail.Status) string {
    parts := make([]string, len(statuses))
    for i, status := range statuses {
31 unmodified lines

}

func newTrailCreateCmd() *cobra.Command {
    var title, body, base, branch, status string
    var title, body, base, branch, status, phase string
    var checkout bool

cmd := &cobra.Command{
        Use:   "create",
        Short: "Create a trail for the current or a new branch",
        Args:  cobra.NoArgs,
        RunE: func(cmd *cobra.Command, _ []string) error {
            return runTrailCreate(cmd, title, body, base, branch, status, checkout)
            return runTrailCreate(cmd, title, body, base, branch, status, phase, checkout)
        },
    }

cmd.Flags().StringVar(&base, "base", "", "Base branch (defaults to detected default branch)")
    cmd.Flags().StringVar(&branch, "branch", "", "Branch for the trail (defaults to current branch)")
    cmd.Flags().StringVar(&status, "status", "", "Initial status (defaults to draft)")
    cmd.Flags().StringVar(&phase, "phase", "", "Initial trail phase (for example, whether the trail has code yet)")
    cmd.Flags().BoolVar(&checkout, "checkout", false, "Check out the branch after creating it")

return cmd
}

//nolint:cyclop // sequential steps for creating a trail — splitting would obscure the flow
func runTrailCreate(cmd *cobra.Command, title, body, base, branch, statusStr string, checkout bool) error {
func runTrailCreate(cmd *cobra.Command, title, body, base, branch, statusStr, phase string, checkout bool) error {
    ctx := cmd.Context()
    w := cmd.OutOrStdout()
    errW := cmd.ErrOrStderr()

// --- Phase 1: Local git operations (no API calls) ---

repo, err := strategy.OpenRepository(ctx)
    if err != nil {
        return fmt.Errorf("failed to open repository: %w", err)
    }
    defer repo.Close()

// Determine base branch
    // Determine base branch.
    if base == "" {
        base = strategy.GetDefaultBranchName(repo)
        if base == ""
    5 unmodified lines

interactive := !cmd.Flags().Changed("title") && !cmd.Flags().Changed("branch")

if interactive {
        // Interactive flow: title → body → branch (derived) → status
        // Interactive flow: title → body → branch (derived) → status.
        if err := runTrailCreateInteractive(&title, &body, &branch, &statusStr); err != nil {
            return handleFormCancellation(w, "Trail creation", err)
        }
    } else {
        // Non-interactive: derive missing values from provided flags
        // Non-interactive: derive missing values from provided flags.
        if branch == "" {
            if cmd.Flags().Changed("title") {
                branch = slugifyTitle(title)
    5 unmodified lines

title = trail.HumanizeBranchName(branch)
    }
    }
    title = strings.TrimSpace(title)
    base = strings.TrimSpace(base)
    branch = strings.TrimSpace(branch)
    statusStr = strings.TrimSpace(statusStr)
    phase = strings.TrimSpace(phase)
    if title == "" {
        return errors.New("trail title is required")
    }
    if branch == "" {
        return errors.New("branch name is required")
    }
    if statusStr == "" {
        statusStr = string(trail.StatusDraft)
    }
    if status := trail.Status(statusStr); !status.IsValid() {
        return fmt.Errorf("invalid status %q: valid values are %s", statusStr, formatValidStatuses())
    }

// Create the local branch if it doesn't exist
    needsCreation := branchNeedsCreation(repo, branch)
    if needsCreation {
        if err := createBranch(repo, branch); err != nil {
            return fmt.Errorf("failed to create branch %q: %w", branch, err)
        }
        fmt.Fprintf(w, "Created branch %s\n", branch)
    } else if currentBranch != branch {
        fmt.Fprintf(w, "Note: trail will be created for branch %q (not the current branch)\n", branch)
    }

// Push the branch so the API can reference it
    if needsCreation {
        if err := pushBranchToOrigin(branch); err != nil {
            fmt.Fprintf(errW, "Warning: failed to push branch: %v\n", err)
        } else {
            fmt.Fprintf(w, "Pushed branch %s to origin\n", branch)
        }
    }

// --- Phase 2: API operations ---

// Create the trail remotely before mutating the local worktree. This keeps
    // auth/API failures from leaving behind local branches or attempted pushes.
    var createResp api.TrailCreateResponse
    if err := runAuthenticatedDataAPI(ctx, cmd.ErrOrStderr(), trailInsecureHTTP(cmd), func(ctx context.Context, client *api.Client) error {
        forge, owner, repoName, err := resolveTrailRemote(ctx)
7 unmodified lines

BranchName: branch,
            Base:       base,
            Status:     statusStr,
            Phase:      phase,
        },

resp, err := client.Post(ctx, trailsBasePath(forge, owner, repoName), createReq)
15 unmodified lines

fmt.Fprintf(w, "Created trail %q for branch %s (ID: %s)\n", createResp.Trail.Title, createResp.Trail.Branch, createResp.Trail.ID)

// --- Phase 3: Post-creation local operations ---
    // Post-creation local operations. These are best-effort conveniences after
    // the source of truth (the API trail) has been created successfully.
    if needsCreation {
        if err := createBranch(repo, branch); err != nil {
            return fmt.Errorf("failed to create branch %q: %w", branch, err)
        }
        fmt.Fprintf(w, "Created branch %s\n", branch)
    } else if currentBranch != branch {
        fmt.Fprintf(w, "Note: trail was created for branch %q (not the current branch)\n", branch)
    }

if needsCreation {
        if err := pushBranchToOrigin(branch); err != nil {
            fmt.Fprintf(errW, "Warning: failed to push branch: %v\n", err)
        } else {
            fmt.Fprintf(w, "Pushed branch %s to origin\n", branch)
        }
    }

if needsCreation && currentBranch != branch {
        shouldCheckout := checkout
22 unmodified lines

}

func newTrailUpdateCmd() *cobra.Command {
    var statusStr, title, body, branch string
    var statusStr, title, body, branch, phase string
    var labelAdd, labelRemove []string

cmd := &cobra.Command{
        Use:   "update",
        Short: "Update trail metadata",
        Args:  cobra.NoArgs,
        RunE: func(cmd *cobra.Command, _ []string) error {
            return runTrailUpdate(cmd.Context(), cmd.OutOrStdout(), cmd.ErrOrStderr(), trailInsecureHTTP(cmd), statusStr, title, body, branch, labelAdd, labelRemove)
            return runTrailUpdate(cmd.Context(), cmd.OutOrStdout(), cmd.ErrOrStderr(), trailInsecureHTTP(cmd), trailUpdateInputs{
                Status:        statusStr,
                StatusChanged: cmd.Flags().Changed("status"),
                Title:         title,
                TitleChanged:  cmd.Flags().Changed("title"),
                Body:          body,
                BodyChanged:   cmd.Flags().Changed("body"),
                Phase:         phase,
                PhaseChanged:  cmd.Flags().Changed("phase"),
                Branch:        branch,
                LabelAdd:      labelAdd,
                LabelRemove:   labelRemove,
            })
        },
    }

cmd.Flags().StringVar(&statusStr, "status", "", "Update status")
    cmd.Flags().StringVar(&title, "title", "", "Update title")
    cmd.Flags().StringVar(&body, "body", "", "Update body")
    cmd.Flags().StringVar(&phase, "phase", "", "Update phase (for example, whether the trail has code yet)")
    cmd.Flags().StringVar(&branch, "branch", "", "Branch to update trail for (defaults to current)")
    cmd.Flags().StringSliceVar(&labelAdd, "add-label", nil, "Add label(s)")
    cmd.Flags().StringSliceVar(&labelRemove, "remove-label", nil, "Remove label(s)")
1 unmodified line

return cmd
}

func runTrailUpdate(ctx context.Context, w, errW io.Writer, insecureHTTP bool, statusStr, title, body, branch string, labelAdd, labelRemove []string) error {
type trailUpdateInputs struct {
    Status        string
    StatusChanged bool
    Title         string
    TitleChanged  bool
    Body          string
    BodyChanged   bool
    Phase         string
    PhaseChanged  bool
    Branch        string
    LabelAdd      []string
    LabelRemove   []string
}

func runTrailUpdate(ctx context.Context, w, errW io.Writer, insecureHTTP bool, inputs trailUpdateInputs) error {
    return runAuthenticatedDataAPI(ctx, errW, insecureHTTP, func(ctx context.Context, client *api.Client) error {
        forge, owner, repoName, err := resolveTrailRemote(ctx)
        if err != nil {
            return err
        }

// Determine branch
        // Determine branch.
        branch := inputs.Branch
        if branch == "" {
            branch, err = GetCurrentBranch(ctx)
            if err != nil {
1 unmodified line

}
        }

// Find the trail by branch
        // Find the trail by branch.
        found, err := findTrailByBranch(ctx, client, forge, owner, repoName, branch)
        if err != nil {
            return err
        }
        if found == nil {
            return fmt.Errorf("no trail found for branch %q", branch)
        }

// Interactive mode when no flags are provided
        noFlags := statusStr == "" && title == "" && body == "" && labelAdd == nil && labelRemove == nil
        // Interactive mode when no update flags are provided.
        statusStr := inputs.Status
        title := inputs.Title
        body := inputs.Body
        phase := inputs.Phase
        noFlags := !inputs.StatusChanged && !inputs.TitleChanged && !inputs.BodyChanged && !inputs.PhaseChanged && inputs.LabelAdd == nil && inputs.LabelRemove == nil
        if noFlags {
            metadata := found.ToMetadata()
            // Build status options with current value as default.
11 unmodified lines

statusStr = string(metadata.Status)
            title = metadata.Title
            body = metadata.Body
            phase = metadata.Phase

form := NewAccessibleForm(
                huh.NewGroup(

huh.NewText().
                        Title("Body").
                        Value(&body),
                        huh.NewInput().
                            Title("Phase").
                            Value(&phase),
                    ),
                )
            if formErr := form.Run(); formErr != nil {
                return handleFormCancellation(w, "Trail update", formErr)
            }
            inputs.StatusChanged = true
            inputs.TitleChanged = true
            inputs.BodyChanged = true
            inputs.PhaseChanged = true
    }

// Validate status if provided
        if statusStr != "" {
            status := trail.Status(statusStr)
            if !status.IsValid() {
                return fmt.Errorf("invalid status %q: valid values are %s", statusStr, formatValidStatuses())
            }
        statusStr = strings.TrimSpace(statusStr)
        title = strings.TrimSpace(title)
        phase = strings.TrimSpace(phase)
        if err := validateTrailUpdateFields(trailUpdateInputs{
            Status:        statusStr,
            StatusChanged: inputs.StatusChanged,
            Title:         title,
            TitleChanged:  inputs.TitleChanged,
        }); err != nil {
            return err
        }

// Build update request with only changed fields
        updateReq := buildTrailUpdateRequest(found, statusStr, title, body, labelAdd, labelRemove)
        // Build update request with only changed fields.
        updateReq := buildTrailUpdateRequest(found, trailUpdateInputs{
            Status:        statusStr,
            StatusChanged: inputs.StatusChanged,
            Title:         title,
            TitleChanged:  inputs.TitleChanged,
            Body:          body,
            BodyChanged:   inputs.BodyChanged,
            Phase:         strings.TrimSpace(phase),
            PhaseChanged:  inputs.PhaseChanged,
            LabelAdd:      inputs.LabelAdd,
            LabelRemove:   inputs.LabelRemove,
        })

// The single-trail endpoint is keyed by trail number, not id; the server
        // rejects an id here with "Invalid trail number format".
19 unmodified lines

})
}

func validateTrailUpdateFields(inputs trailUpdateInputs) error {
    if inputs.TitleChanged && strings.TrimSpace(inputs.Title) == "" {
        return errors.New("trail title is required")
    }
    if inputs.StatusChanged {
        status := trail.Status(strings.TrimSpace(inputs.Status))
        if !status.IsValid() {
            return fmt.Errorf("invalid status %q: valid values are %s", inputs.Status, formatValidStatuses())
        }
    }
    return nil
}

// buildTrailUpdateRequest constructs a PATCH request body from the current trail and the requested changes.
func buildTrailUpdateRequest(current *api.TrailResource, statusStr, title, body string, labelAdd, labelRemove []string) api.TrailUpdateRequest {
func buildTrailUpdateRequest(current *api.TrailResource, inputs trailUpdateInputs) api.TrailUpdateRequest {
    var req api.TrailUpdateRequest

if statusStr != "" {
        req.Status = &statusStr
    if inputs.StatusChanged {
        req.Status = &inputs.Status
    }
    if title != "" {
        req.Title = &title
    if inputs.TitleChanged {
        req.Title = &inputs.Title
    }
    if body != "" {
        req.Body = &body
    if inputs.BodyChanged {
        req.Body = &inputs.Body
    }
    if inputs.PhaseChanged {
        req.Phase = &inputs.Phase
    }

// Handle label changes: merge adds, remove removes
    if len(labelAdd) > 0 || len(labelRemove) > 0 {
        labels := make([]string, 0, len(current.Labels)+len(labelAdd))
    // Handle label changes: merge adds, remove removes.
    if len(inputs.LabelAdd) > 0 || len(inputs.LabelRemove) > 0 {
        labels := make([]string, 0, len(current.Labels)+len(inputs.LabelAdd))
        labels = append(labels, current.Labels...)
        for _, l := range labelAdd {
        for _, l := range inputs.LabelAdd {
            found := false
            for _, existing := range labels {
                if existing == l {
5 unmodified lines

labels = append(labels, l)
            }
        }
        for _, l := range labelRemove {
        for _, l := range inputs.LabelRemove {
            for i, existing := range labels {
                if existing == l {
                    labels = append(labels[:i], labels[i+1:]...)

101 unmodified lines

}

func findTrail(ctx context.Context, client *api.Client, forge, owner, repo string, match func(api.TrailResource) bool) (*api.TrailResource, error) {
    // The list endpoint paginates (default 50 rows); request the server max
    // so lookups don't miss less recently updated trails. Trails beyond the
    // first 200 are still invisible here — fixing that needs a server-side
    // branch filter or the by-number detail endpoint.
    resp, err := client.Get(ctx, trailsBasePath(forge, owner, repo)+trailListQuery(nil, "", trailListServerMaxLimit))
    if err != nil {
        return nil, fmt.Errorf("list trails: %w", err)
    }
    defer resp.Body.Close()
    if err := checkTrailResponse(resp); err != nil {
        return nil, err
    }
    // The list endpoint paginates; walk all pages so branch/number/id lookups do
    // not silently miss older trails beyond the first server-max page.
offset := 0
for {
    resp, err := client.Get(ctx, trailsBasePath(forge, owner, repo)+trailListQueryWithOffset(nil, "", trailListServerMaxLimit, offset))
    if err != nil {
        return nil, fmt.Errorf("list trails: %w", err)
    }

var listResp api.TrailListResponse
    if err := api.DecodeJSON(resp, &listResp); err != nil {
        return nil, fmt.Errorf("decode trail list: %w", err)
    }
    var listResp api.TrailListResponse
    decodeErr := func() error {
        defer resp.Body.Close()
        if err := checkTrailResponse(resp); err != nil {
            return err
        }
        if err := api.DecodeJSON(resp, &listResp); err != nil {
            return fmt.Errorf("decode trail list: %w", err)
        }
        return nil
    }()
    if decodeErr != nil {
        return nil, decodeErr
    }

for i := range listResp.Trails {
        if match(listResp.Trails[i]) {
            return &listResp.Trails[i], nil
        }
    }

pageLen := len(listResp.Trails)
    if pageLen == 0 || pageLen < trailListServerMaxLimit {
        break
    }
    offset += pageLen
    if listResp.Total > 0 && offset >= listResp.Total {
        break
    }
}
return nil, nil //nolint:nilnil // nil, nil means "not found" — callers check both
``