mirror of
https://github.com/go-gitea/gitea
synced 2026-02-03 07:40:36 +00:00
Refactor git command stdio pipe (#36422)
Most potential deadlock problems should have been fixed, and new code is unlikely to cause new problems with the new design. Also raise the minimum Git version required to 2.6.0 (released in 2015)
This commit is contained in:
Generated
-10
@@ -409,16 +409,6 @@
|
|||||||
"path": "github.com/dimiro1/reply/LICENSE",
|
"path": "github.com/dimiro1/reply/LICENSE",
|
||||||
"licenseText": "MIT License\n\nCopyright (c) Discourse\nCopyright (c) Claudemiro\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
|
"licenseText": "MIT License\n\nCopyright (c) Discourse\nCopyright (c) Claudemiro\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "github.com/djherbis/buffer",
|
|
||||||
"path": "github.com/djherbis/buffer/LICENSE.txt",
|
|
||||||
"licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Dustin H\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "github.com/djherbis/nio/v3",
|
|
||||||
"path": "github.com/djherbis/nio/v3/LICENSE.txt",
|
|
||||||
"licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Dustin H\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "github.com/dlclark/regexp2",
|
"name": "github.com/dlclark/regexp2",
|
||||||
"path": "github.com/dlclark/regexp2/LICENSE",
|
"path": "github.com/dlclark/regexp2/LICENSE",
|
||||||
|
|||||||
@@ -39,8 +39,6 @@ require (
|
|||||||
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20251013092601-6327009efd21
|
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20251013092601-6327009efd21
|
||||||
github.com/chi-middleware/proxy v1.1.1
|
github.com/chi-middleware/proxy v1.1.1
|
||||||
github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21
|
github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21
|
||||||
github.com/djherbis/buffer v1.2.0
|
|
||||||
github.com/djherbis/nio/v3 v3.0.1
|
|
||||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707
|
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707
|
||||||
github.com/dustin/go-humanize v1.0.1
|
github.com/dustin/go-humanize v1.0.1
|
||||||
github.com/editorconfig/editorconfig-core-go/v2 v2.6.3
|
github.com/editorconfig/editorconfig-core-go/v2 v2.6.3
|
||||||
|
|||||||
@@ -260,11 +260,6 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
|
|||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21 h1:PdsjTl0Cg+ZJgOx/CFV5NNgO1ThTreqdgKYiDCMHJwA=
|
github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21 h1:PdsjTl0Cg+ZJgOx/CFV5NNgO1ThTreqdgKYiDCMHJwA=
|
||||||
github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21/go.mod h1:xJvkyD6Y2rZapGvPJLYo9dyx1s5dxBEDPa8T3YTuOk0=
|
github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21/go.mod h1:xJvkyD6Y2rZapGvPJLYo9dyx1s5dxBEDPa8T3YTuOk0=
|
||||||
github.com/djherbis/buffer v1.1.0/go.mod h1:VwN8VdFkMY0DCALdY8o00d3IZ6Amz/UNVMWcSaJT44o=
|
|
||||||
github.com/djherbis/buffer v1.2.0 h1:PH5Dd2ss0C7CRRhQCZ2u7MssF+No9ide8Ye71nPHcrQ=
|
|
||||||
github.com/djherbis/buffer v1.2.0/go.mod h1:fjnebbZjCUpPinBRD+TDwXSOeNQ7fPQWLfGQqiAiUyE=
|
|
||||||
github.com/djherbis/nio/v3 v3.0.1 h1:6wxhnuppteMa6RHA4L81Dq7ThkZH8SwnDzXDYy95vB4=
|
|
||||||
github.com/djherbis/nio/v3 v3.0.1/go.mod h1:Ng4h80pbZFMla1yKzm61cF0tqqilXZYrogmWgZxOcmg=
|
|
||||||
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"io"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ import (
|
|||||||
type BatchChecker struct {
|
type BatchChecker struct {
|
||||||
attributesNum int
|
attributesNum int
|
||||||
repo *git.Repository
|
repo *git.Repository
|
||||||
stdinWriter *os.File
|
stdinWriter io.WriteCloser
|
||||||
stdOut *nulSeparatedAttributeWriter
|
stdOut *nulSeparatedAttributeWriter
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
@@ -60,10 +60,7 @@ func NewBatchChecker(repo *git.Repository, treeish string, attributes []string)
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
stdinReader, stdinWriter, err := os.Pipe()
|
stdinWriter, stdinWriterClose := cmd.MakeStdinPipe()
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
checker.stdinWriter = stdinWriter
|
checker.stdinWriter = stdinWriter
|
||||||
|
|
||||||
lw := new(nulSeparatedAttributeWriter)
|
lw := new(nulSeparatedAttributeWriter)
|
||||||
@@ -71,21 +68,19 @@ func NewBatchChecker(repo *git.Repository, treeish string, attributes []string)
|
|||||||
lw.closed = make(chan struct{})
|
lw.closed = make(chan struct{})
|
||||||
checker.stdOut = lw
|
checker.stdOut = lw
|
||||||
|
|
||||||
go func() {
|
cmd.WithEnv(envs).
|
||||||
defer func() {
|
WithDir(repo.Path).
|
||||||
_ = stdinReader.Close()
|
WithStdoutCopy(lw)
|
||||||
_ = lw.Close()
|
|
||||||
}()
|
|
||||||
err := cmd.WithEnv(envs).
|
|
||||||
WithDir(repo.Path).
|
|
||||||
WithStdin(stdinReader).
|
|
||||||
WithStdout(lw).
|
|
||||||
RunWithStderr(ctx)
|
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer stdinWriterClose()
|
||||||
|
defer checker.cancel()
|
||||||
|
defer lw.Close()
|
||||||
|
|
||||||
|
err := cmd.RunWithStderr(ctx)
|
||||||
if err != nil && !gitcmd.IsErrorCanceledOrKilled(err) {
|
if err != nil && !gitcmd.IsErrorCanceledOrKilled(err) {
|
||||||
log.Error("Attribute checker for commit %s exits with error: %v", treeish, err)
|
log.Error("Attribute checker for commit %s exits with error: %v", treeish, err)
|
||||||
}
|
}
|
||||||
checker.cancel()
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return checker, nil
|
return checker, nil
|
||||||
|
|||||||
@@ -68,15 +68,14 @@ func CheckAttributes(ctx context.Context, gitRepo *git.Repository, treeish strin
|
|||||||
}
|
}
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
stdOut := new(bytes.Buffer)
|
stdout, _, err := cmd.WithEnv(append(os.Environ(), envs...)).
|
||||||
if err := cmd.WithEnv(append(os.Environ(), envs...)).
|
|
||||||
WithDir(gitRepo.Path).
|
WithDir(gitRepo.Path).
|
||||||
WithStdout(stdOut).
|
RunStdBytes(ctx)
|
||||||
RunWithStderr(ctx); err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to run check-attr: %w", err)
|
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 {
|
if len(fields)%3 != 1 {
|
||||||
return nil, errors.New("wrong number of fields in return from check-attr")
|
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) {
|
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 {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
info, err := catFileBatchParseInfoLine(b.getBatch().reader)
|
info, err := catFileBatchParseInfoLine(b.getBatch().respReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
return info, b.getBatch().reader, nil
|
return info, b.getBatch().respReader, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *catFileBatchCommand) QueryInfo(obj string) (*CatFileObject, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return catFileBatchParseInfoLine(b.getBatch().reader)
|
return catFileBatchParseInfoLine(b.getBatch().respReader)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *catFileBatchCommand) Close() {
|
func (b *catFileBatchCommand) Close() {
|
||||||
|
|||||||
@@ -50,23 +50,23 @@ func (b *catFileBatchLegacy) getBatchCheck() *catFileBatchCommunicator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *catFileBatchLegacy) QueryContent(obj string) (*CatFileObject, BufferedReader, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
info, err := catFileBatchParseInfoLine(b.getBatchContent().reader)
|
info, err := catFileBatchParseInfoLine(b.getBatchContent().respReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
return info, b.getBatchContent().reader, nil
|
return info, b.getBatchContent().respReader, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *catFileBatchLegacy) QueryInfo(obj string) (*CatFileObject, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return catFileBatchParseInfoLine(b.getBatchCheck().reader)
|
return catFileBatchParseInfoLine(b.getBatchCheck().respReader)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *catFileBatchLegacy) Close() {
|
func (b *catFileBatchLegacy) Close() {
|
||||||
|
|||||||
@@ -18,46 +18,40 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type catFileBatchCommunicator struct {
|
type catFileBatchCommunicator struct {
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
reader *bufio.Reader
|
reqWriter io.Writer
|
||||||
writer io.Writer
|
respReader *bufio.Reader
|
||||||
|
|
||||||
debugGitCmd *gitcmd.Command
|
debugGitCmd *gitcmd.Command
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *catFileBatchCommunicator) Close() {
|
func (b *catFileBatchCommunicator) Close() {
|
||||||
if b.cancel != nil {
|
if b.cancel != nil {
|
||||||
b.cancel()
|
b.cancel()
|
||||||
b.reader = nil
|
|
||||||
b.writer = nil
|
|
||||||
b.cancel = 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
|
// 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 {
|
func newCatFileBatch(ctx context.Context, repoPath string, cmdCatFile *gitcmd.Command) (ret *catFileBatchCommunicator) {
|
||||||
// We often want to feed the commits in order into cat-file --batch, followed by their trees and subtrees as necessary.
|
|
||||||
ctx, ctxCancel := context.WithCancelCause(ctx)
|
ctx, ctxCancel := context.WithCancelCause(ctx)
|
||||||
|
|
||||||
var batchStdinWriter io.WriteCloser
|
// We often want to feed the commits in order into cat-file --batch, followed by their trees and subtrees as necessary.
|
||||||
var batchStdoutReader io.ReadCloser
|
stdinWriter, stdoutReader, pipeClose := cmdCatFile.MakeStdinStdoutPipe()
|
||||||
cmdCatFile = cmdCatFile.
|
ret = &catFileBatchCommunicator{
|
||||||
WithDir(repoPath).
|
debugGitCmd: cmdCatFile,
|
||||||
WithStdinWriter(&batchStdinWriter).
|
cancel: func() { ctxCancel(nil) },
|
||||||
WithStdoutReader(&batchStdoutReader)
|
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 {
|
if err != nil {
|
||||||
log.Error("Unable to start git command %v: %v", cmdCatFile.LogString(), err)
|
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
|
// 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
|
// so just return a dummy communicator that does nothing, almost the same behavior as before, not bad
|
||||||
return &catFileBatchCommunicator{
|
ctxCancel(err)
|
||||||
writer: io.Discard,
|
pipeClose()
|
||||||
reader: bufio.NewReader(bytes.NewReader(nil)),
|
return ret
|
||||||
cancel: func() {
|
|
||||||
ctxCancel(err)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
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)
|
log.Error("cat-file --batch command failed in repo %s, error: %v", repoPath, err)
|
||||||
}
|
}
|
||||||
ctxCancel(err)
|
ctxCancel(err)
|
||||||
|
pipeClose()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// use a buffered reader to read from the cat-file --batch (StringReader.ReadString)
|
return ret
|
||||||
batchReader := bufio.NewReaderSize(batchStdoutReader, 32*1024)
|
|
||||||
|
|
||||||
return &catFileBatchCommunicator{
|
|
||||||
writer: batchStdinWriter,
|
|
||||||
reader: batchReader,
|
|
||||||
cancel: func() {
|
|
||||||
ctxCancel(nil)
|
|
||||||
},
|
|
||||||
debugGitCmd: cmdCatFile,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// catFileBatchParseInfoLine reads the header line from cat-file --batch
|
// catFileBatchParseInfoLine reads the header line from cat-file --batch
|
||||||
|
|||||||
@@ -66,10 +66,10 @@ func testCatFileBatch(t *testing.T) {
|
|||||||
switch b := batch.(type) {
|
switch b := batch.(type) {
|
||||||
case *catFileBatchLegacy:
|
case *catFileBatchLegacy:
|
||||||
c = b.batchCheck
|
c = b.batchCheck
|
||||||
_, _ = c.writer.Write([]byte("in-complete-line-"))
|
_, _ = c.reqWriter.Write([]byte("in-complete-line-"))
|
||||||
case *catFileBatchCommand:
|
case *catFileBatchCommand:
|
||||||
c = b.batch
|
c = b.batch
|
||||||
_, _ = c.writer.Write([]byte("info"))
|
_, _ = c.reqWriter.Write([]byte("info"))
|
||||||
default:
|
default:
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
return
|
return
|
||||||
@@ -78,8 +78,8 @@ func testCatFileBatch(t *testing.T) {
|
|||||||
wg := sync.WaitGroup{}
|
wg := sync.WaitGroup{}
|
||||||
wg.Go(func() {
|
wg.Go(func() {
|
||||||
buf := make([]byte, 100)
|
buf := make([]byte, 100)
|
||||||
_, _ = c.reader.Read(buf)
|
_, _ = c.respReader.Read(buf)
|
||||||
n, errRead := c.reader.Read(buf)
|
n, errRead := c.respReader.Read(buf)
|
||||||
assert.Zero(t, n)
|
assert.Zero(t, n)
|
||||||
assert.ErrorIs(t, errRead, io.EOF) // the pipe is closed due to command being killed
|
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).
|
return cmd.WithDir(repo.Path).
|
||||||
WithStdout(writer).
|
WithStdoutCopy(writer).
|
||||||
RunWithStderr(repo.Ctx)
|
RunWithStderr(repo.Ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,11 +286,10 @@ func GetAffectedFiles(repo *Repository, branchName, oldCommitID, newCommitID str
|
|||||||
affectedFiles := make([]string, 0, 32)
|
affectedFiles := make([]string, 0, 32)
|
||||||
|
|
||||||
// Run `git diff --name-only` to get the names of the changed files
|
// Run `git diff --name-only` to get the names of the changed files
|
||||||
var stdoutReader io.ReadCloser
|
cmd := gitcmd.NewCommand("diff", "--name-only").AddDynamicArguments(oldCommitID, newCommitID)
|
||||||
err := gitcmd.NewCommand("diff", "--name-only").AddDynamicArguments(oldCommitID, newCommitID).
|
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
|
||||||
WithEnv(env).
|
defer stdoutReaderClose()
|
||||||
WithDir(repo.Path).
|
err := cmd.WithEnv(env).WithDir(repo.Path).
|
||||||
WithStdoutReader(&stdoutReader).
|
|
||||||
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
||||||
// Now scan the output from the command
|
// Now scan the output from the command
|
||||||
scanner := bufio.NewScanner(stdoutReader)
|
scanner := bufio.NewScanner(stdoutReader)
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ import (
|
|||||||
"github.com/hashicorp/go-version"
|
"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 {
|
type Features struct {
|
||||||
gitVersion *version.Version
|
gitVersion *version.Version
|
||||||
|
|||||||
@@ -28,9 +28,6 @@ import (
|
|||||||
// In most cases, it shouldn't be used. Use AddXxx function instead
|
// In most cases, it shouldn't be used. Use AddXxx function instead
|
||||||
type TrustedCmdArgs []internal.CmdArg
|
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.
|
// Command represents a command with its subcommands or arguments.
|
||||||
type Command struct {
|
type Command struct {
|
||||||
callerInfo string
|
callerInfo string
|
||||||
@@ -47,9 +44,14 @@ type Command struct {
|
|||||||
cmdFinished process.FinishedFunc
|
cmdFinished process.FinishedFunc
|
||||||
cmdStartTime time.Time
|
cmdStartTime time.Time
|
||||||
|
|
||||||
cmdStdinWriter *io.WriteCloser
|
parentPipeFiles []*os.File
|
||||||
cmdStdoutReader *io.ReadCloser
|
childrenPipeFiles []*os.File
|
||||||
cmdStderrReader *io.ReadCloser
|
|
||||||
|
// 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
|
cmdManagedStderr *bytes.Buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,19 +217,6 @@ type runOpts struct {
|
|||||||
// The correct approach is to use `--git-dir" global argument
|
// The correct approach is to use `--git-dir" global argument
|
||||||
Dir string
|
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
|
PipelineFunc func(Context) error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +248,7 @@ func commonBaseEnvs() []string {
|
|||||||
// CommonGitCmdEnvs returns the common environment variables for a "git" command.
|
// CommonGitCmdEnvs returns the common environment variables for a "git" command.
|
||||||
func CommonGitCmdEnvs() []string {
|
func CommonGitCmdEnvs() []string {
|
||||||
return append(commonBaseEnvs(), []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
|
"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
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Command) WithStdoutReader(r *io.ReadCloser) *Command {
|
func (c *Command) makeStdoutStderr(w *io.Writer) (PipeReader, func()) {
|
||||||
c.cmdStdoutReader = r
|
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
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithStdout is deprecated, use WithStdoutReader instead
|
func (c *Command) WithStdoutBuffer(w PipeBufferWriter) *Command {
|
||||||
func (c *Command) WithStdout(stdout io.Writer) *Command {
|
c.cmdStdout = w
|
||||||
c.opts.Stdout = stdout
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Command) WithStderrReader(r *io.ReadCloser) *Command {
|
// WithStdinCopy and WithStdoutCopy are general functions that accept any io.Reader / io.Writer.
|
||||||
c.cmdStderrReader = r
|
// 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
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Command) WithStdinWriter(w *io.WriteCloser) *Command {
|
func (c *Command) WithStdoutCopy(w io.Writer) *Command {
|
||||||
c.cmdStdinWriter = w
|
c.cmdStdout = w
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithStdin is deprecated, use WithStdinWriter instead
|
|
||||||
func (c *Command) WithStdin(stdin io.Reader) *Command {
|
|
||||||
c.opts.Stdin = stdin
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,9 +383,10 @@ func (c *Command) Start(ctx context.Context) (retErr error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
|
c.closePipeFiles(c.childrenPipeFiles)
|
||||||
if retErr != nil {
|
if retErr != nil {
|
||||||
// release the pipes to avoid resource leak
|
// release the pipes to avoid resource leak since the command failed to start
|
||||||
c.closeStdioPipes()
|
c.closePipeFiles(c.parentPipeFiles)
|
||||||
// if error occurs, we must also finish the task, otherwise, cmdFinished will be called in "Wait" function
|
// if error occurs, we must also finish the task, otherwise, cmdFinished will be called in "Wait" function
|
||||||
if c.cmdFinished != nil {
|
if c.cmdFinished != nil {
|
||||||
c.cmdFinished()
|
c.cmdFinished()
|
||||||
@@ -359,7 +395,7 @@ func (c *Command) Start(ctx context.Context) (retErr error) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
if len(c.preErrors) != 0 {
|
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...)
|
err := errors.Join(c.preErrors...)
|
||||||
log.Error("git command: %s, error: %s", c.LogString(), err)
|
log.Error("git command: %s, error: %s", c.LogString(), err)
|
||||||
return err
|
return err
|
||||||
@@ -386,7 +422,7 @@ func (c *Command) Start(ctx context.Context) (retErr error) {
|
|||||||
|
|
||||||
c.cmdStartTime = time.Now()
|
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 {
|
if c.opts.Env == nil {
|
||||||
c.cmd.Env = os.Environ()
|
c.cmd.Env = os.Environ()
|
||||||
} else {
|
} else {
|
||||||
@@ -396,52 +432,38 @@ func (c *Command) Start(ctx context.Context) (retErr error) {
|
|||||||
process.SetSysProcAttribute(c.cmd)
|
process.SetSysProcAttribute(c.cmd)
|
||||||
c.cmd.Env = append(c.cmd.Env, CommonGitCmdEnvs()...)
|
c.cmd.Env = append(c.cmd.Env, CommonGitCmdEnvs()...)
|
||||||
c.cmd.Dir = c.opts.Dir
|
c.cmd.Dir = c.opts.Dir
|
||||||
c.cmd.Stdout = c.opts.Stdout
|
c.cmd.Stdout = c.cmdStdout
|
||||||
c.cmd.Stdin = c.opts.Stdin
|
c.cmd.Stdin = c.cmdStdin
|
||||||
|
c.cmd.Stderr = c.cmdStderr
|
||||||
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
|
|
||||||
}
|
|
||||||
return c.cmd.Start()
|
return c.cmd.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Command) closeStdioPipes() {
|
func (c *Command) closePipeFiles(files []*os.File) {
|
||||||
safeClosePtrCloser(c.cmdStdoutReader)
|
for _, f := range files {
|
||||||
safeClosePtrCloser(c.cmdStderrReader)
|
_ = f.Close()
|
||||||
safeClosePtrCloser(c.cmdStdinWriter)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Command) Wait() error {
|
func (c *Command) Wait() error {
|
||||||
defer func() {
|
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()
|
c.cmdFinished()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if c.opts.PipelineFunc != nil {
|
if c.opts.PipelineFunc != nil {
|
||||||
errCallback := c.opts.PipelineFunc(&cmdContext{Context: c.cmdCtx, cmd: c})
|
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 stdio pipes
|
// 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(errCallback)
|
c.cmdCancel(errPipeline)
|
||||||
c.closeStdioPipes()
|
c.closePipeFiles(c.parentPipeFiles)
|
||||||
errWait := c.cmd.Wait()
|
errWait := c.cmd.Wait()
|
||||||
errCause := context.Cause(c.cmdCtx)
|
errCause := context.Cause(c.cmdCtx)
|
||||||
// the pipeline function should be able to know whether it succeeds or fails
|
// 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 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
|
// 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 {
|
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.cmdManagedStderr = &bytes.Buffer{}
|
||||||
|
c.cmdStderr = c.cmdManagedStderr
|
||||||
err := c.Start(ctx)
|
err := c.Start(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &runStdError{err: err}
|
return &runStdError{err: err}
|
||||||
@@ -470,7 +496,7 @@ func (c *Command) StartWithStderr(ctx context.Context) RunStdError {
|
|||||||
|
|
||||||
func (c *Command) WaitWithStderr() RunStdError {
|
func (c *Command) WaitWithStderr() RunStdError {
|
||||||
if c.cmdManagedStderr == nil {
|
if c.cmdManagedStderr == nil {
|
||||||
panic("CombineStderr needs managed (but not caller-provided) stderr pipe")
|
panic("managed stderr buffer is not initialized")
|
||||||
}
|
}
|
||||||
errWait := c.Wait()
|
errWait := c.Wait()
|
||||||
if errWait == nil {
|
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) {
|
func (c *Command) runStdBytes(ctx context.Context) ([]byte, []byte, RunStdError) {
|
||||||
if c.opts.Stdout != nil || c.cmdStdoutReader != nil || c.cmdStderrReader != nil {
|
if c.cmdStdout != nil || c.cmdStderr != 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
|
// 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")
|
panic("stdout and stderr field must be nil when using RunStdBytes")
|
||||||
}
|
}
|
||||||
stdoutBuf := &bytes.Buffer{}
|
stdoutBuf := &bytes.Buffer{}
|
||||||
err := c.WithParentCallerInfo().
|
err := c.WithParentCallerInfo().WithStdoutBuffer(stdoutBuf).RunWithStderr(ctx)
|
||||||
WithStdout(stdoutBuf).
|
|
||||||
RunWithStderr(ctx)
|
|
||||||
return stdoutBuf.Bytes(), c.cmdManagedStderr.Bytes(), err
|
return stdoutBuf.Bytes(), c.cmdManagedStderr.Bytes(), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -121,7 +121,10 @@ func TestRunWithContextTimeout(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
t.Run("WithTimeout", func(t *testing.T) {
|
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)
|
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
|
// 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)
|
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"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -74,9 +73,9 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO
|
|||||||
cmd.AddDashesAndList(opts.PathspecList...)
|
cmd.AddDashesAndList(opts.PathspecList...)
|
||||||
opts.MaxResultLimit = util.IfZero(opts.MaxResultLimit, 50)
|
opts.MaxResultLimit = util.IfZero(opts.MaxResultLimit, 50)
|
||||||
|
|
||||||
var stdoutReader io.ReadCloser
|
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
|
||||||
|
defer stdoutReaderClose()
|
||||||
err := cmd.WithDir(repo.Path).
|
err := cmd.WithDir(repo.Path).
|
||||||
WithStdoutReader(&stdoutReader).
|
|
||||||
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
||||||
isInBlock := false
|
isInBlock := false
|
||||||
rd := bufio.NewReaderSize(stdoutReader, util.IfZero(opts.MaxLineLength, 16*1024))
|
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/container"
|
||||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
"github.com/djherbis/buffer"
|
|
||||||
"github.com/djherbis/nio/v3"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// LogNameStatusRepo opens git log --raw in the provided repo and returns a stdin pipe, a stdout reader and cancel function
|
// 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()) {
|
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
|
// 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 := gitcmd.NewCommand()
|
||||||
cmd.AddArguments("log", "--name-status", "-c", "--format=commit%x00%H %P%x00", "--parents", "--no-renames", "-t", "-z").AddDynamicArguments(head)
|
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...)
|
cmd.AddDashesAndList(files...)
|
||||||
|
|
||||||
|
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
|
||||||
|
ctx, ctxCancel := context.WithCancel(ctx)
|
||||||
go func() {
|
go func() {
|
||||||
err := cmd.WithDir(repository).
|
err := cmd.WithDir(repository).RunWithStderr(ctx)
|
||||||
WithStdout(stdoutWriter).
|
if err != nil && !errors.Is(err, context.Canceled) {
|
||||||
RunWithStderr(ctx)
|
log.Error("Unable to run git command %v: %v", cmd.LogString(), err)
|
||||||
_ = stdoutWriter.CloseWithError(err)
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
bufReader := bufio.NewReaderSize(stdoutReader, 32*1024)
|
bufReader := bufio.NewReaderSize(stdoutReader, 32*1024)
|
||||||
|
|
||||||
return bufReader, cancel
|
return bufReader, func() {
|
||||||
|
ctxCancel()
|
||||||
|
stdoutReaderClose()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogNameStatusRepoParser parses a git log raw output from LogRawRepo
|
// LogNameStatusRepoParser parses a git log raw output from LogRawRepo
|
||||||
|
|||||||
@@ -6,67 +6,33 @@ package pipeline
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CatFileBatchCheck runs cat-file with --batch-check
|
// CatFileBatchCheck runs cat-file with --batch-check
|
||||||
func CatFileBatchCheck(ctx context.Context, shasToCheckReader *io.PipeReader, catFileCheckWriter *io.PipeWriter, wg *sync.WaitGroup, tmpBasePath string) {
|
func CatFileBatchCheck(ctx context.Context, cmd *gitcmd.Command, tmpBasePath string) error {
|
||||||
defer wg.Done()
|
cmd.AddArguments("cat-file", "--batch-check")
|
||||||
defer shasToCheckReader.Close()
|
return cmd.WithDir(tmpBasePath).RunWithStderr(ctx)
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CatFileBatchCheckAllObjects runs cat-file with --batch-check --batch-all
|
// 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) {
|
func CatFileBatchCheckAllObjects(ctx context.Context, cmd *gitcmd.Command, tmpBasePath string) error {
|
||||||
defer wg.Done()
|
return cmd.AddArguments("cat-file", "--batch-check", "--batch-all-objects").WithDir(tmpBasePath).RunWithStderr(ctx)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CatFileBatch runs cat-file --batch
|
// CatFileBatch runs cat-file --batch
|
||||||
func CatFileBatch(ctx context.Context, shasToBatchReader *io.PipeReader, catFileBatchWriter *io.PipeWriter, wg *sync.WaitGroup, tmpBasePath string) {
|
func CatFileBatch(ctx context.Context, cmd *gitcmd.Command, tmpBasePath string) error {
|
||||||
defer wg.Done()
|
return cmd.AddArguments("cat-file", "--batch").WithDir(tmpBasePath).RunWithStderr(ctx)
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// BlobsLessThan1024FromCatFileBatchCheck reads a pipeline from cat-file --batch-check and returns the blobs <1024 in size
|
// 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) {
|
func BlobsLessThan1024FromCatFileBatchCheck(in io.ReadCloser, out io.WriteCloser) error {
|
||||||
defer wg.Done()
|
defer out.Close()
|
||||||
defer catFileCheckReader.Close()
|
scanner := bufio.NewScanner(in)
|
||||||
scanner := bufio.NewScanner(catFileCheckReader)
|
|
||||||
defer func() {
|
|
||||||
_ = shasToBatchWriter.CloseWithError(scanner.Err())
|
|
||||||
}()
|
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
if len(line) == 0 {
|
if len(line) == 0 {
|
||||||
@@ -82,12 +48,12 @@ func BlobsLessThan1024FromCatFileBatchCheck(catFileCheckReader *io.PipeReader, s
|
|||||||
}
|
}
|
||||||
toWrite := []byte(fields[0] + "\n")
|
toWrite := []byte(fields[0] + "\n")
|
||||||
for len(toWrite) > 0 {
|
for len(toWrite) > 0 {
|
||||||
n, err := shasToBatchWriter.Write(toWrite)
|
n, err := out.Write(toWrite)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = catFileCheckReader.CloseWithError(err)
|
return err
|
||||||
break
|
|
||||||
}
|
}
|
||||||
toWrite = toWrite[n:]
|
toWrite = toWrite[n:]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return scanner.Err()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
package pipeline
|
package pipeline
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
@@ -26,7 +25,3 @@ type lfsResultSlice []*LFSResult
|
|||||||
func (a lfsResultSlice) Len() int { return len(a) }
|
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) 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 (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
|
package pipeline
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
|
||||||
@@ -24,7 +23,6 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
|
|||||||
resultsMap := map[string]*LFSResult{}
|
resultsMap := map[string]*LFSResult{}
|
||||||
results := make([]*LFSResult, 0)
|
results := make([]*LFSResult, 0)
|
||||||
|
|
||||||
basePath := repo.Path
|
|
||||||
gogitRepo := repo.GoGitRepo()
|
gogitRepo := repo.GoGitRepo()
|
||||||
|
|
||||||
commitsIter, err := gogitRepo.Log(&gogit.LogOptions{
|
commitsIter, err := gogitRepo.Log(&gogit.LogOptions{
|
||||||
@@ -32,7 +30,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
|
|||||||
All: true,
|
All: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
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 {
|
err = commitsIter.ForEach(func(gitCommit *object.Commit) error {
|
||||||
@@ -66,7 +64,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil && err != io.EOF {
|
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 {
|
for _, result := range resultsMap {
|
||||||
@@ -82,65 +80,6 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
sort.Sort(lfsResultSlice(results))
|
sort.Sort(lfsResultSlice(results))
|
||||||
|
err = fillResultNameRev(repo.Ctx, repo.Path, results)
|
||||||
// Should really use a go-git function here but name-rev is not completed and recapitulating it is not simple
|
return results, err
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,34 +12,27 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FindLFSFile finds commits that contain a provided pointer file hash
|
// 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{}
|
resultsMap := map[string]*LFSResult{}
|
||||||
results := make([]*LFSResult, 0)
|
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.
|
// 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
|
// so let's create a batch stdin and stdout
|
||||||
batch, cancel, err := repo.CatFileBatch(repo.Ctx)
|
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))
|
sort.Sort(lfsResultSlice(results))
|
||||||
|
err = fillResultNameRev(repo.Ctx, repo.Path, results)
|
||||||
// Should really use a go-git function here but name-rev is not completed and recapitulating it is not simple
|
return results, err
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,25 +4,54 @@
|
|||||||
package pipeline
|
package pipeline
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"errors"
|
||||||
"io"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||||
|
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NameRevStdin runs name-rev --stdin
|
func fillResultNameRev(ctx context.Context, basePath string, results []*LFSResult) error {
|
||||||
func NameRevStdin(ctx context.Context, shasToNameReader *io.PipeReader, nameRevStdinWriter *io.PipeWriter, wg *sync.WaitGroup, tmpBasePath string) {
|
// Should really use a go-git function here but name-rev is not completed and recapitulating it is not simple
|
||||||
defer wg.Done()
|
wg := errgroup.Group{}
|
||||||
defer shasToNameReader.Close()
|
cmd := gitcmd.NewCommand("name-rev", "--stdin", "--name-only", "--always").WithDir(basePath)
|
||||||
defer nameRevStdinWriter.Close()
|
stdin, stdinClose := cmd.MakeStdinPipe()
|
||||||
|
stdout, stdoutClose := cmd.MakeStdoutPipe()
|
||||||
|
defer stdinClose()
|
||||||
|
defer stdoutClose()
|
||||||
|
|
||||||
if err := gitcmd.NewCommand("name-rev", "--stdin", "--name-only", "--always").
|
wg.Go(func() error {
|
||||||
WithDir(tmpBasePath).
|
scanner := bufio.NewScanner(stdout)
|
||||||
WithStdin(shasToNameReader).
|
i := 0
|
||||||
WithStdout(nameRevStdinWriter).
|
for scanner.Scan() {
|
||||||
RunWithStderr(ctx); err != nil {
|
line := scanner.Text()
|
||||||
_ = shasToNameReader.CloseWithError(fmt.Errorf("git name-rev [%s]: %w", tmpBasePath, err))
|
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 (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
"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
|
// 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) {
|
func RevListObjects(ctx context.Context, cmd *gitcmd.Command, tmpBasePath, headSHA, baseSHA string) error {
|
||||||
defer wg.Done()
|
cmd.AddArguments("rev-list", "--objects").AddDynamicArguments(headSHA)
|
||||||
defer revListWriter.Close()
|
|
||||||
|
|
||||||
cmd := gitcmd.NewCommand("rev-list", "--objects").AddDynamicArguments(headSHA)
|
|
||||||
if baseSHA != "" {
|
if baseSHA != "" {
|
||||||
cmd = cmd.AddArguments("--not").AddDynamicArguments(baseSHA)
|
cmd = cmd.AddArguments("--not").AddDynamicArguments(baseSHA)
|
||||||
}
|
}
|
||||||
if err := cmd.WithDir(tmpBasePath).
|
return cmd.WithDir(tmpBasePath).RunWithStderr(ctx)
|
||||||
WithStdout(revListWriter).
|
|
||||||
RunWithStderr(ctx); err != nil {
|
|
||||||
errChan <- fmt.Errorf("git rev-list [%s]: %w", tmpBasePath, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// BlobsFromRevListObjects reads a RevListAllObjects and only selects blobs
|
// BlobsFromRevListObjects reads a RevListAllObjects and only selects blobs
|
||||||
func BlobsFromRevListObjects(revListReader *io.PipeReader, shasToCheckWriter *io.PipeWriter, wg *sync.WaitGroup) {
|
func BlobsFromRevListObjects(in io.ReadCloser, out io.WriteCloser) error {
|
||||||
defer wg.Done()
|
defer out.Close()
|
||||||
defer revListReader.Close()
|
scanner := bufio.NewScanner(in)
|
||||||
scanner := bufio.NewScanner(revListReader)
|
|
||||||
defer func() {
|
|
||||||
_ = shasToCheckWriter.CloseWithError(scanner.Err())
|
|
||||||
}()
|
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
if len(line) == 0 {
|
if len(line) == 0 {
|
||||||
@@ -63,12 +36,12 @@ func BlobsFromRevListObjects(revListReader *io.PipeReader, shasToCheckWriter *io
|
|||||||
}
|
}
|
||||||
toWrite := []byte(fields[0] + "\n")
|
toWrite := []byte(fields[0] + "\n")
|
||||||
for len(toWrite) > 0 {
|
for len(toWrite) > 0 {
|
||||||
n, err := shasToCheckWriter.Write(toWrite)
|
n, err := out.Write(toWrite)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = revListReader.CloseWithError(err)
|
return err
|
||||||
break
|
|
||||||
}
|
}
|
||||||
toWrite = toWrite[n:]
|
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) {
|
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
|
i := 0
|
||||||
bufReader := bufio.NewReader(stdoutReader)
|
args := gitcmd.TrustedCmdArgs{"for-each-ref", "--format=%(objectname) %(refname)"}
|
||||||
for i < skip {
|
args = append(args, extraArgs...)
|
||||||
_, isPrefix, err := bufReader.ReadLine()
|
cmd := gitcmd.NewCommand(args...)
|
||||||
if err == io.EOF {
|
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
|
||||||
return i, nil
|
defer stdoutReaderClose()
|
||||||
}
|
cmd.WithDir(repoPath).
|
||||||
if err != nil {
|
WithPipelineFunc(func(c gitcmd.Context) error {
|
||||||
return 0, err
|
bufReader := bufio.NewReader(stdoutReader)
|
||||||
}
|
for i < skip {
|
||||||
if !isPrefix {
|
_, isPrefix, err := bufReader.ReadLine()
|
||||||
i++
|
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 {
|
return i, err
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRefsBySha returns all references filtered with prefix that belong to a sha commit hash
|
// 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
|
// CommitsByFileAndRange return the commits according revision file and the page
|
||||||
func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) ([]*Commit, error) {
|
func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) ([]*Commit, error) {
|
||||||
stdoutReader, stdoutWriter := io.Pipe()
|
gitCmd := gitcmd.NewCommand("rev-list").
|
||||||
defer func() {
|
AddOptionFormat("--max-count=%d", setting.Git.CommitsRangeSize).
|
||||||
_ = stdoutReader.Close()
|
AddOptionFormat("--skip=%d", (opts.Page-1)*setting.Git.CommitsRangeSize)
|
||||||
_ = stdoutWriter.Close()
|
gitCmd.AddDynamicArguments(opts.Revision)
|
||||||
}()
|
|
||||||
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)
|
|
||||||
|
|
||||||
if opts.Not != "" {
|
if opts.Not != "" {
|
||||||
gitCmd.AddOptionValues("--not", 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.Since != "" {
|
||||||
|
gitCmd.AddOptionFormat("--since=%s", opts.Since)
|
||||||
|
}
|
||||||
|
if opts.Until != "" {
|
||||||
|
gitCmd.AddOptionFormat("--until=%s", opts.Until)
|
||||||
|
}
|
||||||
|
gitCmd.AddDashesAndList(opts.File)
|
||||||
|
|
||||||
length := objectFormat.FullLength()
|
var commits []*Commit
|
||||||
commits := []*Commit{}
|
stdoutReader, stdoutReaderClose := gitCmd.MakeStdoutPipe()
|
||||||
shaline := make([]byte, length+1)
|
defer stdoutReaderClose()
|
||||||
for {
|
err := gitCmd.WithDir(repo.Path).
|
||||||
n, err := io.ReadFull(stdoutReader, shaline)
|
WithPipelineFunc(func(context gitcmd.Context) error {
|
||||||
if err != nil || n < length {
|
objectFormat, err := repo.GetObjectFormat()
|
||||||
if err == io.EOF {
|
if err != nil {
|
||||||
err = nil
|
return err
|
||||||
}
|
}
|
||||||
return commits, err
|
|
||||||
}
|
length := objectFormat.FullLength()
|
||||||
objectID, err := NewIDFromString(string(shaline[0:length]))
|
shaline := make([]byte, length+1)
|
||||||
if err != nil {
|
for {
|
||||||
return nil, err
|
n, err := io.ReadFull(stdoutReader, shaline)
|
||||||
}
|
if err != nil || n < length {
|
||||||
commit, err := repo.getCommit(objectID)
|
if err == io.EOF {
|
||||||
if err != nil {
|
err = nil
|
||||||
return nil, err
|
}
|
||||||
}
|
return err
|
||||||
commits = append(commits, commit)
|
}
|
||||||
}
|
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
|
// 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).
|
AddDynamicArguments(base + separator + head).
|
||||||
AddArguments("--").
|
AddArguments("--").
|
||||||
WithDir(repo.Path).
|
WithDir(repo.Path).
|
||||||
WithStdout(w).
|
WithStdoutCopy(w).
|
||||||
RunWithStderr(repo.Ctx); err != nil {
|
RunWithStderr(repo.Ctx); err != nil {
|
||||||
if strings.Contains(err.Stderr(), "no merge base") {
|
if strings.Contains(err.Stderr(), "no merge base") {
|
||||||
// git >= 2.28 now returns an error if base and head have become unrelated.
|
// 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).
|
AddDynamicArguments(base, head).
|
||||||
AddArguments("--").
|
AddArguments("--").
|
||||||
WithDir(repo.Path).
|
WithDir(repo.Path).
|
||||||
WithStdout(w).
|
WithStdoutCopy(w).
|
||||||
RunWithStderr(repo.Ctx); err == nil {
|
RunWithStderr(repo.Ctx); err == nil {
|
||||||
return w.numLines, 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 {
|
func (repo *Repository) GetDiff(compareArg string, w io.Writer) error {
|
||||||
return gitcmd.NewCommand("diff", "-p").AddDynamicArguments(compareArg).
|
return gitcmd.NewCommand("diff", "-p").AddDynamicArguments(compareArg).
|
||||||
WithDir(repo.Path).
|
WithDir(repo.Path).
|
||||||
WithStdout(w).
|
WithStdoutCopy(w).
|
||||||
Run(repo.Ctx)
|
Run(repo.Ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ func (repo *Repository) GetDiffBinary(compareArg string, w io.Writer) error {
|
|||||||
return gitcmd.NewCommand("diff", "-p", "--binary", "--histogram").
|
return gitcmd.NewCommand("diff", "-p", "--binary", "--histogram").
|
||||||
AddDynamicArguments(compareArg).
|
AddDynamicArguments(compareArg).
|
||||||
WithDir(repo.Path).
|
WithDir(repo.Path).
|
||||||
WithStdout(w).
|
WithStdoutCopy(w).
|
||||||
Run(repo.Ctx)
|
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 {
|
func (repo *Repository) GetPatch(compareArg string, w io.Writer) error {
|
||||||
return gitcmd.NewCommand("format-patch", "--binary", "--stdout").AddDynamicArguments(compareArg).
|
return gitcmd.NewCommand("format-patch", "--binary", "--stdout").AddDynamicArguments(compareArg).
|
||||||
WithDir(repo.Path).
|
WithDir(repo.Path).
|
||||||
WithStdout(w).
|
WithStdoutCopy(w).
|
||||||
Run(repo.Ctx)
|
Run(repo.Ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ func (repo *Repository) RemoveFilesFromIndex(filenames ...string) error {
|
|||||||
}
|
}
|
||||||
return cmd.
|
return cmd.
|
||||||
WithDir(repo.Path).
|
WithDir(repo.Path).
|
||||||
WithStdin(bytes.NewReader(input.Bytes())).
|
WithStdinBytes(input.Bytes()).
|
||||||
RunWithStderr(repo.Ctx)
|
RunWithStderr(repo.Ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ func (repo *Repository) AddObjectsToIndex(objects ...IndexObjectInfo) error {
|
|||||||
}
|
}
|
||||||
return cmd.
|
return cmd.
|
||||||
WithDir(repo.Path).
|
WithDir(repo.Path).
|
||||||
WithStdin(bytes.NewReader(input.Bytes())).
|
WithStdinBytes(input.Bytes()).
|
||||||
RunWithStderr(repo.Ctx)
|
RunWithStderr(repo.Ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||||
@@ -32,18 +31,12 @@ func (o ObjectType) Bytes() []byte {
|
|||||||
return []byte(o)
|
return []byte(o)
|
||||||
}
|
}
|
||||||
|
|
||||||
type EmptyReader struct{}
|
|
||||||
|
|
||||||
func (EmptyReader) Read(p []byte) (int, error) {
|
|
||||||
return 0, io.EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *Repository) GetObjectFormat() (ObjectFormat, error) {
|
func (repo *Repository) GetObjectFormat() (ObjectFormat, error) {
|
||||||
if repo != nil && repo.objectFormat != nil {
|
if repo != nil && repo.objectFormat != nil {
|
||||||
return repo.objectFormat, nil
|
return repo.objectFormat, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
str, err := repo.hashObject(EmptyReader{}, false)
|
str, err := repo.hashObjectBytes(nil, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -57,16 +50,16 @@ func (repo *Repository) GetObjectFormat() (ObjectFormat, error) {
|
|||||||
return repo.objectFormat, nil
|
return repo.objectFormat, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// HashObject takes a reader and returns hash for that reader
|
// HashObjectBytes returns hash for the content
|
||||||
func (repo *Repository) HashObject(reader io.Reader) (ObjectID, error) {
|
func (repo *Repository) HashObjectBytes(buf []byte) (ObjectID, error) {
|
||||||
idStr, err := repo.hashObject(reader, true)
|
idStr, err := repo.hashObjectBytes(buf, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return NewIDFromString(idStr)
|
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
|
var cmd *gitcmd.Command
|
||||||
if save {
|
if save {
|
||||||
cmd = gitcmd.NewCommand("hash-object", "-w", "--stdin")
|
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.
|
stdout, _, err := cmd.
|
||||||
WithDir(repo.Path).
|
WithDir(repo.Path).
|
||||||
WithStdin(reader).
|
WithStdinBytes(buf).
|
||||||
RunStdString(repo.Ctx)
|
RunStdString(repo.Ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|||||||
@@ -15,69 +15,61 @@ import (
|
|||||||
|
|
||||||
// GetRefsFiltered returns all references of the repository that matches patterm exactly or starting with.
|
// GetRefsFiltered returns all references of the repository that matches patterm exactly or starting with.
|
||||||
func (repo *Repository) GetRefsFiltered(pattern string) ([]*Reference, error) {
|
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)
|
refs := make([]*Reference, 0)
|
||||||
bufReader := bufio.NewReader(stdoutReader)
|
cmd := gitcmd.NewCommand("for-each-ref")
|
||||||
for {
|
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
|
||||||
// The output of for-each-ref is simply a list:
|
defer stdoutReaderClose()
|
||||||
// <sha> SP <type> TAB <ref> LF
|
err := cmd.WithDir(repo.Path).
|
||||||
sha, err := bufReader.ReadString(' ')
|
WithPipelineFunc(func(context gitcmd.Context) error {
|
||||||
if err == io.EOF {
|
bufReader := bufio.NewReader(stdoutReader)
|
||||||
break
|
for {
|
||||||
}
|
// The output of for-each-ref is simply a list:
|
||||||
if err != nil {
|
// <sha> SP <type> TAB <ref> LF
|
||||||
return nil, err
|
sha, err := bufReader.ReadString(' ')
|
||||||
}
|
if err == io.EOF {
|
||||||
sha = sha[:len(sha)-1]
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sha = sha[:len(sha)-1]
|
||||||
|
|
||||||
typ, err := bufReader.ReadString('\t')
|
typ, err := bufReader.ReadString('\t')
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
// This should not happen, but we'll tolerate it
|
// This should not happen, but we'll tolerate it
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
typ = typ[:len(typ)-1]
|
typ = typ[:len(typ)-1]
|
||||||
|
|
||||||
refName, err := bufReader.ReadString('\n')
|
refName, err := bufReader.ReadString('\n')
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
// This should not happen, but we'll tolerate it
|
// This should not happen, but we'll tolerate it
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
refName = refName[:len(refName)-1]
|
refName = refName[:len(refName)-1]
|
||||||
|
|
||||||
// refName cannot be HEAD but can be remotes or stash
|
// refName cannot be HEAD but can be remotes or stash
|
||||||
if strings.HasPrefix(refName, RemotePrefix) || refName == "/refs/stash" {
|
if strings.HasPrefix(refName, RemotePrefix) || refName == "/refs/stash" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if pattern == "" || strings.HasPrefix(refName, pattern) {
|
if pattern == "" || strings.HasPrefix(refName, pattern) {
|
||||||
r := &Reference{
|
r := &Reference{
|
||||||
Name: refName,
|
Name: refName,
|
||||||
Object: MustIDFromString(sha),
|
Object: MustIDFromString(sha),
|
||||||
Type: typ,
|
Type: typ,
|
||||||
repo: repo,
|
repo: repo,
|
||||||
|
}
|
||||||
|
refs = append(refs, r)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
refs = append(refs, r)
|
return nil
|
||||||
}
|
}).RunWithStderr(repo.Ctx)
|
||||||
}
|
return refs, err
|
||||||
|
|
||||||
return refs, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ package git
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -62,10 +61,10 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string)
|
|||||||
gitCmd.AddArguments("--first-parent").AddDynamicArguments(branch)
|
gitCmd.AddArguments("--first-parent").AddDynamicArguments(branch)
|
||||||
}
|
}
|
||||||
|
|
||||||
var stdoutReader io.ReadCloser
|
stdoutReader, stdoutReaderClose := gitCmd.MakeStdoutPipe()
|
||||||
|
defer stdoutReaderClose()
|
||||||
err = gitCmd.
|
err = gitCmd.
|
||||||
WithDir(repo.Path).
|
WithDir(repo.Path).
|
||||||
WithStdoutReader(&stdoutReader).
|
|
||||||
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
||||||
scanner := bufio.NewScanner(stdoutReader)
|
scanner := bufio.NewScanner(stdoutReader)
|
||||||
scanner.Split(bufio.ScanLines)
|
scanner.Split(bufio.ScanLines)
|
||||||
@@ -117,7 +116,6 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err = scanner.Err(); err != nil {
|
if err = scanner.Err(); err != nil {
|
||||||
_ = stdoutReader.Close()
|
|
||||||
return fmt.Errorf("GetCodeActivityStats scan: %w", err)
|
return fmt.Errorf("GetCodeActivityStats scan: %w", err)
|
||||||
}
|
}
|
||||||
a := make([]*CodeActivityAuthor, 0, len(authors))
|
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.AuthorCount = int64(len(authors))
|
||||||
stats.ChangedFiles = int64(len(files))
|
stats.ChangedFiles = int64(len(files))
|
||||||
stats.Authors = a
|
stats.Authors = a
|
||||||
_ = stdoutReader.Close()
|
|
||||||
return nil
|
return nil
|
||||||
}).
|
}).
|
||||||
RunWithStderr(repo.Ctx)
|
RunWithStderr(repo.Ctx)
|
||||||
|
|||||||
+32
-36
@@ -6,7 +6,6 @@ package git
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git/foreachref"
|
"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
|
// 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")
|
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
|
var tags []*Tag
|
||||||
parser := forEachRefFmt.Parser(stdoutReader)
|
var tagsTotal int
|
||||||
for {
|
cmd := gitcmd.NewCommand("for-each-ref")
|
||||||
ref := parser.Next()
|
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
|
||||||
if ref == nil {
|
defer stdoutReaderClose()
|
||||||
break
|
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)
|
tag, err := parseTagRef(ref)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, fmt.Errorf("GetTagInfos: parse tag: %w", err)
|
return fmt.Errorf("GetTagInfos: parse tag: %w", err)
|
||||||
}
|
}
|
||||||
tags = append(tags, tag)
|
tags = append(tags, tag)
|
||||||
}
|
}
|
||||||
if err := parser.Err(); err != nil {
|
if err := parser.Err(); err != nil {
|
||||||
return nil, 0, fmt.Errorf("GetTagInfos: parse output: %w", err)
|
return fmt.Errorf("GetTagInfos: parse output: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sortTagsByTime(tags)
|
sortTagsByTime(tags)
|
||||||
tagsTotal := len(tags)
|
tagsTotal = len(tags)
|
||||||
if page != 0 {
|
if page != 0 {
|
||||||
tags = util.PaginateSlice(tags, page, pageSize).([]*Tag)
|
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.
|
// 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).
|
stdout, _, err := cmd.WithEnv(env).
|
||||||
WithDir(repo.Path).
|
WithDir(repo.Path).
|
||||||
WithStdin(messageBytes).
|
WithStdinBytes(messageBytes.Bytes()).
|
||||||
RunStdString(repo.Ctx)
|
RunStdString(repo.Ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"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
|
// 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.
|
// 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) {
|
func GetTemplateSubmoduleCommits(ctx context.Context, repoPath string) (submoduleCommits []TemplateSubmoduleCommit, _ error) {
|
||||||
var stdoutReader io.ReadCloser
|
cmd := gitcmd.NewCommand("ls-tree", "-r", "--", "HEAD")
|
||||||
err := gitcmd.NewCommand("ls-tree", "-r", "--", "HEAD").
|
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
|
||||||
WithDir(repoPath).
|
defer stdoutReaderClose()
|
||||||
WithStdoutReader(&stdoutReader).
|
err := cmd.WithDir(repoPath).
|
||||||
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
||||||
scanner := bufio.NewScanner(stdoutReader)
|
scanner := bufio.NewScanner(stdoutReader)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func CreateArchive(ctx context.Context, repo Repository, format string, target i
|
|||||||
paths[i] = path.Clean(paths[i])
|
paths[i] = path.Clean(paths[i])
|
||||||
}
|
}
|
||||||
cmd.AddDynamicArguments(paths...)
|
cmd.AddDynamicArguments(paths...)
|
||||||
return RunCmdWithStderr(ctx, repo, cmd.WithStdout(target))
|
return RunCmdWithStderr(ctx, repo, cmd.WithStdoutCopy(target))
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateBundle create bundle content to the target path
|
// CreateBundle create bundle content to the target path
|
||||||
|
|||||||
+25
-36
@@ -8,7 +8,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||||
@@ -34,8 +33,6 @@ type BlamePart struct {
|
|||||||
|
|
||||||
// BlameReader returns part of file blame one by one
|
// BlameReader returns part of file blame one by one
|
||||||
type BlameReader struct {
|
type BlameReader struct {
|
||||||
output io.WriteCloser
|
|
||||||
reader io.ReadCloser
|
|
||||||
bufferedReader *bufio.Reader
|
bufferedReader *bufio.Reader
|
||||||
done chan error
|
done chan error
|
||||||
lastSha *string
|
lastSha *string
|
||||||
@@ -131,34 +128,42 @@ func (r *BlameReader) Close() error {
|
|||||||
|
|
||||||
err := <-r.done
|
err := <-r.done
|
||||||
r.bufferedReader = nil
|
r.bufferedReader = nil
|
||||||
_ = r.reader.Close()
|
r.cleanup()
|
||||||
_ = r.output.Close()
|
|
||||||
for _, cleanup := range r.cleanupFuncs {
|
|
||||||
if cleanup != nil {
|
|
||||||
cleanup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *BlameReader) cleanup() {
|
||||||
|
for _, cleanup := range r.cleanupFuncs {
|
||||||
|
cleanup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// CreateBlameReader creates reader for given repository, commit and file
|
// CreateBlameReader creates reader for given repository, commit and file
|
||||||
func CreateBlameReader(ctx context.Context, objectFormat git.ObjectFormat, repo Repository, commit *git.Commit, file string, bypassBlameIgnore bool) (rd *BlameReader, err error) {
|
func CreateBlameReader(ctx context.Context, objectFormat git.ObjectFormat, repo Repository, commit *git.Commit, file string, bypassBlameIgnore bool) (rd *BlameReader, retErr error) {
|
||||||
var ignoreRevsFileName string
|
|
||||||
var ignoreRevsFileCleanup func()
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil && ignoreRevsFileCleanup != nil {
|
if retErr != nil {
|
||||||
ignoreRevsFileCleanup()
|
rd.cleanup()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
rd = &BlameReader{
|
||||||
|
done: make(chan error, 1),
|
||||||
|
objectFormat: objectFormat,
|
||||||
|
}
|
||||||
|
|
||||||
cmd := gitcmd.NewCommand("blame", "--porcelain")
|
cmd := gitcmd.NewCommand("blame", "--porcelain")
|
||||||
|
|
||||||
|
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
|
||||||
|
rd.bufferedReader = bufio.NewReader(stdoutReader)
|
||||||
|
rd.cleanupFuncs = append(rd.cleanupFuncs, stdoutReaderClose)
|
||||||
|
|
||||||
if git.DefaultFeatures().CheckVersionAtLeast("2.23") && !bypassBlameIgnore {
|
if git.DefaultFeatures().CheckVersionAtLeast("2.23") && !bypassBlameIgnore {
|
||||||
ignoreRevsFileName, ignoreRevsFileCleanup, err = tryCreateBlameIgnoreRevsFile(commit)
|
ignoreRevsFileName, ignoreRevsFileCleanup, err := tryCreateBlameIgnoreRevsFile(commit)
|
||||||
if err != nil && !git.IsErrNotExist(err) {
|
if err != nil && !git.IsErrNotExist(err) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
} else if err == nil {
|
||||||
if ignoreRevsFileName != "" {
|
rd.ignoreRevsFile = ignoreRevsFileName
|
||||||
|
rd.cleanupFuncs = append(rd.cleanupFuncs, ignoreRevsFileCleanup)
|
||||||
// Possible improvement: use --ignore-revs-file /dev/stdin on unix
|
// Possible improvement: use --ignore-revs-file /dev/stdin on unix
|
||||||
// There is no equivalent on Windows. May be implemented if Gitea uses an external git backend.
|
// There is no equivalent on Windows. May be implemented if Gitea uses an external git backend.
|
||||||
cmd.AddOptionValues("--ignore-revs-file", ignoreRevsFileName)
|
cmd.AddOptionValues("--ignore-revs-file", ignoreRevsFileName)
|
||||||
@@ -167,28 +172,12 @@ func CreateBlameReader(ctx context.Context, objectFormat git.ObjectFormat, repo
|
|||||||
|
|
||||||
cmd.AddDynamicArguments(commit.ID.String()).AddDashesAndList(file)
|
cmd.AddDynamicArguments(commit.ID.String()).AddDashesAndList(file)
|
||||||
|
|
||||||
done := make(chan error, 1)
|
|
||||||
reader, stdout, err := os.Pipe()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
go func() {
|
go func() {
|
||||||
// TODO: it doesn't work for directories (the directories shouldn't be "blamed"), and the "err" should be returned by "Read" but not by "Close"
|
// TODO: it doesn't work for directories (the directories shouldn't be "blamed"), and the "err" should be returned by "Read" but not by "Close"
|
||||||
err := RunCmdWithStderr(ctx, repo, cmd.WithStdout(stdout))
|
rd.done <- RunCmdWithStderr(ctx, repo, cmd)
|
||||||
done <- err
|
|
||||||
_ = stdout.Close()
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
bufferedReader := bufio.NewReader(reader)
|
return rd, nil
|
||||||
return &BlameReader{
|
|
||||||
output: stdout,
|
|
||||||
reader: reader,
|
|
||||||
bufferedReader: bufferedReader,
|
|
||||||
done: done,
|
|
||||||
ignoreRevsFile: ignoreRevsFileName,
|
|
||||||
objectFormat: objectFormat,
|
|
||||||
cleanupFuncs: []func(){ignoreRevsFileCleanup},
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func tryCreateBlameIgnoreRevsFile(commit *git.Commit) (string, func(), error) {
|
func tryCreateBlameIgnoreRevsFile(commit *git.Commit) (string, func(), error) {
|
||||||
|
|||||||
@@ -67,20 +67,18 @@ func parseCommitFileStatus(fileStatus *CommitFileStatus, stdout io.Reader) {
|
|||||||
|
|
||||||
// GetCommitFileStatus returns file status of commit in given repository.
|
// GetCommitFileStatus returns file status of commit in given repository.
|
||||||
func GetCommitFileStatus(ctx context.Context, repo Repository, commitID string) (*CommitFileStatus, error) {
|
func GetCommitFileStatus(ctx context.Context, repo Repository, commitID string) (*CommitFileStatus, error) {
|
||||||
stdout, w := io.Pipe()
|
cmd := gitcmd.NewCommand("log", "--name-status", "-m", "--pretty=format:", "--first-parent", "--no-renames", "-z", "-1")
|
||||||
|
stdout, stdoutClose := cmd.MakeStdoutPipe()
|
||||||
|
defer stdoutClose()
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
fileStatus := NewCommitFileStatus()
|
fileStatus := NewCommitFileStatus()
|
||||||
go func() {
|
go func() {
|
||||||
parseCommitFileStatus(fileStatus, stdout)
|
parseCommitFileStatus(fileStatus, stdout)
|
||||||
close(done)
|
close(done)
|
||||||
}()
|
}()
|
||||||
|
err := cmd.AddDynamicArguments(commitID).
|
||||||
err := gitcmd.NewCommand("log", "--name-status", "-m", "--pretty=format:", "--first-parent", "--no-renames", "-z", "-1").
|
|
||||||
AddDynamicArguments(commitID).
|
|
||||||
WithDir(repoPath(repo)).
|
WithDir(repoPath(repo)).
|
||||||
WithStdout(w).
|
|
||||||
RunWithStderr(ctx)
|
RunWithStderr(ctx)
|
||||||
_ = w.Close() // Close writer to exit parsing goroutine
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,6 @@ func parseDiffStat(stdout string) (numFiles, totalAdditions, totalDeletions int,
|
|||||||
func GetReverseRawDiff(ctx context.Context, repo Repository, commitID string, writer io.Writer) error {
|
func GetReverseRawDiff(ctx context.Context, repo Repository, commitID string, writer io.Writer) error {
|
||||||
return RunCmdWithStderr(ctx, repo, gitcmd.NewCommand("show", "--pretty=format:revert %H%n", "-R").
|
return RunCmdWithStderr(ctx, repo, gitcmd.NewCommand("show", "--pretty=format:revert %H%n", "-R").
|
||||||
AddDynamicArguments(commitID).
|
AddDynamicArguments(commitID).
|
||||||
WithStdout(writer),
|
WithStdoutCopy(writer),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// SearchPointerBlobs scans the whole repository for LFS pointer files
|
// SearchPointerBlobs scans the whole repository for LFS pointer files
|
||||||
func SearchPointerBlobs(ctx context.Context, repo *git.Repository, pointerChan chan<- PointerBlob, errChan chan<- error) {
|
func SearchPointerBlobs(ctx context.Context, repo *git.Repository, pointerChan chan<- PointerBlob) error {
|
||||||
gitRepo := repo.GoGitRepo()
|
gitRepo := repo.GoGitRepo()
|
||||||
|
|
||||||
err := func() error {
|
err := func() error {
|
||||||
@@ -49,14 +49,7 @@ func SearchPointerBlobs(ctx context.Context, repo *git.Repository, pointerChan c
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}()
|
}()
|
||||||
if err != nil {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
default:
|
|
||||||
errChan <- err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
close(pointerChan)
|
close(pointerChan)
|
||||||
close(errChan)
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,96 +8,84 @@ package lfs
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||||
"code.gitea.io/gitea/modules/git/pipeline"
|
"code.gitea.io/gitea/modules/git/pipeline"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SearchPointerBlobs scans the whole repository for LFS pointer files
|
// SearchPointerBlobs scans the whole repository for LFS pointer files
|
||||||
func SearchPointerBlobs(ctx context.Context, repo *git.Repository, pointerChan chan<- PointerBlob, errChan chan<- error) {
|
func SearchPointerBlobs(ctx context.Context, repo *git.Repository, pointerChan chan<- PointerBlob) error {
|
||||||
basePath := repo.Path
|
cmd1AllObjs, cmd3BatchContent := gitcmd.NewCommand(), gitcmd.NewCommand()
|
||||||
|
|
||||||
catFileCheckReader, catFileCheckWriter := io.Pipe()
|
cmd1AllObjsStdout, cmd1AllObjsStdoutClose := cmd1AllObjs.MakeStdoutPipe()
|
||||||
shasToBatchReader, shasToBatchWriter := io.Pipe()
|
defer cmd1AllObjsStdoutClose()
|
||||||
catFileBatchReader, catFileBatchWriter := io.Pipe()
|
|
||||||
|
|
||||||
wg := sync.WaitGroup{}
|
cmd3BatchContentIn, cmd3BatchContentOut, cmd3BatchContentClose := cmd3BatchContent.MakeStdinStdoutPipe()
|
||||||
wg.Add(4)
|
defer cmd3BatchContentClose()
|
||||||
|
|
||||||
// Create the go-routines in reverse order.
|
|
||||||
|
|
||||||
|
// Create the go-routines in reverse order (update: the order is not needed any more, the pipes are properly prepared)
|
||||||
|
wg := errgroup.Group{}
|
||||||
// 4. Take the output of cat-file --batch and check if each file in turn
|
// 4. Take the output of cat-file --batch and check if each file in turn
|
||||||
// to see if they're pointers to files in the LFS store
|
// to see if they're pointers to files in the LFS store
|
||||||
go createPointerResultsFromCatFileBatch(ctx, catFileBatchReader, &wg, pointerChan)
|
wg.Go(func() error {
|
||||||
|
return createPointerResultsFromCatFileBatch(cmd3BatchContentOut, pointerChan)
|
||||||
|
})
|
||||||
|
|
||||||
// 3. Take the shas of the blobs and batch read them
|
// 3. Take the shas of the blobs and batch read them
|
||||||
go pipeline.CatFileBatch(ctx, shasToBatchReader, catFileBatchWriter, &wg, basePath)
|
wg.Go(func() error {
|
||||||
|
return pipeline.CatFileBatch(ctx, cmd3BatchContent, repo.Path)
|
||||||
|
})
|
||||||
|
|
||||||
// 2. From the provided objects restrict to blobs <=1k
|
// 2. From the provided objects restrict to blobs <=1k
|
||||||
go pipeline.BlobsLessThan1024FromCatFileBatchCheck(catFileCheckReader, shasToBatchWriter, &wg)
|
wg.Go(func() error {
|
||||||
|
return pipeline.BlobsLessThan1024FromCatFileBatchCheck(cmd1AllObjsStdout, cmd3BatchContentIn)
|
||||||
|
})
|
||||||
|
|
||||||
// 1. Run batch-check on all objects in the repository
|
// 1. Run batch-check on all objects in the repository
|
||||||
if !git.DefaultFeatures().CheckVersionAtLeast("2.6.0") {
|
wg.Go(func() error {
|
||||||
revListReader, revListWriter := io.Pipe()
|
return pipeline.CatFileBatchCheckAllObjects(ctx, cmd1AllObjs, repo.Path)
|
||||||
shasToCheckReader, shasToCheckWriter := io.Pipe()
|
})
|
||||||
wg.Add(2)
|
err := wg.Wait()
|
||||||
go pipeline.CatFileBatchCheck(ctx, shasToCheckReader, catFileCheckWriter, &wg, basePath)
|
|
||||||
go pipeline.BlobsFromRevListObjects(revListReader, shasToCheckWriter, &wg)
|
|
||||||
go pipeline.RevListAllObjects(ctx, revListWriter, &wg, basePath, errChan)
|
|
||||||
} else {
|
|
||||||
go pipeline.CatFileBatchCheckAllObjects(ctx, catFileCheckWriter, &wg, basePath, errChan)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
close(pointerChan)
|
close(pointerChan)
|
||||||
close(errChan)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func createPointerResultsFromCatFileBatch(ctx context.Context, catFileBatchReader *io.PipeReader, wg *sync.WaitGroup, pointerChan chan<- PointerBlob) {
|
func createPointerResultsFromCatFileBatch(catFileBatchReader io.ReadCloser, pointerChan chan<- PointerBlob) error {
|
||||||
defer wg.Done()
|
|
||||||
defer catFileBatchReader.Close()
|
defer catFileBatchReader.Close()
|
||||||
|
|
||||||
bufferedReader := bufio.NewReader(catFileBatchReader)
|
bufferedReader := bufio.NewReader(catFileBatchReader)
|
||||||
buf := make([]byte, 1025)
|
buf := make([]byte, 1025)
|
||||||
|
|
||||||
loop:
|
|
||||||
for {
|
for {
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
break loop
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// File descriptor line: sha
|
// File descriptor line: sha
|
||||||
sha, err := bufferedReader.ReadString(' ')
|
sha, err := bufferedReader.ReadString(' ')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = catFileBatchReader.CloseWithError(err)
|
return util.Iif(errors.Is(err, io.EOF), nil, err)
|
||||||
break
|
|
||||||
}
|
}
|
||||||
sha = strings.TrimSpace(sha)
|
sha = strings.TrimSpace(sha)
|
||||||
// Throw away the blob
|
// Throw away the blob
|
||||||
if _, err := bufferedReader.ReadString(' '); err != nil {
|
if _, err := bufferedReader.ReadString(' '); err != nil {
|
||||||
_ = catFileBatchReader.CloseWithError(err)
|
return err
|
||||||
break
|
|
||||||
}
|
}
|
||||||
sizeStr, err := bufferedReader.ReadString('\n')
|
sizeStr, err := bufferedReader.ReadString('\n')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = catFileBatchReader.CloseWithError(err)
|
return err
|
||||||
break
|
|
||||||
}
|
}
|
||||||
size, err := strconv.Atoi(sizeStr[:len(sizeStr)-1])
|
size, err := strconv.Atoi(sizeStr[:len(sizeStr)-1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = catFileBatchReader.CloseWithError(err)
|
return err
|
||||||
break
|
|
||||||
}
|
}
|
||||||
pointerBuf := buf[:size+1]
|
pointerBuf := buf[:size+1]
|
||||||
if _, err := io.ReadFull(bufferedReader, pointerBuf); err != nil {
|
if _, err := io.ReadFull(bufferedReader, pointerBuf); err != nil {
|
||||||
_ = catFileBatchReader.CloseWithError(err)
|
return err
|
||||||
break
|
|
||||||
}
|
}
|
||||||
pointerBuf = pointerBuf[:size]
|
pointerBuf = pointerBuf[:size]
|
||||||
// Now we need to check if the pointerBuf is an LFS pointer
|
// Now we need to check if the pointerBuf is an LFS pointer
|
||||||
@@ -105,7 +93,6 @@ loop:
|
|||||||
if !pointer.IsValid() {
|
if !pointer.IsValid() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
pointerChan <- PointerBlob{Hash: sha, Pointer: pointer}
|
pointerChan <- PointerBlob{Hash: sha, Pointer: pointer}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,9 @@ func StoreMissingLfsObjectsInRepository(ctx context.Context, repo *repo_model.Re
|
|||||||
|
|
||||||
pointerChan := make(chan lfs.PointerBlob)
|
pointerChan := make(chan lfs.PointerBlob)
|
||||||
errChan := make(chan error, 1)
|
errChan := make(chan error, 1)
|
||||||
go lfs.SearchPointerBlobs(ctx, gitRepo, pointerChan, errChan)
|
go func() {
|
||||||
|
errChan <- lfs.SearchPointerBlobs(ctx, gitRepo, pointerChan)
|
||||||
|
}()
|
||||||
|
|
||||||
downloadObjects := func(pointers []lfs.Pointer) error {
|
downloadObjects := func(pointers []lfs.Pointer) error {
|
||||||
err := lfsClient.Download(ctx, pointers, func(p lfs.Pointer, content io.ReadCloser, objectError error) error {
|
err := lfsClient.Download(ctx, pointers, func(p lfs.Pointer, content io.ReadCloser, objectError error) error {
|
||||||
@@ -150,13 +152,12 @@ func StoreMissingLfsObjectsInRepository(ctx context.Context, repo *repo_model.Re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err, has := <-errChan
|
err := <-errChan
|
||||||
if has {
|
if err != nil {
|
||||||
log.Error("Repo[%-v]: Error enumerating LFS objects for repository: %v", repo, err)
|
log.Error("Repo[%-v]: Error enumerating LFS objects for repository: %v", repo, err)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// shortRelease to reduce load memory, this struct can replace repo_model.Release
|
// shortRelease to reduce load memory, this struct can replace repo_model.Release
|
||||||
|
|||||||
@@ -27,10 +27,11 @@ func verifyCommits(oldCommitID, newCommitID string, repo *git.Repository, env []
|
|||||||
command = gitcmd.NewCommand("rev-list").AddDynamicArguments(oldCommitID + "..." + newCommitID)
|
command = gitcmd.NewCommand("rev-list").AddDynamicArguments(oldCommitID + "..." + newCommitID)
|
||||||
}
|
}
|
||||||
// This is safe as force pushes are already forbidden
|
// This is safe as force pushes are already forbidden
|
||||||
var stdoutReader io.ReadCloser
|
stdoutReader, stdoutReaderClose := command.MakeStdoutPipe()
|
||||||
|
defer stdoutReaderClose()
|
||||||
|
|
||||||
err := command.WithEnv(env).
|
err := command.WithEnv(env).
|
||||||
WithDir(repo.Path).
|
WithDir(repo.Path).
|
||||||
WithStdoutReader(&stdoutReader).
|
|
||||||
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
||||||
err := readAndVerifyCommitsFromShaReader(stdoutReader, repo, env)
|
err := readAndVerifyCommitsFromShaReader(stdoutReader, repo, env)
|
||||||
return ctx.CancelWithCause(err)
|
return ctx.CancelWithCause(err)
|
||||||
@@ -56,11 +57,12 @@ func readAndVerifyCommitsFromShaReader(input io.ReadCloser, repo *git.Repository
|
|||||||
|
|
||||||
func readAndVerifyCommit(sha string, repo *git.Repository, env []string) error {
|
func readAndVerifyCommit(sha string, repo *git.Repository, env []string) error {
|
||||||
commitID := git.MustIDFromString(sha)
|
commitID := git.MustIDFromString(sha)
|
||||||
var stdoutReader io.ReadCloser
|
cmd := gitcmd.NewCommand("cat-file", "commit").AddDynamicArguments(sha)
|
||||||
return gitcmd.NewCommand("cat-file", "commit").AddDynamicArguments(sha).
|
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
|
||||||
WithEnv(env).
|
defer stdoutReaderClose()
|
||||||
|
|
||||||
|
return cmd.WithEnv(env).
|
||||||
WithDir(repo.Path).
|
WithDir(repo.Path).
|
||||||
WithStdoutReader(&stdoutReader).
|
|
||||||
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
||||||
commit, err := git.CommitFromReader(repo, commitID, stdoutReader)
|
commit, err := git.CommitFromReader(repo, commitID, stdoutReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -446,8 +446,8 @@ func serviceRPC(ctx *context.Context, service string) {
|
|||||||
|
|
||||||
if err := gitrepo.RunCmdWithStderr(ctx, h.getStorageRepo(), cmd.AddArguments(".").
|
if err := gitrepo.RunCmdWithStderr(ctx, h.getStorageRepo(), cmd.AddArguments(".").
|
||||||
WithEnv(append(os.Environ(), h.environ...)).
|
WithEnv(append(os.Environ(), h.environ...)).
|
||||||
WithStdin(reqBody).
|
WithStdinCopy(reqBody).
|
||||||
WithStdout(ctx.Resp),
|
WithStdoutCopy(ctx.Resp),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
if !gitcmd.IsErrorCanceledOrKilled(err) {
|
if !gitcmd.IsErrorCanceledOrKilled(err) {
|
||||||
log.Error("Fail to serve RPC(%s) in %s: %v", service, h.getStorageRepo().RelativePath(), err)
|
log.Error("Fail to serve RPC(%s) in %s: %v", service, h.getStorageRepo().RelativePath(), err)
|
||||||
|
|||||||
@@ -407,7 +407,9 @@ func LFSPointerFiles(ctx *context.Context) {
|
|||||||
err = func() error {
|
err = func() error {
|
||||||
pointerChan := make(chan lfs.PointerBlob)
|
pointerChan := make(chan lfs.PointerBlob)
|
||||||
errChan := make(chan error, 1)
|
errChan := make(chan error, 1)
|
||||||
go lfs.SearchPointerBlobs(ctx, ctx.Repo.GitRepo, pointerChan, errChan)
|
go func() {
|
||||||
|
errChan <- lfs.SearchPointerBlobs(ctx, ctx.Repo.GitRepo, pointerChan)
|
||||||
|
}()
|
||||||
|
|
||||||
numPointers := 0
|
numPointers := 0
|
||||||
var numAssociated, numNoExist, numAssociatable int
|
var numAssociated, numNoExist, numAssociatable int
|
||||||
@@ -483,11 +485,6 @@ func LFSPointerFiles(ctx *context.Context) {
|
|||||||
results = append(results, result)
|
results = append(results, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
err, has := <-errChan
|
|
||||||
if has {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Data["Pointers"] = results
|
ctx.Data["Pointers"] = results
|
||||||
ctx.Data["NumPointers"] = numPointers
|
ctx.Data["NumPointers"] = numPointers
|
||||||
ctx.Data["NumAssociated"] = numAssociated
|
ctx.Data["NumAssociated"] = numAssociated
|
||||||
@@ -495,7 +492,8 @@ func LFSPointerFiles(ctx *context.Context) {
|
|||||||
ctx.Data["NumNoExist"] = numNoExist
|
ctx.Data["NumNoExist"] = numNoExist
|
||||||
ctx.Data["NumNotAssociated"] = numPointers - numAssociated
|
ctx.Data["NumNotAssociated"] = numPointers - numAssociated
|
||||||
|
|
||||||
return nil
|
err := <-errChan
|
||||||
|
return err
|
||||||
}()
|
}()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("LFSPointerFiles", err)
|
ctx.ServerError("LFSPointerFiles", err)
|
||||||
|
|||||||
@@ -1263,21 +1263,14 @@ func getDiffBasic(ctx context.Context, gitRepo *git.Repository, opts *DiffOption
|
|||||||
cmdCtx, cmdCancel := context.WithCancel(ctx)
|
cmdCtx, cmdCancel := context.WithCancel(ctx)
|
||||||
defer cmdCancel()
|
defer cmdCancel()
|
||||||
|
|
||||||
reader, writer := io.Pipe()
|
reader, readerClose := cmdDiff.MakeStdoutPipe()
|
||||||
defer func() {
|
defer readerClose()
|
||||||
_ = reader.Close()
|
|
||||||
_ = writer.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if err := cmdDiff.
|
if err := cmdDiff.
|
||||||
WithDir(repoPath).
|
WithDir(repoPath).
|
||||||
WithStdout(writer).
|
|
||||||
RunWithStderr(cmdCtx); err != nil && !gitcmd.IsErrorCanceledOrKilled(err) {
|
RunWithStderr(cmdCtx); err != nil && !gitcmd.IsErrorCanceledOrKilled(err) {
|
||||||
log.Error("error during GetDiff(git diff dir: %s): %v", repoPath, err)
|
log.Error("error during GetDiff(git diff dir: %s): %v", repoPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = writer.Close()
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
diff, err := ParsePatch(cmdCtx, opts.MaxLines, opts.MaxLineCharacters, opts.MaxFiles, reader, parsePatchSkipToFile)
|
diff, err := ParsePatch(cmdCtx, opts.MaxLines, opts.MaxLineCharacters, opts.MaxFiles, reader, parsePatchSkipToFile)
|
||||||
|
|||||||
@@ -896,7 +896,7 @@ func (g *GiteaLocalUploader) CreateReviews(ctx context.Context, reviews ...*base
|
|||||||
comment.TreePath = util.PathJoinRel(comment.TreePath)
|
comment.TreePath = util.PathJoinRel(comment.TreePath)
|
||||||
|
|
||||||
var patch string
|
var patch string
|
||||||
reader, writer := io.Pipe()
|
reader, writer := io.Pipe() // FIXME: use os.Pipe to avoid deadlock
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = reader.Close()
|
_ = reader.Close()
|
||||||
_ = writer.Close()
|
_ = writer.Close()
|
||||||
|
|||||||
@@ -192,7 +192,9 @@ func pushAllLFSObjects(ctx context.Context, gitRepo *git.Repository, lfsClient l
|
|||||||
|
|
||||||
pointerChan := make(chan lfs.PointerBlob)
|
pointerChan := make(chan lfs.PointerBlob)
|
||||||
errChan := make(chan error, 1)
|
errChan := make(chan error, 1)
|
||||||
go lfs.SearchPointerBlobs(ctx, gitRepo, pointerChan, errChan)
|
go func() {
|
||||||
|
errChan <- lfs.SearchPointerBlobs(ctx, gitRepo, pointerChan)
|
||||||
|
}()
|
||||||
|
|
||||||
uploadObjects := func(pointers []lfs.Pointer) error {
|
uploadObjects := func(pointers []lfs.Pointer) error {
|
||||||
err := lfsClient.Upload(ctx, pointers, func(p lfs.Pointer, objectError error) (io.ReadCloser, error) {
|
err := lfsClient.Upload(ctx, pointers, func(p lfs.Pointer, objectError error) (io.ReadCloser, error) {
|
||||||
@@ -242,13 +244,12 @@ func pushAllLFSObjects(ctx context.Context, gitRepo *git.Repository, lfsClient l
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err, has := <-errChan
|
err := <-errChan
|
||||||
if has {
|
if err != nil {
|
||||||
log.Error("Error enumerating LFS objects for repository: %v", err)
|
log.Error("Error enumerating LFS objects for repository: %v", err)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncPushMirrorWithSyncOnCommit(ctx context.Context, repoID int64) {
|
func syncPushMirrorWithSyncOnCommit(ctx context.Context, repoID int64) {
|
||||||
|
|||||||
+45
-42
@@ -7,15 +7,19 @@ package pull
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
|
||||||
|
|
||||||
git_model "code.gitea.io/gitea/models/git"
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
|
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||||
"code.gitea.io/gitea/modules/git/pipeline"
|
"code.gitea.io/gitea/modules/git/pipeline"
|
||||||
"code.gitea.io/gitea/modules/lfs"
|
"code.gitea.io/gitea/modules/lfs"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LFSPush pushes lfs objects referred to in new commits in the head repository from the base repository
|
// LFSPush pushes lfs objects referred to in new commits in the head repository from the base repository
|
||||||
@@ -26,81 +30,82 @@ func LFSPush(ctx context.Context, tmpBasePath, mergeHeadSHA, mergeBaseSHA string
|
|||||||
// ensure only blobs and <=1k size then pass in to git cat-file --batch
|
// ensure only blobs and <=1k size then pass in to git cat-file --batch
|
||||||
// to read each sha and check each as a pointer
|
// to read each sha and check each as a pointer
|
||||||
// Then if they are lfs -> add them to the baseRepo
|
// Then if they are lfs -> add them to the baseRepo
|
||||||
revListReader, revListWriter := io.Pipe()
|
|
||||||
shasToCheckReader, shasToCheckWriter := io.Pipe()
|
cmd1RevList, cmd3BathCheck, cmd5BatchContent := gitcmd.NewCommand(), gitcmd.NewCommand(), gitcmd.NewCommand()
|
||||||
catFileCheckReader, catFileCheckWriter := io.Pipe()
|
cmd1RevListOut, cmd1RevListClose := cmd1RevList.MakeStdoutPipe()
|
||||||
shasToBatchReader, shasToBatchWriter := io.Pipe()
|
defer cmd1RevListClose()
|
||||||
catFileBatchReader, catFileBatchWriter := io.Pipe()
|
|
||||||
errChan := make(chan error, 1)
|
cmd3BatchCheckIn, cmd3BatchCheckOut, cmd3BatchCheckClose := cmd3BathCheck.MakeStdinStdoutPipe()
|
||||||
wg := sync.WaitGroup{}
|
defer cmd3BatchCheckClose()
|
||||||
wg.Add(6)
|
|
||||||
// Create the go-routines in reverse order.
|
cmd5BatchContentIn, cmd5BatchContentOut, cmd5BatchContentClose := cmd5BatchContent.MakeStdinStdoutPipe()
|
||||||
|
defer cmd5BatchContentClose()
|
||||||
|
|
||||||
|
// Create the go-routines in reverse order (update: the order is not needed any more, the pipes are properly prepared)
|
||||||
|
wg := &errgroup.Group{}
|
||||||
|
|
||||||
// 6. Take the output of cat-file --batch and check if each file in turn
|
// 6. Take the output of cat-file --batch and check if each file in turn
|
||||||
// to see if they're pointers to files in the LFS store associated with
|
// to see if they're pointers to files in the LFS store associated with
|
||||||
// the head repo and add them to the base repo if so
|
// the head repo and add them to the base repo if so
|
||||||
go createLFSMetaObjectsFromCatFileBatch(ctx, catFileBatchReader, &wg, pr)
|
wg.Go(func() error {
|
||||||
|
return createLFSMetaObjectsFromCatFileBatch(ctx, cmd5BatchContentOut, pr)
|
||||||
|
})
|
||||||
|
|
||||||
// 5. Take the shas of the blobs and batch read them
|
// 5. Take the shas of the blobs and batch read them
|
||||||
go pipeline.CatFileBatch(ctx, shasToBatchReader, catFileBatchWriter, &wg, tmpBasePath)
|
wg.Go(func() error {
|
||||||
|
return pipeline.CatFileBatch(ctx, cmd5BatchContent, tmpBasePath)
|
||||||
|
})
|
||||||
|
|
||||||
// 4. From the provided objects restrict to blobs <=1k
|
// 4. From the provided objects restrict to blobs <=1k
|
||||||
go pipeline.BlobsLessThan1024FromCatFileBatchCheck(catFileCheckReader, shasToBatchWriter, &wg)
|
wg.Go(func() error {
|
||||||
|
return pipeline.BlobsLessThan1024FromCatFileBatchCheck(cmd3BatchCheckOut, cmd5BatchContentIn)
|
||||||
|
})
|
||||||
|
|
||||||
// 3. Run batch-check on the objects retrieved from rev-list
|
// 3. Run batch-check on the objects retrieved from rev-list
|
||||||
go pipeline.CatFileBatchCheck(ctx, shasToCheckReader, catFileCheckWriter, &wg, tmpBasePath)
|
wg.Go(func() error {
|
||||||
|
return pipeline.CatFileBatchCheck(ctx, cmd3BathCheck, tmpBasePath)
|
||||||
|
})
|
||||||
|
|
||||||
// 2. Check each object retrieved rejecting those without names as they will be commits or trees
|
// 2. Check each object retrieved rejecting those without names as they will be commits or trees
|
||||||
go pipeline.BlobsFromRevListObjects(revListReader, shasToCheckWriter, &wg)
|
wg.Go(func() error {
|
||||||
|
return pipeline.BlobsFromRevListObjects(cmd1RevListOut, cmd3BatchCheckIn)
|
||||||
|
})
|
||||||
|
|
||||||
// 1. Run rev-list objects from mergeHead to mergeBase
|
// 1. Run rev-list objects from mergeHead to mergeBase
|
||||||
go pipeline.RevListObjects(ctx, revListWriter, &wg, tmpBasePath, mergeHeadSHA, mergeBaseSHA, errChan)
|
wg.Go(func() error {
|
||||||
|
return pipeline.RevListObjects(ctx, cmd1RevList, tmpBasePath, mergeHeadSHA, mergeBaseSHA)
|
||||||
|
})
|
||||||
|
|
||||||
wg.Wait()
|
return wg.Wait()
|
||||||
select {
|
|
||||||
case err, has := <-errChan:
|
|
||||||
if has {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func createLFSMetaObjectsFromCatFileBatch(ctx context.Context, catFileBatchReader *io.PipeReader, wg *sync.WaitGroup, pr *issues_model.PullRequest) {
|
func createLFSMetaObjectsFromCatFileBatch(ctx context.Context, catFileBatchReader io.ReadCloser, pr *issues_model.PullRequest) error {
|
||||||
defer wg.Done()
|
|
||||||
defer catFileBatchReader.Close()
|
defer catFileBatchReader.Close()
|
||||||
|
|
||||||
contentStore := lfs.NewContentStore()
|
contentStore := lfs.NewContentStore()
|
||||||
|
|
||||||
bufferedReader := bufio.NewReader(catFileBatchReader)
|
bufferedReader := bufio.NewReader(catFileBatchReader)
|
||||||
buf := make([]byte, 1025)
|
buf := make([]byte, 1025)
|
||||||
for {
|
for {
|
||||||
// File descriptor line: sha
|
// File descriptor line: sha
|
||||||
_, err := bufferedReader.ReadString(' ')
|
_, err := bufferedReader.ReadString(' ')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = catFileBatchReader.CloseWithError(err)
|
return util.Iif(errors.Is(err, io.EOF), nil, err)
|
||||||
break
|
|
||||||
}
|
}
|
||||||
// Throw away the blob
|
// Throw away the blob
|
||||||
if _, err := bufferedReader.ReadString(' '); err != nil {
|
if _, err := bufferedReader.ReadString(' '); err != nil {
|
||||||
_ = catFileBatchReader.CloseWithError(err)
|
return err
|
||||||
break
|
|
||||||
}
|
}
|
||||||
sizeStr, err := bufferedReader.ReadString('\n')
|
sizeStr, err := bufferedReader.ReadString('\n')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = catFileBatchReader.CloseWithError(err)
|
return err
|
||||||
break
|
|
||||||
}
|
}
|
||||||
size, err := strconv.Atoi(sizeStr[:len(sizeStr)-1])
|
size, err := strconv.Atoi(sizeStr[:len(sizeStr)-1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = catFileBatchReader.CloseWithError(err)
|
return err
|
||||||
break
|
|
||||||
}
|
}
|
||||||
pointerBuf := buf[:size+1]
|
pointerBuf := buf[:size+1]
|
||||||
if _, err := io.ReadFull(bufferedReader, pointerBuf); err != nil {
|
if _, err := io.ReadFull(bufferedReader, pointerBuf); err != nil {
|
||||||
_ = catFileBatchReader.CloseWithError(err)
|
return err
|
||||||
break
|
|
||||||
}
|
}
|
||||||
pointerBuf = pointerBuf[:size]
|
pointerBuf = pointerBuf[:size]
|
||||||
// Now we need to check if the pointerBuf is an LFS pointer
|
// Now we need to check if the pointerBuf is an LFS pointer
|
||||||
@@ -120,15 +125,13 @@ func createLFSMetaObjectsFromCatFileBatch(ctx context.Context, catFileBatchReade
|
|||||||
log.Warn("During merge of: %d in %-v, there is a pointer to LFS Oid: %s which although present in the LFS store is not associated with the head repo %-v", pr.Index, pr.BaseRepo, pointer.Oid, pr.HeadRepo)
|
log.Warn("During merge of: %d in %-v, there is a pointer to LFS Oid: %s which although present in the LFS store is not associated with the head repo %-v", pr.Index, pr.BaseRepo, pointer.Oid, pr.HeadRepo)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
_ = catFileBatchReader.CloseWithError(err)
|
return err
|
||||||
break
|
|
||||||
}
|
}
|
||||||
// OK we have a pointer that is associated with the head repo
|
// OK we have a pointer that is associated with the head repo
|
||||||
// and is actually a file in the LFS
|
// and is actually a file in the LFS
|
||||||
// Therefore it should be associated with the base repo
|
// Therefore it should be associated with the base repo
|
||||||
if _, err := git_model.NewLFSMetaObject(ctx, pr.BaseRepoID, pointer); err != nil {
|
if _, err := git_model.NewLFSMetaObject(ctx, pr.BaseRepoID, pointer); err != nil {
|
||||||
_ = catFileBatchReader.CloseWithError(err)
|
return err
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func (ctx *mergeContext) PrepareGitCmd(cmd *gitcmd.Command) *gitcmd.Command {
|
|||||||
return cmd.WithEnv(ctx.env).
|
return cmd.WithEnv(ctx.env).
|
||||||
WithDir(ctx.tmpBasePath).
|
WithDir(ctx.tmpBasePath).
|
||||||
WithParentCallerInfo().
|
WithParentCallerInfo().
|
||||||
WithStdout(ctx.outbuf)
|
WithStdoutBuffer(ctx.outbuf)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrSHADoesNotMatch represents a "SHADoesNotMatch" kind of error.
|
// ErrSHADoesNotMatch represents a "SHADoesNotMatch" kind of error.
|
||||||
@@ -219,11 +219,11 @@ func getDiffTree(ctx context.Context, repoPath, baseBranch, headBranch string, o
|
|||||||
return 0, nil, nil
|
return 0, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var diffOutReader io.ReadCloser
|
cmd := gitcmd.NewCommand("diff-tree", "--no-commit-id", "--name-only", "-r", "-r", "-z", "--root")
|
||||||
err := gitcmd.NewCommand("diff-tree", "--no-commit-id", "--name-only", "-r", "-r", "-z", "--root").
|
diffOutReader, diffOutReaderClose := cmd.MakeStdoutPipe()
|
||||||
AddDynamicArguments(baseBranch, headBranch).
|
defer diffOutReaderClose()
|
||||||
|
err := cmd.AddDynamicArguments(baseBranch, headBranch).
|
||||||
WithDir(repoPath).
|
WithDir(repoPath).
|
||||||
WithStdoutReader(&diffOutReader).
|
|
||||||
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
||||||
// Now scan the output from the command
|
// Now scan the output from the command
|
||||||
scanner := bufio.NewScanner(diffOutReader)
|
scanner := bufio.NewScanner(diffOutReader)
|
||||||
|
|||||||
@@ -414,13 +414,13 @@ func checkConflicts(ctx context.Context, pr *issues_model.PullRequest, gitRepo *
|
|||||||
// - alternatively we can do the equivalent of:
|
// - alternatively we can do the equivalent of:
|
||||||
// `git apply --check ... | grep ...`
|
// `git apply --check ... | grep ...`
|
||||||
// meaning we don't store all the conflicts unnecessarily.
|
// meaning we don't store all the conflicts unnecessarily.
|
||||||
var stderrReader io.ReadCloser
|
stderrReader, stderrReaderClose := cmdApply.MakeStderrPipe()
|
||||||
|
defer stderrReaderClose()
|
||||||
|
|
||||||
// 8. Run the check command
|
// 8. Run the check command
|
||||||
conflict = false
|
conflict = false
|
||||||
err = cmdApply.
|
err = cmdApply.
|
||||||
WithDir(tmpBasePath).
|
WithDir(tmpBasePath).
|
||||||
WithStderrReader(&stderrReader).
|
|
||||||
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
||||||
const prefix = "error: patch failed:"
|
const prefix = "error: patch failed:"
|
||||||
const errorPrefix = "error: "
|
const errorPrefix = "error: "
|
||||||
|
|||||||
@@ -59,10 +59,10 @@ func readUnmergedLsFileLines(ctx context.Context, tmpBasePath string, outputChan
|
|||||||
close(outputChan)
|
close(outputChan)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var lsFilesReader io.ReadCloser
|
cmd := gitcmd.NewCommand("ls-files", "-u", "-z")
|
||||||
err := gitcmd.NewCommand("ls-files", "-u", "-z").
|
lsFilesReader, lsFilesReaderClose := cmd.MakeStdoutPipe()
|
||||||
WithDir(tmpBasePath).
|
defer lsFilesReaderClose()
|
||||||
WithStdoutReader(&lsFilesReader).
|
err := cmd.WithDir(tmpBasePath).
|
||||||
WithPipelineFunc(func(_ gitcmd.Context) error {
|
WithPipelineFunc(func(_ gitcmd.Context) error {
|
||||||
bufferedReader := bufio.NewReader(lsFilesReader)
|
bufferedReader := bufio.NewReader(lsFilesReader)
|
||||||
|
|
||||||
|
|||||||
@@ -526,9 +526,9 @@ func checkIfPRContentChanged(ctx context.Context, pr *issues_model.PullRequest,
|
|||||||
|
|
||||||
cmd := gitcmd.NewCommand("diff", "--name-only", "-z").AddDynamicArguments(newCommitID, oldCommitID, mergeBase)
|
cmd := gitcmd.NewCommand("diff", "--name-only", "-z").AddDynamicArguments(newCommitID, oldCommitID, mergeBase)
|
||||||
|
|
||||||
var stdoutReader io.ReadCloser
|
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
|
||||||
|
defer stdoutReaderClose()
|
||||||
if err := cmd.WithDir(prCtx.tmpBasePath).
|
if err := cmd.WithDir(prCtx.tmpBasePath).
|
||||||
WithStdoutReader(&stdoutReader).
|
|
||||||
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
||||||
return util.IsEmptyReader(stdoutReader)
|
return util.IsEmptyReader(stdoutReader)
|
||||||
}).
|
}).
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_mo
|
|||||||
if len(commitID) == 0 {
|
if len(commitID) == 0 {
|
||||||
commitID = headCommitID
|
commitID = headCommitID
|
||||||
}
|
}
|
||||||
reader, writer := io.Pipe()
|
reader, writer := io.Pipe() // FIXME: use os.Pipe to avoid deadlock
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = reader.Close()
|
_ = reader.Close()
|
||||||
_ = writer.Close()
|
_ = writer.Close()
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
package pull
|
package pull
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
|
|
||||||
git_model "code.gitea.io/gitea/models/git"
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
@@ -32,7 +32,7 @@ type prTmpRepoContext struct {
|
|||||||
context.Context
|
context.Context
|
||||||
tmpBasePath string
|
tmpBasePath string
|
||||||
pr *issues_model.PullRequest
|
pr *issues_model.PullRequest
|
||||||
outbuf *strings.Builder // we keep these around to help reduce needless buffer recreation, any use should be preceded by a Reset and preferably after use
|
outbuf *bytes.Buffer // we keep these around to help reduce needless buffer recreation, any use should be preceded by a Reset and preferably after use
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrepareGitCmd prepares a git command with the correct directory, environment, and output buffers
|
// PrepareGitCmd prepares a git command with the correct directory, environment, and output buffers
|
||||||
@@ -40,7 +40,7 @@ type prTmpRepoContext struct {
|
|||||||
// Do NOT use it with gitcmd.RunStd*() functions, otherwise it will panic
|
// Do NOT use it with gitcmd.RunStd*() functions, otherwise it will panic
|
||||||
func (ctx *prTmpRepoContext) PrepareGitCmd(cmd *gitcmd.Command) *gitcmd.Command {
|
func (ctx *prTmpRepoContext) PrepareGitCmd(cmd *gitcmd.Command) *gitcmd.Command {
|
||||||
ctx.outbuf.Reset()
|
ctx.outbuf.Reset()
|
||||||
return cmd.WithDir(ctx.tmpBasePath).WithStdout(ctx.outbuf)
|
return cmd.WithDir(ctx.tmpBasePath).WithStdoutBuffer(ctx.outbuf)
|
||||||
}
|
}
|
||||||
|
|
||||||
// createTemporaryRepoForPR creates a temporary repo with "base" for pr.BaseBranch and "tracking" for pr.HeadBranch
|
// createTemporaryRepoForPR creates a temporary repo with "base" for pr.BaseBranch and "tracking" for pr.HeadBranch
|
||||||
@@ -82,7 +82,7 @@ func createTemporaryRepoForPR(ctx context.Context, pr *issues_model.PullRequest)
|
|||||||
Context: ctx,
|
Context: ctx,
|
||||||
tmpBasePath: tmpBasePath,
|
tmpBasePath: tmpBasePath,
|
||||||
pr: pr,
|
pr: pr,
|
||||||
outbuf: &strings.Builder{},
|
outbuf: &bytes.Buffer{},
|
||||||
}
|
}
|
||||||
|
|
||||||
baseRepoPath := pr.BaseRepo.RepoPath()
|
baseRepoPath := pr.BaseRepo.RepoPath()
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ func updateHeadByRebaseOnToBase(ctx context.Context, pr *issues_model.PullReques
|
|||||||
pr.Index,
|
pr.Index,
|
||||||
)).
|
)).
|
||||||
WithDir(mergeCtx.tmpBasePath).
|
WithDir(mergeCtx.tmpBasePath).
|
||||||
WithStdout(mergeCtx.outbuf).
|
WithStdoutBuffer(mergeCtx.outbuf).
|
||||||
RunWithStderr(ctx); err != nil {
|
RunWithStderr(ctx); err != nil {
|
||||||
if strings.Contains(err.Stderr(), "non-fast-forward") {
|
if strings.Contains(err.Stderr(), "non-fast-forward") {
|
||||||
return &git.ErrPushOutOfDate{
|
return &git.ErrPushOutOfDate{
|
||||||
|
|||||||
@@ -264,12 +264,12 @@ func checkBranchName(ctx context.Context, repo *repo_model.Repository, name stri
|
|||||||
return git_model.ErrBranchAlreadyExists{
|
return git_model.ErrBranchAlreadyExists{
|
||||||
BranchName: name,
|
BranchName: name,
|
||||||
}
|
}
|
||||||
// If branchRefName like a/b but we want to create a branch named a then we have a conflict
|
// If branchRefName like "a/b" but we want to create a branch named a then we have a conflict
|
||||||
case strings.HasPrefix(branchRefName, name+"/"):
|
case strings.HasPrefix(branchRefName, name+"/"):
|
||||||
return git_model.ErrBranchNameConflict{
|
return git_model.ErrBranchNameConflict{
|
||||||
BranchName: branchRefName,
|
BranchName: branchRefName,
|
||||||
}
|
}
|
||||||
// Conversely if branchRefName like a but we want to create a branch named a/b then we also have a conflict
|
// Conversely if branchRefName like "a" but we want to create a branch named "a/b" then we also have a conflict
|
||||||
case strings.HasPrefix(name, branchRefName+"/"):
|
case strings.HasPrefix(name, branchRefName+"/"):
|
||||||
return git_model.ErrBranchNameConflict{
|
return git_model.ErrBranchNameConflict{
|
||||||
BranchName: branchRefName,
|
BranchName: branchRefName,
|
||||||
@@ -281,7 +281,6 @@ func checkBranchName(ctx context.Context, repo *repo_model.Repository, name stri
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -122,10 +121,11 @@ func getExtendedCommitStats(repo *git.Repository, revision string /*, limit int
|
|||||||
// AddOptionFormat("--max-count=%d", limit)
|
// AddOptionFormat("--max-count=%d", limit)
|
||||||
gitCmd.AddDynamicArguments(baseCommit.ID.String())
|
gitCmd.AddDynamicArguments(baseCommit.ID.String())
|
||||||
|
|
||||||
var stdoutReader io.ReadCloser
|
stdoutReader, stdoutReaderClose := gitCmd.MakeStdoutPipe()
|
||||||
|
defer stdoutReaderClose()
|
||||||
|
|
||||||
var extendedCommitStats []*ExtendedCommitStats
|
var extendedCommitStats []*ExtendedCommitStats
|
||||||
err = gitCmd.WithDir(repo.Path).
|
err = gitCmd.WithDir(repo.Path).
|
||||||
WithStdoutReader(&stdoutReader).
|
|
||||||
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
||||||
scanner := bufio.NewScanner(stdoutReader)
|
scanner := bufio.NewScanner(stdoutReader)
|
||||||
|
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := cmdApply.WithDir(t.basePath).
|
if err := cmdApply.WithDir(t.basePath).
|
||||||
WithStdin(strings.NewReader(opts.Content)).
|
WithStdinBytes([]byte(opts.Content)).
|
||||||
RunWithStderr(ctx); err != nil {
|
RunWithStderr(ctx); err != nil {
|
||||||
return nil, fmt.Errorf("git apply error: %w", err)
|
return nil, fmt.Errorf("git apply error: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ func (t *TemporaryUploadRepository) LsFiles(ctx context.Context, filenames ...st
|
|||||||
stdOut := new(bytes.Buffer)
|
stdOut := new(bytes.Buffer)
|
||||||
if err := gitcmd.NewCommand("ls-files", "-z").AddDashesAndList(filenames...).
|
if err := gitcmd.NewCommand("ls-files", "-z").AddDashesAndList(filenames...).
|
||||||
WithDir(t.basePath).
|
WithDir(t.basePath).
|
||||||
WithStdout(stdOut).
|
WithStdoutBuffer(stdOut).
|
||||||
RunWithStderr(ctx); err != nil {
|
RunWithStderr(ctx); err != nil {
|
||||||
return nil, fmt.Errorf("unable to run git ls-files for temporary repo of: %s, error: %w", t.repo.FullName(), err)
|
return nil, fmt.Errorf("unable to run git ls-files for temporary repo of: %s, error: %w", t.repo.FullName(), err)
|
||||||
}
|
}
|
||||||
@@ -155,7 +155,7 @@ func (t *TemporaryUploadRepository) RemoveFilesFromIndex(ctx context.Context, fi
|
|||||||
|
|
||||||
if err := gitcmd.NewCommand("update-index", "--remove", "-z", "--index-info").
|
if err := gitcmd.NewCommand("update-index", "--remove", "-z", "--index-info").
|
||||||
WithDir(t.basePath).
|
WithDir(t.basePath).
|
||||||
WithStdin(stdIn).
|
WithStdinBytes(stdIn.Bytes()).
|
||||||
RunWithStderr(ctx); err != nil {
|
RunWithStderr(ctx); err != nil {
|
||||||
return fmt.Errorf("unable to update-index for temporary repo: %q, error: %w", t.repo.FullName(), err)
|
return fmt.Errorf("unable to update-index for temporary repo: %q, error: %w", t.repo.FullName(), err)
|
||||||
}
|
}
|
||||||
@@ -167,8 +167,8 @@ func (t *TemporaryUploadRepository) HashObjectAndWrite(ctx context.Context, cont
|
|||||||
stdOut := new(bytes.Buffer)
|
stdOut := new(bytes.Buffer)
|
||||||
if err := gitcmd.NewCommand("hash-object", "-w", "--stdin").
|
if err := gitcmd.NewCommand("hash-object", "-w", "--stdin").
|
||||||
WithDir(t.basePath).
|
WithDir(t.basePath).
|
||||||
WithStdout(stdOut).
|
WithStdoutBuffer(stdOut).
|
||||||
WithStdin(content).
|
WithStdinCopy(content).
|
||||||
RunWithStderr(ctx); err != nil {
|
RunWithStderr(ctx); err != nil {
|
||||||
return "", fmt.Errorf("unable to hash-object to temporary repo: %s, error: %w", t.repo.FullName(), err)
|
return "", fmt.Errorf("unable to hash-object to temporary repo: %s, error: %w", t.repo.FullName(), err)
|
||||||
}
|
}
|
||||||
@@ -330,8 +330,8 @@ func (t *TemporaryUploadRepository) CommitTree(ctx context.Context, opts *Commit
|
|||||||
if err := cmdCommitTree.
|
if err := cmdCommitTree.
|
||||||
WithEnv(env).
|
WithEnv(env).
|
||||||
WithDir(t.basePath).
|
WithDir(t.basePath).
|
||||||
WithStdout(stdout).
|
WithStdoutBuffer(stdout).
|
||||||
WithStdin(messageBytes).
|
WithStdinBytes(messageBytes.Bytes()).
|
||||||
RunWithStderr(ctx); err != nil {
|
RunWithStderr(ctx); err != nil {
|
||||||
return "", fmt.Errorf("unable to commit-tree in temporary repo: %s Error: %w", t.repo.FullName(), err)
|
return "", fmt.Errorf("unable to commit-tree in temporary repo: %s Error: %w", t.repo.FullName(), err)
|
||||||
}
|
}
|
||||||
@@ -363,11 +363,12 @@ func (t *TemporaryUploadRepository) Push(ctx context.Context, doer *user_model.U
|
|||||||
// DiffIndex returns a Diff of the current index to the head
|
// DiffIndex returns a Diff of the current index to the head
|
||||||
func (t *TemporaryUploadRepository) DiffIndex(ctx context.Context) (*gitdiff.Diff, error) {
|
func (t *TemporaryUploadRepository) DiffIndex(ctx context.Context) (*gitdiff.Diff, error) {
|
||||||
var diff *gitdiff.Diff
|
var diff *gitdiff.Diff
|
||||||
var stdoutReader io.ReadCloser
|
cmd := gitcmd.NewCommand("diff-index", "--src-prefix=\\a/", "--dst-prefix=\\b/", "--cached", "-p", "HEAD")
|
||||||
err := gitcmd.NewCommand("diff-index", "--src-prefix=\\a/", "--dst-prefix=\\b/", "--cached", "-p", "HEAD").
|
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
|
||||||
WithTimeout(30 * time.Second).
|
defer stdoutReaderClose()
|
||||||
|
|
||||||
|
err := cmd.WithTimeout(30 * time.Second).
|
||||||
WithDir(t.basePath).
|
WithDir(t.basePath).
|
||||||
WithStdoutReader(&stdoutReader).
|
|
||||||
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
||||||
var diffErr error
|
var diffErr error
|
||||||
diff, diffErr = gitdiff.ParsePatch(ctx, setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, stdoutReader, "")
|
diff, diffErr = gitdiff.ParsePatch(ctx, setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, stdoutReader, "")
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ package gitgraph
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||||
@@ -45,10 +44,10 @@ func GetCommitGraph(r *git.Repository, page, maxAllowedColors int, hidePRRefs bo
|
|||||||
|
|
||||||
commitsToSkip := setting.UI.GraphMaxCommitNum * (page - 1)
|
commitsToSkip := setting.UI.GraphMaxCommitNum * (page - 1)
|
||||||
|
|
||||||
var stdoutReader io.ReadCloser
|
stdoutReader, stdoutReaderClose := graphCmd.MakeStdoutPipe()
|
||||||
|
defer stdoutReaderClose()
|
||||||
if err := graphCmd.
|
if err := graphCmd.
|
||||||
WithDir(r.Path).
|
WithDir(r.Path).
|
||||||
WithStdoutReader(&stdoutReader).
|
|
||||||
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
||||||
scanner := bufio.NewScanner(stdoutReader)
|
scanner := bufio.NewScanner(stdoutReader)
|
||||||
parser := &Parser{}
|
parser := &Parser{}
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
|
|||||||
|
|
||||||
// FIXME: The wiki doesn't have lfs support at present - if this changes need to check attributes here
|
// FIXME: The wiki doesn't have lfs support at present - if this changes need to check attributes here
|
||||||
|
|
||||||
objectHash, err := gitRepo.HashObject(strings.NewReader(content))
|
objectHash, err := gitRepo.HashObjectBytes([]byte(content))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("HashObject failed: %v", err)
|
log.Error("HashObject failed: %v", err)
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -405,16 +405,13 @@ func TestCantMergeUnrelated(t *testing.T) {
|
|||||||
err := gitcmd.NewCommand("read-tree", "--empty").WithDir(path).Run(t.Context())
|
err := gitcmd.NewCommand("read-tree", "--empty").WithDir(path).Run(t.Context())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
stdin := strings.NewReader("Unrelated File")
|
stdout, _, err := gitcmd.NewCommand("hash-object", "-w", "--stdin").
|
||||||
var stdout strings.Builder
|
|
||||||
err = gitcmd.NewCommand("hash-object", "-w", "--stdin").
|
|
||||||
WithDir(path).
|
WithDir(path).
|
||||||
WithStdin(stdin).
|
WithStdinBytes([]byte("Unrelated File")).
|
||||||
WithStdout(&stdout).
|
RunStdString(t.Context())
|
||||||
Run(t.Context())
|
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
sha := strings.TrimSpace(stdout.String())
|
sha := strings.TrimSpace(stdout)
|
||||||
|
|
||||||
_, _, err = gitcmd.NewCommand("update-index", "--add", "--replace", "--cacheinfo").
|
_, _, err = gitcmd.NewCommand("update-index", "--add", "--replace", "--cacheinfo").
|
||||||
AddDynamicArguments("100644", sha, "somewher-over-the-rainbow").
|
AddDynamicArguments("100644", sha, "somewher-over-the-rainbow").
|
||||||
@@ -441,15 +438,13 @@ func TestCantMergeUnrelated(t *testing.T) {
|
|||||||
_, _ = messageBytes.WriteString("Unrelated")
|
_, _ = messageBytes.WriteString("Unrelated")
|
||||||
_, _ = messageBytes.WriteString("\n")
|
_, _ = messageBytes.WriteString("\n")
|
||||||
|
|
||||||
stdout.Reset()
|
stdout, _, err = gitcmd.NewCommand("commit-tree").AddDynamicArguments(treeSha).
|
||||||
err = gitcmd.NewCommand("commit-tree").AddDynamicArguments(treeSha).
|
|
||||||
WithEnv(env).
|
WithEnv(env).
|
||||||
WithDir(path).
|
WithDir(path).
|
||||||
WithStdin(messageBytes).
|
WithStdinBytes(messageBytes.Bytes()).
|
||||||
WithStdout(&stdout).
|
RunStdString(t.Context())
|
||||||
Run(t.Context())
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
commitSha := strings.TrimSpace(stdout.String())
|
commitSha := strings.TrimSpace(stdout)
|
||||||
|
|
||||||
_, _, err = gitcmd.NewCommand("branch", "unrelated").
|
_, _, err = gitcmd.NewCommand("branch", "unrelated").
|
||||||
AddDynamicArguments(commitSha).
|
AddDynamicArguments(commitSha).
|
||||||
|
|||||||
Reference in New Issue
Block a user