Files
gitea-MIRROR/modules/git/repo_compare.go
wxiaoguang 772f365254 refactor: use RunWithStderr for git commands (#40295)
If the stderr output is not handled, then it needs to use RunWithStderr
to make sure the git error message is included in the returned error.
Otherwise, when the git command fails, the error log won't show useful
information.

Ref:

* #39795 : no useful information in log messages
*
https://github.com/go-gitea/gitea/pull/40160#pullrequestreview-5752720866
2026-11-09 17:59:52 +00:00

127 lines
4.2 KiB
Go

// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
"strings"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/git/gitrepo"
)
type lineCountWriter struct {
numLines int
}
// Write counts the number of newlines in the provided bytestream
func (l *lineCountWriter) Write(p []byte) (n int, err error) {
n = len(p)
l.numLines += bytes.Count(p, []byte{'\000'})
return n, err
}
// GetDiffNumChangedFiles counts the number of changed files
// This is substantially quicker than shortstat but...
func (repo *Repository) GetDiffNumChangedFiles(ctx context.Context, base, head string, directComparison bool) (int, error) {
// Now there is git diff --shortstat but this appears to be slower than simply iterating with --nameonly
w := &lineCountWriter{}
separator := "..."
if directComparison {
separator = ".."
}
if err := gitcmd.NewCommand("diff", "-z", "--name-only").
AddDynamicArguments(base + separator + head).
AddArguments("--").
WithRepo(repo).
WithStdoutCopy(w).
RunWithStderr(ctx); err != nil {
return 0, err
}
return w.numLines, nil
}
// GetDiff generates and returns patch data between given revisions, optimized for human readability
func (repo *Repository) GetDiff(ctx context.Context, compareArg string, w io.Writer) error {
return gitcmd.NewCommand("diff", "-p").AddDynamicArguments(compareArg).
WithRepo(repo).
WithStdoutCopy(w).
RunWithStderr(ctx)
}
// GetDiffBinary generates and returns patch data between given revisions, including binary diffs.
func (repo *Repository) GetDiffBinary(ctx context.Context, compareArg string, w io.Writer) error {
return gitcmd.NewCommand("diff", "-p", "--binary", "--histogram").
AddDynamicArguments(compareArg).
WithRepo(repo).
WithStdoutCopy(w).
RunWithStderr(ctx)
}
// GetPatch generates and returns format-patch data between given revisions, able to be used with `git apply`
func (repo *Repository) GetPatch(ctx context.Context, compareArg string, w io.Writer) error {
return gitcmd.NewCommand("format-patch", "--binary", "--stdout").AddDynamicArguments(compareArg).
WithRepo(repo).
WithStdoutCopy(w).
RunWithStderr(ctx)
}
// GetFilesChangedBetween returns a list of all files that have been changed between the given commits
// If base is undefined empty SHA (zeros), it only returns the files changed in the head commit
// If base is the SHA of an empty tree (EmptyTreeSHA), it returns the files changes from the initial commit to the head commit
func (repo *Repository) GetFilesChangedBetween(ctx context.Context, base, head string) ([]string, error) {
objectFormat, err := repo.GetObjectFormat(ctx)
if err != nil {
return nil, err
}
cmd := gitcmd.NewCommand("diff-tree", "--name-only", "--root", "--no-commit-id", "-r", "-z")
if base == objectFormat.EmptyObjectID().String() {
cmd.AddDynamicArguments(head)
} else {
cmd.AddDynamicArguments(base, head)
}
stdout, _, err := cmd.WithRepo(repo).RunStdString(ctx)
if err != nil {
return nil, err
}
split := strings.Split(stdout, "\000")
// Because Git will always emit filenames with a terminal NUL ignore the last entry in the split - which will always be empty.
if len(split) > 0 {
split = split[:len(split)-1]
}
return split, err
}
// ReadPatchCommit will check if a diff patch exists and return stats
func (repo *Repository) ReadPatchCommit(prID int64) (commitSHA string, err error) {
// Migrated repositories download patches to "pulls" location
repoFS := gitrepo.RepoLocalFS(repo)
loadPatch, err := repoFS.Open(fmt.Sprintf("pulls/%d.patch", prID))
if err != nil {
return "", err
}
defer loadPatch.Close()
// Read only the first line of the patch - usually it contains the first commit made in patch
scanner := bufio.NewScanner(loadPatch)
scanner.Scan()
// Parse the Patch stats, sometimes Migration returns a 404 for the patch file
commitSHAGroups := globalVars().patchCommits.FindStringSubmatch(scanner.Text())
if len(commitSHAGroups) != 0 {
commitSHA = commitSHAGroups[1]
} else {
return "", errors.New("patch file doesn't contain valid commit ID")
}
return commitSHA, nil
}