Repository: cli/cli (go)

Issue:
Add field to retrieve linked PRs (closingPRsReferences) in `gh issue view <number`
### Describe the feature or problem you’d like to solve

As per my comment in #10529, `gh issue view <number>` should list the linked PRs that would close the issue

### Proposed solution

Exposing `closingPRsReferences` as a field. E.g. `gh view issue<number> --json closingPRsReferences`

### Additional context

This is an improvement linked to [my comment](https://github.com/cli/cli/issues/10529#issuecomment-2701532800) in #10529 


Pull request description:
[gh issue view] Expose `closedByPullRequestsReferences` JSON field
Fixes #10563.

## Acceptance Tests

### `gh issue view --help`

```shell
$ bin/gh issue view --help
Display the title, body, and other information about an issue.

With `--web` flag, open the issue in a web browser instead.

For more information about output formatting flags, see `gh help formatting`.

USAGE
  gh issue view {<number> | <url>} [flags]

FLAGS
  -c, --comments          View issue comments
  -q, --jq expression     Filter JSON output using a jq expression
      --json fields       Output JSON with the specified fields
  -t, --template string   Format JSON output using a Go template; see "gh help formatting"
  -w, --web               Open an issue in the browser

INHERITED FLAGS
      --help                     Show help for command
  -R, --repo [HOST/]OWNER/REPO   Select another repository using the [HOST/]OWNER/REPO format

JSON FIELDS
  assignees, author, body, closed, closedAt, closedByPullRequestsReferences,
  comments, createdAt, id, isPinned, labels, milestone, number, projectCards,
  projectItems, reactionGroups, state, stateReason, title, updatedAt, url

LEARN MORE
  Use `gh <command> <subcommand> --help` for more information about a command.
  Read the manual at https://cli.github.com/manual
  Learn about exit codes using `gh help exit-codes`
  Learn about accessibility experiences using `gh help accessibility`
```

### `gh issue view <number> --json closedByPullRequestsReferences`

```shell
$ bin/gh issue view 10529 --repo cli/cli --json closedByPullRequestsReferences
{
  "closedByPullRequestsReferences": [
    {
      "id": "PR_kwDODKw3uc6Nbtk1",
      "number": 10544,
      "repository": {
        "id": "MDEwOlJlcG9zaXRvcnkyMTI2MTMwNDk=",
        "name": "cli",
        "owner": {
          "id": "MDEyOk9yZ2FuaXphdGlvbjU5NzA0NzEx",
          "login": "cli"
        }
      },
      "url": "https://github.com/cli/cli/pull/10544"
    }
  ]
}
```

---

### Prompt not resetting on error

Fixed another related issue of prompt not getting back to normal when the issue is not found e.g. issues are disabled:

```shell
$ bin/gh issue view 101 --repo iamazeem/cli
the 'iamazeem/cli' repository has disabled issues
```

Before:

![gh-issue-view-prompt-error](https://github.com/user-attachments/assets/dbbde12e-899c-496d-b656-ca9c0fe56e09)

After:

![gh-issue-view-prompt-error-fixed](https://github.com/user-attachments/assets/b1aa1aa0-eaf3-4adb-967a-7245df135949)

Signed-off-by: Azeem Sajid <azeem.sajid@gmail.com>

Patch:
diff --git a/api/export_pr.go b/api/export_pr.go
index 7ae1a4ff4..9b030c39e 100644
--- a/api/export_pr.go
+++ b/api/export_pr.go
@@ -28,6 +28,24 @@ func (issue *Issue) ExportData(fields []string) map[string]interface{} {
 				})
 			}
 			data[f] = items
+		case "closedByPullRequestsReferences":
+			items := make([]map[string]interface{}, 0, len(issue.ClosedByPullRequestsReferences.Nodes))
+			for _, n := range issue.ClosedByPullRequestsReferences.Nodes {
+				items = append(items, map[string]interface{}{
+					"id":     n.ID,
+					"number": n.Number,
+					"url":    n.URL,
+					"repository": map[string]interface{}{
+						"id":   n.Repository.ID,
+						"name": n.Repository.Name,
+						"owner": map[string]interface{}{
+							"id":    n.Repository.Owner.ID,
+							"login": n.Repository.Owner.Login,
+						},
+					},
+				})
+			}
+			data[f] = items
 		default:
 			sf := fieldByName(v, f)
 			data[f] = sf.Interface()
@@ -143,7 +161,6 @@ func (pr *PullRequest) ExportData(fields []string) map[string]interface{} {
 			items := make([]map[string]interface{}, 0, len(pr.ClosingIssuesReferences.Nodes))
 			for _, n := range pr.ClosingIssuesReferences.Nodes {
 				items = append(items, map[string]interface{}{
-
 					"id":     n.ID,
 					"number": n.Number,
 					"url":    n.URL,
diff --git a/api/queries_issue.go b/api/queries_issue.go
index 094b6b198..f09360152 100644
--- a/api/queries_issue.go
+++ b/api/queries_issue.go
@@ -44,6 +44,28 @@ type Issue struct {
 	Milestone        *Milestone
 	ReactionGroups   ReactionGroups
 	IsPinned         bool
+
+	ClosedByPullRequestsReferences ClosedByPullRequestsReferences
+}
+
+type ClosedByPullRequestsReferences struct {
+	Nodes []struct {
+		ID         string
+		Number     int
+		URL        string
+		Repository struct {
+			ID    string
+			Name  string
+			Owner struct {
+				ID    string
+				Login string
+			}
+		}
+	}
+	PageInfo struct {
+		HasNextPage bool
+		EndCursor   string
+	}
 }
 
 // return values for Issue.Typename
diff --git a/api/query_builder.go b/api/query_builder.go
index 4c45da3c1..47fb4c225 100644
--- a/api/query_builder.go
+++ b/api/query_builder.go
@@ -56,6 +56,25 @@ var issueCommentLast = shortenQuery(`
 	}
 `)
 
+var issueClosedByPullRequestsReferences = shortenQuery(`
+	closedByPullRequestsReferences(first: 100) {
+		nodes {
+			id,
+			number,
+			url,
+			repository {
+				id,
+				name,
+				owner {
+					id,
+					login
+				}
+			}
+		}
+		pageInfo{hasNextPage,endCursor}
+	}
+`)
+
 var prReviewRequests = shortenQuery(`
 	reviewRequests(first: 100) {
 		nodes {
@@ -296,6 +315,7 @@ var sharedIssuePRFields = []string{
 var issueOnlyFields = []string{
 	"isPinned",
 	"stateReason",
+	"closedByPullRequestsReferences",
 }
 
 var IssueFields = append(sharedIssuePRFields, issueOnlyFields...)
@@ -388,6 +408,8 @@ func IssueGraphQL(fields []string) string {
 			q = append(q, StatusCheckRollupGraphQLWithCountByState())
 		case "closingIssuesReferences":
 			q = append(q, prClosingIssuesReferences)
+		case "closedByPullRequestsReferences":
+			q = append(q, issueClosedByPullRequestsReferences)
 		default:
 			q = append(q, field)
 		}
diff --git a/pkg/cmd/issue/view/http.go b/pkg/cmd/issue/view/http.go
index e4f756436..4adc71802 100644
--- a/pkg/cmd/issue/view/http.go
+++ b/pkg/cmd/issue/view/http.go
@@ -53,3 +53,42 @@ func preloadIssueComments(client *http.Client, repo ghrepo.Interface, issue *api
 	issue.Comments.PageInfo.HasNextPage = false
 	return nil
 }
+
+func preloadClosedByPullRequestsReferences(client *http.Client, repo ghrepo.Interface, issue *api.Issue) error {
+	if !issue.ClosedByPullRequestsReferences.PageInfo.HasNextPage {
+		return nil
+	}
+
+	type response struct {
+		Node struct {
+			Issue struct {
+				ClosedByPullRequestsReferences api.ClosedByPullRequestsReferences `graphql:"closedByPullRequestsReferences(first: 100, after: $endCursor)"`
+			} `graphql:"...on Issue"`
+		} `graphql:"node(id: $id)"`
+	}
+
+	variables := map[string]interface{}{
+		"id":        githubv4.ID(issue.ID),
+		"endCursor": githubv4.String(issue.ClosedByPullRequestsReferences.PageInfo.EndCursor),
+	}
+
+	gql := api.NewClientFromHTTP(client)
+
+	for {
+		var query response
+		err := gql.Query(repo.RepoHost(), "closedByPullRequestsReferences", &query, variables)
+		if err != nil {
+			return err
+		}
+
+		issue.ClosedByPullRequestsReferences.Nodes = append(issue.ClosedByPullRequestsReferences.Nodes, query.Node.Issue.ClosedByPullRequestsReferences.Nodes...)
+
+		if !query.Node.Issue.ClosedByPullRequestsReferences.PageInfo.HasNextPage {
+			break
+		}
+		variables["endCursor"] = githubv4.String(query.Node.Issue.ClosedByPullRequestsReferences.PageInfo.EndCursor)
+	}
+
+	issue.ClosedByPullRequestsReferences.PageInfo.HasNextPage = false
+	return nil
+}
diff --git a/pkg/cmd/issue/view/view.go b/pkg/cmd/issue/view/view.go
index a9e25513b..3b02a3f2d 100644
--- a/pkg/cmd/issue/view/view.go
+++ b/pkg/cmd/issue/view/view.go
@@ -1,7 +1,6 @@
 package view
 
 import (
-	"errors"
 	"fmt"
 	"io"
 	"net/http"
@@ -134,6 +133,8 @@ func viewRun(opts *ViewOptions) error {
 	opts.IO.DetectTerminalTheme()
 
 	opts.IO.StartProgressIndicator()
+	defer opts.IO.StopProgressIndicator()
+
 	lookupFields.Add("id")
 
 	issue, err := issueShared.FindIssueOrPR(httpClient, baseRepo, opts.IssueNumber, lookupFields.ToSlice())
@@ -144,18 +145,21 @@ func viewRun(opts *ViewOptions) error {
 	if lookupFields.Contains("comments") {
 		// FIXME: this re-fetches the comments connection even though the initial set of 100 were
 		// fetched in the previous request.
-		err = preloadIssueComments(httpClient, baseRepo, issue)
+		err := preloadIssueComments(httpClient, baseRepo, issue)
+		if err != nil {
+			return err
+		}
 	}
-	opts.IO.StopProgressIndicator()
-	if err != nil {
-		var loadErr *issueShared.PartialLoadError
-		if opts.Exporter == nil && errors.As(err, &loadErr) {
-			fmt.Fprintf(opts.IO.ErrOut, "warning: %s\n", loadErr.Error())
-		} else {
+
+	if lookupFields.Contains("closedByPullRequestsReferences") {
+		err := preloadClosedByPullRequestsReferences(httpClient, baseRepo, issue)
+		if err != nil {
 			return err
 		}
 	}
 
+	opts.IO.StopProgressIndicator()
+
 	if opts.WebMode {
 		openURL := issue.URL
 		if opts.IO.IsStdoutTTY() {


Test patch:
diff --git a/api/export_pr_test.go b/api/export_pr_test.go
index 09a1dffe8..1f310693e 100644
--- a/api/export_pr_test.go
+++ b/api/export_pr_test.go
@@ -107,6 +107,70 @@ func TestIssue_ExportData(t *testing.T) {
 				}
 			`),
 		},
+		{
+			name:   "linked pull requests",
+			fields: []string{"closedByPullRequestsReferences"},
+			inputJSON: heredoc.Doc(`
+				{ "closedByPullRequestsReferences": { "nodes": [
+					{
+						"id": "I_123",
+						"number": 123,
+						"url": "https://github.com/cli/cli/pull/123",
+						"repository": {
+							"id": "R_123",
+							"name": "cli",
+							"owner": {
+								"id": "O_123",
+								"login": "cli"
+							}
+						}
+					},
+					{
+						"id": "I_456",
+						"number": 456,
+						"url": "https://github.com/cli/cli/pull/456",
+						"repository": {
+							"id": "R_456",
+							"name": "cli",
+							"owner": {
+								"id": "O_456",
+								"login": "cli"
+							}
+						}
+					}
+				] } }
+			`),
+			outputJSON: heredoc.Doc(`
+				{ "closedByPullRequestsReferences": [
+					{
+						"id": "I_123",
+						"number": 123,
+						"repository": {
+							"id": "R_123",
+							"name": "cli",
+							"owner": {
+								"id": "O_123",
+								"login": "cli"
+							}
+						},
+						"url": "https://github.com/cli/cli/pull/123"
+					},
+					{
+						"id": "I_456",
+						"number": 456,
+						"repository": {
+							"id": "R_456",
+							"name": "cli",
+							"owner": {
+								"id": "O_456",
+								"login": "cli"
+							}
+						},
+						"url": "https://github.com/cli/cli/pull/456"
+					}
+				] }
+			`),
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
@@ -120,7 +184,14 @@ func TestIssue_ExportData(t *testing.T) {
 			enc := json.NewEncoder(&buf)
 			enc.SetIndent("", "\t")
 			require.NoError(t, enc.Encode(exported))
-			assert.Equal(t, tt.outputJSON, buf.String())
+
+			var gotData interface{}
+			dec = json.NewDecoder(&buf)
+			require.NoError(t, dec.Decode(&gotData))
+			var expectData interface{}
+			require.NoError(t, json.Unmarshal([]byte(tt.outputJSON), &expectData))
+
+			assert.Equal(t, expectData, gotData)
 		})
 	}
 }
diff --git a/pkg/cmd/issue/view/view_test.go b/pkg/cmd/issue/view/view_test.go
index 391a288fb..71b0884a1 100644
--- a/pkg/cmd/issue/view/view_test.go
+++ b/pkg/cmd/issue/view/view_test.go
@@ -31,6 +31,7 @@ func TestJSONFields(t *testing.T) {
 		"body",
 		"closed",
 		"comments",
+		"closedByPullRequestsReferences",
 		"createdAt",
 		"closedAt",
 		"id",