diff --git a/integrations/integration_test.go b/integrations/integration_test.go index 687591d5fa3f4..559d295de5d3c 100644 --- a/integrations/integration_test.go +++ b/integrations/integration_test.go @@ -174,10 +174,13 @@ func initIntegrationTest() { setting.LoadForTest() setting.Repository.DefaultBranch = "master" // many test code still assume that default branch is called "master" _ = util.RemoveAll(repo_module.LocalCopyPath()) + + os.MkdirAll(setting.RepoRootPath, os.ModePerm) + git.CheckLFSVersion() setting.InitDBConfig() if err := storage.Init(); err != nil { - fmt.Printf("Init storage failed: %v", err) + fmt.Printf("Init storage failed: %v\n", err) os.Exit(1) } diff --git a/modules/git/batch_reader.go b/modules/git/batch_reader.go index 902fa897185f5..17c66f4887a8d 100644 --- a/modules/git/batch_reader.go +++ b/modules/git/batch_reader.go @@ -33,7 +33,7 @@ type WriteCloserError interface { func EnsureValidGitRepository(ctx context.Context, repoPath string) error { stderr := strings.Builder{} err := NewCommand(ctx, "rev-parse"). - SetDescription(fmt.Sprintf("%s rev-parse [repo_path: %s]", GitExecutable, repoPath)). + SetDescription(fmt.Sprintf("rev-parse [repo_path: %s]", repoPath)). Run(&RunOpts{ Dir: repoPath, Stderr: &stderr, @@ -69,7 +69,7 @@ func CatFileBatchCheck(ctx context.Context, repoPath string) (WriteCloserError, go func() { stderr := strings.Builder{} err := NewCommand(ctx, "cat-file", "--batch-check"). - SetDescription(fmt.Sprintf("%s cat-file --batch-check [repo_path: %s] (%s:%d)", GitExecutable, repoPath, filename, line)). + SetDescription(fmt.Sprintf("cat-file --batch-check [repo_path: %s] (%s:%d)", repoPath, filename, line)). Run(&RunOpts{ Dir: repoPath, Stdin: batchStdinReader, @@ -119,7 +119,7 @@ func CatFileBatch(ctx context.Context, repoPath string) (WriteCloserError, *bufi go func() { stderr := strings.Builder{} err := NewCommand(ctx, "cat-file", "--batch"). - SetDescription(fmt.Sprintf("%s cat-file --batch [repo_path: %s] (%s:%d)", GitExecutable, repoPath, filename, line)). + SetDescription(fmt.Sprintf("cat-file --batch [repo_path: %s] (%s:%d)", repoPath, filename, line)). Run(&RunOpts{ Dir: repoPath, Stdin: batchStdinReader, diff --git a/modules/git/blame.go b/modules/git/blame.go index 1653ecbf854af..56e0d0a1c99cc 100644 --- a/modules/git/blame.go +++ b/modules/git/blame.go @@ -10,10 +10,7 @@ import ( "fmt" "io" "os" - "os/exec" "regexp" - - "code.gitea.io/gitea/modules/process" ) // BlamePart represents block of blame - continuous lines with one sha @@ -24,12 +21,11 @@ type BlamePart struct { // BlameReader returns part of file blame one by one type BlameReader struct { - cmd *exec.Cmd - output io.ReadCloser - reader *bufio.Reader - lastSha *string - cancel context.CancelFunc // Cancels the context that this reader runs in - finished process.FinishedFunc // Tells the process manager we're finished and it can remove the associated process from the process table + cmd *CommandProxy + output io.WriteCloser + reader io.ReadCloser + done chan error + lastSha *string } var shaLineRegex = regexp.MustCompile("^([a-z0-9]{40})") @@ -38,7 +34,7 @@ var shaLineRegex = regexp.MustCompile("^([a-z0-9]{40})") func (r *BlameReader) NextPart() (*BlamePart, error) { var blamePart *BlamePart - reader := r.reader + reader := bufio.NewReader(r.reader) if r.lastSha != nil { blamePart = &BlamePart{*r.lastSha, make([]string, 0)} @@ -100,51 +96,40 @@ func (r *BlameReader) NextPart() (*BlamePart, error) { // Close BlameReader - don't run NextPart after invoking that func (r *BlameReader) Close() error { - defer r.finished() // Only remove the process from the process table when the underlying command is closed - r.cancel() // However, first cancel our own context early - + err := <-r.done + _ = r.reader.Close() _ = r.output.Close() - if err := r.cmd.Wait(); err != nil { - return fmt.Errorf("Wait: %v", err) - } - - return nil + return err } // CreateBlameReader creates reader for given repository, commit and file func CreateBlameReader(ctx context.Context, repoPath, commitID, file string) (*BlameReader, error) { - return createBlameReader(ctx, repoPath, GitExecutable, "blame", commitID, "--porcelain", "--", file) -} + cmd := NewCommandContextNoGlobals(ctx, "blame", commitID, "--porcelain", "--", file) + cmd.SetDescription(fmt.Sprintf("GetBlame [repo_path: %s]", repoPath)) -func createBlameReader(ctx context.Context, dir string, command ...string) (*BlameReader, error) { - // Here we use the provided context - this should be tied to the request performing the blame so that it does not hang around. - ctx, cancel, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("GetBlame [repo_path: %s]", dir)) - - cmd := exec.CommandContext(ctx, command[0], command[1:]...) - cmd.Dir = dir - cmd.Stderr = os.Stderr - process.SetSysProcAttribute(cmd) - - stdout, err := cmd.StdoutPipe() + reader, stdout, err := os.Pipe() if err != nil { - defer finished() - return nil, fmt.Errorf("StdoutPipe: %v", err) + return nil, err } - if err = cmd.Start(); err != nil { - defer finished() - _ = stdout.Close() - return nil, fmt.Errorf("Start: %v", err) - } + done := make(chan error, 1) - reader := bufio.NewReader(stdout) + go func(cmd *CommandProxy, dir string, stdout io.WriteCloser, done chan error) { + if err := cmd.Run(&RunOpts{ + Dir: dir, + Stdout: stdout, + Stderr: os.Stderr, + }); err == nil { + stdout.Close() + } + done <- err + }(cmd, repoPath, stdout, done) return &BlameReader{ - cmd: cmd, - output: stdout, - reader: reader, - cancel: cancel, - finished: finished, + cmd: cmd, + output: stdout, + reader: reader, + done: done, }, nil } diff --git a/modules/git/blame_test.go b/modules/git/blame_test.go index 4bee8cd27a962..6c8958839e697 100644 --- a/modules/git/blame_test.go +++ b/modules/git/blame_test.go @@ -6,139 +6,36 @@ package git import ( "context" - "os" "testing" "github.com/stretchr/testify/assert" ) -const exampleBlame = ` -4b92a6c2df28054ad766bc262f308db9f6066596 1 1 1 -author Unknown -author-mail -author-time 1392833071 -author-tz -0500 -committer Unknown -committer-mail -committer-time 1392833071 -committer-tz -0500 -summary Add code of delete user -previous be0ba9ea88aff8a658d0495d36accf944b74888d gogs.go -filename gogs.go - // Copyright 2014 The Gogs Authors. All rights reserved. -ce21ed6c3490cdfad797319cbb1145e2330a8fef 2 2 1 -author Joubert RedRat -author-mail -author-time 1482322397 -author-tz -0200 -committer Lunny Xiao -committer-mail -committer-time 1482322397 -committer-tz +0800 -summary Remove remaining Gogs reference on locales and cmd (#430) -previous 618407c018cdf668ceedde7454c42fb22ba422d8 main.go -filename main.go - // Copyright 2016 The Gitea Authors. All rights reserved. -4b92a6c2df28054ad766bc262f308db9f6066596 2 3 2 -author Unknown -author-mail -author-time 1392833071 -author-tz -0500 -committer Unknown -committer-mail -committer-time 1392833071 -committer-tz -0500 -summary Add code of delete user -previous be0ba9ea88aff8a658d0495d36accf944b74888d gogs.go -filename gogs.go - // Use of this source code is governed by a MIT-style -4b92a6c2df28054ad766bc262f308db9f6066596 3 4 -author Unknown -author-mail -author-time 1392833071 -author-tz -0500 -committer Unknown -committer-mail -committer-time 1392833071 -committer-tz -0500 -summary Add code of delete user -previous be0ba9ea88aff8a658d0495d36accf944b74888d gogs.go -filename gogs.go - // license that can be found in the LICENSE file. - -e2aa991e10ffd924a828ec149951f2f20eecead2 6 6 2 -author Lunny Xiao -author-mail -author-time 1478872595 -author-tz +0800 -committer Sandro Santilli -committer-mail -committer-time 1478872595 -committer-tz +0100 -summary ask for go get from code.gitea.io/gitea and change gogs to gitea on main file (#146) -previous 5fc370e332171b8658caed771b48585576f11737 main.go -filename main.go - // Gitea (git with a cup of tea) is a painless self-hosted Git Service. -e2aa991e10ffd924a828ec149951f2f20eecead2 7 7 - package main // import "code.gitea.io/gitea" -` - func TestReadingBlameOutput(t *testing.T) { - tempFile, err := os.CreateTemp("", ".txt") - if err != nil { - panic(err) - } - - defer tempFile.Close() - - if _, err = tempFile.WriteString(exampleBlame); err != nil { - panic(err) - } ctx, cancel := context.WithCancel(context.Background()) defer cancel() - blameReader, err := createBlameReader(ctx, "", "cat", tempFile.Name()) - if err != nil { - panic(err) - } + blameReader, err := CreateBlameReader(ctx, "./tests/repos/repo5_pulls", "f32b0a9dfd09a60f616f29158f772cedd89942d2", "README.md") + assert.NoError(t, err) defer blameReader.Close() parts := []*BlamePart{ { - "4b92a6c2df28054ad766bc262f308db9f6066596", - []string{ - "// Copyright 2014 The Gogs Authors. All rights reserved.", - }, - }, - { - "ce21ed6c3490cdfad797319cbb1145e2330a8fef", - []string{ - "// Copyright 2016 The Gitea Authors. All rights reserved.", - }, - }, - { - "4b92a6c2df28054ad766bc262f308db9f6066596", + "72866af952e98d02a73003501836074b286a78f6", []string{ - "// Use of this source code is governed by a MIT-style", - "// license that can be found in the LICENSE file.", - "", + "# test_repo", + "Test repository for testing migration from github to gitea", }, }, { - "e2aa991e10ffd924a828ec149951f2f20eecead2", - []string{ - "// Gitea (git with a cup of tea) is a painless self-hosted Git Service.", - "package main // import \"code.gitea.io/gitea\"", - }, + "f32b0a9dfd09a60f616f29158f772cedd89942d2", + []string{}, }, - nil, } for _, part := range parts { actualPart, err := blameReader.NextPart() - if err != nil { - panic(err) - } + assert.NoError(t, err) assert.Equal(t, part, actualPart) } } diff --git a/modules/git/cmd/command.go b/modules/git/cmd/command.go new file mode 100644 index 0000000000000..f7d70e27a3112 --- /dev/null +++ b/modules/git/cmd/command.go @@ -0,0 +1,35 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package cmd + +import ( + "context" + "io" + "time" +) + +// RunOpts represents parameters to run the command +type RunOpts struct { + Env []string + Timeout time.Duration + Dir string + Stdout, Stderr io.Writer + Stdin io.Reader + PipelineFunc func(context.Context, context.CancelFunc) error +} + +// Command represents a git command +type Command interface { + String() string + SetParentContext(ctx context.Context) Command + SetDescription(desc string) Command + AddArguments(args ...string) Command + Run(*RunOpts) error +} + +// Service represents a service to create git command +type Service interface { + NewCommand(ctx context.Context, gloablArgsLength int, args ...string) Command +} diff --git a/modules/git/cmd/local.go b/modules/git/cmd/local.go new file mode 100644 index 0000000000000..eb8dee74b6b5c --- /dev/null +++ b/modules/git/cmd/local.go @@ -0,0 +1,178 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package cmd + +import ( + "context" + "fmt" + "os" + "os/exec" + "strings" + "time" + + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/process" + "code.gitea.io/gitea/modules/util" +) + +// LocalCommand represents a local git command +type LocalCommand struct { + service *LocalService + args []string + parentContext context.Context + desc string + globalArgsLength int +} + +var _ Command = &LocalCommand{} + +func (c *LocalCommand) String() string { + if len(c.args) == 0 { + return c.service.GitExecutable + } + return fmt.Sprintf("%s %s", c.service.GitExecutable, strings.Join(c.args, " ")) +} + +// SetParentContext sets the parent context for this command +func (c *LocalCommand) SetParentContext(ctx context.Context) Command { + c.parentContext = ctx + return c +} + +// SetDescription sets the description for this command which be returned on +// c.String() +func (c *LocalCommand) SetDescription(desc string) Command { + c.desc = desc + return c +} + +// AddArguments adds new argument(s) to the command. +func (c *LocalCommand) AddArguments(args ...string) Command { + c.args = append(c.args, args...) + return c +} + +// defaultLocale is the default LC_ALL to run git commands in. +const defaultLocale = "C" + +// Run runs the command with the RunOpts +func (c *LocalCommand) Run(opts *RunOpts) error { + if opts == nil { + opts = &RunOpts{} + } + if opts.Timeout <= 0 { + opts.Timeout = c.service.defaultTimeout + } + + if len(opts.Dir) == 0 { + log.Debug("%s", c) + } else { + log.Debug("%s: %v", opts.Dir, c) + } + + desc := c.desc + if desc == "" { + args := c.args[c.globalArgsLength:] + var argSensitiveURLIndexes []int + for i, arg := range c.args { + if strings.Contains(arg, "://") && strings.Contains(arg, "@") { + argSensitiveURLIndexes = append(argSensitiveURLIndexes, i) + } + } + if len(argSensitiveURLIndexes) > 0 { + args = make([]string, len(c.args)) + copy(args, c.args) + for _, urlArgIndex := range argSensitiveURLIndexes { + args[urlArgIndex] = util.SanitizeCredentialURLs(args[urlArgIndex]) + } + } + desc = fmt.Sprintf("%s %s [repo_path: %s]", c.args[0], strings.Join(args, " "), opts.Dir) + } + desc = fmt.Sprintf("[%s] %s", c.service.GitExecutable, desc) + + ctx, cancel, finished := process.GetManager().AddContextTimeout(c.parentContext, opts.Timeout, desc) + defer finished() + + cmd := exec.CommandContext(ctx, c.service.GitExecutable, c.args...) + if opts.Env == nil { + cmd.Env = os.Environ() + } else { + cmd.Env = opts.Env + } + + cmd.Env = append( + cmd.Env, + fmt.Sprintf("LC_ALL=%s", defaultLocale), + // avoid prompting for credentials interactively, supported since git v2.3 + "GIT_TERMINAL_PROMPT=0", + // ignore replace references (https://git-scm.com/docs/git-replace) + "GIT_NO_REPLACE_OBJECTS=1", + ) + + process.SetSysProcAttribute(cmd) + cmd.Dir = opts.Dir + cmd.Stdout = opts.Stdout + cmd.Stderr = opts.Stderr + cmd.Stdin = opts.Stdin + if err := cmd.Start(); err != nil { + return err + } + + if opts.PipelineFunc != nil { + err := opts.PipelineFunc(ctx, cancel) + if err != nil { + cancel() + _ = cmd.Wait() + return err + } + } + + if err := cmd.Wait(); err != nil && ctx.Err() != context.DeadlineExceeded { + return err + } + + return ctx.Err() +} + +// defaultGitExecutable is the command name of git +// Could be updated to an absolute path while initialization +const defaultGitExecutable = "git" + +// LocalService represents a command service to create local git commands +type LocalService struct { + GitExecutable string // git binary location + RepoRootPath string // repository storage root directory + defaultTimeout time.Duration +} + +var _ Service = &LocalService{} + +// NewLocalService returns a local service +func NewLocalService(gitExecutable, repoRootPath string, defaultTimeout time.Duration) *LocalService { + // If path is empty, we use the default value of GitExecutable "git" to search for the location of git. + if gitExecutable == "" { + gitExecutable = defaultGitExecutable + } + absPath, err := exec.LookPath(gitExecutable) + if err != nil { + panic(fmt.Sprintf("Git not found: %v", err)) + } + + return &LocalService{ + GitExecutable: absPath, + RepoRootPath: repoRootPath, + defaultTimeout: defaultTimeout, + } +} + +// NewCommand creates and returns a new Git Command based on given command and arguments. +func (s *LocalService) NewCommand(ctx context.Context, gloablArgsLength int, args ...string) Command { + return &LocalCommand{ + service: s, + args: args, + parentContext: ctx, + globalArgsLength: gloablArgsLength, + } +} diff --git a/modules/git/command.go b/modules/git/command.go index f6344dbfd1ae0..e98ee7b9b5434 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -8,180 +8,77 @@ package git import ( "bytes" "context" - "fmt" - "io" - "os" - "os/exec" "strings" + "sync" "time" "unsafe" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/process" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/git/cmd" + "code.gitea.io/gitea/modules/setting" ) var ( // globalCommandArgs global command args for external package setting globalCommandArgs []string - // defaultCommandExecutionTimeout default command execution timeout duration - defaultCommandExecutionTimeout = 360 * time.Second + // cmdService represents a command service + cmdService cmd.Service + cmdServiceOnce sync.Once ) -// DefaultLocale is the default LC_ALL to run git commands in. -const DefaultLocale = "C" - -// Command represents a command with its subcommands or arguments. -type Command struct { - name string - args []string - parentContext context.Context - desc string - globalArgsLength int +func getCmdService() cmd.Service { + cmdServiceOnce.Do(func() { + cmdService = cmd.NewLocalService(setting.Git.Path, setting.RepoRootPath, time.Duration(setting.Git.Timeout.Default)*time.Second) + }) + return cmdService } -func (c *Command) String() string { - if len(c.args) == 0 { - return c.name - } - return fmt.Sprintf("%s %s", c.name, strings.Join(c.args, " ")) +// CommandProxy represents a command proxy with its subcommands or arguments. +type CommandProxy struct { + cmd.Command } // NewCommand creates and returns a new Git Command based on given command and arguments. -func NewCommand(ctx context.Context, args ...string) *Command { +func NewCommand(ctx context.Context, args ...string) *CommandProxy { // Make an explicit copy of globalCommandArgs, otherwise append might overwrite it cargs := make([]string, len(globalCommandArgs)) copy(cargs, globalCommandArgs) - return &Command{ - name: GitExecutable, - args: append(cargs, args...), - parentContext: ctx, - globalArgsLength: len(globalCommandArgs), + return &CommandProxy{ + Command: getCmdService().NewCommand(ctx, len(cargs), append(cargs, args...)...), } } // NewCommandNoGlobals creates and returns a new Git Command based on given command and arguments only with the specify args and don't care global command args -func NewCommandNoGlobals(args ...string) *Command { +func NewCommandNoGlobals(args ...string) *CommandProxy { return NewCommandContextNoGlobals(DefaultContext, args...) } // NewCommandContextNoGlobals creates and returns a new Git Command based on given command and arguments only with the specify args and don't care global command args -func NewCommandContextNoGlobals(ctx context.Context, args ...string) *Command { - return &Command{ - name: GitExecutable, - args: args, - parentContext: ctx, +func NewCommandContextNoGlobals(ctx context.Context, args ...string) *CommandProxy { + return &CommandProxy{ + Command: getCmdService().NewCommand(ctx, 0, args...), } } // SetParentContext sets the parent context for this command -func (c *Command) SetParentContext(ctx context.Context) *Command { - c.parentContext = ctx +func (c *CommandProxy) SetParentContext(ctx context.Context) *CommandProxy { + c.Command.SetParentContext(ctx) return c } // SetDescription sets the description for this command which be returned on // c.String() -func (c *Command) SetDescription(desc string) *Command { - c.desc = desc +func (c *CommandProxy) SetDescription(desc string) *CommandProxy { + c.Command.SetDescription(desc) return c } // AddArguments adds new argument(s) to the command. -func (c *Command) AddArguments(args ...string) *Command { - c.args = append(c.args, args...) +func (c *CommandProxy) AddArguments(args ...string) *CommandProxy { + c.Command.AddArguments(args...) return c } -// RunOpts represents parameters to run the command -type RunOpts struct { - Env []string - Timeout time.Duration - Dir string - Stdout, Stderr io.Writer - Stdin io.Reader - PipelineFunc func(context.Context, context.CancelFunc) error -} - -// Run runs the command with the RunOpts -func (c *Command) Run(opts *RunOpts) error { - if opts == nil { - opts = &RunOpts{} - } - if opts.Timeout <= 0 { - opts.Timeout = defaultCommandExecutionTimeout - } - - if len(opts.Dir) == 0 { - log.Debug("%s", c) - } else { - log.Debug("%s: %v", opts.Dir, c) - } - - desc := c.desc - if desc == "" { - args := c.args[c.globalArgsLength:] - var argSensitiveURLIndexes []int - for i, arg := range c.args { - if strings.Contains(arg, "://") && strings.Contains(arg, "@") { - argSensitiveURLIndexes = append(argSensitiveURLIndexes, i) - } - } - if len(argSensitiveURLIndexes) > 0 { - args = make([]string, len(c.args)) - copy(args, c.args) - for _, urlArgIndex := range argSensitiveURLIndexes { - args[urlArgIndex] = util.SanitizeCredentialURLs(args[urlArgIndex]) - } - } - desc = fmt.Sprintf("%s %s [repo_path: %s]", c.name, strings.Join(args, " "), opts.Dir) - } - - ctx, cancel, finished := process.GetManager().AddContextTimeout(c.parentContext, opts.Timeout, desc) - defer finished() - - cmd := exec.CommandContext(ctx, c.name, c.args...) - if opts.Env == nil { - cmd.Env = os.Environ() - } else { - cmd.Env = opts.Env - } - - cmd.Env = append( - cmd.Env, - fmt.Sprintf("LC_ALL=%s", DefaultLocale), - // avoid prompting for credentials interactively, supported since git v2.3 - "GIT_TERMINAL_PROMPT=0", - // ignore replace references (https://git-scm.com/docs/git-replace) - "GIT_NO_REPLACE_OBJECTS=1", - ) - - process.SetSysProcAttribute(cmd) - cmd.Dir = opts.Dir - cmd.Stdout = opts.Stdout - cmd.Stderr = opts.Stderr - cmd.Stdin = opts.Stdin - if err := cmd.Start(); err != nil { - return err - } - - if opts.PipelineFunc != nil { - err := opts.PipelineFunc(ctx, cancel) - if err != nil { - cancel() - _ = cmd.Wait() - return err - } - } - - if err := cmd.Wait(); err != nil && ctx.Err() != context.DeadlineExceeded { - return err - } - - return ctx.Err() -} - type RunStdError interface { error Stderr() string @@ -213,8 +110,11 @@ func bytesToString(b []byte) string { return *(*string)(unsafe.Pointer(&b)) // that's what Golang's strings.Builder.String() does (go/src/strings/builder.go) } +// RunOpts is an alias of cmd.RunOpts +type RunOpts = cmd.RunOpts + // RunStdString runs the command with options and returns stdout/stderr as string. and store stderr to returned error (err combined with stderr). -func (c *Command) RunStdString(opts *RunOpts) (stdout, stderr string, runErr RunStdError) { +func (c *CommandProxy) RunStdString(opts *RunOpts) (stdout, stderr string, runErr RunStdError) { stdoutBytes, stderrBytes, err := c.RunStdBytes(opts) stdout = bytesToString(stdoutBytes) stderr = bytesToString(stderrBytes) @@ -226,7 +126,7 @@ func (c *Command) RunStdString(opts *RunOpts) (stdout, stderr string, runErr Run } // RunStdBytes runs the command with options and returns stdout/stderr as bytes. and store stderr to returned error (err combined with stderr). -func (c *Command) RunStdBytes(opts *RunOpts) (stdout, stderr []byte, runErr RunStdError) { +func (c *CommandProxy) RunStdBytes(opts *RunOpts) (stdout, stderr []byte, runErr RunStdError) { if opts == nil { opts = &RunOpts{} } diff --git a/modules/git/git.go b/modules/git/git.go index 8fad07033006a..6a443ea2e5fa6 100644 --- a/modules/git/git.go +++ b/modules/git/git.go @@ -12,9 +12,10 @@ import ( "os/exec" "runtime" "strings" + "sync" "time" - "code.gitea.io/gitea/modules/process" + git_cmd "code.gitea.io/gitea/modules/git/cmd" "code.gitea.io/gitea/modules/setting" "github.com/hashicorp/go-version" @@ -27,10 +28,6 @@ var ( // If everything works fine, the code for git 1.x could be removed in a separate PR before 1.17 frozen. GitVersionRequired = "2.0.0" - // GitExecutable is the command name of git - // Could be updated to an absolute path while initialization - GitExecutable = "git" - // DefaultContext is the default context to run git commands in // will be overwritten by Init with HammerContext DefaultContext = context.Background() @@ -81,19 +78,9 @@ func LoadGitVersion() error { return err } -// SetExecutablePath changes the path of git executable and checks the file permission and version. -func SetExecutablePath(path string) error { - // If path is empty, we use the default value of GitExecutable "git" to search for the location of git. - if path != "" { - GitExecutable = path - } - absPath, err := exec.LookPath(GitExecutable) - if err != nil { - return fmt.Errorf("git not found: %w", err) - } - GitExecutable = absPath - - err = LoadGitVersion() +// checkGitVersion checks the file permission and version. +func checkGitVersion() error { + err := LoadGitVersion() if err != nil { return fmt.Errorf("unable to load git version: %w", err) } @@ -135,11 +122,7 @@ func VersionInfo() string { func Init(ctx context.Context) error { DefaultContext = ctx - if setting.Git.Timeout.Default > 0 { - defaultCommandExecutionTimeout = time.Duration(setting.Git.Timeout.Default) * time.Second - } - - if err := SetExecutablePath(setting.Git.Path); err != nil { + if err := checkGitVersion(); err != nil { return err } @@ -161,11 +144,6 @@ func Init(ctx context.Context) error { globalCommandArgs = append(globalCommandArgs, "-c", "uploadpack.allowfilter=true", "-c", "uploadpack.allowAnySHA1InWant=true") } - // Save current git version on init to gitVersion otherwise it would require an RWMutex - if err := LoadGitVersion(); err != nil { - return err - } - // Git requires setting user.name and user.email in order to commit changes - if they're not set just add some defaults for configKey, defaultValue := range map[string]string{"user.name": "Gitea", "user.email": "gitea@fake.local"} { if err := checkAndSetConfig(configKey, defaultValue, false); err != nil { @@ -235,48 +213,70 @@ func CheckGitVersionAtLeast(atLeast string) error { return nil } +var gitConfigLock sync.Mutex + +// checkAndSetConfig to avoid config conflict, only allow one go routine call at the same time func checkAndSetConfig(key, defaultValue string, forceToDefault bool) error { - stdout, stderr, err := process.GetManager().Exec("git.Init(get setting)", GitExecutable, "config", "--get", key) - if err != nil { - perr, ok := err.(*process.Error) - if !ok { - return fmt.Errorf("Failed to get git %s(%v) errType %T: %s", key, err, err, stderr) - } - eerr, ok := perr.Err.(*exec.ExitError) + gitConfigLock.Lock() + defer gitConfigLock.Unlock() + + stdout := strings.Builder{} + stderr := strings.Builder{} + if err := NewCommand(DefaultContext, "config", "--get", key). + SetDescription("git.Init(get setting)"). + Run(&git_cmd.RunOpts{ + Stdout: &stdout, + Stderr: &stderr, + }); err != nil { + eerr, ok := err.(*exec.ExitError) if !ok || eerr.ExitCode() != 1 { - return fmt.Errorf("Failed to get git %s(%v) errType %T: %s", key, err, err, stderr) + return fmt.Errorf("failed to get git %s(%v) errType %T: %s", key, err, err, stderr.String()) } } - currValue := strings.TrimSpace(stdout) - + currValue := strings.TrimSpace(stdout.String()) if currValue == defaultValue || (!forceToDefault && len(currValue) > 0) { return nil } - if _, stderr, err = process.GetManager().Exec(fmt.Sprintf("git.Init(set %s)", key), "git", "config", "--global", key, defaultValue); err != nil { - return fmt.Errorf("Failed to set git %s(%s): %s", key, err, stderr) + stderr.Reset() + + if err := NewCommand(DefaultContext, "config", "--global", key, defaultValue). + SetDescription(fmt.Sprintf("git.Init(set %s)", key)). + Run(&git_cmd.RunOpts{ + Stderr: &stderr, + }); err != nil { + return fmt.Errorf("failed to set git %s(%s): %s", key, err, stderr.String()) } return nil } func checkAndAddConfig(key, value string) error { - _, stderr, err := process.GetManager().Exec("git.Init(get setting)", GitExecutable, "config", "--get", key, value) - if err != nil { - perr, ok := err.(*process.Error) - if !ok { - return fmt.Errorf("Failed to get git %s(%v) errType %T: %s", key, err, err, stderr) - } - eerr, ok := perr.Err.(*exec.ExitError) + gitConfigLock.Lock() + defer gitConfigLock.Unlock() + + stdout := strings.Builder{} + stderr := strings.Builder{} + if err := NewCommand(DefaultContext, "config", "--get", key). + SetDescription("git.Init(get setting)"). + Run(&git_cmd.RunOpts{ + Stdout: &stdout, + Stderr: &stderr, + }); err != nil { + eerr, ok := err.(*exec.ExitError) if !ok || eerr.ExitCode() != 1 { - return fmt.Errorf("Failed to get git %s(%v) errType %T: %s", key, err, err, stderr) + return fmt.Errorf("failed to get git %s(%v) errType %T: %s", key, err, err, stderr.String()) } if eerr.ExitCode() == 1 { - if _, stderr, err = process.GetManager().Exec(fmt.Sprintf("git.Init(set %s)", key), "git", "config", "--global", "--add", key, value); err != nil { - return fmt.Errorf("Failed to set git %s(%s): %s", key, err, stderr) + stderr.Reset() + if err := NewCommand(DefaultContext, "config", "--global", "--add", key, value). + SetDescription(fmt.Sprintf("git.Init(set %s)", key)). + Run(&git_cmd.RunOpts{ + Stderr: &stderr, + }); err != nil { + return fmt.Errorf("failed to set git %s(%s): %s", key, err, stderr.String()) } - return nil } } @@ -284,23 +284,31 @@ func checkAndAddConfig(key, value string) error { } func checkAndRemoveConfig(key, value string) error { - _, stderr, err := process.GetManager().Exec("git.Init(get setting)", GitExecutable, "config", "--get", key, value) - if err != nil { - perr, ok := err.(*process.Error) - if !ok { - return fmt.Errorf("Failed to get git %s(%v) errType %T: %s", key, err, err, stderr) - } - eerr, ok := perr.Err.(*exec.ExitError) + gitConfigLock.Lock() + defer gitConfigLock.Unlock() + + stderr := strings.Builder{} + if err := NewCommand(DefaultContext, "config", "--get", key, value). + SetDescription("git.Init(get setting)"). + Run(&git_cmd.RunOpts{ + Stderr: &stderr, + }); err != nil { + eerr, ok := err.(*exec.ExitError) if !ok || eerr.ExitCode() != 1 { - return fmt.Errorf("Failed to get git %s(%v) errType %T: %s", key, err, err, stderr) + return fmt.Errorf("failed to get git %s(%v) errType %T: %s", key, err, err, stderr.String()) } if eerr.ExitCode() == 1 { return nil } } - if _, stderr, err = process.GetManager().Exec(fmt.Sprintf("git.Init(set %s)", key), "git", "config", "--global", "--unset-all", key, value); err != nil { - return fmt.Errorf("Failed to set git %s(%s): %s", key, err, stderr) + stderr.Reset() + if err := NewCommand(DefaultContext, "config", "--global", "--unset-all", key, value). + SetDescription(fmt.Sprintf("git.Init(set %s)", key)). + Run(&git_cmd.RunOpts{ + Stderr: &stderr, + }); err != nil { + return fmt.Errorf("failed to set git %s(%s): %s", key, err, stderr.String()) } return nil diff --git a/modules/git/remote.go b/modules/git/remote.go index 536b1681cecf9..24a4e1b6386b1 100644 --- a/modules/git/remote.go +++ b/modules/git/remote.go @@ -15,7 +15,7 @@ func GetRemoteAddress(ctx context.Context, repoPath, remoteName string) (*url.UR if err != nil { return nil, err } - var cmd *Command + var cmd *CommandProxy if CheckGitVersionAtLeast("2.7") == nil { cmd = NewCommand(ctx, "remote", "get-url", remoteName) } else { diff --git a/modules/git/repo_attribute.go b/modules/git/repo_attribute.go index a18c80c3f12f3..14a7d827ab1a8 100644 --- a/modules/git/repo_attribute.go +++ b/modules/git/repo_attribute.go @@ -120,7 +120,7 @@ type CheckAttributeReader struct { stdinReader io.ReadCloser stdinWriter *os.File stdOut attributeWriter - cmd *Command + cmd *CommandProxy env []string ctx context.Context cancel context.CancelFunc diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 5e317b39ea289..149a2f86a78e0 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -768,7 +768,11 @@ func loadFromConf(allowEmpty bool, extraConfig string) { } StaticRootPath = sec.Key("STATIC_ROOT_PATH").MustString(StaticRootPath) StaticCacheTime = sec.Key("STATIC_CACHE_TIME").MustDuration(6 * time.Hour) - AppDataPath = sec.Key("APP_DATA_PATH").MustString(path.Join(AppWorkPath, "data")) + appDataPath := sec.Key("APP_DATA_PATH").MustString(path.Join(AppWorkPath, "data")) + AppDataPath, err = filepath.Abs(appDataPath) + if err != nil { + log.Fatal("Invalid APP_DATA_PATH '%s': %s", appDataPath, err) + } EnableGzip = sec.Key("ENABLE_GZIP").MustBool() EnablePprof = sec.Key("ENABLE_PPROF").MustBool(false) diff --git a/routers/web/repo/http.go b/routers/web/repo/http.go index 6a85bca16b2f5..ca41172b50b77 100644 --- a/routers/web/repo/http.go +++ b/routers/web/repo/http.go @@ -472,7 +472,7 @@ func serviceRPC(ctx gocontext.Context, h serviceHandler, service string) { var stderr bytes.Buffer cmd := git.NewCommand(h.r.Context(), service, "--stateless-rpc", h.dir) - cmd.SetDescription(fmt.Sprintf("%s %s %s [repo_path: %s]", git.GitExecutable, service, "--stateless-rpc", h.dir)) + cmd.SetDescription(fmt.Sprintf("%s %s [repo_path: %s]", service, "--stateless-rpc", h.dir)) if err := cmd.Run(&git.RunOpts{ Dir: h.dir, Env: append(os.Environ(), h.environ...), diff --git a/services/pull/merge.go b/services/pull/merge.go index fcced65cdf8ca..6986e3573e6e4 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -280,13 +280,13 @@ func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User return "", fmt.Errorf("Unable to write .git/info/sparse-checkout file in tmpBasePath: %v", err) } - var gitConfigCommand func() *git.Command + var gitConfigCommand func() *git.CommandProxy if git.CheckGitVersionAtLeast("1.8.0") == nil { - gitConfigCommand = func() *git.Command { + gitConfigCommand = func() *git.CommandProxy { return git.NewCommand(ctx, "config", "--local") } } else { - gitConfigCommand = func() *git.Command { + gitConfigCommand = func() *git.CommandProxy { return git.NewCommand(ctx, "config") } } @@ -600,7 +600,7 @@ func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User pr.ID, ) - var pushCmd *git.Command + var pushCmd *git.CommandProxy if mergeStyle == repo_model.MergeStyleRebaseUpdate { // force push the rebase result to head branch pushCmd = git.NewCommand(ctx, "push", "-f", "head_repo", stagingBranch+":"+git.BranchPrefix+pr.HeadBranch) @@ -668,7 +668,7 @@ func commitAndSignNoAuthor(ctx context.Context, pr *models.PullRequest, message, return nil } -func runMergeCommand(pr *models.PullRequest, mergeStyle repo_model.MergeStyle, cmd *git.Command, tmpBasePath string) error { +func runMergeCommand(pr *models.PullRequest, mergeStyle repo_model.MergeStyle, cmd *git.CommandProxy, tmpBasePath string) error { var outbuf, errbuf strings.Builder if err := cmd.Run(&git.RunOpts{ Dir: tmpBasePath,