git.go 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. package git
  2. import (
  3. "bufio"
  4. "io"
  5. "os/exec"
  6. "path/filepath"
  7. "regexp"
  8. "strings"
  9. "time"
  10. "github.com/gitleaks/go-gitdiff/gitdiff"
  11. "github.com/rs/zerolog/log"
  12. )
  13. var ErrEncountered bool
  14. // GitLog returns a channel of gitdiff.File objects from the
  15. // git log -p command for the given source.
  16. var quotedOptPattern = regexp.MustCompile(`^(?:"[^"]+"|'[^']+')$`)
  17. func GitLog(source string, logOpts string) (<-chan *gitdiff.File, error) {
  18. sourceClean := filepath.Clean(source)
  19. var cmd *exec.Cmd
  20. if logOpts != "" {
  21. args := []string{"-C", sourceClean, "log", "-p", "-U0"}
  22. // Ensure that the user-provided |logOpts| aren't wrapped in quotes.
  23. // https://github.com/gitleaks/gitleaks/issues/1153
  24. userArgs := strings.Split(logOpts, " ")
  25. var quotedOpts []string
  26. for _, element := range userArgs {
  27. if quotedOptPattern.MatchString(element) {
  28. quotedOpts = append(quotedOpts, element)
  29. }
  30. }
  31. if len(quotedOpts) > 0 {
  32. log.Warn().Msgf("the following `--log-opts` values may not work as expected: %v\n\tsee https://github.com/gitleaks/gitleaks/issues/1153 for more information", quotedOpts)
  33. }
  34. args = append(args, userArgs...)
  35. cmd = exec.Command("git", args...)
  36. } else {
  37. cmd = exec.Command("git", "-C", sourceClean, "log", "-p", "-U0",
  38. "--full-history", "--all")
  39. }
  40. log.Debug().Msgf("executing: %s", cmd.String())
  41. stdout, err := cmd.StdoutPipe()
  42. if err != nil {
  43. return nil, err
  44. }
  45. stderr, err := cmd.StderrPipe()
  46. if err != nil {
  47. return nil, err
  48. }
  49. go listenForStdErr(stderr)
  50. if err := cmd.Start(); err != nil {
  51. return nil, err
  52. }
  53. // HACK: to avoid https://github.com/zricethezav/gitleaks/issues/722
  54. time.Sleep(50 * time.Millisecond)
  55. return gitdiff.Parse(cmd, stdout)
  56. }
  57. // GitDiff returns a channel of gitdiff.File objects from
  58. // the git diff command for the given source.
  59. func GitDiff(source string, staged bool) (<-chan *gitdiff.File, error) {
  60. sourceClean := filepath.Clean(source)
  61. var cmd *exec.Cmd
  62. cmd = exec.Command("git", "-C", sourceClean, "diff", "-U0", ".")
  63. if staged {
  64. cmd = exec.Command("git", "-C", sourceClean, "diff", "-U0",
  65. "--staged", ".")
  66. }
  67. log.Debug().Msgf("executing: %s", cmd.String())
  68. stdout, err := cmd.StdoutPipe()
  69. if err != nil {
  70. return nil, err
  71. }
  72. stderr, err := cmd.StderrPipe()
  73. if err != nil {
  74. return nil, err
  75. }
  76. go listenForStdErr(stderr)
  77. if err := cmd.Start(); err != nil {
  78. return nil, err
  79. }
  80. // HACK: to avoid https://github.com/zricethezav/gitleaks/issues/722
  81. time.Sleep(50 * time.Millisecond)
  82. return gitdiff.Parse(cmd, stdout)
  83. }
  84. // listenForStdErr listens for stderr output from git and prints it to stdout
  85. // then exits with exit code 1
  86. func listenForStdErr(stderr io.ReadCloser) {
  87. scanner := bufio.NewScanner(stderr)
  88. for scanner.Scan() {
  89. // if git throws one of the following errors:
  90. //
  91. // exhaustive rename detection was skipped due to too many files.
  92. // you may want to set your diff.renameLimit variable to at least
  93. // (some large number) and retry the command.
  94. //
  95. // inexact rename detection was skipped due to too many files.
  96. // you may want to set your diff.renameLimit variable to at least
  97. // (some large number) and retry the command.
  98. //
  99. // we skip exiting the program as git log -p/git diff will continue
  100. // to send data to stdout and finish executing. This next bit of
  101. // code prevents gitleaks from stopping mid scan if this error is
  102. // encountered
  103. if strings.Contains(scanner.Text(),
  104. "exhaustive rename detection was skipped") ||
  105. strings.Contains(scanner.Text(),
  106. "inexact rename detection was skipped") ||
  107. strings.Contains(scanner.Text(),
  108. "you may want to set your diff.renameLimit") {
  109. log.Warn().Msg(scanner.Text())
  110. } else {
  111. log.Error().Msgf("[git] %s", scanner.Text())
  112. // asynchronously set this error flag to true so that we can
  113. // capture a log message and exit with a non-zero exit code
  114. // This value should get set before the `git` command exits so it's
  115. // safe-ish, although I know I know, bad practice.
  116. ErrEncountered = true
  117. }
  118. }
  119. }