github.com/zaquestion/lab@v0.25.1/cmd/show_common.go (about) 1 package cmd 2 3 import ( 4 "bufio" 5 "fmt" 6 "regexp" 7 "strconv" 8 "strings" 9 "time" 10 11 "github.com/MakeNowJust/heredoc/v2" 12 "github.com/araddon/dateparse" 13 "github.com/charmbracelet/glamour" 14 "github.com/charmbracelet/glamour/ansi" 15 "github.com/fatih/color" 16 "github.com/jaytaylor/html2text" 17 "github.com/muesli/termenv" 18 gitlab "github.com/xanzy/go-gitlab" 19 "github.com/zaquestion/lab/internal/config" 20 lab "github.com/zaquestion/lab/internal/gitlab" 21 ) 22 23 func inRange(val int, min int, max int) bool { 24 return val >= min && val <= max 25 } 26 27 // maxPadding returns the max value of two string numbers 28 func maxPadding(x int, y int) int { 29 if x > y { 30 return len(strconv.Itoa(x)) 31 } 32 return len(strconv.Itoa(y)) 33 } 34 35 // printDiffLine does a color print of a diff lines. Red lines are removals 36 // and green lines are additions. 37 func printDiffLine(strColor string, maxChars int, sOld string, sNew string, ltext string) string { 38 39 switch strColor { 40 case "green": 41 return color.GreenString("|%*s %*s %s\n", maxChars, sOld, maxChars, sNew, ltext) 42 case "red": 43 return color.RedString("|%*s %*s %s\n", maxChars, sOld, maxChars, sNew, ltext) 44 } 45 return fmt.Sprintf("|%*s %*s %s\n", maxChars, sOld, maxChars, sNew, ltext) 46 } 47 48 // displayDiff displays the diff referenced in a discussion 49 func displayDiff(diff string, newLine int, oldLine int, outputAll bool) string { 50 var ( 51 oldLineNum int = 0 52 newLineNum int = 0 53 maxChars int 54 output bool = false 55 diffOutput string = "" 56 ) 57 58 scanner := bufio.NewScanner(strings.NewReader(diff)) 59 for scanner.Scan() { 60 if regexp.MustCompile(`^@@`).MatchString(scanner.Text()) { 61 s := strings.Split(scanner.Text(), " ") 62 dOld := strings.Split(s[1], ",") 63 dNew := strings.Split(s[2], ",") 64 65 // get the new line number of the first line of the diff 66 newDiffStart, err := strconv.Atoi(strings.Replace(dNew[0], "+", "", -1)) 67 if err != nil { 68 log.Fatal(err) 69 } 70 newDiffRange := 1 71 if len(dNew) == 2 { 72 newDiffRange, err = strconv.Atoi(dNew[1]) 73 if err != nil { 74 log.Fatal(err) 75 } 76 } 77 newDiffEnd := newDiffStart + newDiffRange 78 newLineNum = newDiffStart - 1 79 80 // get the old line number of the first line of the diff 81 oldDiffStart, err := strconv.Atoi(strings.Replace(dOld[0], "-", "", -1)) 82 if err != nil { 83 log.Fatal(err) 84 } 85 86 oldDiffEnd := newDiffRange 87 oldLineNum = oldDiffStart - 1 88 if len(dOld) > 1 { 89 oldDiffRange, err := strconv.Atoi(dOld[1]) 90 if err != nil { 91 log.Fatal(err) 92 } 93 oldDiffEnd = oldDiffStart + oldDiffRange 94 } 95 96 if (oldLine != 0) && inRange(oldLine, oldDiffStart, oldDiffEnd) { 97 output = true 98 } else if (newLine != 0) && inRange(newLine, newDiffStart, newDiffEnd) { 99 output = true 100 } else { 101 output = false 102 } 103 104 if outputAll { 105 mrShowNoColorDiff = true 106 output = true 107 } 108 109 // padding to align diff output (depends on the line numbers' length) 110 // The patch can have, for example, either 111 // @@ -1 +1 @@ 112 // or 113 // @@ -1272,6 +1272,8 @@ 114 // so (len - 1) makes sense in both cases. 115 maxChars = maxPadding(oldDiffEnd, newDiffEnd) + 1 116 } 117 118 if !output { 119 continue 120 } 121 122 var ( 123 sOld string 124 sNew string 125 ) 126 strColor := "" 127 ltext := scanner.Text() 128 ltag := string(ltext[0]) 129 switch ltag { 130 case " ": 131 strColor = "" 132 oldLineNum++ 133 newLineNum++ 134 sOld = strconv.Itoa(oldLineNum) 135 sNew = strconv.Itoa(newLineNum) 136 case "-": 137 strColor = "red" 138 oldLineNum++ 139 sOld = strconv.Itoa(oldLineNum) 140 sNew = " " 141 case "+": 142 strColor = "green" 143 newLineNum++ 144 sOld = " " 145 sNew = strconv.Itoa(newLineNum) 146 } 147 148 // output line 149 if mrShowNoColorDiff { 150 strColor = "" 151 } 152 if outputAll { 153 diffOutput += printDiffLine(strColor, maxChars, sOld, sNew, ltext) 154 } else if newLine != 0 { 155 if newLineNum <= newLine && newLineNum >= (newLine-4) { 156 diffOutput += printDiffLine(strColor, maxChars, sOld, sNew, ltext) 157 } 158 } else if oldLineNum <= oldLine && oldLineNum >= (oldLine-4) { 159 diffOutput += printDiffLine(strColor, maxChars, sOld, sNew, ltext) 160 } 161 } 162 return diffOutput 163 } 164 165 func displayCommitDiscussion(project string, idNum int, note *gitlab.Note) { 166 167 // Previously, the GitLab API only supports showing comments on the 168 // entire changeset and not per-commit. IOW, all diffs were shown 169 // against HEAD. This was confusing in some scenarios, however it 170 // was what the API provided. 171 // 172 // At some point, note.Position.HeadSHA was changed to report the 173 // commit that was being commented on, and note.Position.BaseSHA 174 // points to the commit just before it. I cannot find the GitLab 175 // commit that changed this behaviour but it has changed. 176 // 177 // I am leaving this comment here in case it ever changes again. 178 179 // In some cases the CommitID field is still not populated correctly. 180 // In those cases use the HeadSHA value instead of the CommitID. 181 commitID := note.CommitID 182 if commitID == "" { 183 commitID = note.Position.HeadSHA 184 } 185 186 // Get a unified diff for the entire file 187 ds, err := lab.GetCommitDiff(project, commitID) 188 if err != nil { 189 fmt.Printf(" Could not get diff for commit %s.\n", commitID) 190 return 191 } 192 193 if len(ds) == 0 { 194 log.Fatal(" No diff found for %s.", commitID) 195 } 196 197 // In general, only have to display the NewPath, however there 198 // are some unusual cases where the OldPath may be displayed 199 if note.Position.NewPath == note.Position.OldPath { 200 fmt.Println("commit:" + commitID) 201 fmt.Println("File:" + note.Position.NewPath) 202 } else { 203 fmt.Println("commit:" + commitID) 204 fmt.Println("Files[old:" + note.Position.OldPath + " new:" + note.Position.NewPath + "]") 205 } 206 207 for _, d := range ds { 208 if note.Position.NewPath == d.NewPath && note.Position.OldPath == d.OldPath { 209 newLine := note.Position.NewLine 210 oldLine := note.Position.OldLine 211 if note.Position.LineRange != nil { 212 newLine = note.Position.LineRange.StartRange.NewLine 213 oldLine = note.Position.LineRange.StartRange.OldLine 214 } 215 diffstring := displayDiff(d.Diff, newLine, oldLine, false) 216 fmt.Printf(diffstring) 217 } 218 } 219 fmt.Println("") 220 } 221 222 func getBoldStyle() ansi.StyleConfig { 223 var style ansi.StyleConfig 224 if termenv.HasDarkBackground() { 225 style = glamour.DarkStyleConfig 226 } else { 227 style = glamour.LightStyleConfig 228 } 229 bold := true 230 style.Document.Bold = &bold 231 return style 232 } 233 234 func getTermRenderer(style glamour.TermRendererOption) (*glamour.TermRenderer, error) { 235 r, err := glamour.NewTermRenderer( 236 glamour.WithWordWrap(0), 237 // There are PAGERs and TERMs that supports only 16 colors, 238 // since we aren't a beauty-driven project, lets use it. 239 glamour.WithColorProfile(termenv.ANSI), 240 style, 241 ) 242 return r, err 243 } 244 245 const ( 246 NoteLevelNone = iota 247 NoteLevelComments 248 NoteLevelActivities 249 NoteLevelFull 250 ) 251 252 func printDiscussions(project string, discussions []*gitlab.Discussion, since string, idstr string, idNum int, renderMarkdown bool, noteLevel int) { 253 newAccessTime := time.Now().UTC() 254 255 issueEntry := fmt.Sprintf("%s%d", idstr, idNum) 256 // if specified on command line use that, o/w use config, o/w Now 257 var ( 258 comparetime time.Time 259 err error 260 sinceIsSet = true 261 ) 262 comparetime, err = dateparse.ParseLocal(since) 263 if err != nil || comparetime.IsZero() { 264 comparetime = getMainConfig().GetTime(commandPrefix + issueEntry) 265 if comparetime.IsZero() { 266 comparetime = time.Now().UTC() 267 } 268 sinceIsSet = false 269 } 270 271 mdRendererNormal, _ := getTermRenderer(glamour.WithAutoStyle()) 272 mdRendererBold, _ := getTermRenderer(glamour.WithStyles(getBoldStyle())) 273 274 // for available fields, see 275 // https://godoc.org/github.com/xanzy/go-gitlab#Note 276 // https://godoc.org/github.com/xanzy/go-gitlab#Discussion 277 for _, discussion := range discussions { 278 for i, note := range discussion.Notes { 279 if (noteLevel == NoteLevelActivities && note.System == false) || 280 (noteLevel == NoteLevelComments && note.System == true) { 281 continue 282 } 283 indentHeader, indentNote := "", "" 284 commented := "commented" 285 if !time.Time(*note.CreatedAt).Equal(time.Time(*note.UpdatedAt)) { 286 commented = "updated comment" 287 } 288 289 if !discussion.IndividualNote { 290 indentNote = " " 291 292 if i == 0 { 293 commented = "started a discussion" 294 if note.Position != nil { 295 commented = "started a commit discussion" 296 } 297 } else { 298 indentHeader = " " 299 } 300 } 301 302 noteBody := strings.Replace(note.Body, "\n", "<br>\n", -1) 303 html2textOptions := html2text.Options{ 304 PrettyTables: true, 305 OmitLinks: true, 306 } 307 noteBody, _ = html2text.FromString(noteBody, html2textOptions) 308 mdRenderer := mdRendererNormal 309 printit := color.New().PrintfFunc() 310 if note.System { 311 splitNote := strings.SplitN(noteBody, "\n", 2) 312 313 // system notes are informational messages only 314 // and cannot have replies. Do not output the 315 // note.ID 316 printit( 317 heredoc.Doc(` 318 * %s %s at %s 319 `), 320 note.Author.Username, splitNote[0], time.Time(*note.UpdatedAt).String()) 321 if len(splitNote) == 2 { 322 if renderMarkdown { 323 splitNote[1], _ = mdRenderer.Render(splitNote[1]) 324 } 325 printit( 326 heredoc.Doc(` 327 %s 328 `), 329 splitNote[1]) 330 } 331 continue 332 } 333 334 printit( 335 heredoc.Doc(` 336 %s----------------------------------- 337 `), 338 indentHeader) 339 340 if time.Time(*note.UpdatedAt).After(comparetime) { 341 mdRenderer = mdRendererBold 342 printit = color.New(color.Bold).PrintfFunc() 343 } 344 345 if renderMarkdown { 346 noteBody, _ = mdRenderer.Render(noteBody) 347 } 348 noteBody = strings.Replace(noteBody, "\n", "\n"+indentNote, -1) 349 350 printit(`%s#%d: %s %s at %s 351 352 `, 353 indentHeader, note.ID, note.Author.Username, commented, time.Time(*note.UpdatedAt).String()) 354 if note.Position != nil && i == 0 { 355 displayCommitDiscussion(project, idNum, note) 356 } 357 printit(`%s%s 358 `, 359 360 indentNote, noteBody) 361 } 362 } 363 364 if !sinceIsSet { 365 config.WriteConfigEntry(commandPrefix+issueEntry, newAccessTime, "", "") 366 } 367 }