Skip to content

Commit 149f7a6

Browse files
authored
Refactor git command stdio pipe (#36393)
And remove the incorrect `ensureValidGitRepository`
1 parent 7a2aac4 commit 149f7a6

9 files changed

Lines changed: 210 additions & 122 deletions

modules/git/catfile_batch_command.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ package git
55

66
import (
77
"context"
8+
"os"
9+
"path/filepath"
810

911
"code.gitea.io/gitea/modules/git/gitcmd"
12+
"code.gitea.io/gitea/modules/util"
1013
)
1114

1215
// catFileBatchCommand implements the CatFileBatch interface using the "cat-file --batch-command" command
@@ -21,8 +24,8 @@ type catFileBatchCommand struct {
2124
var _ CatFileBatch = (*catFileBatchCommand)(nil)
2225

2326
func newCatFileBatchCommand(ctx context.Context, repoPath string) (*catFileBatchCommand, error) {
24-
if err := ensureValidGitRepository(ctx, repoPath); err != nil {
25-
return nil, err
27+
if _, err := os.Stat(repoPath); err != nil {
28+
return nil, util.NewNotExistErrorf("repo %q doesn't exist", filepath.Base(repoPath))
2629
}
2730
return &catFileBatchCommand{ctx: ctx, repoPath: repoPath}, nil
2831
}

modules/git/catfile_batch_legacy.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ package git
66
import (
77
"context"
88
"io"
9+
"os"
10+
"path/filepath"
911

1012
"code.gitea.io/gitea/modules/git/gitcmd"
13+
"code.gitea.io/gitea/modules/util"
1114
)
1215

1316
// catFileBatchLegacy implements the CatFileBatch interface using the "cat-file --batch" command and "cat-file --batch-check" command
@@ -24,8 +27,8 @@ type catFileBatchLegacy struct {
2427
var _ CatFileBatchCloser = (*catFileBatchLegacy)(nil)
2528

2629
func newCatFileBatchLegacy(ctx context.Context, repoPath string) (*catFileBatchLegacy, error) {
27-
if err := ensureValidGitRepository(ctx, repoPath); err != nil {
28-
return nil, err
30+
if _, err := os.Stat(repoPath); err != nil {
31+
return nil, util.NewNotExistErrorf("repo %q doesn't exist", filepath.Base(repoPath))
2932
}
3033
return &catFileBatchLegacy{ctx: ctx, repoPath: repoPath}, nil
3134
}

modules/git/catfile_batch_reader.go

Lines changed: 33 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,12 @@ import (
1616
"code.gitea.io/gitea/modules/log"
1717
)
1818

19-
// writeCloserError wraps an io.WriteCloser with an additional CloseWithError function (for nio.Pipe)
20-
type writeCloserError interface {
21-
io.WriteCloser
22-
CloseWithError(err error) error
23-
}
24-
2519
type catFileBatchCommunicator struct {
2620
cancel context.CancelFunc
2721
reader *bufio.Reader
28-
writer writeCloserError
22+
writer io.Writer
23+
24+
debugGitCmd *gitcmd.Command
2925
}
3026

3127
func (b *catFileBatchCommunicator) Close() {
@@ -37,63 +33,41 @@ func (b *catFileBatchCommunicator) Close() {
3733
}
3834
}
3935

40-
// ensureValidGitRepository runs git rev-parse in the repository path - thus ensuring that the repository is a valid repository.
41-
// Run before opening git cat-file.
42-
// This is needed otherwise the git cat-file will hang for invalid repositories.
43-
// FIXME: the comment is from https://github.com/go-gitea/gitea/pull/17991 but it doesn't seem to be true.
44-
// The real problem is that Golang's Cmd.Wait hangs because it waits for the pipes to be closed, but we can't close the pipes before Wait returns
45-
// Need to refactor to use StdinPipe and StdoutPipe
46-
func ensureValidGitRepository(ctx context.Context, repoPath string) error {
47-
stderr := strings.Builder{}
48-
err := gitcmd.NewCommand("rev-parse").
49-
WithDir(repoPath).
50-
WithStderr(&stderr).
51-
Run(ctx)
52-
if err != nil {
53-
return gitcmd.ConcatenateError(err, (&stderr).String())
54-
}
55-
return nil
56-
}
57-
5836
// newCatFileBatch opens git cat-file --batch in the provided repo and returns a stdin pipe, a stdout reader and cancel function
5937
func newCatFileBatch(ctx context.Context, repoPath string, cmdCatFile *gitcmd.Command) *catFileBatchCommunicator {
6038
// We often want to feed the commits in order into cat-file --batch, followed by their trees and subtrees as necessary.
39+
ctx, ctxCancel := context.WithCancelCause(ctx)
6140

62-
// so let's create a batch stdin and stdout
63-
batchStdinReader, batchStdinWriter := io.Pipe()
64-
batchStdoutReader, batchStdoutWriter := io.Pipe()
65-
ctx, ctxCancel := context.WithCancel(ctx)
66-
closed := make(chan struct{})
67-
cancel := func() {
68-
ctxCancel()
69-
_ = batchStdinWriter.Close()
70-
_ = batchStdoutReader.Close()
71-
<-closed
72-
}
41+
var batchStdinWriter io.WriteCloser
42+
var batchStdoutReader io.ReadCloser
43+
stderr := strings.Builder{}
44+
cmdCatFile = cmdCatFile.
45+
WithDir(repoPath).
46+
WithStdinWriter(&batchStdinWriter).
47+
WithStdoutReader(&batchStdoutReader).
48+
WithStderr(&stderr).
49+
WithUseContextTimeout(true)
7350

74-
// Ensure cancel is called as soon as the provided context is cancelled
75-
go func() {
76-
<-ctx.Done()
77-
cancel()
78-
}()
51+
err := cmdCatFile.Start(ctx)
52+
if err != nil {
53+
log.Error("Unable to start git command %v: %v", cmdCatFile.LogString(), err)
54+
// ideally here it should return the error, but it would require refactoring all callers
55+
// so just return a dummy communicator that does nothing, almost the same behavior as before, not bad
56+
return &catFileBatchCommunicator{
57+
writer: io.Discard,
58+
reader: bufio.NewReader(bytes.NewReader(nil)),
59+
cancel: func() {
60+
ctxCancel(err)
61+
},
62+
}
63+
}
7964

8065
go func() {
81-
stderr := strings.Builder{}
82-
err := cmdCatFile.
83-
WithDir(repoPath).
84-
WithStdin(batchStdinReader).
85-
WithStdout(batchStdoutWriter).
86-
WithStderr(&stderr).
87-
WithUseContextTimeout(true).
88-
Run(ctx)
66+
err := cmdCatFile.Wait()
8967
if err != nil {
90-
_ = batchStdoutWriter.CloseWithError(gitcmd.ConcatenateError(err, (&stderr).String()))
91-
_ = batchStdinReader.CloseWithError(gitcmd.ConcatenateError(err, (&stderr).String()))
92-
} else {
93-
_ = batchStdoutWriter.Close()
94-
_ = batchStdinReader.Close()
68+
log.Error("cat-file --batch command failed in repo %s: %v - stderr: %s", repoPath, err, stderr.String())
9569
}
96-
close(closed)
70+
ctxCancel(err)
9771
}()
9872

9973
// use a buffered reader to read from the cat-file --batch (StringReader.ReadString)
@@ -102,7 +76,10 @@ func newCatFileBatch(ctx context.Context, repoPath string, cmdCatFile *gitcmd.Co
10276
return &catFileBatchCommunicator{
10377
writer: batchStdinWriter,
10478
reader: batchReader,
105-
cancel: cancel,
79+
cancel: func() {
80+
ctxCancel(nil)
81+
},
82+
debugGitCmd: cmdCatFile,
10683
}
10784
}
10885

modules/git/catfile_batch_test.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package git
66
import (
77
"io"
88
"path/filepath"
9+
"sync"
910
"testing"
1011

1112
"code.gitea.io/gitea/modules/test"
@@ -25,7 +26,14 @@ func TestCatFileBatch(t *testing.T) {
2526
func testCatFileBatch(t *testing.T) {
2627
t.Run("CorruptedGitRepo", func(t *testing.T) {
2728
tmpDir := t.TempDir()
28-
_, err := NewBatch(t.Context(), tmpDir)
29+
batch, err := NewBatch(t.Context(), tmpDir)
30+
// as long as the directory exists, no error, because we can't really know whether the git repo is valid until we run commands
31+
require.NoError(t, err)
32+
defer batch.Close()
33+
34+
_, err = batch.QueryInfo("e2129701f1a4d54dc44f03c93bca0a2aec7c5449")
35+
require.Error(t, err)
36+
_, err = batch.QueryInfo("e2129701f1a4d54dc44f03c93bca0a2aec7c5449")
2937
require.Error(t, err)
3038
})
3139

@@ -52,4 +60,30 @@ func testCatFileBatch(t *testing.T) {
5260
require.NoError(t, err)
5361
require.Equal(t, "file1\n", string(content))
5462
})
63+
64+
t.Run("QueryTerminated", func(t *testing.T) {
65+
var c *catFileBatchCommunicator
66+
switch b := batch.(type) {
67+
case *catFileBatchLegacy:
68+
c = b.batchCheck
69+
_, _ = c.writer.Write([]byte("in-complete-line-"))
70+
case *catFileBatchCommand:
71+
c = b.batch
72+
_, _ = c.writer.Write([]byte("info"))
73+
default:
74+
t.FailNow()
75+
return
76+
}
77+
78+
wg := sync.WaitGroup{}
79+
wg.Go(func() {
80+
buf := make([]byte, 100)
81+
_, _ = c.reader.Read(buf)
82+
n, errRead := c.reader.Read(buf)
83+
assert.Zero(t, n)
84+
assert.ErrorIs(t, errRead, io.EOF) // the pipe is closed due to command being killed
85+
})
86+
c.debugGitCmd.DebugKill()
87+
wg.Wait()
88+
})
5589
}

0 commit comments

Comments
 (0)