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  }