Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions internal/panels/gitinfo/gitinfo_github.go
Original file line number Diff line number Diff line change
Expand Up @@ -1401,10 +1401,13 @@ func (p *Panel) issueSelectedCmd() tea.Cmd {
iss := items[cursor].issue
return func() tea.Msg {
return panels.IssueSelectedMsg{
Number: iss.Number,
Title: iss.Title,
Body: iss.Body,
State: iss.State,
Number: iss.Number,
Title: iss.Title,
Body: iss.Body,
State: iss.State,
Author: iss.Author,
Assignee: iss.Assignee,
Labels: append([]string(nil), iss.Labels...),
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions internal/panels/gitinfo/gitinfo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1917,6 +1917,9 @@ func TestIssueSelectedCmd_ValidCursor(t *testing.T) {
sel, ok := msg.(panels.IssueSelectedMsg)
require.True(t, ok)
assert.Equal(t, 1, sel.Number)
assert.Equal(t, "testuser", sel.Author)
assert.Equal(t, "testuser", sel.Assignee)
assert.Equal(t, []string{"bug"}, sel.Labels)
}

func TestPRSelectedCmd_ValidCursor(t *testing.T) {
Expand Down
11 changes: 7 additions & 4 deletions internal/panels/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -622,10 +622,13 @@ type GitHubUserMsg struct {
// IssueSelectedMsg is sent when the user selects an issue in the GitHub Issues tab.
// Preview pane should render the issue body as markdown.
type IssueSelectedMsg struct {
Title string
Body string
State string
Number int
Title string
Body string
State string
Author string
Assignee string
Labels []string
Number int
}

// IssueDeselectedMsg is sent when the user deselects an issue (via Escape).
Expand Down
54 changes: 49 additions & 5 deletions internal/panels/preview/preview.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,11 +289,9 @@ func (p *Preview) Update(msg tea.Msg) (panels.Panel, tea.Cmd) {
p.clearSelection()
p.ghMode = true
p.ghPlainText = false
p.ghTitle = fmt.Sprintf("#%d %s", msg.Number, ansi.Strip(msg.Title))
p.ghContent = msg.Body
if p.ghContent == "" {
p.ghContent = "*No description provided.*"
}
safeTitle := ansi.Strip(msg.Title)
p.ghTitle = fmt.Sprintf("#%d %s", msg.Number, safeTitle)
p.ghContent = renderIssuePreviewContent(msg, safeTitle)
p.scrollY = 0
p.lines = markdown.RenderStatic(p.ghContent, p.width)
return p, nil
Expand Down Expand Up @@ -1099,6 +1097,52 @@ func buildMetadataLines(path string, info os.FileInfo) []string {
}
}

func renderIssuePreviewContent(msg panels.IssueSelectedMsg, safeTitle string) string {
var b strings.Builder
_, _ = fmt.Fprintf(&b, "# Issue #%d\n\n**%s**", msg.Number, safeTitle)
if meta := issueMetadataLines(msg); len(meta) > 0 {
b.WriteString("\n\n")
b.WriteString(strings.Join(meta, "\n"))
}
body := msg.Body
if body == "" {
body = "*No description provided.*"
}
b.WriteString("\n\n---\n\n")
b.WriteString(body)
return b.String()
}

func issueMetadataLines(msg panels.IssueSelectedMsg) []string {
var lines []string
if state := strings.TrimSpace(ansi.Strip(msg.State)); state != "" {
lines = append(lines, "State: "+state)
}
if author := strings.TrimSpace(ansi.Strip(msg.Author)); author != "" {
lines = append(lines, "Author: @"+author)
}
if assignee := strings.TrimSpace(ansi.Strip(msg.Assignee)); assignee != "" {
lines = append(lines, "Assignee: @"+assignee)
}
if labels := cleanIssueLabels(msg.Labels); len(labels) > 0 {
lines = append(lines, "Labels: "+strings.Join(labels, ", "))
}
return lines
}

func cleanIssueLabels(labels []string) []string {
clean := make([]string, 0, len(labels))
for _, label := range labels {
label = strings.TrimSpace(ansi.Strip(label))
if label == "" {
continue
}
label = strings.ReplaceAll(label, "`", "'")
clean = append(clean, "`"+label+"`")
}
return clean
}

func buildBinaryMetadataLines(path string, info os.FileInfo, mimeType string, data []byte) []string {
lines := buildMetadataLines(path, info)
if len(data) == 0 {
Expand Down
33 changes: 25 additions & 8 deletions internal/panels/preview/preview_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -987,14 +987,22 @@ func TestIssueSelectedMsg(t *testing.T) {
p.SetSize(80, 30)

_, cmd := p.Update(panels.IssueSelectedMsg{
Number: 42,
Title: "Fix authentication bug",
Body: "The login flow is broken.\n\nSteps to reproduce...",
State: "open",
Number: 42,
Title: "Fix authentication bug",
Body: "The login flow is broken.\n\nSteps to reproduce...",
State: "open",
Author: "octocat",
Assignee: "hubot",
Labels: []string{"bug", "needs triage"},
})

assert.True(t, p.ghMode)
assert.Equal(t, "#42 Fix authentication bug", p.ghTitle)
assert.Contains(t, p.ghContent, "# Issue #42")
assert.Contains(t, p.ghContent, "State: open")
assert.Contains(t, p.ghContent, "Author: @octocat")
assert.Contains(t, p.ghContent, "Assignee: @hubot")
assert.Contains(t, p.ghContent, "Labels: `bug`, `needs triage`")
assert.Contains(t, p.ghContent, "The login flow is broken.")
assert.Equal(t, 0, p.scrollY)
assert.NotNil(t, p.lines)
Expand All @@ -1015,7 +1023,7 @@ func TestIssueSelectedMsg_EmptyBody(t *testing.T) {
})

assert.True(t, p.ghMode)
assert.Equal(t, "*No description provided.*", p.ghContent)
assert.Contains(t, p.ghContent, "*No description provided.*")
}

func TestIssueDeselectedMsg(t *testing.T) {
Expand Down Expand Up @@ -1669,13 +1677,22 @@ func TestIssueSelectedMsg_ANSIInjection(t *testing.T) {
p := New(defaultCfg(), defaultEditorCfg(), nil)
p.SetSize(80, 30)
msg := panels.IssueSelectedMsg{
Number: 42,
Title: "Bug: \x1b[31mRED\x1b[0m injection",
Body: "body text",
Number: 42,
Title: "Bug: \x1b[31mRED\x1b[0m injection",
Body: "body text",
State: "\x1b[31mopen\x1b[0m",
Author: "\x1b[31moctocat\x1b[0m",
Assignee: "\x1b[31mhubot\x1b[0m",
Labels: []string{"\x1b[31mbug\x1b[0m"},
}
p.Update(msg)
assert.NotContains(t, p.ghTitle, "\x1b", "ANSI in issue title should be stripped")
assert.Contains(t, p.ghTitle, "#42 Bug: RED injection")
assert.NotContains(t, p.ghContent, "\x1b", "ANSI in issue metadata should be stripped")
assert.Contains(t, p.ghContent, "State: open")
assert.Contains(t, p.ghContent, "Author: @octocat")
assert.Contains(t, p.ghContent, "Assignee: @hubot")
assert.Contains(t, p.ghContent, "Labels: `bug`")
}

func TestPRSelectedMsg_ANSIInjection(t *testing.T) {
Expand Down
Loading