github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/pr/shared/comments.go (about)

     1  package shared
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"strings"
     7  	"time"
     8  
     9  	"github.com/ungtb10d/cli/v2/api"
    10  	"github.com/ungtb10d/cli/v2/internal/text"
    11  	"github.com/ungtb10d/cli/v2/pkg/iostreams"
    12  	"github.com/ungtb10d/cli/v2/pkg/markdown"
    13  )
    14  
    15  type Comment interface {
    16  	Identifier() string
    17  	AuthorLogin() string
    18  	Association() string
    19  	Content() string
    20  	Created() time.Time
    21  	HiddenReason() string
    22  	IsEdited() bool
    23  	IsHidden() bool
    24  	Link() string
    25  	Reactions() api.ReactionGroups
    26  	Status() string
    27  }
    28  
    29  func RawCommentList(comments api.Comments, reviews api.PullRequestReviews) string {
    30  	sortedComments := sortComments(comments, reviews)
    31  	var b strings.Builder
    32  	for _, comment := range sortedComments {
    33  		fmt.Fprint(&b, formatRawComment(comment))
    34  	}
    35  	return b.String()
    36  }
    37  
    38  func formatRawComment(comment Comment) string {
    39  	if comment.IsHidden() {
    40  		return ""
    41  	}
    42  	var b strings.Builder
    43  	fmt.Fprintf(&b, "author:\t%s\n", comment.AuthorLogin())
    44  	fmt.Fprintf(&b, "association:\t%s\n", strings.ToLower(comment.Association()))
    45  	fmt.Fprintf(&b, "edited:\t%t\n", comment.IsEdited())
    46  	fmt.Fprintf(&b, "status:\t%s\n", formatRawCommentStatus(comment.Status()))
    47  	fmt.Fprintln(&b, "--")
    48  	fmt.Fprintln(&b, comment.Content())
    49  	fmt.Fprintln(&b, "--")
    50  	return b.String()
    51  }
    52  
    53  func CommentList(io *iostreams.IOStreams, comments api.Comments, reviews api.PullRequestReviews, preview bool) (string, error) {
    54  	sortedComments := sortComments(comments, reviews)
    55  	if preview && len(sortedComments) > 0 {
    56  		sortedComments = sortedComments[len(sortedComments)-1:]
    57  	}
    58  	var b strings.Builder
    59  	cs := io.ColorScheme()
    60  	totalCount := comments.TotalCount + reviews.TotalCount
    61  	retrievedCount := len(sortedComments)
    62  	hiddenCount := totalCount - retrievedCount
    63  
    64  	if preview && hiddenCount > 0 {
    65  		fmt.Fprint(&b, cs.Gray(fmt.Sprintf("———————— Not showing %s ————————", text.Pluralize(hiddenCount, "comment"))))
    66  		fmt.Fprintf(&b, "\n\n\n")
    67  	}
    68  
    69  	for i, comment := range sortedComments {
    70  		last := i+1 == retrievedCount
    71  		cmt, err := formatComment(io, comment, last)
    72  		if err != nil {
    73  			return "", err
    74  		}
    75  		fmt.Fprint(&b, cmt)
    76  		if last {
    77  			fmt.Fprintln(&b)
    78  		}
    79  	}
    80  
    81  	if preview && hiddenCount > 0 {
    82  		fmt.Fprint(&b, cs.Gray("Use --comments to view the full conversation"))
    83  		fmt.Fprintln(&b)
    84  	}
    85  
    86  	return b.String(), nil
    87  }
    88  
    89  func formatComment(io *iostreams.IOStreams, comment Comment, newest bool) (string, error) {
    90  	var b strings.Builder
    91  	cs := io.ColorScheme()
    92  
    93  	if comment.IsHidden() {
    94  		return cs.Bold(formatHiddenComment(comment)), nil
    95  	}
    96  
    97  	// Header
    98  	fmt.Fprint(&b, cs.Bold(comment.AuthorLogin()))
    99  	if comment.Status() != "" {
   100  		fmt.Fprint(&b, formatCommentStatus(cs, comment.Status()))
   101  	}
   102  	if comment.Association() != "NONE" {
   103  		fmt.Fprint(&b, cs.Boldf(" (%s)", text.Title(comment.Association())))
   104  	}
   105  	fmt.Fprint(&b, cs.Boldf(" • %s", text.FuzzyAgoAbbr(time.Now(), comment.Created())))
   106  	if comment.IsEdited() {
   107  		fmt.Fprint(&b, cs.Bold(" • Edited"))
   108  	}
   109  	if newest {
   110  		fmt.Fprint(&b, cs.Bold(" • "))
   111  		fmt.Fprint(&b, cs.CyanBold("Newest comment"))
   112  	}
   113  	fmt.Fprintln(&b)
   114  
   115  	// Reactions
   116  	if reactions := ReactionGroupList(comment.Reactions()); reactions != "" {
   117  		fmt.Fprint(&b, reactions)
   118  		fmt.Fprintln(&b)
   119  	}
   120  
   121  	// Body
   122  	var md string
   123  	var err error
   124  	if comment.Content() == "" {
   125  		md = fmt.Sprintf("\n  %s\n\n", cs.Gray("No body provided"))
   126  	} else {
   127  		md, err = markdown.Render(comment.Content(),
   128  			markdown.WithTheme(io.TerminalTheme()),
   129  			markdown.WithWrap(io.TerminalWidth()))
   130  		if err != nil {
   131  			return "", err
   132  		}
   133  	}
   134  	fmt.Fprint(&b, md)
   135  
   136  	// Footer
   137  	if comment.Link() != "" {
   138  		fmt.Fprintf(&b, cs.Gray("View the full review: %s\n\n"), comment.Link())
   139  	}
   140  
   141  	return b.String(), nil
   142  }
   143  
   144  func sortComments(cs api.Comments, rs api.PullRequestReviews) []Comment {
   145  	comments := cs.Nodes
   146  	reviews := rs.Nodes
   147  	var sorted []Comment = make([]Comment, len(comments)+len(reviews))
   148  
   149  	var i int
   150  	for _, c := range comments {
   151  		sorted[i] = c
   152  		i++
   153  	}
   154  	for _, r := range reviews {
   155  		sorted[i] = r
   156  		i++
   157  	}
   158  
   159  	sort.Slice(sorted, func(i, j int) bool {
   160  		return sorted[i].Created().Before(sorted[j].Created())
   161  	})
   162  
   163  	return sorted
   164  }
   165  
   166  const (
   167  	approvedStatus         = "APPROVED"
   168  	changesRequestedStatus = "CHANGES_REQUESTED"
   169  	commentedStatus        = "COMMENTED"
   170  	dismissedStatus        = "DISMISSED"
   171  )
   172  
   173  func formatCommentStatus(cs *iostreams.ColorScheme, status string) string {
   174  	switch status {
   175  	case approvedStatus:
   176  		return fmt.Sprintf(" %s", cs.Green("approved"))
   177  	case changesRequestedStatus:
   178  		return fmt.Sprintf(" %s", cs.Red("requested changes"))
   179  	case commentedStatus, dismissedStatus:
   180  		return fmt.Sprintf(" %s", strings.ToLower(status))
   181  	}
   182  
   183  	return ""
   184  }
   185  
   186  func formatRawCommentStatus(status string) string {
   187  	if status == approvedStatus ||
   188  		status == changesRequestedStatus ||
   189  		status == commentedStatus ||
   190  		status == dismissedStatus {
   191  		return strings.ReplaceAll(strings.ToLower(status), "_", " ")
   192  	}
   193  
   194  	return "none"
   195  }
   196  
   197  func formatHiddenComment(comment Comment) string {
   198  	var b strings.Builder
   199  	fmt.Fprint(&b, comment.AuthorLogin())
   200  	if comment.Association() != "NONE" {
   201  		fmt.Fprintf(&b, " (%s)", text.Title(comment.Association()))
   202  	}
   203  	fmt.Fprintf(&b, " • This comment has been marked as %s\n\n", comment.HiddenReason())
   204  	return b.String()
   205  }