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