mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
134 lines
4.3 KiB
Go
134 lines
4.3 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package git
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/hanzoai/git/modules/git/gitcmd"
|
|
)
|
|
|
|
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(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("--").
|
|
WithDir(repo.Path).
|
|
WithStdoutCopy(w).
|
|
RunWithStderr(repo.Ctx); err != nil {
|
|
if gitcmd.IsStderr(err, gitcmd.StderrNoMergeBase) {
|
|
// git >= 2.28 now returns an error if base and head have become unrelated.
|
|
// it doesn't make sense to count the changed files in this case because UI won't display such diff
|
|
return 0, nil
|
|
}
|
|
return 0, err
|
|
}
|
|
return w.numLines, nil
|
|
}
|
|
|
|
var patchCommits = regexp.MustCompile(`^From\s(\w+)\s`)
|
|
|
|
// GetDiff generates and returns patch data between given revisions, optimized for human readability
|
|
func (repo *Repository) GetDiff(compareArg string, w io.Writer) error {
|
|
return gitcmd.NewCommand("diff", "-p").AddDynamicArguments(compareArg).
|
|
WithDir(repo.Path).
|
|
WithStdoutCopy(w).
|
|
Run(repo.Ctx)
|
|
}
|
|
|
|
// GetDiffBinary generates and returns patch data between given revisions, including binary diffs.
|
|
func (repo *Repository) GetDiffBinary(compareArg string, w io.Writer) error {
|
|
return gitcmd.NewCommand("diff", "-p", "--binary", "--histogram").
|
|
AddDynamicArguments(compareArg).
|
|
WithDir(repo.Path).
|
|
WithStdoutCopy(w).
|
|
Run(repo.Ctx)
|
|
}
|
|
|
|
// GetPatch generates and returns format-patch data between given revisions, able to be used with `git apply`
|
|
func (repo *Repository) GetPatch(compareArg string, w io.Writer) error {
|
|
return gitcmd.NewCommand("format-patch", "--binary", "--stdout").AddDynamicArguments(compareArg).
|
|
WithDir(repo.Path).
|
|
WithStdoutCopy(w).
|
|
Run(repo.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(base, head string) ([]string, error) {
|
|
objectFormat, err := repo.GetObjectFormat()
|
|
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.WithDir(repo.Path).RunStdString(repo.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
|
|
patchFile := fmt.Sprintf("pulls/%d.patch", prID)
|
|
loadPatch, err := os.Open(filepath.Join(repo.Path, patchFile))
|
|
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 := 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
|
|
}
|