pr: open ready merge requests by default, add --draft and pr readyUnverified
The API opens a draft unless told otherwise, and a draft cannot be merged. pr create now sends draft:false (or true with --draft), pr ready <n> [--undo] flips the state, and list/view render an open draft as "draft". Fixes #2 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgkcT3UPxwxukBKojB5eg4
72b5c57 parent: d68b124 modified
cmd/pr.go +59 -3 | @@ -19,6 +19,8 @@ var ( | ||
| 19 | 19 | prHeadOwner string |
| 20 | 20 | prHeadRepo string |
| 21 | 21 | prMergeMethod string |
| 22 | + prDraft bool | |
| 23 | + prReadyUndo bool | |
| 22 | 24 | ) |
| 23 | 25 | |
| 24 | 26 | func init() { |
| @@ -58,6 +60,15 @@ func init() { | ||
| 58 | 60 | createCmd.Flags().StringVarP(&prBody, "body", "b", "", "description body") |
| 59 | 61 | createCmd.Flags().StringVar(&prHeadOwner, "head-owner", "", "fork owner for a cross-repo MR") |
| 60 | 62 | createCmd.Flags().StringVar(&prHeadRepo, "head-repo", "", "fork name for a cross-repo MR") |
| 63 | + createCmd.Flags().BoolVar(&prDraft, "draft", false, "open as a draft (not mergeable until `pr ready`)") | |
| 64 | + | |
| 65 | + readyCmd := &cobra.Command{ | |
| 66 | + Use: "ready <number>", | |
| 67 | + Short: "Mark a draft merge request ready for review (--undo puts it back in draft)", | |
| 68 | + Args: cobra.ExactArgs(1), | |
| 69 | + RunE: runPRReady, | |
| 70 | + } | |
| 71 | + readyCmd.Flags().BoolVar(&prReadyUndo, "undo", false, "convert back to a draft") | |
| 61 | 72 | |
| 62 | 73 | mergeCmd := &cobra.Command{ |
| 63 | 74 | Use: "merge <number>", |
| @@ -89,7 +100,7 @@ func init() { | ||
| 89 | 100 | } |
| 90 | 101 | commentCmd.Flags().StringVarP(&prBody, "body", "b", "", "comment body (required)") |
| 91 | 102 | |
| 92 | - prCmd.AddCommand(listCmd, viewCmd, createCmd, mergeCmd, closeCmd, reopenCmd, commentCmd) | |
| 103 | + prCmd.AddCommand(listCmd, viewCmd, createCmd, readyCmd, mergeCmd, closeCmd, reopenCmd, commentCmd) | |
| 93 | 104 | rootCmd.AddCommand(prCmd) |
| 94 | 105 | } |
| 95 | 106 | |
| @@ -124,7 +135,7 @@ func runPRList(cmd *cobra.Command, _ []string) error { | ||
| 124 | 135 | tw := newTabw(cmd.OutOrStdout()) |
| 125 | 136 | fmt.Fprintln(tw, "#\tSTATE\tTITLE\tHEAD→BASE\tAUTHOR") |
| 126 | 137 | for _, mr := range page.Items { |
| 127 | - fmt.Fprintf(tw, "%d\t%s\t%s\t%s→%s\t%s\n", mr.Number, mr.State, mr.Title, mr.HeadBranch, mr.BaseBranch, mr.Author) | |
| 138 | + fmt.Fprintf(tw, "%d\t%s\t%s\t%s→%s\t%s\n", mr.Number, prStateLabel(mr), mr.Title, mr.HeadBranch, mr.BaseBranch, mr.Author) | |
| 128 | 139 | } |
| 129 | 140 | tw.Flush() |
| 130 | 141 | printPageFooter(cmd, page.Page) |
| @@ -152,7 +163,7 @@ func runPRView(cmd *cobra.Command, args []string) error { | ||
| 152 | 163 | return printJSON(cmd.OutOrStdout(), mr) |
| 153 | 164 | } |
| 154 | 165 | out := cmd.OutOrStdout() |
| 155 | - fmt.Fprintf(out, "#%d %s [%s]\n", mr.Number, mr.Title, mr.State) | |
| 166 | + fmt.Fprintf(out, "#%d %s [%s]\n", mr.Number, mr.Title, prStateLabel(mr.MergeRequest)) | |
| 156 | 167 | fmt.Fprintf(out, "%s wants to merge %s → %s\n", mr.Author, mr.HeadBranch, mr.BaseBranch) |
| 157 | 168 | if mr.CrossRepo { |
| 158 | 169 | fmt.Fprintf(out, "cross-repo from %s/%s\n", mr.HeadOwner, mr.HeadRepo) |
| @@ -181,6 +192,15 @@ func runPRView(cmd *cobra.Command, args []string) error { | ||
| 181 | 192 | return nil |
| 182 | 193 | } |
| 183 | 194 | |
| 195 | +// prStateLabel renders the state column: an open draft says so, because a draft | |
| 196 | +// is refused by `pr merge` and that is the first thing a reader wants to know. | |
| 197 | +func prStateLabel(mr api.MergeRequest) string { | |
| 198 | + if mr.Draft && mr.State == "open" { | |
| 199 | + return "draft" | |
| 200 | + } | |
| 201 | + return mr.State | |
| 202 | +} | |
| 203 | + | |
| 184 | 204 | func joinSubjects(subs []api.Subject) string { |
| 185 | 205 | out := "" |
| 186 | 206 | for i, s := range subs { |
| @@ -208,6 +228,9 @@ func runPRCreate(cmd *cobra.Command, _ []string) error { | ||
| 208 | 228 | if prBase == "" || prHead == "" || prTitle == "" { |
| 209 | 229 | return fmt.Errorf("--base, --head, and --title are required") |
| 210 | 230 | } |
| 231 | + // The API opens a draft unless told otherwise; like gh, the CLI opens a | |
| 232 | + // ready merge request by default and --draft opts in. | |
| 233 | + draft := prDraft | |
| 211 | 234 | mr, err := client.CreateMergeRequest(cmd.Context(), owner, repo, api.MergeRequestCreate{ |
| 212 | 235 | Base: prBase, |
| 213 | 236 | Head: prHead, |
| @@ -215,6 +238,7 @@ func runPRCreate(cmd *cobra.Command, _ []string) error { | ||
| 215 | 238 | Body: prBody, |
| 216 | 239 | HeadOwner: prHeadOwner, |
| 217 | 240 | HeadRepo: prHeadRepo, |
| 241 | + Draft: &draft, | |
| 218 | 242 | }) |
| 219 | 243 | if err != nil { |
| 220 | 244 | return err |
| @@ -222,10 +246,42 @@ func runPRCreate(cmd *cobra.Command, _ []string) error { | ||
| 222 | 246 | if flagJSON { |
| 223 | 247 | return printJSON(cmd.OutOrStdout(), mr) |
| 224 | 248 | } |
| 249 | + if mr.Draft { | |
| 250 | + fmt.Fprintf(cmd.OutOrStdout(), "Opened draft merge request #%d: %s (mark it ready with `rickub pr ready %d`)\n", mr.Number, mr.Title, mr.Number) | |
| 251 | + return nil | |
| 252 | + } | |
| 225 | 253 | fmt.Fprintf(cmd.OutOrStdout(), "Opened merge request #%d: %s\n", mr.Number, mr.Title) |
| 226 | 254 | return nil |
| 227 | 255 | } |
| 228 | 256 | |
| 257 | +func runPRReady(cmd *cobra.Command, args []string) error { | |
| 258 | + client, err := newClient() | |
| 259 | + if err != nil { | |
| 260 | + return err | |
| 261 | + } | |
| 262 | + owner, repo, err := resolveRepo(prRepo) | |
| 263 | + if err != nil { | |
| 264 | + return err | |
| 265 | + } | |
| 266 | + n, err := prNumber(args[0]) | |
| 267 | + if err != nil { | |
| 268 | + return err | |
| 269 | + } | |
| 270 | + mr, err := client.SetMergeRequestDraft(cmd.Context(), owner, repo, n, prReadyUndo) | |
| 271 | + if err != nil { | |
| 272 | + return err | |
| 273 | + } | |
| 274 | + if flagJSON { | |
| 275 | + return printJSON(cmd.OutOrStdout(), mr) | |
| 276 | + } | |
| 277 | + if prReadyUndo { | |
| 278 | + fmt.Fprintf(cmd.OutOrStdout(), "#%d is a draft again\n", mr.Number) | |
| 279 | + return nil | |
| 280 | + } | |
| 281 | + fmt.Fprintf(cmd.OutOrStdout(), "#%d is ready for review\n", mr.Number) | |
| 282 | + return nil | |
| 283 | +} | |
| 284 | + | |
| 229 | 285 | func runPRMerge(cmd *cobra.Command, args []string) error { |
| 230 | 286 | client, err := newClient() |
| 231 | 287 | if err != nil { |
| @@ -19,6 +19,8 @@ var ( | |||
| 19 | prHeadOwner string | 19 | prHeadOwner string |
| 20 | prHeadRepo string | 20 | prHeadRepo string |
| 21 | prMergeMethod string | 21 | prMergeMethod string |
| 22 | + prDraft bool | ||
| 23 | + prReadyUndo bool | ||
| 22 | ) | 24 | ) |
| 23 | 25 | ||
| 24 | func init() { | 26 | func init() { |
| @@ -58,6 +60,15 @@ func init() { | |||
| 58 | createCmd.Flags().StringVarP(&prBody, "body", "b", "", "description body") | 60 | createCmd.Flags().StringVarP(&prBody, "body", "b", "", "description body") |
| 59 | createCmd.Flags().StringVar(&prHeadOwner, "head-owner", "", "fork owner for a cross-repo MR") | 61 | createCmd.Flags().StringVar(&prHeadOwner, "head-owner", "", "fork owner for a cross-repo MR") |
| 60 | createCmd.Flags().StringVar(&prHeadRepo, "head-repo", "", "fork name for a cross-repo MR") | 62 | createCmd.Flags().StringVar(&prHeadRepo, "head-repo", "", "fork name for a cross-repo MR") |
| 63 | + createCmd.Flags().BoolVar(&prDraft, "draft", false, "open as a draft (not mergeable until `pr ready`)") | ||
| 64 | + | ||
| 65 | + readyCmd := &cobra.Command{ | ||
| 66 | + Use: "ready <number>", | ||
| 67 | + Short: "Mark a draft merge request ready for review (--undo puts it back in draft)", | ||
| 68 | + Args: cobra.ExactArgs(1), | ||
| 69 | + RunE: runPRReady, | ||
| 70 | + } | ||
| 71 | + readyCmd.Flags().BoolVar(&prReadyUndo, "undo", false, "convert back to a draft") | ||
| 61 | 72 | ||
| 62 | mergeCmd := &cobra.Command{ | 73 | mergeCmd := &cobra.Command{ |
| 63 | Use: "merge <number>", | 74 | Use: "merge <number>", |
| @@ -89,7 +100,7 @@ func init() { | |||
| 89 | } | 100 | } |
| 90 | commentCmd.Flags().StringVarP(&prBody, "body", "b", "", "comment body (required)") | 101 | commentCmd.Flags().StringVarP(&prBody, "body", "b", "", "comment body (required)") |
| 91 | 102 | ||
| 92 | - prCmd.AddCommand(listCmd, viewCmd, createCmd, mergeCmd, closeCmd, reopenCmd, commentCmd) | 103 | + prCmd.AddCommand(listCmd, viewCmd, createCmd, readyCmd, mergeCmd, closeCmd, reopenCmd, commentCmd) |
| 93 | rootCmd.AddCommand(prCmd) | 104 | rootCmd.AddCommand(prCmd) |
| 94 | } | 105 | } |
| 95 | 106 | ||
| @@ -124,7 +135,7 @@ func runPRList(cmd *cobra.Command, _ []string) error { | |||
| 124 | tw := newTabw(cmd.OutOrStdout()) | 135 | tw := newTabw(cmd.OutOrStdout()) |
| 125 | fmt.Fprintln(tw, "#\tSTATE\tTITLE\tHEAD→BASE\tAUTHOR") | 136 | fmt.Fprintln(tw, "#\tSTATE\tTITLE\tHEAD→BASE\tAUTHOR") |
| 126 | for _, mr := range page.Items { | 137 | for _, mr := range page.Items { |
| 127 | - fmt.Fprintf(tw, "%d\t%s\t%s\t%s→%s\t%s\n", mr.Number, mr.State, mr.Title, mr.HeadBranch, mr.BaseBranch, mr.Author) | 138 | + fmt.Fprintf(tw, "%d\t%s\t%s\t%s→%s\t%s\n", mr.Number, prStateLabel(mr), mr.Title, mr.HeadBranch, mr.BaseBranch, mr.Author) |
| 128 | } | 139 | } |
| 129 | tw.Flush() | 140 | tw.Flush() |
| 130 | printPageFooter(cmd, page.Page) | 141 | printPageFooter(cmd, page.Page) |
| @@ -152,7 +163,7 @@ func runPRView(cmd *cobra.Command, args []string) error { | |||
| 152 | return printJSON(cmd.OutOrStdout(), mr) | 163 | return printJSON(cmd.OutOrStdout(), mr) |
| 153 | } | 164 | } |
| 154 | out := cmd.OutOrStdout() | 165 | out := cmd.OutOrStdout() |
| 155 | - fmt.Fprintf(out, "#%d %s [%s]\n", mr.Number, mr.Title, mr.State) | 166 | + fmt.Fprintf(out, "#%d %s [%s]\n", mr.Number, mr.Title, prStateLabel(mr.MergeRequest)) |
| 156 | fmt.Fprintf(out, "%s wants to merge %s → %s\n", mr.Author, mr.HeadBranch, mr.BaseBranch) | 167 | fmt.Fprintf(out, "%s wants to merge %s → %s\n", mr.Author, mr.HeadBranch, mr.BaseBranch) |
| 157 | if mr.CrossRepo { | 168 | if mr.CrossRepo { |
| 158 | fmt.Fprintf(out, "cross-repo from %s/%s\n", mr.HeadOwner, mr.HeadRepo) | 169 | fmt.Fprintf(out, "cross-repo from %s/%s\n", mr.HeadOwner, mr.HeadRepo) |
| @@ -181,6 +192,15 @@ func runPRView(cmd *cobra.Command, args []string) error { | |||
| 181 | return nil | 192 | return nil |
| 182 | } | 193 | } |
| 183 | 194 | ||
| 195 | +// prStateLabel renders the state column: an open draft says so, because a draft | ||
| 196 | +// is refused by `pr merge` and that is the first thing a reader wants to know. | ||
| 197 | +func prStateLabel(mr api.MergeRequest) string { | ||
| 198 | + if mr.Draft && mr.State == "open" { | ||
| 199 | + return "draft" | ||
| 200 | + } | ||
| 201 | + return mr.State | ||
| 202 | +} | ||
| 203 | + | ||
| 184 | func joinSubjects(subs []api.Subject) string { | 204 | func joinSubjects(subs []api.Subject) string { |
| 185 | out := "" | 205 | out := "" |
| 186 | for i, s := range subs { | 206 | for i, s := range subs { |
| @@ -208,6 +228,9 @@ func runPRCreate(cmd *cobra.Command, _ []string) error { | |||
| 208 | if prBase == "" || prHead == "" || prTitle == "" { | 228 | if prBase == "" || prHead == "" || prTitle == "" { |
| 209 | return fmt.Errorf("--base, --head, and --title are required") | 229 | return fmt.Errorf("--base, --head, and --title are required") |
| 210 | } | 230 | } |
| 231 | + // The API opens a draft unless told otherwise; like gh, the CLI opens a | ||
| 232 | + // ready merge request by default and --draft opts in. | ||
| 233 | + draft := prDraft | ||
| 211 | mr, err := client.CreateMergeRequest(cmd.Context(), owner, repo, api.MergeRequestCreate{ | 234 | mr, err := client.CreateMergeRequest(cmd.Context(), owner, repo, api.MergeRequestCreate{ |
| 212 | Base: prBase, | 235 | Base: prBase, |
| 213 | Head: prHead, | 236 | Head: prHead, |
| @@ -215,6 +238,7 @@ func runPRCreate(cmd *cobra.Command, _ []string) error { | |||
| 215 | Body: prBody, | 238 | Body: prBody, |
| 216 | HeadOwner: prHeadOwner, | 239 | HeadOwner: prHeadOwner, |
| 217 | HeadRepo: prHeadRepo, | 240 | HeadRepo: prHeadRepo, |
| 241 | + Draft: &draft, | ||
| 218 | }) | 242 | }) |
| 219 | if err != nil { | 243 | if err != nil { |
| 220 | return err | 244 | return err |
| @@ -222,10 +246,42 @@ func runPRCreate(cmd *cobra.Command, _ []string) error { | |||
| 222 | if flagJSON { | 246 | if flagJSON { |
| 223 | return printJSON(cmd.OutOrStdout(), mr) | 247 | return printJSON(cmd.OutOrStdout(), mr) |
| 224 | } | 248 | } |
| 249 | + if mr.Draft { | ||
| 250 | + fmt.Fprintf(cmd.OutOrStdout(), "Opened draft merge request #%d: %s (mark it ready with `rickub pr ready %d`)\n", mr.Number, mr.Title, mr.Number) | ||
| 251 | + return nil | ||
| 252 | + } | ||
| 225 | fmt.Fprintf(cmd.OutOrStdout(), "Opened merge request #%d: %s\n", mr.Number, mr.Title) | 253 | fmt.Fprintf(cmd.OutOrStdout(), "Opened merge request #%d: %s\n", mr.Number, mr.Title) |
| 226 | return nil | 254 | return nil |
| 227 | } | 255 | } |
| 228 | 256 | ||
| 257 | +func runPRReady(cmd *cobra.Command, args []string) error { | ||
| 258 | + client, err := newClient() | ||
| 259 | + if err != nil { | ||
| 260 | + return err | ||
| 261 | + } | ||
| 262 | + owner, repo, err := resolveRepo(prRepo) | ||
| 263 | + if err != nil { | ||
| 264 | + return err | ||
| 265 | + } | ||
| 266 | + n, err := prNumber(args[0]) | ||
| 267 | + if err != nil { | ||
| 268 | + return err | ||
| 269 | + } | ||
| 270 | + mr, err := client.SetMergeRequestDraft(cmd.Context(), owner, repo, n, prReadyUndo) | ||
| 271 | + if err != nil { | ||
| 272 | + return err | ||
| 273 | + } | ||
| 274 | + if flagJSON { | ||
| 275 | + return printJSON(cmd.OutOrStdout(), mr) | ||
| 276 | + } | ||
| 277 | + if prReadyUndo { | ||
| 278 | + fmt.Fprintf(cmd.OutOrStdout(), "#%d is a draft again\n", mr.Number) | ||
| 279 | + return nil | ||
| 280 | + } | ||
| 281 | + fmt.Fprintf(cmd.OutOrStdout(), "#%d is ready for review\n", mr.Number) | ||
| 282 | + return nil | ||
| 283 | +} | ||
| 284 | + | ||
| 229 | func runPRMerge(cmd *cobra.Command, args []string) error { | 285 | func runPRMerge(cmd *cobra.Command, args []string) error { |
| 230 | client, err := newClient() | 286 | client, err := newClient() |
| 231 | if err != nil { | 287 | if err != nil { |
modified
internal/api/endpoints.go +31 -0 | @@ -162,6 +162,14 @@ func (c *Client) ReopenMergeRequest(ctx context.Context, owner, repo string, num | ||
| 162 | 162 | return &mr, err |
| 163 | 163 | } |
| 164 | 164 | |
| 165 | +// SetMergeRequestDraft marks a merge request ready for review (draft=false) | |
| 166 | +// or back to draft (draft=true). | |
| 167 | +func (c *Client) SetMergeRequestDraft(ctx context.Context, owner, repo string, number int, draft bool) (*MergeRequest, error) { | |
| 168 | + var mr MergeRequest | |
| 169 | + err := c.do(ctx, Request{Method: http.MethodPost, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/merge-requests/" + itoa(number) + "/draft", Body: map[string]bool{"draft": draft}}, &mr) | |
| 170 | + return &mr, err | |
| 171 | +} | |
| 172 | + | |
| 165 | 173 | // CommentMergeRequest adds a comment to a merge request. |
| 166 | 174 | func (c *Client) CommentMergeRequest(ctx context.Context, owner, repo string, number int, body string) (*Comment, error) { |
| 167 | 175 | var cm Comment |
| @@ -314,6 +322,29 @@ func (c *Client) ListLabels(ctx context.Context, owner, repo string) ([]Label, e | ||
| 314 | 322 | return l, err |
| 315 | 323 | } |
| 316 | 324 | |
| 325 | +// CreateLabel creates a label; an empty color lets the server pick its default. | |
| 326 | +func (c *Client) CreateLabel(ctx context.Context, owner, repo, name, color string) (*Label, error) { | |
| 327 | + body := map[string]any{"name": name} | |
| 328 | + if color != "" { | |
| 329 | + body["color"] = color | |
| 330 | + } | |
| 331 | + var l Label | |
| 332 | + err := c.do(ctx, Request{Method: http.MethodPost, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/labels", Body: body}, &l) | |
| 333 | + return &l, err | |
| 334 | +} | |
| 335 | + | |
| 336 | +// UpdateLabel patches a label by id; absent fields keep their values. | |
| 337 | +func (c *Client) UpdateLabel(ctx context.Context, owner, repo, id string, in map[string]any) (*Label, error) { | |
| 338 | + var l Label | |
| 339 | + err := c.do(ctx, Request{Method: http.MethodPatch, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/labels/" + seg(id), Body: in}, &l) | |
| 340 | + return &l, err | |
| 341 | +} | |
| 342 | + | |
| 343 | +// DeleteLabel deletes a label by id. | |
| 344 | +func (c *Client) DeleteLabel(ctx context.Context, owner, repo, id string) error { | |
| 345 | + return c.do(ctx, Request{Method: http.MethodDelete, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/labels/" + seg(id)}, nil) | |
| 346 | +} | |
| 347 | + | |
| 317 | 348 | // ListMilestones lists a repo's milestones (state: open default, closed, all). |
| 318 | 349 | func (c *Client) ListMilestones(ctx context.Context, owner, repo, state string) ([]Milestone, error) { |
| 319 | 350 | extra := url.Values{} |
| @@ -162,6 +162,14 @@ func (c *Client) ReopenMergeRequest(ctx context.Context, owner, repo string, num | |||
| 162 | return &mr, err | 162 | return &mr, err |
| 163 | } | 163 | } |
| 164 | 164 | ||
| 165 | +// SetMergeRequestDraft marks a merge request ready for review (draft=false) | ||
| 166 | +// or back to draft (draft=true). | ||
| 167 | +func (c *Client) SetMergeRequestDraft(ctx context.Context, owner, repo string, number int, draft bool) (*MergeRequest, error) { | ||
| 168 | + var mr MergeRequest | ||
| 169 | + err := c.do(ctx, Request{Method: http.MethodPost, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/merge-requests/" + itoa(number) + "/draft", Body: map[string]bool{"draft": draft}}, &mr) | ||
| 170 | + return &mr, err | ||
| 171 | +} | ||
| 172 | + | ||
| 165 | // CommentMergeRequest adds a comment to a merge request. | 173 | // CommentMergeRequest adds a comment to a merge request. |
| 166 | func (c *Client) CommentMergeRequest(ctx context.Context, owner, repo string, number int, body string) (*Comment, error) { | 174 | func (c *Client) CommentMergeRequest(ctx context.Context, owner, repo string, number int, body string) (*Comment, error) { |
| 167 | var cm Comment | 175 | var cm Comment |
| @@ -314,6 +322,29 @@ func (c *Client) ListLabels(ctx context.Context, owner, repo string) ([]Label, e | |||
| 314 | return l, err | 322 | return l, err |
| 315 | } | 323 | } |
| 316 | 324 | ||
| 325 | +// CreateLabel creates a label; an empty color lets the server pick its default. | ||
| 326 | +func (c *Client) CreateLabel(ctx context.Context, owner, repo, name, color string) (*Label, error) { | ||
| 327 | + body := map[string]any{"name": name} | ||
| 328 | + if color != "" { | ||
| 329 | + body["color"] = color | ||
| 330 | + } | ||
| 331 | + var l Label | ||
| 332 | + err := c.do(ctx, Request{Method: http.MethodPost, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/labels", Body: body}, &l) | ||
| 333 | + return &l, err | ||
| 334 | +} | ||
| 335 | + | ||
| 336 | +// UpdateLabel patches a label by id; absent fields keep their values. | ||
| 337 | +func (c *Client) UpdateLabel(ctx context.Context, owner, repo, id string, in map[string]any) (*Label, error) { | ||
| 338 | + var l Label | ||
| 339 | + err := c.do(ctx, Request{Method: http.MethodPatch, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/labels/" + seg(id), Body: in}, &l) | ||
| 340 | + return &l, err | ||
| 341 | +} | ||
| 342 | + | ||
| 343 | +// DeleteLabel deletes a label by id. | ||
| 344 | +func (c *Client) DeleteLabel(ctx context.Context, owner, repo, id string) error { | ||
| 345 | + return c.do(ctx, Request{Method: http.MethodDelete, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/labels/" + seg(id)}, nil) | ||
| 346 | +} | ||
| 347 | + | ||
| 317 | // ListMilestones lists a repo's milestones (state: open default, closed, all). | 348 | // ListMilestones lists a repo's milestones (state: open default, closed, all). |
| 318 | func (c *Client) ListMilestones(ctx context.Context, owner, repo, state string) ([]Milestone, error) { | 349 | func (c *Client) ListMilestones(ctx context.Context, owner, repo, state string) ([]Milestone, error) { |
| 319 | extra := url.Values{} | 350 | extra := url.Values{} |
modified
internal/api/models.go +4 -0 | @@ -56,6 +56,7 @@ type MergeRequest struct { | ||
| 56 | 56 | Title string `json:"title"` |
| 57 | 57 | Body string `json:"body"` |
| 58 | 58 | State string `json:"state"` |
| 59 | + Draft bool `json:"draft"` | |
| 59 | 60 | Author string `json:"author"` |
| 60 | 61 | BaseBranch string `json:"base_branch"` |
| 61 | 62 | HeadBranch string `json:"head_branch"` |
| @@ -85,6 +86,9 @@ type MergeRequestCreate struct { | ||
| 85 | 86 | Body string `json:"body,omitempty"` |
| 86 | 87 | HeadOwner string `json:"head_owner,omitempty"` |
| 87 | 88 | HeadRepo string `json:"head_repo,omitempty"` |
| 89 | + // Draft is sent explicitly: the API opens a merge request as a draft when | |
| 90 | + // the field is absent, and a draft cannot be merged until marked ready. | |
| 91 | + Draft *bool `json:"draft,omitempty"` | |
| 88 | 92 | } |
| 89 | 93 | |
| 90 | 94 | // Comment is an MR comment. |
| @@ -56,6 +56,7 @@ type MergeRequest struct { | |||
| 56 | Title string `json:"title"` | 56 | Title string `json:"title"` |
| 57 | Body string `json:"body"` | 57 | Body string `json:"body"` |
| 58 | State string `json:"state"` | 58 | State string `json:"state"` |
| 59 | + Draft bool `json:"draft"` | ||
| 59 | Author string `json:"author"` | 60 | Author string `json:"author"` |
| 60 | BaseBranch string `json:"base_branch"` | 61 | BaseBranch string `json:"base_branch"` |
| 61 | HeadBranch string `json:"head_branch"` | 62 | HeadBranch string `json:"head_branch"` |
| @@ -85,6 +86,9 @@ type MergeRequestCreate struct { | |||
| 85 | Body string `json:"body,omitempty"` | 86 | Body string `json:"body,omitempty"` |
| 86 | HeadOwner string `json:"head_owner,omitempty"` | 87 | HeadOwner string `json:"head_owner,omitempty"` |
| 87 | HeadRepo string `json:"head_repo,omitempty"` | 88 | HeadRepo string `json:"head_repo,omitempty"` |
| 89 | + // Draft is sent explicitly: the API opens a merge request as a draft when | ||
| 90 | + // the field is absent, and a draft cannot be merged until marked ready. | ||
| 91 | + Draft *bool `json:"draft,omitempty"` | ||
| 88 | } | 92 | } |
| 89 | 93 | ||
| 90 | // Comment is an MR comment. | 94 | // Comment is an MR comment. |