Skip to content

Commit

Permalink
Enable show more files in diff for git <2.31 (#17733)
Browse files Browse the repository at this point in the history
Unfortunately due to a misread on my behalf I missed that git diff only learned
--skip-to in version 2.31.0. Thus this functionality was not working on older versions
of git.

This PR adds a handler that simply allows for us to skip reading the diffs until
we find the correct file to skip to.

Fix #17731

Signed-off-by: Andrew Thornton <art27@cantab.net>
  • Loading branch information
zeripath committed Nov 20, 2021
1 parent 0d69e64 commit 931d0cf
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 32 deletions.
2 changes: 1 addition & 1 deletion modules/repofiles/temp_repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ func (t *TemporaryUploadRepository) DiffIndex() (*gitdiff.Diff, error) {
if err := git.NewCommand("diff-index", "--src-prefix=\\a/", "--dst-prefix=\\b/", "--cached", "-p", "HEAD").
RunInDirTimeoutEnvFullPipelineFunc(nil, 30*time.Second, t.basePath, stdoutWriter, stderr, nil, func(ctx context.Context, cancel context.CancelFunc) error {
_ = stdoutWriter.Close()
diff, finalErr = gitdiff.ParsePatch(setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, stdoutReader)
diff, finalErr = gitdiff.ParsePatch(setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, stdoutReader, "")
if finalErr != nil {
log.Error("ParsePatch: %v", finalErr)
cancel()
Expand Down
2 changes: 1 addition & 1 deletion services/gitdiff/csv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ c,d,e`,
}

for n, c := range cases {
diff, err := ParsePatch(setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(c.diff))
diff, err := ParsePatch(setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(c.diff), "")
if err != nil {
t.Errorf("ParsePatch failed: %s", err)
}
Expand Down
88 changes: 69 additions & 19 deletions services/gitdiff/gitdiff.go
Original file line number Diff line number Diff line change
Expand Up @@ -694,9 +694,11 @@ func (diff *Diff) LoadComments(issue *models.Issue, currentUser *models.User) er
const cmdDiffHead = "diff --git "

// ParsePatch builds a Diff object from a io.Reader and some parameters.
func ParsePatch(maxLines, maxLineCharacters, maxFiles int, reader io.Reader) (*Diff, error) {
func ParsePatch(maxLines, maxLineCharacters, maxFiles int, reader io.Reader, skipToFile string) (*Diff, error) {
var curFile *DiffFile

skipping := skipToFile != ""

diff := &Diff{Files: make([]*DiffFile, 0)}

sb := strings.Builder{}
Expand All @@ -721,24 +723,36 @@ parsingLoop:
// 1. A patch file always begins with `diff --git ` + `a/path b/path` (possibly quoted)
// if it does not we have bad input!
if !strings.HasPrefix(line, cmdDiffHead) {
return diff, fmt.Errorf("Invalid first file line: %s", line)
return diff, fmt.Errorf("invalid first file line: %s", line)
}

// TODO: Handle skipping first n files
if len(diff.Files) >= maxFiles {

lastFile := createDiffFile(diff, line)
diff.End = lastFile.Name
diff.IsIncomplete = true
_, err := io.Copy(io.Discard, reader)
if err != nil {
// By the definition of io.Copy this never returns io.EOF
return diff, fmt.Errorf("Copy: %v", err)
return diff, fmt.Errorf("error during io.Copy: %w", err)
}
break parsingLoop
}

curFile = createDiffFile(diff, line)
if skipping {
if curFile.Name != skipToFile {
line, err = skipToNextDiffHead(input)
if err != nil {
if err == io.EOF {
return diff, nil
}
return diff, err
}
continue
}
skipping = false
}

diff.Files = append(diff.Files, curFile)

// 2. It is followed by one or more extended header lines:
Expand Down Expand Up @@ -892,7 +906,7 @@ parsingLoop:
lineBytes, isFragment, err = input.ReadLine()
if err != nil {
// Now by the definition of ReadLine this cannot be io.EOF
return diff, fmt.Errorf("Unable to ReadLine: %v", err)
return diff, fmt.Errorf("unable to ReadLine: %w", err)
}
_, _ = sb.Write(lineBytes)
}
Expand Down Expand Up @@ -955,6 +969,36 @@ parsingLoop:
return diff, nil
}

func skipToNextDiffHead(input *bufio.Reader) (line string, err error) {
// need to skip until the next cmdDiffHead
isFragment, wasFragment := false, false
var lineBytes []byte
for {
lineBytes, isFragment, err = input.ReadLine()
if err != nil {
return
}
if wasFragment {
wasFragment = isFragment
continue
}
if bytes.HasPrefix(lineBytes, []byte(cmdDiffHead)) {
break
}
wasFragment = isFragment
}
line = string(lineBytes)
if isFragment {
var tail string
tail, err = input.ReadString('\n')
if err != nil {
return
}
line += tail
}
return
}

func parseHunks(curFile *DiffFile, maxLines, maxLineCharacters int, input *bufio.Reader) (lineBytes []byte, isFragment bool, err error) {
sb := strings.Builder{}

Expand All @@ -974,7 +1018,7 @@ func parseHunks(curFile *DiffFile, maxLines, maxLineCharacters int, input *bufio
_, isFragment, err = input.ReadLine()
if err != nil {
// Now by the definition of ReadLine this cannot be io.EOF
err = fmt.Errorf("Unable to ReadLine: %v", err)
err = fmt.Errorf("unable to ReadLine: %w", err)
return
}
}
Expand All @@ -984,7 +1028,7 @@ func parseHunks(curFile *DiffFile, maxLines, maxLineCharacters int, input *bufio
if err == io.EOF {
return
}
err = fmt.Errorf("Unable to ReadLine: %v", err)
err = fmt.Errorf("unable to ReadLine: %w", err)
return
}
if lineBytes[0] == 'd' {
Expand All @@ -1006,7 +1050,7 @@ func parseHunks(curFile *DiffFile, maxLines, maxLineCharacters int, input *bufio
lineBytes, isFragment, err = input.ReadLine()
if err != nil {
// Now by the definition of ReadLine this cannot be io.EOF
err = fmt.Errorf("Unable to ReadLine: %v", err)
err = fmt.Errorf("unable to ReadLine: %w", err)
return
}
_, _ = sb.Write(lineBytes)
Expand Down Expand Up @@ -1037,7 +1081,7 @@ func parseHunks(curFile *DiffFile, maxLines, maxLineCharacters int, input *bufio
}
// This is used only to indicate that the current file does not have a terminal newline
if !bytes.Equal(lineBytes, []byte("\\ No newline at end of file")) {
err = fmt.Errorf("Unexpected line in hunk: %s", string(lineBytes))
err = fmt.Errorf("unexpected line in hunk: %s", string(lineBytes))
return
}
// Technically this should be the end the file!
Expand Down Expand Up @@ -1106,7 +1150,7 @@ func parseHunks(curFile *DiffFile, maxLines, maxLineCharacters int, input *bufio
curSection.Lines = append(curSection.Lines, diffLine)
default:
// This is unexpected
err = fmt.Errorf("Unexpected line in hunk: %s", string(lineBytes))
err = fmt.Errorf("unexpected line in hunk: %s", string(lineBytes))
return
}

Expand All @@ -1118,7 +1162,7 @@ func parseHunks(curFile *DiffFile, maxLines, maxLineCharacters int, input *bufio
lineBytes, isFragment, err = input.ReadLine()
if err != nil {
// Now by the definition of ReadLine this cannot be io.EOF
err = fmt.Errorf("Unable to ReadLine: %v", err)
err = fmt.Errorf("unable to ReadLine: %w", err)
return
}
}
Expand Down Expand Up @@ -1279,8 +1323,14 @@ func GetDiffRangeWithWhitespaceBehavior(gitRepo *git.Repository, beforeCommitID,
diffArgs = append(diffArgs, afterCommitID)
beforeCommitID = actualBeforeCommitID
}
if skipTo != "" {

// In git 2.31, git diff learned --skip-to which we can use to shortcut skip to file
// so if we are using at least this version of git we don't have to tell ParsePatch to do
// the skipping for us
parsePatchSkipToFile := skipTo
if skipTo != "" && git.CheckGitVersionAtLeast("2.31") == nil {
diffArgs = append(diffArgs, "--skip-to="+skipTo)
parsePatchSkipToFile = ""
}
cmd := exec.CommandContext(ctx, git.GitExecutable, diffArgs...)

Expand All @@ -1289,19 +1339,19 @@ func GetDiffRangeWithWhitespaceBehavior(gitRepo *git.Repository, beforeCommitID,

stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, fmt.Errorf("StdoutPipe: %v", err)
return nil, fmt.Errorf("error creating StdoutPipe: %w", err)
}

if err = cmd.Start(); err != nil {
return nil, fmt.Errorf("Start: %v", err)
return nil, fmt.Errorf("error during Start: %w", err)
}

pid := process.GetManager().Add(fmt.Sprintf("GetDiffRange [repo_path: %s]", repoPath), cancel)
defer process.GetManager().Remove(pid)

diff, err := ParsePatch(maxLines, maxLineCharacters, maxFiles, stdout)
diff, err := ParsePatch(maxLines, maxLineCharacters, maxFiles, stdout, parsePatchSkipToFile)
if err != nil {
return nil, fmt.Errorf("ParsePatch: %v", err)
return nil, fmt.Errorf("unable to ParsePatch: %w", err)
}
diff.Start = skipTo

Expand Down Expand Up @@ -1383,7 +1433,7 @@ func GetDiffRangeWithWhitespaceBehavior(gitRepo *git.Repository, beforeCommitID,
}

if err = cmd.Wait(); err != nil {
return nil, fmt.Errorf("Wait: %v", err)
return nil, fmt.Errorf("error during cmd.Wait: %w", err)
}

separator := "..."
Expand Down Expand Up @@ -1418,7 +1468,7 @@ func GetDiffCommitWithWhitespaceBehavior(gitRepo *git.Repository, commitID, skip
// CommentAsDiff returns c.Patch as *Diff
func CommentAsDiff(c *models.Comment) (*Diff, error) {
diff, err := ParsePatch(setting.Git.MaxGitDiffLines,
setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(c.Patch))
setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(c.Patch), "")
if err != nil {
log.Error("Unable to parse patch: %v", err)
return nil, err
Expand Down
Loading

0 comments on commit 931d0cf

Please sign in to comment.