Skip to content

Commit 1fee7ed

Browse files
author
Ismar Iljazovic
committed
feat: add remote links to issue view (ankitpokhrel#873)
Cherry-picked from upstream PR ankitpokhrel#873 by vkareh. Shows external/remote links in the issue view below linked issues.
1 parent 36e4420 commit 1fee7ed

File tree

5 files changed

+121
-3
lines changed

5 files changed

+121
-3
lines changed

api/client.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ func ProxyGetIssueRaw(c *jira.Client, key string) (string, error) {
108108
// ProxyGetIssue uses either a v2 or v3 version of the Jira GET /issue/{key}
109109
// endpoint to fetch the issue details based on configured installation type.
110110
// Defaults to v3 if installation type is not defined in the config.
111+
// Also fetches remote links for the issue.
111112
func ProxyGetIssue(c *jira.Client, key string, opts ...filter.Filter) (*jira.Issue, error) {
112113
var (
113114
iss *jira.Issue
@@ -122,6 +123,19 @@ func ProxyGetIssue(c *jira.Client, key string, opts ...filter.Filter) (*jira.Iss
122123
iss, err = c.GetIssue(key, opts...)
123124
}
124125

126+
if err != nil {
127+
return iss, err
128+
}
129+
130+
// Fetch remote links for the issue
131+
remoteLinks, err := c.GetIssueRemoteLinks(key)
132+
if err != nil {
133+
// Don't fail the entire request if remote links can't be fetched
134+
// Just log and continue without remote links
135+
remoteLinks = []jira.RemoteLink{}
136+
}
137+
iss.Fields.RemoteLinks = remoteLinks
138+
125139
return iss, err
126140
}
127141

internal/cmd/issue/view/view.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
package view
33

44
import (
5+
"encoding/json"
56
"fmt"
67

78
"github.com/spf13/cobra"
@@ -83,7 +84,20 @@ func viewRaw(cmd *cobra.Command, args []string) {
8384
defer s.Stop()
8485

8586
client := api.DefaultClient(debug)
86-
return api.ProxyGetIssueRaw(client, key)
87+
88+
// Fetch the issue with remote links included (same as structured view)
89+
issue, err := api.ProxyGetIssue(client, key)
90+
if err != nil {
91+
return "", err
92+
}
93+
94+
// Convert back to JSON for raw output
95+
rawJSON, err := json.MarshalIndent(issue, "", " ")
96+
if err != nil {
97+
return "", err
98+
}
99+
100+
return string(rawJSON), nil
87101
}()
88102
cmdutil.ExitIfError(err)
89103

internal/view/issue.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ func (i Issue) String() string {
115115
if len(i.Data.Fields.IssueLinks) > 0 {
116116
s.WriteString(fmt.Sprintf("\n\n%s\n\n%s\n", i.separator("Linked Issues"), i.linkedIssues()))
117117
}
118+
if len(i.Data.Fields.RemoteLinks) > 0 {
119+
s.WriteString(fmt.Sprintf("\n\n%s\n\n%s\n", i.separator("External Links"), i.remoteLinks()))
120+
}
118121
total := i.Data.Fields.Comment.Total
119122
if total > 0 && i.Options.NumComments > 0 {
120123
sep := fmt.Sprintf("%d Comments", total)
@@ -172,6 +175,17 @@ func (i Issue) fragments() []fragment {
172175
)
173176
}
174177

178+
if len(i.Data.Fields.RemoteLinks) > 0 {
179+
scraps = append(
180+
scraps,
181+
newBlankFragment(singleLineSpacing),
182+
fragment{Body: i.separator("External Links")},
183+
newBlankFragment(sectionSpacing),
184+
fragment{Body: i.remoteLinks()},
185+
newBlankFragment(singleLineSpacing),
186+
)
187+
}
188+
175189
if i.Data.Fields.Comment.Total > 0 && i.Options.NumComments > 0 {
176190
scraps = append(
177191
scraps,
@@ -392,6 +406,40 @@ func (i Issue) linkedIssues() string {
392406
return linked.String()
393407
}
394408

409+
func (i Issue) remoteLinks() string {
410+
if len(i.Data.Fields.RemoteLinks) == 0 {
411+
return ""
412+
}
413+
414+
var (
415+
remote strings.Builder
416+
maxTitleLen int
417+
summaryLen = defaultSummaryLength
418+
)
419+
420+
// Calculate max lengths for formatting
421+
for _, link := range i.Data.Fields.RemoteLinks {
422+
maxTitleLen = max(len(link.Object.Title), maxTitleLen)
423+
}
424+
425+
if maxTitleLen < summaryLen {
426+
summaryLen = maxTitleLen
427+
}
428+
429+
remote.WriteString("\n")
430+
for _, link := range i.Data.Fields.RemoteLinks {
431+
remote.WriteString(
432+
fmt.Sprintf(
433+
" %s\n %s\n\n",
434+
coloredOut(shortenAndPad(link.Object.Title, summaryLen), color.FgCyan, color.Bold),
435+
coloredOut(link.Object.URL, color.FgBlue, color.Underline),
436+
),
437+
)
438+
}
439+
440+
return remote.String()
441+
}
442+
395443
func (i Issue) comments() []issueComment {
396444
total := i.Data.Fields.Comment.Total
397445
comments := make([]issueComment, 0, total)

pkg/jira/issue.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,37 @@ func (c *Client) RemoteLinkIssue(issueID, title, url string) error {
476476
return nil
477477
}
478478

479+
// GetIssueRemoteLinks fetches remote links for an issue using GET /issue/{issueId}/remotelink endpoint.
480+
func (c *Client) GetIssueRemoteLinks(issueID string) ([]RemoteLink, error) {
481+
path := fmt.Sprintf("/issue/%s/remotelink", issueID)
482+
483+
res, err := c.GetV2(context.Background(), path, nil)
484+
if err != nil {
485+
return nil, err
486+
}
487+
if res == nil {
488+
return nil, ErrEmptyResponse
489+
}
490+
defer func() { _ = res.Body.Close() }()
491+
492+
if res.StatusCode != http.StatusOK {
493+
return nil, formatUnexpectedResponse(res)
494+
}
495+
496+
body, err := io.ReadAll(res.Body)
497+
if err != nil {
498+
return nil, err
499+
}
500+
501+
var remoteLinks []RemoteLink
502+
err = json.Unmarshal(body, &remoteLinks)
503+
if err != nil {
504+
return nil, err
505+
}
506+
507+
return remoteLinks, nil
508+
}
509+
479510
// WatchIssue adds user as a watcher using v2 version of the POST /issue/{key}/watchers endpoint.
480511
func (c *Client) WatchIssue(key, watcher string) error {
481512
return c.watchIssue(key, watcher, apiVersion3)

pkg/jira/types.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,9 @@ type IssueFields struct {
124124
InwardIssue *Issue `json:"inwardIssue,omitempty"`
125125
OutwardIssue *Issue `json:"outwardIssue,omitempty"`
126126
} `json:"issueLinks"`
127-
Created string `json:"created"`
128-
Updated string `json:"updated"`
127+
RemoteLinks []RemoteLink `json:"remoteLinks,omitempty"`
128+
Created string `json:"created"`
129+
Updated string `json:"updated"`
129130
}
130131

131132
// Field holds field info.
@@ -167,6 +168,16 @@ type IssueLinkType struct {
167168
Outward string `json:"outward"`
168169
}
169170

171+
// RemoteLink holds remote link info.
172+
type RemoteLink struct {
173+
ID int `json:"id"`
174+
Self string `json:"self"`
175+
Object struct {
176+
URL string `json:"url"`
177+
Title string `json:"title"`
178+
} `json:"object"`
179+
}
180+
170181
// Sprint holds sprint info.
171182
type Sprint struct {
172183
ID int `json:"id"`

0 commit comments

Comments
 (0)