mirror of https://gitlab.com/gitlab-org/cli.git
279 lines
8.5 KiB
Go
279 lines
8.5 KiB
Go
package view
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"gitlab.com/gitlab-org/cli/pkg/iostreams"
|
|
|
|
"gitlab.com/gitlab-org/cli/api"
|
|
"gitlab.com/gitlab-org/cli/commands/cmdutils"
|
|
"gitlab.com/gitlab-org/cli/commands/mr/mrutils"
|
|
"gitlab.com/gitlab-org/cli/pkg/utils"
|
|
|
|
"github.com/MakeNowJust/heredoc"
|
|
"github.com/spf13/cobra"
|
|
"github.com/xanzy/go-gitlab"
|
|
)
|
|
|
|
type ViewOpts struct {
|
|
ShowComments bool
|
|
ShowSystemLogs bool
|
|
OpenInBrowser bool
|
|
|
|
CommentPageNumber int
|
|
CommentLimit int
|
|
|
|
IO *iostreams.IOStreams
|
|
}
|
|
|
|
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.Bool(true),
|
|
RenderHTML: gitlab.Bool(true),
|
|
IncludeRebaseInProgress: gitlab.Bool(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/ee/api/projects.html#get-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.String("asc"),
|
|
}
|
|
l.Page = opts.CommentPageNumber
|
|
l.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 f.IO.IsOutputTTY() {
|
|
return printTTYMRPreview(opts, mr, mrApprovals, notes)
|
|
}
|
|
return printRawMRPreview(opts, mr)
|
|
},
|
|
}
|
|
|
|
mrViewCmd.Flags().BoolVarP(&opts.ShowComments, "comments", "c", false, "Show mr comments and activities")
|
|
mrViewCmd.Flags().BoolVarP(&opts.ShowSystemLogs, "system-logs", "s", false, "Show system activities / logs")
|
|
mrViewCmd.Flags().BoolVarP(&opts.OpenInBrowser, "web", "w", false, "Open mr 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 {
|
|
var labels string
|
|
for _, l := range mr.Labels {
|
|
labels += " " + l + ","
|
|
}
|
|
return strings.Trim(labels, ", ")
|
|
}
|
|
|
|
func assigneesList(mr *gitlab.MergeRequest) string {
|
|
var assignees string
|
|
for _, a := range mr.Assignees {
|
|
assignees += " " + a.Username + ","
|
|
}
|
|
return strings.Trim(assignees, ", ")
|
|
}
|
|
|
|
func reviewersList(mr *gitlab.MergeRequest) string {
|
|
var reviewers string
|
|
for _, a := range mr.Reviewers {
|
|
reviewers += " " + a.Username + ","
|
|
}
|
|
return strings.Trim(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 {
|
|
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))
|
|
}
|
|
|
|
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, "There are no comments on this merge request")
|
|
}
|
|
}
|
|
|
|
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) error {
|
|
out := opts.IO.StdOut
|
|
assignees := assigneesList(mr)
|
|
reviewers := reviewersList(mr)
|
|
labels := labelsList(mr)
|
|
|
|
fmt.Fprintf(out, "title:\t%s\n", mr.Title)
|
|
fmt.Fprintf(out, "state:\t%s\n", mrState(opts.IO.Color(), mr))
|
|
fmt.Fprintf(out, "author:\t%s\n", mr.Author.Username)
|
|
fmt.Fprintf(out, "labels:\t%s\n", labels)
|
|
fmt.Fprintf(out, "assignees:\t%s\n", assignees)
|
|
fmt.Fprintf(out, "reviewers:\t%s\n", reviewers)
|
|
fmt.Fprintf(out, "comments:\t%d\n", mr.UserNotesCount)
|
|
if mr.Milestone != nil {
|
|
fmt.Fprintf(out, "milestone:\t%s\n", mr.Milestone.Title)
|
|
}
|
|
fmt.Fprintf(out, "number:\t%d\n", mr.IID)
|
|
fmt.Fprintf(out, "url:\t%s\n", mr.WebURL)
|
|
|
|
fmt.Fprintln(out, "--")
|
|
fmt.Fprintln(out, mr.Description)
|
|
|
|
return nil
|
|
}
|