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
cmd/entire/cli
api
Mtrail_types.go+4
Mtrail_types_test.go+4/-1
trail
Mtrail.go+1
Mtrail_cmd.go+256/-97
Mtrail_cmd_test.go+122
Mtrail_review_cmd.go+86/-3
Mtrail_review_cmd_test.go+40
Mtrail_watch_cmd.go+1/-1
Mtrail_watch_cmd_test.go+6/-3
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
``