commands/mr/view/mr_view.go (251 lines of code) (raw):
package view
import (
"encoding/json"
"fmt"
"strings"
"gitlab.com/gitlab-org/cli/pkg/iostreams"
"gitlab.com/gitlab-org/cli/api"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
issuableView "gitlab.com/gitlab-org/cli/commands/issuable/view"
"gitlab.com/gitlab-org/cli/commands/mr/mrutils"
"gitlab.com/gitlab-org/cli/pkg/utils"
"github.com/MakeNowJust/heredoc/v2"
"github.com/spf13/cobra"
gitlab "gitlab.com/gitlab-org/api/client-go"
)
type ViewOpts struct {
ShowComments bool
ShowSystemLogs bool
OpenInBrowser bool
OutputFormat string
CommentPageNumber int
CommentLimit int
IO *iostreams.IOStreams
}
type MRWithNotes struct {
*gitlab.MergeRequest
Notes []*gitlab.Note
}
func NewCmdView(f *cmdutils.Factory) *cobra.Command {
opts := &ViewOpts{
IO: f.IO,
}
mrViewCmd := &cobra.Command{
Use: "view {<id> | <branch>}",
Short: `Display the title, body, and other information about a merge request.`,
Long: ``,
Aliases: []string{"show"},
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
apiClient, err := f.HttpClient()
if err != nil {
return err
}
mr, baseRepo, err := mrutils.MRFromArgsWithOpts(f, args, &gitlab.GetMergeRequestsOptions{
IncludeDivergedCommitsCount: gitlab.Ptr(true),
RenderHTML: gitlab.Ptr(true),
IncludeRebaseInProgress: gitlab.Ptr(true),
}, "any")
if err != nil {
return err
}
// Optional: check for approval state of the MR (if the project supports it). In the event of a failure
// for this step, move forward assuming MR approvals are not supported. See below.
//
// NOTE: the API documentation says that project details have `approvals_before_merge` for GitLab Premium
// https://docs.gitlab.com/api/projects/#get-a-single-project. Unfortunately, the API client used
// does not provide the necessary ability to determine if this value was present or not in the response JSON
// since Project.ApprovalsBeforeMerge is a non-pointer type. Because of this, this step will either succeed
// and show approval state or it will fail silently
mrApprovals, _ := api.GetMRApprovalState(apiClient, baseRepo.FullName(), mr.IID)
cfg, _ := f.Config()
if opts.OpenInBrowser { // open in browser if --web flag is specified
if f.IO.IsOutputTTY() {
fmt.Fprintf(f.IO.StdErr, "Opening %s in your browser.\n", utils.DisplayURL(mr.WebURL))
}
browser, _ := cfg.Get(baseRepo.RepoHost(), "browser")
return utils.OpenInBrowser(mr.WebURL, browser)
}
notes := []*gitlab.Note{}
if opts.ShowComments {
l := &gitlab.ListMergeRequestNotesOptions{
Sort: gitlab.Ptr("asc"),
ListOptions: gitlab.ListOptions{
Page: opts.CommentPageNumber,
PerPage: opts.CommentLimit,
},
}
notes, err = api.ListMRNotes(apiClient, baseRepo.FullName(), mr.IID, l)
if err != nil {
return err
}
}
glamourStyle, _ := cfg.Get(baseRepo.RepoHost(), "glamour_style")
f.IO.ResolveBackgroundColor(glamourStyle)
if err := f.IO.StartPager(); err != nil {
return err
}
defer f.IO.StopPager()
if opts.OutputFormat == "json" {
return printJSONMR(opts, mr, notes)
}
if f.IO.IsOutputTTY() {
return printTTYMRPreview(opts, mr, mrApprovals, notes)
}
return printRawMRPreview(opts, mr, notes)
},
}
mrViewCmd.Flags().BoolVarP(&opts.ShowComments, "comments", "c", false, "Show merge request comments and activities.")
mrViewCmd.Flags().BoolVarP(&opts.ShowSystemLogs, "system-logs", "s", false, "Show system activities and logs.")
mrViewCmd.Flags().StringVarP(&opts.OutputFormat, "output", "F", "text", "Format output as: text, json.")
mrViewCmd.Flags().BoolVarP(&opts.OpenInBrowser, "web", "w", false, "Open merge request in a browser. Uses default browser or browser specified in BROWSER variable.")
mrViewCmd.Flags().IntVarP(&opts.CommentPageNumber, "page", "p", 0, "Page number.")
mrViewCmd.Flags().IntVarP(&opts.CommentLimit, "per-page", "P", 20, "Number of items to list per page.")
return mrViewCmd
}
func labelsList(mr *gitlab.MergeRequest) string {
return strings.Join(mr.Labels, ", ")
}
func assigneesList(mr *gitlab.MergeRequest) string {
assignees := utils.Map(mr.Assignees, func(a *gitlab.BasicUser) string {
return a.Username
})
return strings.Join(assignees, ", ")
}
func reviewersList(mr *gitlab.MergeRequest) string {
reviewers := utils.Map(mr.Reviewers, func(r *gitlab.BasicUser) string {
return r.Username
})
return strings.Join(reviewers, ", ")
}
func mrState(c *iostreams.ColorPalette, mr *gitlab.MergeRequest) (mrState string) {
if mr.State == "opened" {
mrState = c.Green("open")
} else if mr.State == "merged" {
mrState = c.Blue(mr.State)
} else {
mrState = c.Red(mr.State)
}
return mrState
}
func printTTYMRPreview(opts *ViewOpts, mr *gitlab.MergeRequest, mrApprovals *gitlab.MergeRequestApprovalState, notes []*gitlab.Note) error {
c := opts.IO.Color()
out := opts.IO.StdOut
mrTimeAgo := utils.TimeToPrettyTimeAgo(*mr.CreatedAt)
// Header
fmt.Fprint(out, mrState(c, mr))
fmt.Fprintf(out, c.Gray(" • opened by %s %s\n"), mr.Author.Username, mrTimeAgo)
fmt.Fprint(out, mr.Title)
fmt.Fprintf(out, c.Gray(" !%d"), mr.IID)
fmt.Fprintln(out)
// Description
if mr.Description != "" {
mr.Description, _ = utils.RenderMarkdown(mr.Description, opts.IO.BackgroundColor())
fmt.Fprintln(out, mr.Description)
}
fmt.Fprintf(out, c.Gray("\n%d upvotes • %d downvotes • %d comments\n"), mr.Upvotes, mr.Downvotes, mr.UserNotesCount)
// Meta information
if labels := labelsList(mr); labels != "" {
fmt.Fprint(out, c.Bold("Labels: "))
fmt.Fprintln(out, labels)
}
if assignees := assigneesList(mr); assignees != "" {
fmt.Fprint(out, c.Bold("Assignees: "))
fmt.Fprintln(out, assignees)
}
if reviewers := reviewersList(mr); reviewers != "" {
fmt.Fprint(out, c.Bold("Reviewers: "))
fmt.Fprintln(out, reviewers)
}
if mr.Milestone != nil {
fmt.Fprint(out, c.Bold("Milestone: "))
fmt.Fprintln(out, mr.Milestone.Title)
}
if mr.State == "closed" {
fmt.Fprintf(out, "Closed by: %s %s\n", mr.ClosedBy.Username, mrTimeAgo)
}
if mr.Pipeline != nil {
fmt.Fprint(out, c.Bold("Pipeline status: "))
var status string
switch s := mr.Pipeline.Status; s {
case "failed":
status = c.Red(s)
case "success":
status = c.Green(s)
default:
status = c.Gray(s)
}
fmt.Fprintf(out, "%s (View pipeline with `%s`)\n", status, c.Bold("glab ci view "+mr.SourceBranch))
if mr.MergeWhenPipelineSucceeds && mr.Pipeline.Status != "success" {
fmt.Fprintf(out, "%s Requires pipeline to succeed before merging.\n", c.WarnIcon())
}
}
if mrApprovals != nil {
fmt.Fprintln(out, c.Bold("Approvals status:"))
mrutils.PrintMRApprovalState(opts.IO, mrApprovals)
}
fmt.Fprintf(out, "%s This merge request has %s changes.\n", c.GreenCheck(), c.Yellow(mr.ChangesCount))
if mr.State == "merged" && mr.MergedBy != nil { //nolint:staticcheck
fmt.Fprintf(out, "%s The changes were merged into %s by %s %s.\n", c.GreenCheck(), mr.TargetBranch, mr.MergedBy.Name, utils.TimeToPrettyTimeAgo(*mr.MergedAt)) //nolint:staticcheck
}
if mr.HasConflicts {
fmt.Fprintf(out, c.Red("%s This branch has conflicts that must be resolved.\n"), c.FailedIcon())
}
// Comments
if opts.ShowComments {
fmt.Fprintln(out, heredoc.Doc(`
--------------------------------------------
Comments / Notes
--------------------------------------------
`))
if len(notes) > 0 {
for _, note := range notes {
if note.System && !opts.ShowSystemLogs {
continue
}
createdAt := utils.TimeToPrettyTimeAgo(*note.CreatedAt)
fmt.Fprint(out, note.Author.Username)
if note.System {
fmt.Fprintf(out, " %s ", note.Body)
fmt.Fprintln(out, c.Gray(createdAt))
} else {
body, _ := utils.RenderMarkdown(note.Body, opts.IO.BackgroundColor())
fmt.Fprint(out, " commented ")
fmt.Fprintf(out, c.Gray("%s\n"), createdAt)
fmt.Fprintln(out, utils.Indent(body, " "))
}
fmt.Fprintln(out)
}
} else {
fmt.Fprintln(out, "This merge request has no comments.")
}
}
fmt.Fprintln(out)
fmt.Fprintf(out, c.Gray("View this merge request on GitLab: %s\n"), mr.WebURL)
return nil
}
func printRawMRPreview(opts *ViewOpts, mr *gitlab.MergeRequest, notes []*gitlab.Note) error {
fmt.Fprint(opts.IO.StdOut, rawMRPreview(opts, mr, notes))
return nil
}
func rawMRPreview(opts *ViewOpts, mr *gitlab.MergeRequest, notes []*gitlab.Note) string {
var out string
assignees := assigneesList(mr)
reviewers := reviewersList(mr)
labels := labelsList(mr)
out += fmt.Sprintf("title:\t%s\n", mr.Title)
out += fmt.Sprintf("state:\t%s\n", mrState(opts.IO.Color(), mr))
out += fmt.Sprintf("author:\t%s\n", mr.Author.Username)
out += fmt.Sprintf("labels:\t%s\n", labels)
out += fmt.Sprintf("assignees:\t%s\n", assignees)
out += fmt.Sprintf("reviewers:\t%s\n", reviewers)
out += fmt.Sprintf("comments:\t%d\n", mr.UserNotesCount)
if mr.Milestone != nil {
out += fmt.Sprintf("milestone:\t%s\n", mr.Milestone.Title)
}
out += fmt.Sprintf("number:\t%d\n", mr.IID)
out += fmt.Sprintf("url:\t%s\n", mr.WebURL)
out += "--\n"
out += fmt.Sprintf("%s\n", mr.Description)
out += issuableView.RawIssuableNotes(notes, opts.ShowComments, opts.ShowSystemLogs, "merge request")
return out
}
func printJSONMR(opts *ViewOpts, mr *gitlab.MergeRequest, notes []*gitlab.Note) error {
if opts.ShowComments {
extendedMR := MRWithNotes{mr, notes}
mrJSON, _ := json.Marshal(extendedMR)
fmt.Fprintln(opts.IO.StdOut, string(mrJSON))
} else {
mrJSON, _ := json.Marshal(mr)
fmt.Fprintln(opts.IO.StdOut, string(mrJSON))
}
return nil
}