mirror of
https://github.com/github/gh-stack.git
synced 2026-09-14 20:26:28 +08:00
a82dc3ef1d
* Add stack Number field to local model and schema
The new Stacks REST API exposes a human-facing stack number (shown in the
github.com UI) alongside the internal stack id. Add a Number field to the
stack.Stack model and document it in schema.json so it can be persisted in
the .git/gh-stack file. Purely additive; behavior is unchanged until callers
populate it.
Copilot-Session: 03673c26-a245-42da-93ed-dfcebc92a740
* Cut over stack operations to the public Stacks REST API
Replace the private cli_internal stack endpoints with the new public
Stacks REST API (/repos/{owner}/{repo}/stacks):
- ListStacks / FindStackForPR (?pull_request= filter) / GetStack for reads
- CreateStack, which now returns the created stack including its number
- AddToStack for delta-only appends (there is no full-replace endpoint)
- Unstack for server-driven removal (204 dissolved / 200 partial / 422)
Migrate all callers (checkout, submit, link, sync, unstack, utils) and
drop the client-side unstack eligibility pre-check — the server now
decides which PRs can be unstacked. checkout discovers stacks via the
pull_request filter; submit/link express updates as append-only deltas;
unstack adopts partial-unstack semantics, keeping local tracking when
PRs remain stacked on GitHub.
RemoteStack now carries the stack number, and stack updates resolve a
stack's number from its internal id for stack files that predate the
Number field.
Copilot-Session: 03673c26-a245-42da-93ed-dfcebc92a740
* Remove the personal access token (PAT) limitation
The new Stacks REST API is public, so any user authenticated with the
GitHub CLI (including via a PAT with repo scope) can perform stack
operations once the feature is enabled for their repository. Remove the
PAT detection and the private-preview gating:
- Delete Config.WarnIfPAT / IsPersonalAccessToken and the TokenForHostFn
test hook (internal/config/auth.go is no longer needed).
- Drop the submit pre-flight that aborted on a PAT.
- Rename warnStacksUnavailableOrPAT to warnStacksUnavailable and simplify
it to the "stacked PRs not enabled" message.
Copilot-Session: 03673c26-a245-42da-93ed-dfcebc92a740
* address review comments
592 lines
18 KiB
Go
592 lines
18 KiB
Go
package github
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"net/http"
|
|
|
|
"github.com/cli/go-gh/v2/pkg/api"
|
|
graphql "github.com/cli/shurcooL-graphql"
|
|
)
|
|
|
|
// MergeQueueEntry represents a merge queue entry. When the GraphQL field
|
|
// mergeQueueEntry is null (PR not queued), the pointer will be nil.
|
|
type MergeQueueEntry struct {
|
|
ID string `graphql:"id"`
|
|
}
|
|
|
|
// AutoMergeRequest represents an auto-merge configuration on a PR.
|
|
// When the GraphQL field autoMergeRequest is null (auto-merge not enabled),
|
|
// the pointer will be nil.
|
|
type AutoMergeRequest struct {
|
|
EnabledAt string `graphql:"enabledAt"`
|
|
}
|
|
|
|
// PullRequest represents a GitHub pull request.
|
|
type PullRequest struct {
|
|
ID string `graphql:"id"`
|
|
Number int `graphql:"number"`
|
|
State string `graphql:"state"`
|
|
URL string `graphql:"url"`
|
|
Title string `graphql:"title"`
|
|
Body string `graphql:"body"`
|
|
HeadRefName string `graphql:"headRefName"`
|
|
BaseRefName string `graphql:"baseRefName"`
|
|
IsDraft bool `graphql:"isDraft"`
|
|
Merged bool `graphql:"merged"`
|
|
MergeQueueEntry *MergeQueueEntry `graphql:"mergeQueueEntry"`
|
|
AutoMergeRequest *AutoMergeRequest `graphql:"autoMergeRequest"`
|
|
}
|
|
|
|
// IsQueued reports whether the pull request is currently in a merge queue.
|
|
func (pr *PullRequest) IsQueued() bool {
|
|
return pr != nil && pr.MergeQueueEntry != nil && pr.MergeQueueEntry.ID != ""
|
|
}
|
|
|
|
// IsAutoMergeEnabled reports whether the pull request has auto-merge enabled.
|
|
func (pr *PullRequest) IsAutoMergeEnabled() bool {
|
|
return pr != nil && pr.AutoMergeRequest != nil
|
|
}
|
|
|
|
// Client wraps GitHub API operations.
|
|
type Client struct {
|
|
gql *api.GraphQLClient
|
|
rest *api.RESTClient
|
|
host string
|
|
owner string
|
|
repo string
|
|
slug string
|
|
}
|
|
|
|
// NewClient creates a new GitHub API client for the given repository.
|
|
// The host parameter specifies the GitHub hostname (e.g. "github.com" or a
|
|
// GHES hostname like "github.mycompany.com"). If empty, it defaults to
|
|
// "github.com".
|
|
func NewClient(host, owner, repo string) (*Client, error) {
|
|
if host == "" {
|
|
host = "github.com"
|
|
}
|
|
opts := api.ClientOptions{Host: host}
|
|
gql, err := api.NewGraphQLClient(opts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating GraphQL client: %w", err)
|
|
}
|
|
rest, err := api.NewRESTClient(opts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating REST client: %w", err)
|
|
}
|
|
return &Client{
|
|
gql: gql,
|
|
rest: rest,
|
|
host: host,
|
|
owner: owner,
|
|
repo: repo,
|
|
slug: owner + "/" + repo,
|
|
}, nil
|
|
}
|
|
|
|
// PRURL constructs the web URL for a pull request on the given host.
|
|
func PRURL(host, owner, repo string, number int) string {
|
|
if host == "" {
|
|
host = "github.com"
|
|
}
|
|
return fmt.Sprintf("https://%s/%s/%s/pull/%d", host, owner, repo, number)
|
|
}
|
|
|
|
// FindPRForBranch finds an open PR by head branch name.
|
|
func (c *Client) FindPRForBranch(branch string) (*PullRequest, error) {
|
|
var query struct {
|
|
Repository struct {
|
|
PullRequests struct {
|
|
Nodes []struct {
|
|
ID string `graphql:"id"`
|
|
Number int `graphql:"number"`
|
|
URL string `graphql:"url"`
|
|
Title string `graphql:"title"`
|
|
Body string `graphql:"body"`
|
|
BaseRefName string `graphql:"baseRefName"`
|
|
IsDraft bool `graphql:"isDraft"`
|
|
MergeQueueEntry *MergeQueueEntry `graphql:"mergeQueueEntry"`
|
|
AutoMergeRequest *AutoMergeRequest `graphql:"autoMergeRequest"`
|
|
}
|
|
} `graphql:"pullRequests(headRefName: $head, states: [OPEN], first: 1)"`
|
|
} `graphql:"repository(owner: $owner, name: $name)"`
|
|
}
|
|
|
|
variables := map[string]interface{}{
|
|
"owner": graphql.String(c.owner),
|
|
"name": graphql.String(c.repo),
|
|
"head": graphql.String(branch),
|
|
}
|
|
|
|
if err := c.gql.Query("FindPRForBranch", &query, variables); err != nil {
|
|
return nil, fmt.Errorf("querying PRs: %w", err)
|
|
}
|
|
|
|
nodes := query.Repository.PullRequests.Nodes
|
|
if len(nodes) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
n := nodes[0]
|
|
return &PullRequest{
|
|
ID: n.ID,
|
|
Number: n.Number,
|
|
URL: n.URL,
|
|
Title: n.Title,
|
|
Body: n.Body,
|
|
BaseRefName: n.BaseRefName,
|
|
IsDraft: n.IsDraft,
|
|
MergeQueueEntry: n.MergeQueueEntry,
|
|
AutoMergeRequest: n.AutoMergeRequest,
|
|
}, nil
|
|
}
|
|
|
|
// CreatePR creates a new pull request.
|
|
func (c *Client) CreatePR(base, head, title, body string, draft bool) (*PullRequest, error) {
|
|
var mutation struct {
|
|
CreatePullRequest struct {
|
|
PullRequest struct {
|
|
ID string
|
|
Number int
|
|
URL string `graphql:"url"`
|
|
}
|
|
} `graphql:"createPullRequest(input: $input)"`
|
|
}
|
|
|
|
repoID, err := c.repositoryID()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
type CreatePullRequestInput struct {
|
|
RepositoryID string `json:"repositoryId"`
|
|
BaseRefName string `json:"baseRefName"`
|
|
HeadRefName string `json:"headRefName"`
|
|
Title string `json:"title"`
|
|
Body string `json:"body,omitempty"`
|
|
Draft bool `json:"draft"`
|
|
}
|
|
|
|
variables := map[string]interface{}{
|
|
"input": CreatePullRequestInput{
|
|
RepositoryID: repoID,
|
|
BaseRefName: base,
|
|
HeadRefName: head,
|
|
Title: title,
|
|
Body: body,
|
|
Draft: draft,
|
|
},
|
|
}
|
|
|
|
if err := c.gql.Mutate("CreatePullRequest", &mutation, variables); err != nil {
|
|
return nil, fmt.Errorf("creating PR: %w", err)
|
|
}
|
|
|
|
pr := mutation.CreatePullRequest.PullRequest
|
|
return &PullRequest{
|
|
ID: pr.ID,
|
|
Number: pr.Number,
|
|
URL: pr.URL,
|
|
}, nil
|
|
}
|
|
|
|
// UpdatePRBase updates the base branch of an existing pull request.
|
|
func (c *Client) UpdatePRBase(number int, base string) error {
|
|
type updatePRRequest struct {
|
|
Base string `json:"base"`
|
|
}
|
|
|
|
body, err := json.Marshal(updatePRRequest{Base: base})
|
|
if err != nil {
|
|
return fmt.Errorf("marshaling request: %w", err)
|
|
}
|
|
|
|
path := fmt.Sprintf("repos/%s/%s/pulls/%d", c.owner, c.repo, number)
|
|
return c.rest.Patch(path, bytes.NewReader(body), nil)
|
|
}
|
|
|
|
// MarkPRReadyForReview converts a draft pull request to ready for review.
|
|
func (c *Client) MarkPRReadyForReview(prID string) error {
|
|
var mutation struct {
|
|
MarkPullRequestReadyForReview struct {
|
|
PullRequest struct {
|
|
ID string
|
|
}
|
|
} `graphql:"markPullRequestReadyForReview(input: $input)"`
|
|
}
|
|
|
|
type MarkPullRequestReadyForReviewInput struct {
|
|
PullRequestID string `json:"pullRequestId"`
|
|
}
|
|
|
|
variables := map[string]interface{}{
|
|
"input": MarkPullRequestReadyForReviewInput{
|
|
PullRequestID: prID,
|
|
},
|
|
}
|
|
|
|
if err := c.gql.Mutate("MarkPullRequestReadyForReview", &mutation, variables); err != nil {
|
|
return fmt.Errorf("marking PR ready for review: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DisableAutoMerge disables auto-merge on a pull request.
|
|
func (c *Client) DisableAutoMerge(prID string) error {
|
|
var mutation struct {
|
|
DisablePullRequestAutoMerge struct {
|
|
PullRequest struct {
|
|
ID string
|
|
}
|
|
} `graphql:"disablePullRequestAutoMerge(input: $input)"`
|
|
}
|
|
|
|
type DisablePullRequestAutoMergeInput struct {
|
|
PullRequestID string `json:"pullRequestId"`
|
|
}
|
|
|
|
variables := map[string]interface{}{
|
|
"input": DisablePullRequestAutoMergeInput{
|
|
PullRequestID: prID,
|
|
},
|
|
}
|
|
|
|
if err := c.gql.Mutate("DisablePullRequestAutoMerge", &mutation, variables); err != nil {
|
|
return fmt.Errorf("disabling auto-merge: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) repositoryID() (string, error) {
|
|
var query struct {
|
|
Repository struct {
|
|
ID string
|
|
} `graphql:"repository(owner: $owner, name: $name)"`
|
|
}
|
|
|
|
variables := map[string]interface{}{
|
|
"owner": graphql.String(c.owner),
|
|
"name": graphql.String(c.repo),
|
|
}
|
|
|
|
if err := c.gql.Query("RepositoryID", &query, variables); err != nil {
|
|
return "", fmt.Errorf("fetching repository ID: %w", err)
|
|
}
|
|
|
|
return query.Repository.ID, nil
|
|
}
|
|
|
|
// PRDetails holds enriched pull request data for display in the TUI.
|
|
type PRDetails struct {
|
|
Number int
|
|
State string // OPEN, CLOSED, MERGED
|
|
URL string
|
|
Title string
|
|
Body string
|
|
IsDraft bool
|
|
Merged bool
|
|
IsQueued bool
|
|
}
|
|
|
|
// FindPRDetailsForBranch fetches enriched PR data for display purposes.
|
|
// Returns nil without error if no PR exists for the branch.
|
|
func (c *Client) FindPRDetailsForBranch(branch string) (*PRDetails, error) {
|
|
var query struct {
|
|
Repository struct {
|
|
PullRequests struct {
|
|
Nodes []struct {
|
|
Number int `graphql:"number"`
|
|
State string `graphql:"state"`
|
|
URL string `graphql:"url"`
|
|
IsDraft bool `graphql:"isDraft"`
|
|
Merged bool `graphql:"merged"`
|
|
MergeQueueEntry *MergeQueueEntry `graphql:"mergeQueueEntry"`
|
|
}
|
|
} `graphql:"pullRequests(headRefName: $head, last: 1)"`
|
|
} `graphql:"repository(owner: $owner, name: $name)"`
|
|
}
|
|
|
|
variables := map[string]interface{}{
|
|
"owner": graphql.String(c.owner),
|
|
"name": graphql.String(c.repo),
|
|
"head": graphql.String(branch),
|
|
}
|
|
|
|
if err := c.gql.Query("FindPRDetailsForBranch", &query, variables); err != nil {
|
|
return nil, fmt.Errorf("querying PR details: %w", err)
|
|
}
|
|
|
|
nodes := query.Repository.PullRequests.Nodes
|
|
if len(nodes) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
n := nodes[0]
|
|
return &PRDetails{
|
|
Number: n.Number,
|
|
State: n.State,
|
|
URL: n.URL,
|
|
IsDraft: n.IsDraft,
|
|
Merged: n.Merged,
|
|
IsQueued: n.MergeQueueEntry != nil && n.MergeQueueEntry.ID != "",
|
|
}, nil
|
|
}
|
|
|
|
// FindPRByNumber fetches a pull request by its number.
|
|
func (c *Client) FindPRByNumber(number int) (*PullRequest, error) {
|
|
gqlNumber, err := toGraphQLInt(number)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var query struct {
|
|
Repository struct {
|
|
PullRequest struct {
|
|
ID string `graphql:"id"`
|
|
Number int `graphql:"number"`
|
|
State string `graphql:"state"`
|
|
URL string `graphql:"url"`
|
|
Title string `graphql:"title"`
|
|
Body string `graphql:"body"`
|
|
HeadRefName string `graphql:"headRefName"`
|
|
BaseRefName string `graphql:"baseRefName"`
|
|
IsDraft bool `graphql:"isDraft"`
|
|
Merged bool `graphql:"merged"`
|
|
MergeQueueEntry *MergeQueueEntry `graphql:"mergeQueueEntry"`
|
|
AutoMergeRequest *AutoMergeRequest `graphql:"autoMergeRequest"`
|
|
} `graphql:"pullRequest(number: $number)"`
|
|
} `graphql:"repository(owner: $owner, name: $name)"`
|
|
}
|
|
|
|
variables := map[string]interface{}{
|
|
"owner": graphql.String(c.owner),
|
|
"name": graphql.String(c.repo),
|
|
"number": gqlNumber,
|
|
}
|
|
|
|
if err := c.gql.Query("FindPRByNumber", &query, variables); err != nil {
|
|
return nil, fmt.Errorf("querying PR #%d: %w", number, err)
|
|
}
|
|
|
|
n := query.Repository.PullRequest
|
|
if n.Number == 0 && n.ID == "" {
|
|
return nil, nil
|
|
}
|
|
return &PullRequest{
|
|
ID: n.ID,
|
|
Number: n.Number,
|
|
State: n.State,
|
|
URL: n.URL,
|
|
Title: n.Title,
|
|
Body: n.Body,
|
|
HeadRefName: n.HeadRefName,
|
|
BaseRefName: n.BaseRefName,
|
|
IsDraft: n.IsDraft,
|
|
Merged: n.Merged,
|
|
MergeQueueEntry: n.MergeQueueEntry,
|
|
AutoMergeRequest: n.AutoMergeRequest,
|
|
}, nil
|
|
}
|
|
|
|
func toGraphQLInt(n int) (graphql.Int, error) {
|
|
if n < math.MinInt32 || n > math.MaxInt32 {
|
|
return 0, fmt.Errorf("number %d is out of GraphQL Int range", n)
|
|
}
|
|
return graphql.Int(n), nil
|
|
}
|
|
|
|
// RemoteStackBase describes the base ref (and optionally SHA) of a stack.
|
|
type RemoteStackBase struct {
|
|
Ref string `json:"ref"`
|
|
Sha string `json:"sha,omitempty"`
|
|
}
|
|
|
|
// RemoteStackPRHead describes the head ref of a pull request in a stack.
|
|
type RemoteStackPRHead struct {
|
|
Ref string `json:"ref"`
|
|
Sha string `json:"sha"`
|
|
}
|
|
|
|
// RemoteStackPR is a pull request entry within a remote stack, as returned by
|
|
// the Stacks REST API list/detail endpoints.
|
|
type RemoteStackPR struct {
|
|
Number int `json:"number"`
|
|
State string `json:"state"` // open, closed
|
|
Draft bool `json:"draft"`
|
|
MergedAt *string `json:"merged_at"`
|
|
Head RemoteStackPRHead `json:"head"`
|
|
}
|
|
|
|
// IsMerged reports whether the pull request has been merged.
|
|
func (p RemoteStackPR) IsMerged() bool {
|
|
return p.MergedAt != nil && *p.MergedAt != ""
|
|
}
|
|
|
|
// RemoteStack represents a stack of pull requests as returned by the public
|
|
// Stacks REST API (GET/POST /repos/{owner}/{repo}/stacks...). ID is the
|
|
// internal identifier; Number is the human-facing stack number shown in the
|
|
// github.com UI and used to address the stack in API paths.
|
|
//
|
|
// The API returns pull_requests as an array of objects; UnmarshalJSON flattens
|
|
// them to the ordered PullRequests numbers (bottom to top) and preserves the
|
|
// full entries in PRDetails for callers that need head refs or PR state.
|
|
type RemoteStack struct {
|
|
ID int `json:"id"`
|
|
Number int `json:"number"`
|
|
NodeID string `json:"node_id"`
|
|
URL string `json:"url"`
|
|
Base RemoteStackBase `json:"base"`
|
|
Open bool `json:"open"`
|
|
CreatedAt string `json:"created_at"`
|
|
PullRequests []int `json:"-"`
|
|
PRDetails []RemoteStackPR `json:"-"`
|
|
}
|
|
|
|
// UnmarshalJSON decodes the Stacks REST API representation, deriving the
|
|
// ordered PullRequests numbers from the pull_requests objects.
|
|
func (s *RemoteStack) UnmarshalJSON(data []byte) error {
|
|
type wire struct {
|
|
ID int `json:"id"`
|
|
Number int `json:"number"`
|
|
NodeID string `json:"node_id"`
|
|
URL string `json:"url"`
|
|
Base RemoteStackBase `json:"base"`
|
|
Open bool `json:"open"`
|
|
CreatedAt string `json:"created_at"`
|
|
PullRequests []RemoteStackPR `json:"pull_requests"`
|
|
}
|
|
var w wire
|
|
if err := json.Unmarshal(data, &w); err != nil {
|
|
return err
|
|
}
|
|
s.ID = w.ID
|
|
s.Number = w.Number
|
|
s.NodeID = w.NodeID
|
|
s.URL = w.URL
|
|
s.Base = w.Base
|
|
s.Open = w.Open
|
|
s.CreatedAt = w.CreatedAt
|
|
s.PRDetails = w.PullRequests
|
|
s.PullRequests = make([]int, len(w.PullRequests))
|
|
for i, p := range w.PullRequests {
|
|
s.PullRequests[i] = p.Number
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// PRNumbers returns the ordered pull request numbers in the stack, from bottom
|
|
// to top.
|
|
func (s *RemoteStack) PRNumbers() []int {
|
|
return s.PullRequests
|
|
}
|
|
|
|
// ListStacks returns all stacks in the repository, ordered by stack number
|
|
// (descending). Returns an empty slice if no stacks exist. A 404 response
|
|
// indicates stacked PRs are not enabled for this repository.
|
|
func (c *Client) ListStacks() ([]RemoteStack, error) {
|
|
path := fmt.Sprintf("repos/%s/%s/stacks", c.owner, c.repo)
|
|
var stacks []RemoteStack
|
|
if err := c.rest.Get(path, &stacks); err != nil {
|
|
return nil, err
|
|
}
|
|
if stacks == nil {
|
|
stacks = []RemoteStack{}
|
|
}
|
|
return stacks, nil
|
|
}
|
|
|
|
// FindStackForPR returns the stack that contains the given pull request number,
|
|
// using the list endpoint's server-side pull_request filter. Returns nil
|
|
// (without error) when the PR is not part of any stack.
|
|
func (c *Client) FindStackForPR(prNumber int) (*RemoteStack, error) {
|
|
path := fmt.Sprintf("repos/%s/%s/stacks?pull_request=%d", c.owner, c.repo, prNumber)
|
|
var stacks []RemoteStack
|
|
if err := c.rest.Get(path, &stacks); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(stacks) == 0 {
|
|
return nil, nil
|
|
}
|
|
return &stacks[0], nil
|
|
}
|
|
|
|
// GetStack fetches a single stack by its stack number.
|
|
func (c *Client) GetStack(stackNumber int) (*RemoteStack, error) {
|
|
path := fmt.Sprintf("repos/%s/%s/stacks/%d", c.owner, c.repo, stackNumber)
|
|
var rs RemoteStack
|
|
if err := c.rest.Get(path, &rs); err != nil {
|
|
return nil, err
|
|
}
|
|
return &rs, nil
|
|
}
|
|
|
|
// CreateStack creates a stack on GitHub from an ordered list of PR numbers.
|
|
// The PR numbers must be ordered from bottom to top of the stack (at least two)
|
|
// and must form a valid base-to-head chain. Returns the created stack.
|
|
func (c *Client) CreateStack(prNumbers []int) (*RemoteStack, error) {
|
|
type createStackRequest struct {
|
|
PullRequests []int `json:"pull_requests"`
|
|
}
|
|
|
|
body, err := json.Marshal(createStackRequest{PullRequests: prNumbers})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshaling request: %w", err)
|
|
}
|
|
|
|
path := fmt.Sprintf("repos/%s/%s/stacks", c.owner, c.repo)
|
|
var rs RemoteStack
|
|
if err := c.rest.Post(path, bytes.NewReader(body), &rs); err != nil {
|
|
return nil, err
|
|
}
|
|
return &rs, nil
|
|
}
|
|
|
|
// AddToStack appends pull requests to the top of an existing stack. Only the
|
|
// new PR numbers (the delta) should be provided, ordered from the current top
|
|
// of the stack upward. Returns the updated stack.
|
|
func (c *Client) AddToStack(stackNumber int, prNumbers []int) (*RemoteStack, error) {
|
|
type addToStackRequest struct {
|
|
PullRequests []int `json:"pull_requests"`
|
|
}
|
|
|
|
body, err := json.Marshal(addToStackRequest{PullRequests: prNumbers})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshaling request: %w", err)
|
|
}
|
|
|
|
path := fmt.Sprintf("repos/%s/%s/stacks/%d/add", c.owner, c.repo, stackNumber)
|
|
var rs RemoteStack
|
|
if err := c.rest.Post(path, bytes.NewReader(body), &rs); err != nil {
|
|
return nil, err
|
|
}
|
|
return &rs, nil
|
|
}
|
|
|
|
// Unstack removes unlocked pull requests from a stack. The server leaves PRs
|
|
// that cannot be unstacked (queued for merge or with auto-merge enabled) in
|
|
// place. When PRs remain, the updated stack is returned with dissolved=false;
|
|
// when none remain the stack is destroyed and dissolved=true (HTTP 204).
|
|
func (c *Client) Unstack(stackNumber int) (rs *RemoteStack, dissolved bool, err error) {
|
|
path := fmt.Sprintf("repos/%s/%s/stacks/%d/unstack", c.owner, c.repo, stackNumber)
|
|
resp, err := c.rest.Request(http.MethodPost, path, nil)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode == http.StatusNoContent {
|
|
return nil, true, nil
|
|
}
|
|
|
|
var remaining RemoteStack
|
|
if decErr := json.NewDecoder(resp.Body).Decode(&remaining); decErr != nil {
|
|
return nil, false, fmt.Errorf("decoding unstack response: %w", decErr)
|
|
}
|
|
return &remaining, false, nil
|
|
}
|