mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-27 09:49:02 +02:00
Merge remote-tracking branch 'upstream/main' into limit-repo-size
This commit is contained in:
@@ -7,7 +7,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
type BatchChecker struct {
|
||||
attributesNum int
|
||||
repo *git.Repository
|
||||
stdinWriter *os.File
|
||||
stdinWriter io.WriteCloser
|
||||
stdOut *nulSeparatedAttributeWriter
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
@@ -60,10 +60,7 @@ func NewBatchChecker(repo *git.Repository, treeish string, attributes []string)
|
||||
},
|
||||
}
|
||||
|
||||
stdinReader, stdinWriter, err := os.Pipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stdinWriter, stdinWriterClose := cmd.MakeStdinPipe()
|
||||
checker.stdinWriter = stdinWriter
|
||||
|
||||
lw := new(nulSeparatedAttributeWriter)
|
||||
@@ -71,21 +68,19 @@ func NewBatchChecker(repo *git.Repository, treeish string, attributes []string)
|
||||
lw.closed = make(chan struct{})
|
||||
checker.stdOut = lw
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
_ = stdinReader.Close()
|
||||
_ = lw.Close()
|
||||
}()
|
||||
err := cmd.WithEnv(envs).
|
||||
WithDir(repo.Path).
|
||||
WithStdin(stdinReader).
|
||||
WithStdout(lw).
|
||||
RunWithStderr(ctx)
|
||||
cmd.WithEnv(envs).
|
||||
WithDir(repo.Path).
|
||||
WithStdoutCopy(lw)
|
||||
|
||||
go func() {
|
||||
defer stdinWriterClose()
|
||||
defer checker.cancel()
|
||||
defer lw.Close()
|
||||
|
||||
err := cmd.RunWithStderr(ctx)
|
||||
if err != nil && !gitcmd.IsErrorCanceledOrKilled(err) {
|
||||
log.Error("Attribute checker for commit %s exits with error: %v", treeish, err)
|
||||
}
|
||||
checker.cancel()
|
||||
}()
|
||||
|
||||
return checker, nil
|
||||
|
||||
@@ -68,15 +68,14 @@ func CheckAttributes(ctx context.Context, gitRepo *git.Repository, treeish strin
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
stdOut := new(bytes.Buffer)
|
||||
if err := cmd.WithEnv(append(os.Environ(), envs...)).
|
||||
stdout, _, err := cmd.WithEnv(append(os.Environ(), envs...)).
|
||||
WithDir(gitRepo.Path).
|
||||
WithStdout(stdOut).
|
||||
RunWithStderr(ctx); err != nil {
|
||||
RunStdBytes(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to run check-attr: %w", err)
|
||||
}
|
||||
|
||||
fields := bytes.Split(stdOut.Bytes(), []byte{'\000'})
|
||||
fields := bytes.Split(stdout, []byte{'\000'})
|
||||
if len(fields)%3 != 1 {
|
||||
return nil, errors.New("wrong number of fields in return from check-attr")
|
||||
}
|
||||
|
||||
@@ -39,23 +39,23 @@ func (b *catFileBatchCommand) getBatch() *catFileBatchCommunicator {
|
||||
}
|
||||
|
||||
func (b *catFileBatchCommand) QueryContent(obj string) (*CatFileObject, BufferedReader, error) {
|
||||
_, err := b.getBatch().writer.Write([]byte("contents " + obj + "\n"))
|
||||
_, err := b.getBatch().reqWriter.Write([]byte("contents " + obj + "\n"))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
info, err := catFileBatchParseInfoLine(b.getBatch().reader)
|
||||
info, err := catFileBatchParseInfoLine(b.getBatch().respReader)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return info, b.getBatch().reader, nil
|
||||
return info, b.getBatch().respReader, nil
|
||||
}
|
||||
|
||||
func (b *catFileBatchCommand) QueryInfo(obj string) (*CatFileObject, error) {
|
||||
_, err := b.getBatch().writer.Write([]byte("info " + obj + "\n"))
|
||||
_, err := b.getBatch().reqWriter.Write([]byte("info " + obj + "\n"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return catFileBatchParseInfoLine(b.getBatch().reader)
|
||||
return catFileBatchParseInfoLine(b.getBatch().respReader)
|
||||
}
|
||||
|
||||
func (b *catFileBatchCommand) Close() {
|
||||
|
||||
@@ -50,23 +50,23 @@ func (b *catFileBatchLegacy) getBatchCheck() *catFileBatchCommunicator {
|
||||
}
|
||||
|
||||
func (b *catFileBatchLegacy) QueryContent(obj string) (*CatFileObject, BufferedReader, error) {
|
||||
_, err := io.WriteString(b.getBatchContent().writer, obj+"\n")
|
||||
_, err := io.WriteString(b.getBatchContent().reqWriter, obj+"\n")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
info, err := catFileBatchParseInfoLine(b.getBatchContent().reader)
|
||||
info, err := catFileBatchParseInfoLine(b.getBatchContent().respReader)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return info, b.getBatchContent().reader, nil
|
||||
return info, b.getBatchContent().respReader, nil
|
||||
}
|
||||
|
||||
func (b *catFileBatchLegacy) QueryInfo(obj string) (*CatFileObject, error) {
|
||||
_, err := io.WriteString(b.getBatchCheck().writer, obj+"\n")
|
||||
_, err := io.WriteString(b.getBatchCheck().reqWriter, obj+"\n")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return catFileBatchParseInfoLine(b.getBatchCheck().reader)
|
||||
return catFileBatchParseInfoLine(b.getBatchCheck().respReader)
|
||||
}
|
||||
|
||||
func (b *catFileBatchLegacy) Close() {
|
||||
|
||||
@@ -18,46 +18,40 @@ import (
|
||||
)
|
||||
|
||||
type catFileBatchCommunicator struct {
|
||||
cancel context.CancelFunc
|
||||
reader *bufio.Reader
|
||||
writer io.Writer
|
||||
|
||||
cancel context.CancelFunc
|
||||
reqWriter io.Writer
|
||||
respReader *bufio.Reader
|
||||
debugGitCmd *gitcmd.Command
|
||||
}
|
||||
|
||||
func (b *catFileBatchCommunicator) Close() {
|
||||
if b.cancel != nil {
|
||||
b.cancel()
|
||||
b.reader = nil
|
||||
b.writer = nil
|
||||
b.cancel = nil
|
||||
}
|
||||
}
|
||||
|
||||
// newCatFileBatch opens git cat-file --batch in the provided repo and returns a stdin pipe, a stdout reader and cancel function
|
||||
func newCatFileBatch(ctx context.Context, repoPath string, cmdCatFile *gitcmd.Command) *catFileBatchCommunicator {
|
||||
// We often want to feed the commits in order into cat-file --batch, followed by their trees and subtrees as necessary.
|
||||
func newCatFileBatch(ctx context.Context, repoPath string, cmdCatFile *gitcmd.Command) (ret *catFileBatchCommunicator) {
|
||||
ctx, ctxCancel := context.WithCancelCause(ctx)
|
||||
|
||||
var batchStdinWriter io.WriteCloser
|
||||
var batchStdoutReader io.ReadCloser
|
||||
cmdCatFile = cmdCatFile.
|
||||
WithDir(repoPath).
|
||||
WithStdinWriter(&batchStdinWriter).
|
||||
WithStdoutReader(&batchStdoutReader)
|
||||
// We often want to feed the commits in order into cat-file --batch, followed by their trees and subtrees as necessary.
|
||||
stdinWriter, stdoutReader, pipeClose := cmdCatFile.MakeStdinStdoutPipe()
|
||||
ret = &catFileBatchCommunicator{
|
||||
debugGitCmd: cmdCatFile,
|
||||
cancel: func() { ctxCancel(nil) },
|
||||
reqWriter: stdinWriter,
|
||||
respReader: bufio.NewReaderSize(stdoutReader, 32*1024), // use a buffered reader for rich operations
|
||||
}
|
||||
|
||||
err := cmdCatFile.StartWithStderr(ctx)
|
||||
err := cmdCatFile.WithDir(repoPath).StartWithStderr(ctx)
|
||||
if err != nil {
|
||||
log.Error("Unable to start git command %v: %v", cmdCatFile.LogString(), err)
|
||||
// ideally here it should return the error, but it would require refactoring all callers
|
||||
// so just return a dummy communicator that does nothing, almost the same behavior as before, not bad
|
||||
return &catFileBatchCommunicator{
|
||||
writer: io.Discard,
|
||||
reader: bufio.NewReader(bytes.NewReader(nil)),
|
||||
cancel: func() {
|
||||
ctxCancel(err)
|
||||
},
|
||||
}
|
||||
ctxCancel(err)
|
||||
pipeClose()
|
||||
return ret
|
||||
}
|
||||
|
||||
go func() {
|
||||
@@ -66,19 +60,10 @@ func newCatFileBatch(ctx context.Context, repoPath string, cmdCatFile *gitcmd.Co
|
||||
log.Error("cat-file --batch command failed in repo %s, error: %v", repoPath, err)
|
||||
}
|
||||
ctxCancel(err)
|
||||
pipeClose()
|
||||
}()
|
||||
|
||||
// use a buffered reader to read from the cat-file --batch (StringReader.ReadString)
|
||||
batchReader := bufio.NewReaderSize(batchStdoutReader, 32*1024)
|
||||
|
||||
return &catFileBatchCommunicator{
|
||||
writer: batchStdinWriter,
|
||||
reader: batchReader,
|
||||
cancel: func() {
|
||||
ctxCancel(nil)
|
||||
},
|
||||
debugGitCmd: cmdCatFile,
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// catFileBatchParseInfoLine reads the header line from cat-file --batch
|
||||
|
||||
@@ -66,10 +66,10 @@ func testCatFileBatch(t *testing.T) {
|
||||
switch b := batch.(type) {
|
||||
case *catFileBatchLegacy:
|
||||
c = b.batchCheck
|
||||
_, _ = c.writer.Write([]byte("in-complete-line-"))
|
||||
_, _ = c.reqWriter.Write([]byte("in-complete-line-"))
|
||||
case *catFileBatchCommand:
|
||||
c = b.batch
|
||||
_, _ = c.writer.Write([]byte("info"))
|
||||
_, _ = c.reqWriter.Write([]byte("info"))
|
||||
default:
|
||||
t.FailNow()
|
||||
return
|
||||
@@ -78,8 +78,8 @@ func testCatFileBatch(t *testing.T) {
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Go(func() {
|
||||
buf := make([]byte, 100)
|
||||
_, _ = c.reader.Read(buf)
|
||||
n, errRead := c.reader.Read(buf)
|
||||
_, _ = c.respReader.Read(buf)
|
||||
n, errRead := c.respReader.Read(buf)
|
||||
assert.Zero(t, n)
|
||||
assert.ErrorIs(t, errRead, io.EOF) // the pipe is closed due to command being killed
|
||||
})
|
||||
|
||||
+5
-6
@@ -78,7 +78,7 @@ func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diff
|
||||
}
|
||||
|
||||
return cmd.WithDir(repo.Path).
|
||||
WithStdout(writer).
|
||||
WithStdoutCopy(writer).
|
||||
RunWithStderr(repo.Ctx)
|
||||
}
|
||||
|
||||
@@ -286,11 +286,10 @@ func GetAffectedFiles(repo *Repository, branchName, oldCommitID, newCommitID str
|
||||
affectedFiles := make([]string, 0, 32)
|
||||
|
||||
// Run `git diff --name-only` to get the names of the changed files
|
||||
var stdoutReader io.ReadCloser
|
||||
err := gitcmd.NewCommand("diff", "--name-only").AddDynamicArguments(oldCommitID, newCommitID).
|
||||
WithEnv(env).
|
||||
WithDir(repo.Path).
|
||||
WithStdoutReader(&stdoutReader).
|
||||
cmd := gitcmd.NewCommand("diff", "--name-only").AddDynamicArguments(oldCommitID, newCommitID)
|
||||
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
|
||||
defer stdoutReaderClose()
|
||||
err := cmd.WithEnv(env).WithDir(repo.Path).
|
||||
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
||||
// Now scan the output from the command
|
||||
scanner := bufio.NewScanner(stdoutReader)
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ import (
|
||||
"github.com/hashicorp/go-version"
|
||||
)
|
||||
|
||||
const RequiredVersion = "2.0.0" // the minimum Git version required
|
||||
const RequiredVersion = "2.6.0" // the minimum Git version required
|
||||
|
||||
type Features struct {
|
||||
gitVersion *version.Version
|
||||
|
||||
@@ -28,9 +28,6 @@ import (
|
||||
// In most cases, it shouldn't be used. Use AddXxx function instead
|
||||
type TrustedCmdArgs []internal.CmdArg
|
||||
|
||||
// DefaultLocale is the default LC_ALL to run git commands in.
|
||||
const DefaultLocale = "C"
|
||||
|
||||
// Command represents a command with its subcommands or arguments.
|
||||
type Command struct {
|
||||
callerInfo string
|
||||
@@ -47,9 +44,14 @@ type Command struct {
|
||||
cmdFinished process.FinishedFunc
|
||||
cmdStartTime time.Time
|
||||
|
||||
cmdStdinWriter *io.WriteCloser
|
||||
cmdStdoutReader *io.ReadCloser
|
||||
cmdStderrReader *io.ReadCloser
|
||||
parentPipeFiles []*os.File
|
||||
childrenPipeFiles []*os.File
|
||||
|
||||
// only os.Pipe and in-memory buffers can work with Stdin safely, see https://github.com/golang/go/issues/77227 if the command would exit unexpectedly
|
||||
cmdStdin io.Reader
|
||||
cmdStdout io.Writer
|
||||
cmdStderr io.Writer
|
||||
|
||||
cmdManagedStderr *bytes.Buffer
|
||||
}
|
||||
|
||||
@@ -215,19 +217,6 @@ type runOpts struct {
|
||||
// The correct approach is to use `--git-dir" global argument
|
||||
Dir string
|
||||
|
||||
Stdout io.Writer
|
||||
|
||||
// Stdin is used for passing input to the command
|
||||
// The caller must make sure the Stdin writer is closed properly to finish the Run function.
|
||||
// Otherwise, the Run function may hang for long time or forever, especially when the Git's context deadline is not the same as the caller's.
|
||||
// Some common mistakes:
|
||||
// * `defer stdinWriter.Close()` then call `cmd.Run()`: the Run() would never return if the command is killed by timeout
|
||||
// * `go { case <- parentContext.Done(): stdinWriter.Close() }` with `cmd.Run(DefaultTimeout)`: the command would have been killed by timeout but the Run doesn't return until stdinWriter.Close()
|
||||
// * `go { if stdoutReader.Read() err != nil: stdinWriter.Close() }` with `cmd.Run()`: the stdoutReader may never return error if the command is killed by timeout
|
||||
// In the future, ideally the git module itself should have full control of the stdin, to avoid such problems and make it easier to refactor to a better architecture.
|
||||
// Use new functions like WithStdinWriter to avoid such problems.
|
||||
Stdin io.Reader
|
||||
|
||||
PipelineFunc func(Context) error
|
||||
}
|
||||
|
||||
@@ -259,7 +248,7 @@ func commonBaseEnvs() []string {
|
||||
// CommonGitCmdEnvs returns the common environment variables for a "git" command.
|
||||
func CommonGitCmdEnvs() []string {
|
||||
return append(commonBaseEnvs(), []string{
|
||||
"LC_ALL=" + DefaultLocale,
|
||||
"LC_ALL=C", // ensure git output is in English, error messages are parsed in English
|
||||
"GIT_TERMINAL_PROMPT=0", // avoid prompting for credentials interactively, supported since git v2.3
|
||||
}...)
|
||||
}
|
||||
@@ -286,30 +275,76 @@ func (c *Command) WithTimeout(timeout time.Duration) *Command {
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Command) WithStdoutReader(r *io.ReadCloser) *Command {
|
||||
c.cmdStdoutReader = r
|
||||
func (c *Command) makeStdoutStderr(w *io.Writer) (PipeReader, func()) {
|
||||
pr, pw, err := os.Pipe()
|
||||
if err != nil {
|
||||
c.preErrors = append(c.preErrors, err)
|
||||
return &pipeNull{err}, func() {}
|
||||
}
|
||||
c.childrenPipeFiles = append(c.childrenPipeFiles, pw)
|
||||
c.parentPipeFiles = append(c.parentPipeFiles, pr)
|
||||
*w /* stdout, stderr */ = pw
|
||||
return &pipeReader{f: pr}, func() { pr.Close() }
|
||||
}
|
||||
|
||||
// MakeStdinPipe creates a writer for the command's stdin.
|
||||
// The returned closer function must be called by the caller to close the pipe.
|
||||
func (c *Command) MakeStdinPipe() (writer PipeWriter, closer func()) {
|
||||
pr, pw, err := os.Pipe()
|
||||
if err != nil {
|
||||
c.preErrors = append(c.preErrors, err)
|
||||
return &pipeNull{err}, func() {}
|
||||
}
|
||||
c.childrenPipeFiles = append(c.childrenPipeFiles, pr)
|
||||
c.parentPipeFiles = append(c.parentPipeFiles, pw)
|
||||
c.cmdStdin = pr
|
||||
return &pipeWriter{pw}, func() { pw.Close() }
|
||||
}
|
||||
|
||||
// MakeStdoutPipe creates a reader for the command's stdout.
|
||||
// The returned closer function must be called by the caller to close the pipe.
|
||||
// After the pipe reader is closed, the unread data will be discarded.
|
||||
func (c *Command) MakeStdoutPipe() (reader PipeReader, closer func()) {
|
||||
return c.makeStdoutStderr(&c.cmdStdout)
|
||||
}
|
||||
|
||||
// MakeStderrPipe is like MakeStdoutPipe, but for stderr.
|
||||
func (c *Command) MakeStderrPipe() (reader PipeReader, closer func()) {
|
||||
return c.makeStdoutStderr(&c.cmdStderr)
|
||||
}
|
||||
|
||||
func (c *Command) MakeStdinStdoutPipe() (stdin PipeWriter, stdout PipeReader, closer func()) {
|
||||
stdin, stdinClose := c.MakeStdinPipe()
|
||||
stdout, stdoutClose := c.MakeStdoutPipe()
|
||||
return stdin, stdout, func() {
|
||||
stdinClose()
|
||||
stdoutClose()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Command) WithStdinBytes(stdin []byte) *Command {
|
||||
c.cmdStdin = bytes.NewReader(stdin)
|
||||
return c
|
||||
}
|
||||
|
||||
// WithStdout is deprecated, use WithStdoutReader instead
|
||||
func (c *Command) WithStdout(stdout io.Writer) *Command {
|
||||
c.opts.Stdout = stdout
|
||||
func (c *Command) WithStdoutBuffer(w PipeBufferWriter) *Command {
|
||||
c.cmdStdout = w
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Command) WithStderrReader(r *io.ReadCloser) *Command {
|
||||
c.cmdStderrReader = r
|
||||
// WithStdinCopy and WithStdoutCopy are general functions that accept any io.Reader / io.Writer.
|
||||
// In this case, Golang exec.Cmd will start new internal goroutines to do io.Copy between pipes and provided Reader/Writer.
|
||||
// If the reader or writer is blocked and never returns, then the io.Copy won't finish, then exec.Cmd.Wait won't return, which may cause deadlocks.
|
||||
// A typical deadlock example is:
|
||||
// * `r,w:=io.Pipe(); cmd.Stdin=r; defer w.Close(); cmd.Run()`: the Run() will never return because stdin reader is blocked forever and w.Close() will never be called.
|
||||
// If the reader/writer won't block forever (for example: read from a file or buffer), then these functions are safe to use.
|
||||
func (c *Command) WithStdinCopy(w io.Reader) *Command {
|
||||
c.cmdStdin = w
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Command) WithStdinWriter(w *io.WriteCloser) *Command {
|
||||
c.cmdStdinWriter = w
|
||||
return c
|
||||
}
|
||||
|
||||
// WithStdin is deprecated, use WithStdinWriter instead
|
||||
func (c *Command) WithStdin(stdin io.Reader) *Command {
|
||||
c.opts.Stdin = stdin
|
||||
func (c *Command) WithStdoutCopy(w io.Writer) *Command {
|
||||
c.cmdStdout = w
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -348,9 +383,10 @@ func (c *Command) Start(ctx context.Context) (retErr error) {
|
||||
}
|
||||
|
||||
defer func() {
|
||||
c.closePipeFiles(c.childrenPipeFiles)
|
||||
if retErr != nil {
|
||||
// release the pipes to avoid resource leak
|
||||
c.closeStdioPipes()
|
||||
// release the pipes to avoid resource leak since the command failed to start
|
||||
c.closePipeFiles(c.parentPipeFiles)
|
||||
// if error occurs, we must also finish the task, otherwise, cmdFinished will be called in "Wait" function
|
||||
if c.cmdFinished != nil {
|
||||
c.cmdFinished()
|
||||
@@ -359,7 +395,7 @@ func (c *Command) Start(ctx context.Context) (retErr error) {
|
||||
}()
|
||||
|
||||
if len(c.preErrors) != 0 {
|
||||
// In most cases, such error shouldn't happen. If it happens, it must be a programming error, so we log it as error level with more details
|
||||
// In most cases, such error shouldn't happen. If it happens, log it as error level with more details
|
||||
err := errors.Join(c.preErrors...)
|
||||
log.Error("git command: %s, error: %s", c.LogString(), err)
|
||||
return err
|
||||
@@ -386,7 +422,7 @@ func (c *Command) Start(ctx context.Context) (retErr error) {
|
||||
|
||||
c.cmdStartTime = time.Now()
|
||||
|
||||
c.cmd = exec.CommandContext(ctx, c.prog, append(c.configArgs, c.args...)...)
|
||||
c.cmd = exec.CommandContext(c.cmdCtx, c.prog, append(c.configArgs, c.args...)...)
|
||||
if c.opts.Env == nil {
|
||||
c.cmd.Env = os.Environ()
|
||||
} else {
|
||||
@@ -396,52 +432,38 @@ func (c *Command) Start(ctx context.Context) (retErr error) {
|
||||
process.SetSysProcAttribute(c.cmd)
|
||||
c.cmd.Env = append(c.cmd.Env, CommonGitCmdEnvs()...)
|
||||
c.cmd.Dir = c.opts.Dir
|
||||
c.cmd.Stdout = c.opts.Stdout
|
||||
c.cmd.Stdin = c.opts.Stdin
|
||||
|
||||
if _, err := safeAssignPipe(c.cmdStdinWriter, c.cmd.StdinPipe); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := safeAssignPipe(c.cmdStdoutReader, c.cmd.StdoutPipe); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := safeAssignPipe(c.cmdStderrReader, c.cmd.StderrPipe); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.cmdManagedStderr != nil {
|
||||
if c.cmd.Stderr != nil {
|
||||
panic("CombineStderr needs managed (but not caller-provided) stderr pipe")
|
||||
}
|
||||
c.cmd.Stderr = c.cmdManagedStderr
|
||||
}
|
||||
c.cmd.Stdout = c.cmdStdout
|
||||
c.cmd.Stdin = c.cmdStdin
|
||||
c.cmd.Stderr = c.cmdStderr
|
||||
return c.cmd.Start()
|
||||
}
|
||||
|
||||
func (c *Command) closeStdioPipes() {
|
||||
safeClosePtrCloser(c.cmdStdoutReader)
|
||||
safeClosePtrCloser(c.cmdStderrReader)
|
||||
safeClosePtrCloser(c.cmdStdinWriter)
|
||||
func (c *Command) closePipeFiles(files []*os.File) {
|
||||
for _, f := range files {
|
||||
_ = f.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Command) Wait() error {
|
||||
defer func() {
|
||||
c.closeStdioPipes()
|
||||
// The reader in another goroutine might be still reading the stdout, so we shouldn't close the pipes here
|
||||
// MakeStdoutPipe returns a closer function to force callers to close the pipe correctly
|
||||
// Here we only need to mark the command as finished
|
||||
c.cmdFinished()
|
||||
}()
|
||||
|
||||
if c.opts.PipelineFunc != nil {
|
||||
errCallback := c.opts.PipelineFunc(&cmdContext{Context: c.cmdCtx, cmd: c})
|
||||
// after the pipeline function returns, we can safely cancel the command context and close the stdio pipes
|
||||
c.cmdCancel(errCallback)
|
||||
c.closeStdioPipes()
|
||||
errPipeline := c.opts.PipelineFunc(&cmdContext{Context: c.cmdCtx, cmd: c})
|
||||
// after the pipeline function returns, we can safely cancel the command context and close the pipes, the data in pipes should have been consumed
|
||||
c.cmdCancel(errPipeline)
|
||||
c.closePipeFiles(c.parentPipeFiles)
|
||||
errWait := c.cmd.Wait()
|
||||
errCause := context.Cause(c.cmdCtx)
|
||||
// the pipeline function should be able to know whether it succeeds or fails
|
||||
if errCallback == nil && (errCause == nil || errors.Is(errCause, context.Canceled)) {
|
||||
if errPipeline == nil && (errCause == nil || errors.Is(errCause, context.Canceled)) {
|
||||
return nil
|
||||
}
|
||||
return errors.Join(errCallback, errCause, errWait)
|
||||
return errors.Join(wrapPipelineError(errPipeline), errCause, errWait)
|
||||
}
|
||||
|
||||
// there might be other goroutines using the context or pipes, so we just wait for the command to finish
|
||||
@@ -460,7 +482,11 @@ func (c *Command) Wait() error {
|
||||
}
|
||||
|
||||
func (c *Command) StartWithStderr(ctx context.Context) RunStdError {
|
||||
if c.cmdStderr != nil {
|
||||
panic("caller-provided stderr receiver doesn't work with managed stderr buffer")
|
||||
}
|
||||
c.cmdManagedStderr = &bytes.Buffer{}
|
||||
c.cmdStderr = c.cmdManagedStderr
|
||||
err := c.Start(ctx)
|
||||
if err != nil {
|
||||
return &runStdError{err: err}
|
||||
@@ -470,7 +496,7 @@ func (c *Command) StartWithStderr(ctx context.Context) RunStdError {
|
||||
|
||||
func (c *Command) WaitWithStderr() RunStdError {
|
||||
if c.cmdManagedStderr == nil {
|
||||
panic("CombineStderr needs managed (but not caller-provided) stderr pipe")
|
||||
panic("managed stderr buffer is not initialized")
|
||||
}
|
||||
errWait := c.Wait()
|
||||
if errWait == nil {
|
||||
@@ -506,14 +532,12 @@ func (c *Command) RunStdBytes(ctx context.Context) (stdout, stderr []byte, runEr
|
||||
}
|
||||
|
||||
func (c *Command) runStdBytes(ctx context.Context) ([]byte, []byte, RunStdError) {
|
||||
if c.opts.Stdout != nil || c.cmdStdoutReader != nil || c.cmdStderrReader != nil {
|
||||
// we must panic here, otherwise there would be bugs if developers set Stdin/Stderr by mistake, and it would be very difficult to debug
|
||||
if c.cmdStdout != nil || c.cmdStderr != nil {
|
||||
// it must panic here, otherwise there would be bugs if developers set other Stdin/Stderr by mistake, and it would be very difficult to debug
|
||||
panic("stdout and stderr field must be nil when using RunStdBytes")
|
||||
}
|
||||
stdoutBuf := &bytes.Buffer{}
|
||||
err := c.WithParentCallerInfo().
|
||||
WithStdout(stdoutBuf).
|
||||
RunWithStderr(ctx)
|
||||
err := c.WithParentCallerInfo().WithStdoutBuffer(stdoutBuf).RunWithStderr(ctx)
|
||||
return stdoutBuf.Bytes(), c.cmdManagedStderr.Bytes(), err
|
||||
}
|
||||
|
||||
|
||||
@@ -121,7 +121,10 @@ func TestRunWithContextTimeout(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
})
|
||||
t.Run("WithTimeout", func(t *testing.T) {
|
||||
err := NewCommand("hash-object", "--stdin").WithTimeout(1 * time.Millisecond).Run(t.Context())
|
||||
cmd := NewCommand("hash-object", "--stdin")
|
||||
_, _, pipeClose := cmd.MakeStdinStdoutPipe()
|
||||
defer pipeClose()
|
||||
err := cmd.WithTimeout(1 * time.Millisecond).Run(t.Context())
|
||||
require.ErrorIs(t, err, context.DeadlineExceeded)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -76,3 +76,26 @@ func IsErrorCanceledOrKilled(err error) bool {
|
||||
// TODO: in the future, we need to use unified error type from gitcmd.Run to check whether it is manually canceled
|
||||
return errors.Is(err, context.Canceled) || IsErrorSignalKilled(err)
|
||||
}
|
||||
|
||||
type pipelineError struct {
|
||||
error
|
||||
}
|
||||
|
||||
func (e pipelineError) Unwrap() error {
|
||||
return e.error
|
||||
}
|
||||
|
||||
func wrapPipelineError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return pipelineError{err}
|
||||
}
|
||||
|
||||
func ErrorAsPipeline(err error) error {
|
||||
var pipelineErr pipelineError
|
||||
if errors.As(err, &pipelineErr) {
|
||||
return pipelineErr.error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package gitcmd
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
type PipeBufferReader interface {
|
||||
Read(p []byte) (n int, err error)
|
||||
Bytes() []byte
|
||||
}
|
||||
|
||||
type PipeBufferWriter interface {
|
||||
Write(p []byte) (n int, err error)
|
||||
Bytes() []byte
|
||||
}
|
||||
|
||||
type PipeReader interface {
|
||||
io.ReadCloser
|
||||
internalOnly()
|
||||
}
|
||||
|
||||
type pipeReader struct {
|
||||
f *os.File
|
||||
}
|
||||
|
||||
func (r *pipeReader) internalOnly() {}
|
||||
|
||||
func (r *pipeReader) Read(p []byte) (n int, err error) {
|
||||
return r.f.Read(p)
|
||||
}
|
||||
|
||||
func (r *pipeReader) Close() error {
|
||||
return r.f.Close()
|
||||
}
|
||||
|
||||
type PipeWriter interface {
|
||||
io.WriteCloser
|
||||
internalOnly()
|
||||
}
|
||||
|
||||
type pipeWriter struct {
|
||||
f *os.File
|
||||
}
|
||||
|
||||
func (w *pipeWriter) internalOnly() {}
|
||||
|
||||
func (w *pipeWriter) Close() error {
|
||||
return w.f.Close()
|
||||
}
|
||||
|
||||
func (w *pipeWriter) Write(p []byte) (n int, err error) {
|
||||
return w.f.Write(p)
|
||||
}
|
||||
|
||||
func (w *pipeWriter) DrainBeforeClose() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type pipeNull struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (p *pipeNull) internalOnly() {}
|
||||
|
||||
func (p *pipeNull) Read([]byte) (n int, err error) {
|
||||
return 0, p.err
|
||||
}
|
||||
|
||||
func (p *pipeNull) Write([]byte) (n int, err error) {
|
||||
return 0, p.err
|
||||
}
|
||||
|
||||
func (p *pipeNull) Close() error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package gitcmd
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
func safeClosePtrCloser[T *io.ReadCloser | *io.WriteCloser](c T) {
|
||||
switch v := any(c).(type) {
|
||||
case *io.ReadCloser:
|
||||
if v != nil && *v != nil {
|
||||
_ = (*v).Close()
|
||||
}
|
||||
case *io.WriteCloser:
|
||||
if v != nil && *v != nil {
|
||||
_ = (*v).Close()
|
||||
}
|
||||
default:
|
||||
panic("unsupported type")
|
||||
}
|
||||
}
|
||||
|
||||
func safeAssignPipe[T any](p *T, fn func() (T, error)) (bool, error) {
|
||||
if p == nil {
|
||||
return false, nil
|
||||
}
|
||||
v, err := fn()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
*p = v
|
||||
return true, nil
|
||||
}
|
||||
+2
-3
@@ -8,7 +8,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -74,9 +73,9 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO
|
||||
cmd.AddDashesAndList(opts.PathspecList...)
|
||||
opts.MaxResultLimit = util.IfZero(opts.MaxResultLimit, 50)
|
||||
|
||||
var stdoutReader io.ReadCloser
|
||||
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
|
||||
defer stdoutReaderClose()
|
||||
err := cmd.WithDir(repo.Path).
|
||||
WithStdoutReader(&stdoutReader).
|
||||
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
||||
isInBlock := false
|
||||
rd := bufio.NewReaderSize(stdoutReader, util.IfZero(opts.MaxLineLength, 16*1024))
|
||||
|
||||
@@ -15,25 +15,12 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
|
||||
"github.com/djherbis/buffer"
|
||||
"github.com/djherbis/nio/v3"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
// LogNameStatusRepo opens git log --raw in the provided repo and returns a stdin pipe, a stdout reader and cancel function
|
||||
func LogNameStatusRepo(ctx context.Context, repository, head, treepath string, paths ...string) (*bufio.Reader, func()) {
|
||||
// We often want to feed the commits in order into cat-file --batch, followed by their trees and sub trees as necessary.
|
||||
// so let's create a batch stdin and stdout
|
||||
stdoutReader, stdoutWriter := nio.Pipe(buffer.New(32 * 1024))
|
||||
|
||||
// Lets also create a context so that we can absolutely ensure that the command should die when we're done
|
||||
ctx, ctxCancel := context.WithCancel(ctx)
|
||||
|
||||
cancel := func() {
|
||||
ctxCancel()
|
||||
_ = stdoutReader.Close()
|
||||
_ = stdoutWriter.Close()
|
||||
}
|
||||
|
||||
cmd := gitcmd.NewCommand()
|
||||
cmd.AddArguments("log", "--name-status", "-c", "--format=commit%x00%H %P%x00", "--parents", "--no-renames", "-t", "-z").AddDynamicArguments(head)
|
||||
@@ -63,16 +50,21 @@ func LogNameStatusRepo(ctx context.Context, repository, head, treepath string, p
|
||||
}
|
||||
cmd.AddDashesAndList(files...)
|
||||
|
||||
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
|
||||
ctx, ctxCancel := context.WithCancel(ctx)
|
||||
go func() {
|
||||
err := cmd.WithDir(repository).
|
||||
WithStdout(stdoutWriter).
|
||||
RunWithStderr(ctx)
|
||||
_ = stdoutWriter.CloseWithError(err)
|
||||
err := cmd.WithDir(repository).RunWithStderr(ctx)
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
log.Error("Unable to run git command %v: %v", cmd.LogString(), err)
|
||||
}
|
||||
}()
|
||||
|
||||
bufReader := bufio.NewReaderSize(stdoutReader, 32*1024)
|
||||
|
||||
return bufReader, cancel
|
||||
return bufReader, func() {
|
||||
ctxCancel()
|
||||
stdoutReaderClose()
|
||||
}
|
||||
}
|
||||
|
||||
// LogNameStatusRepoParser parses a git log raw output from LogRawRepo
|
||||
|
||||
@@ -6,67 +6,33 @@ package pipeline
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
)
|
||||
|
||||
// CatFileBatchCheck runs cat-file with --batch-check
|
||||
func CatFileBatchCheck(ctx context.Context, shasToCheckReader *io.PipeReader, catFileCheckWriter *io.PipeWriter, wg *sync.WaitGroup, tmpBasePath string) {
|
||||
defer wg.Done()
|
||||
defer shasToCheckReader.Close()
|
||||
defer catFileCheckWriter.Close()
|
||||
|
||||
cmd := gitcmd.NewCommand("cat-file", "--batch-check")
|
||||
if err := cmd.WithDir(tmpBasePath).
|
||||
WithStdin(shasToCheckReader).
|
||||
WithStdout(catFileCheckWriter).
|
||||
RunWithStderr(ctx); err != nil {
|
||||
_ = catFileCheckWriter.CloseWithError(fmt.Errorf("git cat-file --batch-check [%s]: %w", tmpBasePath, err))
|
||||
}
|
||||
func CatFileBatchCheck(ctx context.Context, cmd *gitcmd.Command, tmpBasePath string) error {
|
||||
cmd.AddArguments("cat-file", "--batch-check")
|
||||
return cmd.WithDir(tmpBasePath).RunWithStderr(ctx)
|
||||
}
|
||||
|
||||
// CatFileBatchCheckAllObjects runs cat-file with --batch-check --batch-all
|
||||
func CatFileBatchCheckAllObjects(ctx context.Context, catFileCheckWriter *io.PipeWriter, wg *sync.WaitGroup, tmpBasePath string, errChan chan<- error) {
|
||||
defer wg.Done()
|
||||
defer catFileCheckWriter.Close()
|
||||
|
||||
cmd := gitcmd.NewCommand("cat-file", "--batch-check", "--batch-all-objects")
|
||||
if err := cmd.WithDir(tmpBasePath).
|
||||
WithStdout(catFileCheckWriter).
|
||||
RunWithStderr(ctx); err != nil {
|
||||
_ = catFileCheckWriter.CloseWithError(fmt.Errorf("git cat-file --batch-check --batch-all-object [%s]: %w", tmpBasePath, err))
|
||||
errChan <- err
|
||||
}
|
||||
func CatFileBatchCheckAllObjects(ctx context.Context, cmd *gitcmd.Command, tmpBasePath string) error {
|
||||
return cmd.AddArguments("cat-file", "--batch-check", "--batch-all-objects").WithDir(tmpBasePath).RunWithStderr(ctx)
|
||||
}
|
||||
|
||||
// CatFileBatch runs cat-file --batch
|
||||
func CatFileBatch(ctx context.Context, shasToBatchReader *io.PipeReader, catFileBatchWriter *io.PipeWriter, wg *sync.WaitGroup, tmpBasePath string) {
|
||||
defer wg.Done()
|
||||
defer shasToBatchReader.Close()
|
||||
defer catFileBatchWriter.Close()
|
||||
|
||||
if err := gitcmd.NewCommand("cat-file", "--batch").
|
||||
WithDir(tmpBasePath).
|
||||
WithStdin(shasToBatchReader).
|
||||
WithStdout(catFileBatchWriter).
|
||||
RunWithStderr(ctx); err != nil {
|
||||
_ = shasToBatchReader.CloseWithError(fmt.Errorf("git rev-list [%s]: %w", tmpBasePath, err))
|
||||
}
|
||||
func CatFileBatch(ctx context.Context, cmd *gitcmd.Command, tmpBasePath string) error {
|
||||
return cmd.AddArguments("cat-file", "--batch").WithDir(tmpBasePath).RunWithStderr(ctx)
|
||||
}
|
||||
|
||||
// BlobsLessThan1024FromCatFileBatchCheck reads a pipeline from cat-file --batch-check and returns the blobs <1024 in size
|
||||
func BlobsLessThan1024FromCatFileBatchCheck(catFileCheckReader *io.PipeReader, shasToBatchWriter *io.PipeWriter, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
defer catFileCheckReader.Close()
|
||||
scanner := bufio.NewScanner(catFileCheckReader)
|
||||
defer func() {
|
||||
_ = shasToBatchWriter.CloseWithError(scanner.Err())
|
||||
}()
|
||||
func BlobsLessThan1024FromCatFileBatchCheck(in io.ReadCloser, out io.WriteCloser) error {
|
||||
defer out.Close()
|
||||
scanner := bufio.NewScanner(in)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if len(line) == 0 {
|
||||
@@ -82,12 +48,12 @@ func BlobsLessThan1024FromCatFileBatchCheck(catFileCheckReader *io.PipeReader, s
|
||||
}
|
||||
toWrite := []byte(fields[0] + "\n")
|
||||
for len(toWrite) > 0 {
|
||||
n, err := shasToBatchWriter.Write(toWrite)
|
||||
n, err := out.Write(toWrite)
|
||||
if err != nil {
|
||||
_ = catFileCheckReader.CloseWithError(err)
|
||||
break
|
||||
return err
|
||||
}
|
||||
toWrite = toWrite[n:]
|
||||
}
|
||||
}
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
@@ -26,7 +25,3 @@ type lfsResultSlice []*LFSResult
|
||||
func (a lfsResultSlice) Len() int { return len(a) }
|
||||
func (a lfsResultSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a lfsResultSlice) Less(i, j int) bool { return a[j].When.After(a[i].When) }
|
||||
|
||||
func lfsError(msg string, err error) error {
|
||||
return fmt.Errorf("LFS error occurred, %s: err: %w", msg, err)
|
||||
}
|
||||
|
||||
@@ -6,11 +6,10 @@
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
|
||||
@@ -24,7 +23,6 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
|
||||
resultsMap := map[string]*LFSResult{}
|
||||
results := make([]*LFSResult, 0)
|
||||
|
||||
basePath := repo.Path
|
||||
gogitRepo := repo.GoGitRepo()
|
||||
|
||||
commitsIter, err := gogitRepo.Log(&gogit.LogOptions{
|
||||
@@ -32,7 +30,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
|
||||
All: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, lfsError("failed to get GoGit CommitsIter", err)
|
||||
return nil, fmt.Errorf("LFS error occurred, failed to get GoGit CommitsIter: err: %w", err)
|
||||
}
|
||||
|
||||
err = commitsIter.ForEach(func(gitCommit *object.Commit) error {
|
||||
@@ -66,7 +64,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
|
||||
return nil
|
||||
})
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, lfsError("failure in CommitIter.ForEach", err)
|
||||
return nil, fmt.Errorf("LFS error occurred, failure in CommitIter.ForEach: %w", err)
|
||||
}
|
||||
|
||||
for _, result := range resultsMap {
|
||||
@@ -82,65 +80,6 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
|
||||
}
|
||||
|
||||
sort.Sort(lfsResultSlice(results))
|
||||
|
||||
// Should really use a go-git function here but name-rev is not completed and recapitulating it is not simple
|
||||
shasToNameReader, shasToNameWriter := io.Pipe()
|
||||
nameRevStdinReader, nameRevStdinWriter := io.Pipe()
|
||||
errChan := make(chan error, 1)
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(3)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
scanner := bufio.NewScanner(nameRevStdinReader)
|
||||
i := 0
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
result := results[i]
|
||||
result.FullCommitName = line
|
||||
result.BranchName = strings.Split(line, "~")[0]
|
||||
i++
|
||||
}
|
||||
}()
|
||||
go NameRevStdin(repo.Ctx, shasToNameReader, nameRevStdinWriter, &wg, basePath)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer shasToNameWriter.Close()
|
||||
for _, result := range results {
|
||||
i := 0
|
||||
if i < len(result.SHA) {
|
||||
n, err := shasToNameWriter.Write([]byte(result.SHA)[i:])
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
break
|
||||
}
|
||||
i += n
|
||||
}
|
||||
n := 0
|
||||
for n < 1 {
|
||||
n, err = shasToNameWriter.Write([]byte{'\n'})
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
select {
|
||||
case err, has := <-errChan:
|
||||
if has {
|
||||
return nil, lfsError("unable to obtain name for LFS files", err)
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
return results, nil
|
||||
err = fillResultNameRev(repo.Ctx, repo.Path, results)
|
||||
return results, err
|
||||
}
|
||||
|
||||
@@ -12,34 +12,27 @@ import (
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
)
|
||||
|
||||
// FindLFSFile finds commits that contain a provided pointer file hash
|
||||
func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, error) {
|
||||
func FindLFSFile(repo *git.Repository, objectID git.ObjectID) (results []*LFSResult, _ error) {
|
||||
cmd := gitcmd.NewCommand("rev-list", "--all")
|
||||
revListReader, revListReaderClose := cmd.MakeStdoutPipe()
|
||||
defer revListReaderClose()
|
||||
err := cmd.WithDir(repo.Path).
|
||||
WithPipelineFunc(func(context gitcmd.Context) (err error) {
|
||||
results, err = findLFSFileFunc(repo, objectID, revListReader)
|
||||
return err
|
||||
}).RunWithStderr(repo.Ctx)
|
||||
return results, err
|
||||
}
|
||||
|
||||
func findLFSFileFunc(repo *git.Repository, objectID git.ObjectID, revListReader io.Reader) ([]*LFSResult, error) {
|
||||
resultsMap := map[string]*LFSResult{}
|
||||
results := make([]*LFSResult, 0)
|
||||
|
||||
basePath := repo.Path
|
||||
|
||||
// Use rev-list to provide us with all commits in order
|
||||
revListReader, revListWriter := io.Pipe()
|
||||
defer func() {
|
||||
_ = revListWriter.Close()
|
||||
_ = revListReader.Close()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
err := gitcmd.NewCommand("rev-list", "--all").
|
||||
WithDir(repo.Path).
|
||||
WithStdout(revListWriter).
|
||||
RunWithStderr(repo.Ctx)
|
||||
_ = revListWriter.CloseWithError(err)
|
||||
}()
|
||||
|
||||
// Next feed the commits in order into cat-file --batch, followed by their trees and sub trees as necessary.
|
||||
// so let's create a batch stdin and stdout
|
||||
batch, cancel, err := repo.CatFileBatch(repo.Ctx)
|
||||
@@ -158,56 +151,6 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
|
||||
}
|
||||
|
||||
sort.Sort(lfsResultSlice(results))
|
||||
|
||||
// Should really use a go-git function here but name-rev is not completed and recapitulating it is not simple
|
||||
shasToNameReader, shasToNameWriter := io.Pipe()
|
||||
nameRevStdinReader, nameRevStdinWriter := io.Pipe()
|
||||
errChan := make(chan error, 1)
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(3)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
scanner := bufio.NewScanner(nameRevStdinReader)
|
||||
i := 0
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
result := results[i]
|
||||
result.FullCommitName = line
|
||||
result.BranchName = strings.Split(line, "~")[0]
|
||||
i++
|
||||
}
|
||||
}()
|
||||
go NameRevStdin(repo.Ctx, shasToNameReader, nameRevStdinWriter, &wg, basePath)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer shasToNameWriter.Close()
|
||||
for _, result := range results {
|
||||
_, err := shasToNameWriter.Write([]byte(result.SHA))
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
break
|
||||
}
|
||||
_, err = shasToNameWriter.Write([]byte{'\n'})
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
select {
|
||||
case err, has := <-errChan:
|
||||
if has {
|
||||
return nil, lfsError("unable to obtain name for LFS files", err)
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
return results, nil
|
||||
err = fillResultNameRev(repo.Ctx, repo.Path, results)
|
||||
return results, err
|
||||
}
|
||||
|
||||
@@ -4,25 +4,54 @@
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// NameRevStdin runs name-rev --stdin
|
||||
func NameRevStdin(ctx context.Context, shasToNameReader *io.PipeReader, nameRevStdinWriter *io.PipeWriter, wg *sync.WaitGroup, tmpBasePath string) {
|
||||
defer wg.Done()
|
||||
defer shasToNameReader.Close()
|
||||
defer nameRevStdinWriter.Close()
|
||||
func fillResultNameRev(ctx context.Context, basePath string, results []*LFSResult) error {
|
||||
// Should really use a go-git function here but name-rev is not completed and recapitulating it is not simple
|
||||
wg := errgroup.Group{}
|
||||
cmd := gitcmd.NewCommand("name-rev", "--stdin", "--name-only", "--always").WithDir(basePath)
|
||||
stdin, stdinClose := cmd.MakeStdinPipe()
|
||||
stdout, stdoutClose := cmd.MakeStdoutPipe()
|
||||
defer stdinClose()
|
||||
defer stdoutClose()
|
||||
|
||||
if err := gitcmd.NewCommand("name-rev", "--stdin", "--name-only", "--always").
|
||||
WithDir(tmpBasePath).
|
||||
WithStdin(shasToNameReader).
|
||||
WithStdout(nameRevStdinWriter).
|
||||
RunWithStderr(ctx); err != nil {
|
||||
_ = shasToNameReader.CloseWithError(fmt.Errorf("git name-rev [%s]: %w", tmpBasePath, err))
|
||||
}
|
||||
wg.Go(func() error {
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
i := 0
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
result := results[i]
|
||||
result.FullCommitName = line
|
||||
result.BranchName = strings.Split(line, "~")[0]
|
||||
i++
|
||||
}
|
||||
return scanner.Err()
|
||||
})
|
||||
wg.Go(func() error {
|
||||
defer stdinClose()
|
||||
for _, result := range results {
|
||||
_, err := stdin.Write([]byte(result.SHA))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = stdin.Write([]byte{'\n'})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
err := cmd.RunWithStderr(ctx)
|
||||
return errors.Join(err, wg.Wait())
|
||||
}
|
||||
|
||||
@@ -6,52 +6,25 @@ package pipeline
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
)
|
||||
|
||||
// RevListAllObjects runs rev-list --objects --all and writes to a pipewriter
|
||||
func RevListAllObjects(ctx context.Context, revListWriter *io.PipeWriter, wg *sync.WaitGroup, basePath string, errChan chan<- error) {
|
||||
defer wg.Done()
|
||||
defer revListWriter.Close()
|
||||
|
||||
cmd := gitcmd.NewCommand("rev-list", "--objects", "--all")
|
||||
if err := cmd.WithDir(basePath).
|
||||
WithStdout(revListWriter).
|
||||
RunWithStderr(ctx); err != nil {
|
||||
_ = revListWriter.CloseWithError(fmt.Errorf("git rev-list --objects --all [%s]: %w", basePath, err))
|
||||
errChan <- err
|
||||
}
|
||||
}
|
||||
|
||||
// RevListObjects run rev-list --objects from headSHA to baseSHA
|
||||
func RevListObjects(ctx context.Context, revListWriter *io.PipeWriter, wg *sync.WaitGroup, tmpBasePath, headSHA, baseSHA string, errChan chan<- error) {
|
||||
defer wg.Done()
|
||||
defer revListWriter.Close()
|
||||
|
||||
cmd := gitcmd.NewCommand("rev-list", "--objects").AddDynamicArguments(headSHA)
|
||||
func RevListObjects(ctx context.Context, cmd *gitcmd.Command, tmpBasePath, headSHA, baseSHA string) error {
|
||||
cmd.AddArguments("rev-list", "--objects").AddDynamicArguments(headSHA)
|
||||
if baseSHA != "" {
|
||||
cmd = cmd.AddArguments("--not").AddDynamicArguments(baseSHA)
|
||||
}
|
||||
if err := cmd.WithDir(tmpBasePath).
|
||||
WithStdout(revListWriter).
|
||||
RunWithStderr(ctx); err != nil {
|
||||
errChan <- fmt.Errorf("git rev-list [%s]: %w", tmpBasePath, err)
|
||||
}
|
||||
return cmd.WithDir(tmpBasePath).RunWithStderr(ctx)
|
||||
}
|
||||
|
||||
// BlobsFromRevListObjects reads a RevListAllObjects and only selects blobs
|
||||
func BlobsFromRevListObjects(revListReader *io.PipeReader, shasToCheckWriter *io.PipeWriter, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
defer revListReader.Close()
|
||||
scanner := bufio.NewScanner(revListReader)
|
||||
defer func() {
|
||||
_ = shasToCheckWriter.CloseWithError(scanner.Err())
|
||||
}()
|
||||
func BlobsFromRevListObjects(in io.ReadCloser, out io.WriteCloser) error {
|
||||
defer out.Close()
|
||||
scanner := bufio.NewScanner(in)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if len(line) == 0 {
|
||||
@@ -63,12 +36,12 @@ func BlobsFromRevListObjects(revListReader *io.PipeReader, shasToCheckWriter *io
|
||||
}
|
||||
toWrite := []byte(fields[0] + "\n")
|
||||
for len(toWrite) > 0 {
|
||||
n, err := shasToCheckWriter.Write(toWrite)
|
||||
n, err := out.Write(toWrite)
|
||||
if err != nil {
|
||||
_ = revListReader.CloseWithError(err)
|
||||
break
|
||||
return err
|
||||
}
|
||||
toWrite = toWrite[n:]
|
||||
}
|
||||
}
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
)
|
||||
|
||||
// ArchiveType archive types
|
||||
type ArchiveType int
|
||||
|
||||
const (
|
||||
ArchiveUnknown ArchiveType = iota
|
||||
ArchiveZip // 1
|
||||
ArchiveTarGz // 2
|
||||
ArchiveBundle // 3
|
||||
)
|
||||
|
||||
// String converts an ArchiveType to string: the extension of the archive file without prefix dot
|
||||
func (a ArchiveType) String() string {
|
||||
switch a {
|
||||
case ArchiveZip:
|
||||
return "zip"
|
||||
case ArchiveTarGz:
|
||||
return "tar.gz"
|
||||
case ArchiveBundle:
|
||||
return "bundle"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func SplitArchiveNameType(s string) (string, ArchiveType) {
|
||||
switch {
|
||||
case strings.HasSuffix(s, ".zip"):
|
||||
return strings.TrimSuffix(s, ".zip"), ArchiveZip
|
||||
case strings.HasSuffix(s, ".tar.gz"):
|
||||
return strings.TrimSuffix(s, ".tar.gz"), ArchiveTarGz
|
||||
case strings.HasSuffix(s, ".bundle"):
|
||||
return strings.TrimSuffix(s, ".bundle"), ArchiveBundle
|
||||
}
|
||||
return s, ArchiveUnknown
|
||||
}
|
||||
|
||||
// CreateArchive create archive content to the target path
|
||||
func (repo *Repository) CreateArchive(ctx context.Context, format ArchiveType, target io.Writer, usePrefix bool, commitID string) error {
|
||||
if format.String() == "unknown" {
|
||||
return fmt.Errorf("unknown format: %v", format)
|
||||
}
|
||||
|
||||
cmd := gitcmd.NewCommand("archive")
|
||||
if usePrefix {
|
||||
cmd.AddOptionFormat("--prefix=%s", filepath.Base(strings.TrimSuffix(repo.Path, ".git"))+"/")
|
||||
}
|
||||
cmd.AddOptionFormat("--format=%s", format.String())
|
||||
cmd.AddDynamicArguments(commitID)
|
||||
|
||||
return cmd.WithDir(repo.Path).
|
||||
WithStdout(target).
|
||||
RunWithStderr(ctx)
|
||||
}
|
||||
@@ -94,84 +94,81 @@ func callShowRef(ctx context.Context, repoPath, trimPrefix string, extraArgs git
|
||||
}
|
||||
|
||||
func WalkShowRef(ctx context.Context, repoPath string, extraArgs gitcmd.TrustedCmdArgs, skip, limit int, walkfn func(sha1, refname string) error) (countAll int, err error) {
|
||||
stdoutReader, stdoutWriter := io.Pipe()
|
||||
defer func() {
|
||||
_ = stdoutReader.Close()
|
||||
_ = stdoutWriter.Close()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
args := gitcmd.TrustedCmdArgs{"for-each-ref", "--format=%(objectname) %(refname)"}
|
||||
args = append(args, extraArgs...)
|
||||
err := gitcmd.NewCommand(args...).
|
||||
WithDir(repoPath).
|
||||
WithStdout(stdoutWriter).
|
||||
RunWithStderr(ctx)
|
||||
_ = stdoutWriter.CloseWithError(err)
|
||||
}()
|
||||
|
||||
i := 0
|
||||
bufReader := bufio.NewReader(stdoutReader)
|
||||
for i < skip {
|
||||
_, isPrefix, err := bufReader.ReadLine()
|
||||
if err == io.EOF {
|
||||
return i, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !isPrefix {
|
||||
i++
|
||||
}
|
||||
args := gitcmd.TrustedCmdArgs{"for-each-ref", "--format=%(objectname) %(refname)"}
|
||||
args = append(args, extraArgs...)
|
||||
cmd := gitcmd.NewCommand(args...)
|
||||
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
|
||||
defer stdoutReaderClose()
|
||||
cmd.WithDir(repoPath).
|
||||
WithPipelineFunc(func(c gitcmd.Context) error {
|
||||
bufReader := bufio.NewReader(stdoutReader)
|
||||
for i < skip {
|
||||
_, isPrefix, err := bufReader.ReadLine()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isPrefix {
|
||||
i++
|
||||
}
|
||||
}
|
||||
for limit == 0 || i < skip+limit {
|
||||
// The output of show-ref is simply a list:
|
||||
// <sha> SP <ref> LF
|
||||
sha, err := bufReader.ReadString(' ')
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
branchName, err := bufReader.ReadString('\n')
|
||||
if err == io.EOF {
|
||||
// This shouldn't happen... but we'll tolerate it for the sake of peace
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(branchName) > 0 {
|
||||
branchName = branchName[:len(branchName)-1]
|
||||
}
|
||||
|
||||
if len(sha) > 0 {
|
||||
sha = sha[:len(sha)-1]
|
||||
}
|
||||
|
||||
err = walkfn(sha, branchName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i++
|
||||
}
|
||||
// count all refs
|
||||
for limit != 0 {
|
||||
_, isPrefix, err := bufReader.ReadLine()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isPrefix {
|
||||
i++
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
err = cmd.RunWithStderr(ctx)
|
||||
if errPipeline := gitcmd.ErrorAsPipeline(err); errPipeline != nil {
|
||||
return i, errPipeline // keep the old behavior: return pipeline error directly
|
||||
}
|
||||
for limit == 0 || i < skip+limit {
|
||||
// The output of show-ref is simply a list:
|
||||
// <sha> SP <ref> LF
|
||||
sha, err := bufReader.ReadString(' ')
|
||||
if err == io.EOF {
|
||||
return i, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
branchName, err := bufReader.ReadString('\n')
|
||||
if err == io.EOF {
|
||||
// This shouldn't happen... but we'll tolerate it for the sake of peace
|
||||
return i, nil
|
||||
}
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
|
||||
if len(branchName) > 0 {
|
||||
branchName = branchName[:len(branchName)-1]
|
||||
}
|
||||
|
||||
if len(sha) > 0 {
|
||||
sha = sha[:len(sha)-1]
|
||||
}
|
||||
|
||||
err = walkfn(sha, branchName)
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
i++
|
||||
}
|
||||
// count all refs
|
||||
for limit != 0 {
|
||||
_, isPrefix, err := bufReader.ReadLine()
|
||||
if err == io.EOF {
|
||||
return i, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !isPrefix {
|
||||
i++
|
||||
}
|
||||
}
|
||||
return i, nil
|
||||
return i, err
|
||||
}
|
||||
|
||||
// GetRefsBySha returns all references filtered with prefix that belong to a sha commit hash
|
||||
|
||||
+45
-50
@@ -226,60 +226,55 @@ type CommitsByFileAndRangeOptions struct {
|
||||
|
||||
// CommitsByFileAndRange return the commits according revision file and the page
|
||||
func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) ([]*Commit, error) {
|
||||
stdoutReader, stdoutWriter := io.Pipe()
|
||||
defer func() {
|
||||
_ = stdoutReader.Close()
|
||||
_ = stdoutWriter.Close()
|
||||
}()
|
||||
go func() {
|
||||
gitCmd := gitcmd.NewCommand("rev-list").
|
||||
AddOptionFormat("--max-count=%d", setting.Git.CommitsRangeSize).
|
||||
AddOptionFormat("--skip=%d", (opts.Page-1)*setting.Git.CommitsRangeSize)
|
||||
gitCmd.AddDynamicArguments(opts.Revision)
|
||||
gitCmd := gitcmd.NewCommand("rev-list").
|
||||
AddOptionFormat("--max-count=%d", setting.Git.CommitsRangeSize).
|
||||
AddOptionFormat("--skip=%d", (opts.Page-1)*setting.Git.CommitsRangeSize)
|
||||
gitCmd.AddDynamicArguments(opts.Revision)
|
||||
|
||||
if opts.Not != "" {
|
||||
gitCmd.AddOptionValues("--not", opts.Not)
|
||||
}
|
||||
if opts.Since != "" {
|
||||
gitCmd.AddOptionFormat("--since=%s", opts.Since)
|
||||
}
|
||||
if opts.Until != "" {
|
||||
gitCmd.AddOptionFormat("--until=%s", opts.Until)
|
||||
}
|
||||
|
||||
gitCmd.AddDashesAndList(opts.File)
|
||||
err := gitCmd.WithDir(repo.Path).
|
||||
WithStdout(stdoutWriter).
|
||||
RunWithStderr(repo.Ctx)
|
||||
_ = stdoutWriter.CloseWithError(err)
|
||||
}()
|
||||
|
||||
objectFormat, err := repo.GetObjectFormat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if opts.Not != "" {
|
||||
gitCmd.AddOptionValues("--not", opts.Not)
|
||||
}
|
||||
if opts.Since != "" {
|
||||
gitCmd.AddOptionFormat("--since=%s", opts.Since)
|
||||
}
|
||||
if opts.Until != "" {
|
||||
gitCmd.AddOptionFormat("--until=%s", opts.Until)
|
||||
}
|
||||
gitCmd.AddDashesAndList(opts.File)
|
||||
|
||||
length := objectFormat.FullLength()
|
||||
commits := []*Commit{}
|
||||
shaline := make([]byte, length+1)
|
||||
for {
|
||||
n, err := io.ReadFull(stdoutReader, shaline)
|
||||
if err != nil || n < length {
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
var commits []*Commit
|
||||
stdoutReader, stdoutReaderClose := gitCmd.MakeStdoutPipe()
|
||||
defer stdoutReaderClose()
|
||||
err := gitCmd.WithDir(repo.Path).
|
||||
WithPipelineFunc(func(context gitcmd.Context) error {
|
||||
objectFormat, err := repo.GetObjectFormat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return commits, err
|
||||
}
|
||||
objectID, err := NewIDFromString(string(shaline[0:length]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
commit, err := repo.getCommit(objectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
commits = append(commits, commit)
|
||||
}
|
||||
|
||||
length := objectFormat.FullLength()
|
||||
shaline := make([]byte, length+1)
|
||||
for {
|
||||
n, err := io.ReadFull(stdoutReader, shaline)
|
||||
if err != nil || n < length {
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
objectID, err := NewIDFromString(string(shaline[0:length]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
commit, err := repo.getCommit(objectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
commits = append(commits, commit)
|
||||
}
|
||||
}).
|
||||
RunWithStderr(repo.Ctx)
|
||||
return commits, err
|
||||
}
|
||||
|
||||
// FilesCountBetween return the number of files changed between two commits
|
||||
|
||||
@@ -45,7 +45,7 @@ func (repo *Repository) GetDiffNumChangedFiles(base, head string, directComparis
|
||||
AddDynamicArguments(base + separator + head).
|
||||
AddArguments("--").
|
||||
WithDir(repo.Path).
|
||||
WithStdout(w).
|
||||
WithStdoutCopy(w).
|
||||
RunWithStderr(repo.Ctx); err != nil {
|
||||
if strings.Contains(err.Stderr(), "no merge base") {
|
||||
// git >= 2.28 now returns an error if base and head have become unrelated.
|
||||
@@ -55,7 +55,7 @@ func (repo *Repository) GetDiffNumChangedFiles(base, head string, directComparis
|
||||
AddDynamicArguments(base, head).
|
||||
AddArguments("--").
|
||||
WithDir(repo.Path).
|
||||
WithStdout(w).
|
||||
WithStdoutCopy(w).
|
||||
RunWithStderr(repo.Ctx); err == nil {
|
||||
return w.numLines, nil
|
||||
}
|
||||
@@ -71,7 +71,7 @@ var patchCommits = regexp.MustCompile(`^From\s(\w+)\s`)
|
||||
func (repo *Repository) GetDiff(compareArg string, w io.Writer) error {
|
||||
return gitcmd.NewCommand("diff", "-p").AddDynamicArguments(compareArg).
|
||||
WithDir(repo.Path).
|
||||
WithStdout(w).
|
||||
WithStdoutCopy(w).
|
||||
Run(repo.Ctx)
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ func (repo *Repository) GetDiffBinary(compareArg string, w io.Writer) error {
|
||||
return gitcmd.NewCommand("diff", "-p", "--binary", "--histogram").
|
||||
AddDynamicArguments(compareArg).
|
||||
WithDir(repo.Path).
|
||||
WithStdout(w).
|
||||
WithStdoutCopy(w).
|
||||
Run(repo.Ctx)
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ func (repo *Repository) GetDiffBinary(compareArg string, w io.Writer) error {
|
||||
func (repo *Repository) GetPatch(compareArg string, w io.Writer) error {
|
||||
return gitcmd.NewCommand("format-patch", "--binary", "--stdout").AddDynamicArguments(compareArg).
|
||||
WithDir(repo.Path).
|
||||
WithStdout(w).
|
||||
WithStdoutCopy(w).
|
||||
Run(repo.Ctx)
|
||||
}
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ func (repo *Repository) RemoveFilesFromIndex(filenames ...string) error {
|
||||
}
|
||||
return cmd.
|
||||
WithDir(repo.Path).
|
||||
WithStdin(bytes.NewReader(input.Bytes())).
|
||||
WithStdinBytes(input.Bytes()).
|
||||
RunWithStderr(repo.Ctx)
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ func (repo *Repository) AddObjectsToIndex(objects ...IndexObjectInfo) error {
|
||||
}
|
||||
return cmd.
|
||||
WithDir(repo.Path).
|
||||
WithStdin(bytes.NewReader(input.Bytes())).
|
||||
WithStdinBytes(input.Bytes()).
|
||||
RunWithStderr(repo.Ctx)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
@@ -32,18 +31,12 @@ func (o ObjectType) Bytes() []byte {
|
||||
return []byte(o)
|
||||
}
|
||||
|
||||
type EmptyReader struct{}
|
||||
|
||||
func (EmptyReader) Read(p []byte) (int, error) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
func (repo *Repository) GetObjectFormat() (ObjectFormat, error) {
|
||||
if repo != nil && repo.objectFormat != nil {
|
||||
return repo.objectFormat, nil
|
||||
}
|
||||
|
||||
str, err := repo.hashObject(EmptyReader{}, false)
|
||||
str, err := repo.hashObjectBytes(nil, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -57,16 +50,16 @@ func (repo *Repository) GetObjectFormat() (ObjectFormat, error) {
|
||||
return repo.objectFormat, nil
|
||||
}
|
||||
|
||||
// HashObject takes a reader and returns hash for that reader
|
||||
func (repo *Repository) HashObject(reader io.Reader) (ObjectID, error) {
|
||||
idStr, err := repo.hashObject(reader, true)
|
||||
// HashObjectBytes returns hash for the content
|
||||
func (repo *Repository) HashObjectBytes(buf []byte) (ObjectID, error) {
|
||||
idStr, err := repo.hashObjectBytes(buf, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewIDFromString(idStr)
|
||||
}
|
||||
|
||||
func (repo *Repository) hashObject(reader io.Reader, save bool) (string, error) {
|
||||
func (repo *Repository) hashObjectBytes(buf []byte, save bool) (string, error) {
|
||||
var cmd *gitcmd.Command
|
||||
if save {
|
||||
cmd = gitcmd.NewCommand("hash-object", "-w", "--stdin")
|
||||
@@ -75,7 +68,7 @@ func (repo *Repository) hashObject(reader io.Reader, save bool) (string, error)
|
||||
}
|
||||
stdout, _, err := cmd.
|
||||
WithDir(repo.Path).
|
||||
WithStdin(reader).
|
||||
WithStdinBytes(buf).
|
||||
RunStdString(repo.Ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -15,69 +15,61 @@ import (
|
||||
|
||||
// GetRefsFiltered returns all references of the repository that matches patterm exactly or starting with.
|
||||
func (repo *Repository) GetRefsFiltered(pattern string) ([]*Reference, error) {
|
||||
stdoutReader, stdoutWriter := io.Pipe()
|
||||
defer func() {
|
||||
_ = stdoutReader.Close()
|
||||
_ = stdoutWriter.Close()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
err := gitcmd.NewCommand("for-each-ref").
|
||||
WithDir(repo.Path).
|
||||
WithStdout(stdoutWriter).
|
||||
Run(repo.Ctx)
|
||||
_ = stdoutWriter.CloseWithError(err)
|
||||
}()
|
||||
|
||||
refs := make([]*Reference, 0)
|
||||
bufReader := bufio.NewReader(stdoutReader)
|
||||
for {
|
||||
// The output of for-each-ref is simply a list:
|
||||
// <sha> SP <type> TAB <ref> LF
|
||||
sha, err := bufReader.ReadString(' ')
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sha = sha[:len(sha)-1]
|
||||
cmd := gitcmd.NewCommand("for-each-ref")
|
||||
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
|
||||
defer stdoutReaderClose()
|
||||
err := cmd.WithDir(repo.Path).
|
||||
WithPipelineFunc(func(context gitcmd.Context) error {
|
||||
bufReader := bufio.NewReader(stdoutReader)
|
||||
for {
|
||||
// The output of for-each-ref is simply a list:
|
||||
// <sha> SP <type> TAB <ref> LF
|
||||
sha, err := bufReader.ReadString(' ')
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sha = sha[:len(sha)-1]
|
||||
|
||||
typ, err := bufReader.ReadString('\t')
|
||||
if err == io.EOF {
|
||||
// This should not happen, but we'll tolerate it
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
typ = typ[:len(typ)-1]
|
||||
typ, err := bufReader.ReadString('\t')
|
||||
if err == io.EOF {
|
||||
// This should not happen, but we'll tolerate it
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
typ = typ[:len(typ)-1]
|
||||
|
||||
refName, err := bufReader.ReadString('\n')
|
||||
if err == io.EOF {
|
||||
// This should not happen, but we'll tolerate it
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
refName = refName[:len(refName)-1]
|
||||
refName, err := bufReader.ReadString('\n')
|
||||
if err == io.EOF {
|
||||
// This should not happen, but we'll tolerate it
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
refName = refName[:len(refName)-1]
|
||||
|
||||
// refName cannot be HEAD but can be remotes or stash
|
||||
if strings.HasPrefix(refName, RemotePrefix) || refName == "/refs/stash" {
|
||||
continue
|
||||
}
|
||||
// refName cannot be HEAD but can be remotes or stash
|
||||
if strings.HasPrefix(refName, RemotePrefix) || refName == "/refs/stash" {
|
||||
continue
|
||||
}
|
||||
|
||||
if pattern == "" || strings.HasPrefix(refName, pattern) {
|
||||
r := &Reference{
|
||||
Name: refName,
|
||||
Object: MustIDFromString(sha),
|
||||
Type: typ,
|
||||
repo: repo,
|
||||
if pattern == "" || strings.HasPrefix(refName, pattern) {
|
||||
r := &Reference{
|
||||
Name: refName,
|
||||
Object: MustIDFromString(sha),
|
||||
Type: typ,
|
||||
repo: repo,
|
||||
}
|
||||
refs = append(refs, r)
|
||||
}
|
||||
}
|
||||
refs = append(refs, r)
|
||||
}
|
||||
}
|
||||
|
||||
return refs, nil
|
||||
return nil
|
||||
}).RunWithStderr(repo.Ctx)
|
||||
return refs, err
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ package git
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -62,10 +61,10 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string)
|
||||
gitCmd.AddArguments("--first-parent").AddDynamicArguments(branch)
|
||||
}
|
||||
|
||||
var stdoutReader io.ReadCloser
|
||||
stdoutReader, stdoutReaderClose := gitCmd.MakeStdoutPipe()
|
||||
defer stdoutReaderClose()
|
||||
err = gitCmd.
|
||||
WithDir(repo.Path).
|
||||
WithStdoutReader(&stdoutReader).
|
||||
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
||||
scanner := bufio.NewScanner(stdoutReader)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
@@ -117,7 +116,6 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string)
|
||||
}
|
||||
}
|
||||
if err = scanner.Err(); err != nil {
|
||||
_ = stdoutReader.Close()
|
||||
return fmt.Errorf("GetCodeActivityStats scan: %w", err)
|
||||
}
|
||||
a := make([]*CodeActivityAuthor, 0, len(authors))
|
||||
@@ -131,7 +129,6 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string)
|
||||
stats.AuthorCount = int64(len(authors))
|
||||
stats.ChangedFiles = int64(len(files))
|
||||
stats.Authors = a
|
||||
_ = stdoutReader.Close()
|
||||
return nil
|
||||
}).
|
||||
RunWithStderr(repo.Ctx)
|
||||
|
||||
+32
-36
@@ -6,7 +6,6 @@ package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/git/foreachref"
|
||||
@@ -115,45 +114,42 @@ func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) {
|
||||
// https://git-scm.com/docs/git-for-each-ref#Documentation/git-for-each-ref.txt-refname
|
||||
forEachRefFmt := foreachref.NewFormat("objecttype", "refname:lstrip=2", "object", "objectname", "creator", "contents", "contents:signature")
|
||||
|
||||
stdoutReader, stdoutWriter := io.Pipe()
|
||||
defer stdoutReader.Close()
|
||||
defer stdoutWriter.Close()
|
||||
|
||||
go func() {
|
||||
err := gitcmd.NewCommand("for-each-ref").
|
||||
AddOptionFormat("--format=%s", forEachRefFmt.Flag()).
|
||||
AddArguments("--sort", "-*creatordate", "refs/tags").
|
||||
WithDir(repo.Path).
|
||||
WithStdout(stdoutWriter).
|
||||
RunWithStderr(repo.Ctx)
|
||||
_ = stdoutWriter.CloseWithError(err)
|
||||
}()
|
||||
|
||||
var tags []*Tag
|
||||
parser := forEachRefFmt.Parser(stdoutReader)
|
||||
for {
|
||||
ref := parser.Next()
|
||||
if ref == nil {
|
||||
break
|
||||
}
|
||||
var tagsTotal int
|
||||
cmd := gitcmd.NewCommand("for-each-ref")
|
||||
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
|
||||
defer stdoutReaderClose()
|
||||
err := cmd.AddOptionFormat("--format=%s", forEachRefFmt.Flag()).
|
||||
AddArguments("--sort", "-*creatordate", "refs/tags").
|
||||
WithDir(repo.Path).
|
||||
WithPipelineFunc(func(context gitcmd.Context) error {
|
||||
parser := forEachRefFmt.Parser(stdoutReader)
|
||||
for {
|
||||
ref := parser.Next()
|
||||
if ref == nil {
|
||||
break
|
||||
}
|
||||
|
||||
tag, err := parseTagRef(ref)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("GetTagInfos: parse tag: %w", err)
|
||||
}
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
if err := parser.Err(); err != nil {
|
||||
return nil, 0, fmt.Errorf("GetTagInfos: parse output: %w", err)
|
||||
}
|
||||
tag, err := parseTagRef(ref)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetTagInfos: parse tag: %w", err)
|
||||
}
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
if err := parser.Err(); err != nil {
|
||||
return fmt.Errorf("GetTagInfos: parse output: %w", err)
|
||||
}
|
||||
|
||||
sortTagsByTime(tags)
|
||||
tagsTotal := len(tags)
|
||||
if page != 0 {
|
||||
tags = util.PaginateSlice(tags, page, pageSize).([]*Tag)
|
||||
}
|
||||
sortTagsByTime(tags)
|
||||
tagsTotal = len(tags)
|
||||
if page != 0 {
|
||||
tags = util.PaginateSlice(tags, page, pageSize).([]*Tag)
|
||||
}
|
||||
return nil
|
||||
}).
|
||||
RunWithStderr(repo.Ctx)
|
||||
|
||||
return tags, tagsTotal, nil
|
||||
return tags, tagsTotal, err
|
||||
}
|
||||
|
||||
// parseTagRef parses a tag from a 'git for-each-ref'-produced reference.
|
||||
|
||||
@@ -60,7 +60,7 @@ func (repo *Repository) CommitTree(author, committer *Signature, tree *Tree, opt
|
||||
|
||||
stdout, _, err := cmd.WithEnv(env).
|
||||
WithDir(repo.Path).
|
||||
WithStdin(messageBytes).
|
||||
WithStdinBytes(messageBytes.Bytes()).
|
||||
RunStdString(repo.Ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@@ -21,10 +20,10 @@ type TemplateSubmoduleCommit struct {
|
||||
// GetTemplateSubmoduleCommits returns a list of submodules paths and their commits from a repository
|
||||
// This function is only for generating new repos based on existing template, the template couldn't be too large.
|
||||
func GetTemplateSubmoduleCommits(ctx context.Context, repoPath string) (submoduleCommits []TemplateSubmoduleCommit, _ error) {
|
||||
var stdoutReader io.ReadCloser
|
||||
err := gitcmd.NewCommand("ls-tree", "-r", "--", "HEAD").
|
||||
WithDir(repoPath).
|
||||
WithStdoutReader(&stdoutReader).
|
||||
cmd := gitcmd.NewCommand("ls-tree", "-r", "--", "HEAD")
|
||||
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
|
||||
defer stdoutReaderClose()
|
||||
err := cmd.WithDir(repoPath).
|
||||
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
||||
scanner := bufio.NewScanner(stdoutReader)
|
||||
for scanner.Scan() {
|
||||
|
||||
Reference in New Issue
Block a user