Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve checkIfPRContentChanged #22611

Merged
merged 11 commits into from
Jan 28, 2023
107 changes: 56 additions & 51 deletions services/pull/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@
package pull

import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"os"
"regexp"
"strings"
"time"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
Expand Down Expand Up @@ -351,69 +350,75 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
// checkIfPRContentChanged checks if diff to target branch has changed by push
// A commit can be considered to leave the PR untouched if the patch/diff with its merge base is unchanged
func checkIfPRContentChanged(ctx context.Context, pr *issues_model.PullRequest, oldCommitID, newCommitID string) (hasChanged bool, err error) {
if err = pr.LoadHeadRepo(ctx); err != nil {
return false, fmt.Errorf("LoadHeadRepo: %w", err)
} else if pr.HeadRepo == nil {
// corrupt data assumed changed
return true, nil
}

if err = pr.LoadBaseRepo(ctx); err != nil {
return false, fmt.Errorf("LoadBaseRepo: %w", err)
}

headGitRepo, err := git.OpenRepository(ctx, pr.HeadRepo.RepoPath())
tmpBasePath, err := createTemporaryRepo(ctx, pr)
if err != nil {
return false, fmt.Errorf("OpenRepository: %w", err)
}
defer headGitRepo.Close()

// Add a temporary remote.
tmpRemote := "checkIfPRContentChanged-" + fmt.Sprint(time.Now().UnixNano())
if err = headGitRepo.AddRemote(tmpRemote, pr.BaseRepo.RepoPath(), true); err != nil {
return false, fmt.Errorf("AddRemote: %s/%s-%s: %w", pr.HeadRepo.OwnerName, pr.HeadRepo.Name, tmpRemote, err)
log.Error("CreateTemporaryRepo: %v", err)
return false, err
}
defer func() {
if err := headGitRepo.RemoveRemote(tmpRemote); err != nil {
log.Error("checkIfPRContentChanged: RemoveRemote: %s/%s-%s: %v", pr.HeadRepo.OwnerName, pr.HeadRepo.Name, tmpRemote, err)
if err := repo_module.RemoveTemporaryPath(tmpBasePath); err != nil {
log.Error("checkIfPRContentChanged: RemoveTemporaryPath: %s", err)
}
}()
// To synchronize repo and get a base ref
_, base, err := headGitRepo.GetMergeBase(tmpRemote, pr.BaseBranch, pr.HeadBranch)

tmpRepo, err := git.OpenRepository(ctx, tmpBasePath)
if err != nil {
return false, fmt.Errorf("GetMergeBase: %w", err)
return false, fmt.Errorf("OpenRepository: %w", err)
}
defer tmpRepo.Close()

diffBefore := &bytes.Buffer{}
diffAfter := &bytes.Buffer{}
if err := headGitRepo.GetDiffFromMergeBase(base, oldCommitID, diffBefore); err != nil {
// If old commit not found, assume changed.
log.Debug("GetDiffFromMergeBase: %v", err)
return true, nil
}
if err := headGitRepo.GetDiffFromMergeBase(base, newCommitID, diffAfter); err != nil {
// New commit should be found
return false, fmt.Errorf("GetDiffFromMergeBase: %w", err)
// Find the merge-base
_, base, err := tmpRepo.GetMergeBase("", "base", "tracking")
if err != nil {
return false, fmt.Errorf("GetMergeBase: %w", err)
}

diffBeforeLines := bufio.NewScanner(diffBefore)
diffAfterLines := bufio.NewScanner(diffAfter)
cmd := git.NewCommand(ctx, "diff", "--name-only").AddDynamicArguments(newCommitID, oldCommitID, base)
stdoutReader, stdoutWriter, err := os.Pipe()
if err != nil {
return false, fmt.Errorf("unable to open pipe for to run diff: %w", err)
}

changedErr := fmt.Errorf("changed files")

if err := cmd.Run(&git.RunOpts{
Dir: tmpBasePath,
Stdout: stdoutWriter,
PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
_ = stdoutWriter.Close()
defer func() {
_ = stdoutReader.Close()
}()
buf := make([]byte, 1024)
for {
n, err := stdoutReader.Read(buf)
if err != nil {
if err == io.EOF {
return nil
}
return err
}

for diffBeforeLines.Scan() && diffAfterLines.Scan() {
if strings.HasPrefix(diffBeforeLines.Text(), "index") && strings.HasPrefix(diffAfterLines.Text(), "index") {
// file hashes can change without the diff changing
continue
} else if strings.HasPrefix(diffBeforeLines.Text(), "@@") && strings.HasPrefix(diffAfterLines.Text(), "@@") {
// the location of the difference may change
continue
} else if !bytes.Equal(diffBeforeLines.Bytes(), diffAfterLines.Bytes()) {
ts := bytes.TrimSpace(buf[:n])
if len(ts) > 0 {
return changedErr
}
}
},
}); err != nil {
if err == changedErr {
return true, nil
}
}
log.Error("Unable to run diff on %s %s %s in tempRepo for PR[%d]%s/%s...%s/%s: Error: %v",
newCommitID, oldCommitID, base,
pr.ID, pr.BaseRepo.FullName(), pr.BaseBranch, pr.HeadRepo.FullName(), pr.HeadBranch,
err)

if diffBeforeLines.Scan() || diffAfterLines.Scan() {
// Diffs not of equal length
return true, nil
// New commit should be found
return false, fmt.Errorf("Unable to run diff on %s %s %s in tempRepo for PR[%d]%s/%s...%s/%s: %w",
newCommitID, oldCommitID, base,
pr.ID, pr.BaseRepo.FullName(), pr.BaseBranch, pr.HeadRepo.FullName(), pr.HeadBranch,
err)
}

return false, nil
Expand Down