diff options
Diffstat (limited to 'modules/git')
110 files changed, 1895 insertions, 1553 deletions
diff --git a/modules/git/attribute.go b/modules/git/attribute.go deleted file mode 100644 index 4dfa510369..0000000000 --- a/modules/git/attribute.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package git - -import ( - "code.gitea.io/gitea/modules/optional" -) - -const ( - AttributeLinguistVendored = "linguist-vendored" - AttributeLinguistGenerated = "linguist-generated" - AttributeLinguistDocumentation = "linguist-documentation" - AttributeLinguistDetectable = "linguist-detectable" - AttributeLinguistLanguage = "linguist-language" - AttributeGitlabLanguage = "gitlab-language" -) - -// true if "set"/"true", false if "unset"/"false", none otherwise -func AttributeToBool(attr map[string]string, name string) optional.Option[bool] { - switch attr[name] { - case "set", "true": - return optional.Some(true) - case "unset", "false": - return optional.Some(false) - } - return optional.None[bool]() -} - -func AttributeToString(attr map[string]string, name string) optional.Option[string] { - if value, has := attr[name]; has && value != "unspecified" { - return optional.Some(value) - } - return optional.None[string]() -} diff --git a/modules/git/attribute/attribute.go b/modules/git/attribute/attribute.go new file mode 100644 index 0000000000..adf323ef41 --- /dev/null +++ b/modules/git/attribute/attribute.go @@ -0,0 +1,114 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package attribute + +import ( + "strings" + + "code.gitea.io/gitea/modules/optional" +) + +type Attribute string + +const ( + LinguistVendored = "linguist-vendored" + LinguistGenerated = "linguist-generated" + LinguistDocumentation = "linguist-documentation" + LinguistDetectable = "linguist-detectable" + LinguistLanguage = "linguist-language" + GitlabLanguage = "gitlab-language" + Lockable = "lockable" + Filter = "filter" +) + +var LinguistAttributes = []string{ + LinguistVendored, + LinguistGenerated, + LinguistDocumentation, + LinguistDetectable, + LinguistLanguage, + GitlabLanguage, +} + +func (a Attribute) IsUnspecified() bool { + return a == "" || a == "unspecified" +} + +func (a Attribute) ToString() optional.Option[string] { + if !a.IsUnspecified() { + return optional.Some(string(a)) + } + return optional.None[string]() +} + +// ToBool converts the attribute value to optional boolean: true if "set"/"true", false if "unset"/"false", none otherwise +func (a Attribute) ToBool() optional.Option[bool] { + switch a { + case "set", "true": + return optional.Some(true) + case "unset", "false": + return optional.Some(false) + } + return optional.None[bool]() +} + +type Attributes struct { + m map[string]Attribute +} + +func NewAttributes() *Attributes { + return &Attributes{m: make(map[string]Attribute)} +} + +func (attrs *Attributes) Get(name string) Attribute { + if value, has := attrs.m[name]; has { + return value + } + return "" +} + +func (attrs *Attributes) GetVendored() optional.Option[bool] { + return attrs.Get(LinguistVendored).ToBool() +} + +func (attrs *Attributes) GetGenerated() optional.Option[bool] { + return attrs.Get(LinguistGenerated).ToBool() +} + +func (attrs *Attributes) GetDocumentation() optional.Option[bool] { + return attrs.Get(LinguistDocumentation).ToBool() +} + +func (attrs *Attributes) GetDetectable() optional.Option[bool] { + return attrs.Get(LinguistDetectable).ToBool() +} + +func (attrs *Attributes) GetLinguistLanguage() optional.Option[string] { + return attrs.Get(LinguistLanguage).ToString() +} + +func (attrs *Attributes) GetGitlabLanguage() optional.Option[string] { + attrStr := attrs.Get(GitlabLanguage).ToString() + if attrStr.Has() { + raw := attrStr.Value() + // gitlab-language may have additional parameters after the language + // ignore them and just use the main language + // https://docs.gitlab.com/ee/user/project/highlighting.html#override-syntax-highlighting-for-a-file-type + if idx := strings.IndexByte(raw, '?'); idx >= 0 { + return optional.Some(raw[:idx]) + } + } + return attrStr +} + +func (attrs *Attributes) GetLanguage() optional.Option[string] { + // prefer linguist-language over gitlab-language + // if linguist-language is not set, use gitlab-language + // if both are not set, return none + language := attrs.GetLinguistLanguage() + if language.Value() == "" { + language = attrs.GetGitlabLanguage() + } + return language +} diff --git a/modules/git/attribute/attribute_test.go b/modules/git/attribute/attribute_test.go new file mode 100644 index 0000000000..dadb5582a3 --- /dev/null +++ b/modules/git/attribute/attribute_test.go @@ -0,0 +1,37 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package attribute + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Attribute(t *testing.T) { + assert.Empty(t, Attribute("").ToString().Value()) + assert.Empty(t, Attribute("unspecified").ToString().Value()) + assert.Equal(t, "python", Attribute("python").ToString().Value()) + assert.Equal(t, "Java", Attribute("Java").ToString().Value()) + + attributes := Attributes{ + m: map[string]Attribute{ + LinguistGenerated: "true", + LinguistDocumentation: "false", + LinguistDetectable: "set", + LinguistLanguage: "Python", + GitlabLanguage: "Java", + "filter": "unspecified", + "test": "", + }, + } + + assert.Empty(t, attributes.Get("test").ToString().Value()) + assert.Empty(t, attributes.Get("filter").ToString().Value()) + assert.Equal(t, "Python", attributes.Get(LinguistLanguage).ToString().Value()) + assert.Equal(t, "Java", attributes.Get(GitlabLanguage).ToString().Value()) + assert.True(t, attributes.Get(LinguistGenerated).ToBool().Value()) + assert.False(t, attributes.Get(LinguistDocumentation).ToBool().Value()) + assert.True(t, attributes.Get(LinguistDetectable).ToBool().Value()) +} diff --git a/modules/git/attribute/batch.go b/modules/git/attribute/batch.go new file mode 100644 index 0000000000..4e31fda575 --- /dev/null +++ b/modules/git/attribute/batch.go @@ -0,0 +1,216 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package attribute + +import ( + "bytes" + "context" + "fmt" + "os" + "path/filepath" + "time" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" +) + +// BatchChecker provides a reader for check-attribute content that can be long running +type BatchChecker struct { + attributesNum int + repo *git.Repository + stdinWriter *os.File + stdOut *nulSeparatedAttributeWriter + ctx context.Context + cancel context.CancelFunc + cmd *git.Command +} + +// NewBatchChecker creates a check attribute reader for the current repository and provided commit ID +// If treeish is empty, then it will use current working directory, otherwise it will use the provided treeish on the bare repo +func NewBatchChecker(repo *git.Repository, treeish string, attributes []string) (checker *BatchChecker, returnedErr error) { + ctx, cancel := context.WithCancel(repo.Ctx) + defer func() { + if returnedErr != nil { + cancel() + } + }() + + cmd, envs, cleanup, err := checkAttrCommand(repo, treeish, nil, attributes) + if err != nil { + return nil, err + } + defer func() { + if returnedErr != nil { + cleanup() + } + }() + + cmd.AddArguments("--stdin") + + checker = &BatchChecker{ + attributesNum: len(attributes), + repo: repo, + ctx: ctx, + cmd: cmd, + cancel: func() { + cancel() + cleanup() + }, + } + + stdinReader, stdinWriter, err := os.Pipe() + if err != nil { + return nil, err + } + checker.stdinWriter = stdinWriter + + lw := new(nulSeparatedAttributeWriter) + lw.attributes = make(chan attributeTriple, len(attributes)) + lw.closed = make(chan struct{}) + checker.stdOut = lw + + go func() { + defer func() { + _ = stdinReader.Close() + _ = lw.Close() + }() + stdErr := new(bytes.Buffer) + err := cmd.Run(ctx, &git.RunOpts{ + Env: envs, + Dir: repo.Path, + Stdin: stdinReader, + Stdout: lw, + Stderr: stdErr, + }) + + if err != nil && !git.IsErrCanceledOrKilled(err) { + log.Error("Attribute checker for commit %s exits with error: %v", treeish, err) + } + checker.cancel() + }() + + return checker, nil +} + +// CheckPath check attr for given path +func (c *BatchChecker) CheckPath(path string) (rs *Attributes, err error) { + defer func() { + if err != nil && err != c.ctx.Err() { + log.Error("Unexpected error when checking path %s in %s, error: %v", path, filepath.Base(c.repo.Path), err) + } + }() + + select { + case <-c.ctx.Done(): + return nil, c.ctx.Err() + default: + } + + if _, err = c.stdinWriter.Write([]byte(path + "\x00")); err != nil { + defer c.Close() + return nil, err + } + + reportTimeout := func() error { + stdOutClosed := false + select { + case <-c.stdOut.closed: + stdOutClosed = true + default: + } + debugMsg := fmt.Sprintf("check path %q in repo %q", path, filepath.Base(c.repo.Path)) + debugMsg += fmt.Sprintf(", stdOut: tmp=%q, pos=%d, closed=%v", string(c.stdOut.tmp), c.stdOut.pos, stdOutClosed) + if c.cmd != nil { + debugMsg += fmt.Sprintf(", process state: %q", c.cmd.ProcessState()) + } + _ = c.Close() + return fmt.Errorf("CheckPath timeout: %s", debugMsg) + } + + rs = NewAttributes() + for i := 0; i < c.attributesNum; i++ { + select { + case <-time.After(5 * time.Second): + // there is no "hang" problem now. This code is just used to catch other potential problems. + return nil, reportTimeout() + case attr, ok := <-c.stdOut.ReadAttribute(): + if !ok { + return nil, c.ctx.Err() + } + rs.m[attr.Attribute] = Attribute(attr.Value) + case <-c.ctx.Done(): + return nil, c.ctx.Err() + } + } + return rs, nil +} + +func (c *BatchChecker) Close() error { + c.cancel() + err := c.stdinWriter.Close() + return err +} + +type attributeTriple struct { + Filename string + Attribute string + Value string +} + +type nulSeparatedAttributeWriter struct { + tmp []byte + attributes chan attributeTriple + closed chan struct{} + working attributeTriple + pos int +} + +func (wr *nulSeparatedAttributeWriter) Write(p []byte) (n int, err error) { + l, read := len(p), 0 + + nulIdx := bytes.IndexByte(p, '\x00') + for nulIdx >= 0 { + wr.tmp = append(wr.tmp, p[:nulIdx]...) + switch wr.pos { + case 0: + wr.working = attributeTriple{ + Filename: string(wr.tmp), + } + case 1: + wr.working.Attribute = string(wr.tmp) + case 2: + wr.working.Value = string(wr.tmp) + } + wr.tmp = wr.tmp[:0] + wr.pos++ + if wr.pos > 2 { + wr.attributes <- wr.working + wr.pos = 0 + } + read += nulIdx + 1 + if l > read { + p = p[nulIdx+1:] + nulIdx = bytes.IndexByte(p, '\x00') + } else { + return l, nil + } + } + wr.tmp = append(wr.tmp, p...) + return l, nil +} + +func (wr *nulSeparatedAttributeWriter) ReadAttribute() <-chan attributeTriple { + return wr.attributes +} + +func (wr *nulSeparatedAttributeWriter) Close() error { + select { + case <-wr.closed: + return nil + default: + } + close(wr.attributes) + close(wr.closed) + return nil +} diff --git a/modules/git/attribute/batch_test.go b/modules/git/attribute/batch_test.go new file mode 100644 index 0000000000..30a3d805fe --- /dev/null +++ b/modules/git/attribute/batch_test.go @@ -0,0 +1,172 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package attribute + +import ( + "path/filepath" + "testing" + "time" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) { + wr := &nulSeparatedAttributeWriter{ + attributes: make(chan attributeTriple, 5), + } + + testStr := ".gitignore\"\n\x00linguist-vendored\x00unspecified\x00" + + n, err := wr.Write([]byte(testStr)) + + assert.Len(t, testStr, n) + assert.NoError(t, err) + select { + case attr := <-wr.ReadAttribute(): + assert.Equal(t, ".gitignore\"\n", attr.Filename) + assert.Equal(t, LinguistVendored, attr.Attribute) + assert.Equal(t, "unspecified", attr.Value) + case <-time.After(100 * time.Millisecond): + assert.FailNow(t, "took too long to read an attribute from the list") + } + // Write a second attribute again + n, err = wr.Write([]byte(testStr)) + + assert.Len(t, testStr, n) + assert.NoError(t, err) + + select { + case attr := <-wr.ReadAttribute(): + assert.Equal(t, ".gitignore\"\n", attr.Filename) + assert.Equal(t, LinguistVendored, attr.Attribute) + assert.Equal(t, "unspecified", attr.Value) + case <-time.After(100 * time.Millisecond): + assert.FailNow(t, "took too long to read an attribute from the list") + } + + // Write a partial attribute + _, err = wr.Write([]byte("incomplete-file")) + assert.NoError(t, err) + _, err = wr.Write([]byte("name\x00")) + assert.NoError(t, err) + + select { + case <-wr.ReadAttribute(): + assert.FailNow(t, "There should not be an attribute ready to read") + case <-time.After(100 * time.Millisecond): + } + _, err = wr.Write([]byte("attribute\x00")) + assert.NoError(t, err) + select { + case <-wr.ReadAttribute(): + assert.FailNow(t, "There should not be an attribute ready to read") + case <-time.After(100 * time.Millisecond): + } + + _, err = wr.Write([]byte("value\x00")) + assert.NoError(t, err) + + attr := <-wr.ReadAttribute() + assert.Equal(t, "incomplete-filename", attr.Filename) + assert.Equal(t, "attribute", attr.Attribute) + assert.Equal(t, "value", attr.Value) + + _, err = wr.Write([]byte("shouldbe.vendor\x00linguist-vendored\x00set\x00shouldbe.vendor\x00linguist-generated\x00unspecified\x00shouldbe.vendor\x00linguist-language\x00unspecified\x00")) + assert.NoError(t, err) + attr = <-wr.ReadAttribute() + assert.NoError(t, err) + assert.Equal(t, attributeTriple{ + Filename: "shouldbe.vendor", + Attribute: LinguistVendored, + Value: "set", + }, attr) + attr = <-wr.ReadAttribute() + assert.NoError(t, err) + assert.Equal(t, attributeTriple{ + Filename: "shouldbe.vendor", + Attribute: LinguistGenerated, + Value: "unspecified", + }, attr) + attr = <-wr.ReadAttribute() + assert.NoError(t, err) + assert.Equal(t, attributeTriple{ + Filename: "shouldbe.vendor", + Attribute: LinguistLanguage, + Value: "unspecified", + }, attr) +} + +func expectedAttrs() *Attributes { + return &Attributes{ + m: map[string]Attribute{ + LinguistGenerated: "unspecified", + LinguistDetectable: "unspecified", + LinguistDocumentation: "unspecified", + LinguistVendored: "unspecified", + LinguistLanguage: "Python", + GitlabLanguage: "unspecified", + }, + } +} + +func Test_BatchChecker(t *testing.T) { + setting.AppDataPath = t.TempDir() + repoPath := "../tests/repos/language_stats_repo" + gitRepo, err := git.OpenRepository(t.Context(), repoPath) + require.NoError(t, err) + defer gitRepo.Close() + + commitID := "8fee858da5796dfb37704761701bb8e800ad9ef3" + + t.Run("Create index file to run git check-attr", func(t *testing.T) { + defer test.MockVariableValue(&git.DefaultFeatures().SupportCheckAttrOnBare, false)() + checker, err := NewBatchChecker(gitRepo, commitID, LinguistAttributes) + assert.NoError(t, err) + defer checker.Close() + attributes, err := checker.CheckPath("i-am-a-python.p") + assert.NoError(t, err) + assert.Equal(t, expectedAttrs(), attributes) + }) + + // run git check-attr on work tree + t.Run("Run git check-attr on git work tree", func(t *testing.T) { + dir := filepath.Join(t.TempDir(), "test-repo") + err := git.Clone(t.Context(), repoPath, dir, git.CloneRepoOptions{ + Shared: true, + Branch: "master", + }) + assert.NoError(t, err) + + tempRepo, err := git.OpenRepository(t.Context(), dir) + assert.NoError(t, err) + defer tempRepo.Close() + + checker, err := NewBatchChecker(tempRepo, "", LinguistAttributes) + assert.NoError(t, err) + defer checker.Close() + attributes, err := checker.CheckPath("i-am-a-python.p") + assert.NoError(t, err) + assert.Equal(t, expectedAttrs(), attributes) + }) + + if !git.DefaultFeatures().SupportCheckAttrOnBare { + t.Skip("git version 2.40 is required to support run check-attr on bare repo") + return + } + + t.Run("Run git check-attr in bare repository", func(t *testing.T) { + checker, err := NewBatchChecker(gitRepo, commitID, LinguistAttributes) + assert.NoError(t, err) + defer checker.Close() + + attributes, err := checker.CheckPath("i-am-a-python.p") + assert.NoError(t, err) + assert.Equal(t, expectedAttrs(), attributes) + }) +} diff --git a/modules/git/attribute/checker.go b/modules/git/attribute/checker.go new file mode 100644 index 0000000000..167b31416e --- /dev/null +++ b/modules/git/attribute/checker.go @@ -0,0 +1,101 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package attribute + +import ( + "bytes" + "context" + "errors" + "fmt" + "os" + + "code.gitea.io/gitea/modules/git" +) + +func checkAttrCommand(gitRepo *git.Repository, treeish string, filenames, attributes []string) (*git.Command, []string, func(), error) { + cancel := func() {} + envs := []string{"GIT_FLUSH=1"} + cmd := git.NewCommand("check-attr", "-z") + if len(attributes) == 0 { + cmd.AddArguments("--all") + } + + // there is treeish, read from bare repo or temp index created by "read-tree" + if treeish != "" { + if git.DefaultFeatures().SupportCheckAttrOnBare { + cmd.AddArguments("--source") + cmd.AddDynamicArguments(treeish) + } else { + indexFilename, worktree, deleteTemporaryFile, err := gitRepo.ReadTreeToTemporaryIndex(treeish) + if err != nil { + return nil, nil, nil, err + } + + cmd.AddArguments("--cached") + envs = append(envs, + "GIT_INDEX_FILE="+indexFilename, + "GIT_WORK_TREE="+worktree, + ) + cancel = deleteTemporaryFile + } + } else { + // Read from existing index, in cases where the repo is bare and has an index, + // or the work tree contains unstaged changes that shouldn't affect the attribute check. + // It is caller's responsibility to add changed ".gitattributes" into the index if they want to respect the new changes. + cmd.AddArguments("--cached") + } + + cmd.AddDynamicArguments(attributes...) + if len(filenames) > 0 { + cmd.AddDashesAndList(filenames...) + } + return cmd, envs, cancel, nil +} + +type CheckAttributeOpts struct { + Filenames []string + Attributes []string +} + +// CheckAttributes return the attributes of the given filenames and attributes in the given treeish. +// If treeish is empty, then it will use current working directory, otherwise it will use the provided treeish on the bare repo +func CheckAttributes(ctx context.Context, gitRepo *git.Repository, treeish string, opts CheckAttributeOpts) (map[string]*Attributes, error) { + cmd, envs, cancel, err := checkAttrCommand(gitRepo, treeish, opts.Filenames, opts.Attributes) + if err != nil { + return nil, err + } + defer cancel() + + stdOut := new(bytes.Buffer) + stdErr := new(bytes.Buffer) + + if err := cmd.Run(ctx, &git.RunOpts{ + Env: append(os.Environ(), envs...), + Dir: gitRepo.Path, + Stdout: stdOut, + Stderr: stdErr, + }); err != nil { + return nil, fmt.Errorf("failed to run check-attr: %w\n%s\n%s", err, stdOut.String(), stdErr.String()) + } + + fields := bytes.Split(stdOut.Bytes(), []byte{'\000'}) + if len(fields)%3 != 1 { + return nil, errors.New("wrong number of fields in return from check-attr") + } + + attributesMap := make(map[string]*Attributes) + for i := 0; i < (len(fields) / 3); i++ { + filename := string(fields[3*i]) + attribute := string(fields[3*i+1]) + info := string(fields[3*i+2]) + attribute2info, ok := attributesMap[filename] + if !ok { + attribute2info = NewAttributes() + attributesMap[filename] = attribute2info + } + attribute2info.m[attribute] = Attribute(info) + } + + return attributesMap, nil +} diff --git a/modules/git/attribute/checker_test.go b/modules/git/attribute/checker_test.go new file mode 100644 index 0000000000..67fbda8918 --- /dev/null +++ b/modules/git/attribute/checker_test.go @@ -0,0 +1,84 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package attribute + +import ( + "path/filepath" + "testing" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_Checker(t *testing.T) { + setting.AppDataPath = t.TempDir() + repoPath := "../tests/repos/language_stats_repo" + gitRepo, err := git.OpenRepository(t.Context(), repoPath) + require.NoError(t, err) + defer gitRepo.Close() + + commitID := "8fee858da5796dfb37704761701bb8e800ad9ef3" + + t.Run("Create index file to run git check-attr", func(t *testing.T) { + defer test.MockVariableValue(&git.DefaultFeatures().SupportCheckAttrOnBare, false)() + attrs, err := CheckAttributes(t.Context(), gitRepo, commitID, CheckAttributeOpts{ + Filenames: []string{"i-am-a-python.p"}, + Attributes: LinguistAttributes, + }) + assert.NoError(t, err) + assert.Len(t, attrs, 1) + assert.Equal(t, expectedAttrs(), attrs["i-am-a-python.p"]) + }) + + // run git check-attr on work tree + t.Run("Run git check-attr on git work tree", func(t *testing.T) { + dir := filepath.Join(t.TempDir(), "test-repo") + err := git.Clone(t.Context(), repoPath, dir, git.CloneRepoOptions{ + Shared: true, + Branch: "master", + }) + assert.NoError(t, err) + + tempRepo, err := git.OpenRepository(t.Context(), dir) + assert.NoError(t, err) + defer tempRepo.Close() + + attrs, err := CheckAttributes(t.Context(), tempRepo, "", CheckAttributeOpts{ + Filenames: []string{"i-am-a-python.p"}, + Attributes: LinguistAttributes, + }) + assert.NoError(t, err) + assert.Len(t, attrs, 1) + assert.Equal(t, expectedAttrs(), attrs["i-am-a-python.p"]) + }) + + t.Run("Run git check-attr in bare repository using index", func(t *testing.T) { + attrs, err := CheckAttributes(t.Context(), gitRepo, "", CheckAttributeOpts{ + Filenames: []string{"i-am-a-python.p"}, + Attributes: LinguistAttributes, + }) + assert.NoError(t, err) + assert.Len(t, attrs, 1) + assert.Equal(t, expectedAttrs(), attrs["i-am-a-python.p"]) + }) + + if !git.DefaultFeatures().SupportCheckAttrOnBare { + t.Skip("git version 2.40 is required to support run check-attr on bare repo without using index") + return + } + + t.Run("Run git check-attr in bare repository", func(t *testing.T) { + attrs, err := CheckAttributes(t.Context(), gitRepo, commitID, CheckAttributeOpts{ + Filenames: []string{"i-am-a-python.p"}, + Attributes: LinguistAttributes, + }) + assert.NoError(t, err) + assert.Len(t, attrs, 1) + assert.Equal(t, expectedAttrs(), attrs["i-am-a-python.p"]) + }) +} diff --git a/modules/git/attribute/main_test.go b/modules/git/attribute/main_test.go new file mode 100644 index 0000000000..df8241bfb0 --- /dev/null +++ b/modules/git/attribute/main_test.go @@ -0,0 +1,41 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package attribute + +import ( + "context" + "fmt" + "os" + "testing" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" +) + +func testRun(m *testing.M) error { + gitHomePath, err := os.MkdirTemp(os.TempDir(), "git-home") + if err != nil { + return fmt.Errorf("unable to create temp dir: %w", err) + } + defer util.RemoveAll(gitHomePath) + setting.Git.HomePath = gitHomePath + + if err = git.InitFull(context.Background()); err != nil { + return fmt.Errorf("failed to call Init: %w", err) + } + + exitCode := m.Run() + if exitCode != 0 { + return fmt.Errorf("run test failed, ExitCode=%d", exitCode) + } + return nil +} + +func TestMain(m *testing.M) { + if err := testRun(m); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Test failed: %v", err) + os.Exit(1) + } +} diff --git a/modules/git/batch.go b/modules/git/batch.go index 3ec4f1ddcc..f9e1748b54 100644 --- a/modules/git/batch.go +++ b/modules/git/batch.go @@ -14,25 +14,26 @@ type Batch struct { Writer WriteCloserError } -func (repo *Repository) NewBatch(ctx context.Context) (*Batch, error) { +// NewBatch creates a new batch for the given repository, the Close must be invoked before release the batch +func NewBatch(ctx context.Context, repoPath string) (*Batch, error) { // Now because of some insanity with git cat-file not immediately failing if not run in a valid git directory we need to run git rev-parse first! - if err := ensureValidGitRepository(ctx, repo.Path); err != nil { + if err := ensureValidGitRepository(ctx, repoPath); err != nil { return nil, err } var batch Batch - batch.Writer, batch.Reader, batch.cancel = catFileBatch(ctx, repo.Path) + batch.Writer, batch.Reader, batch.cancel = catFileBatch(ctx, repoPath) return &batch, nil } -func (repo *Repository) NewBatchCheck(ctx context.Context) (*Batch, error) { +func NewBatchCheck(ctx context.Context, repoPath string) (*Batch, error) { // Now because of some insanity with git cat-file not immediately failing if not run in a valid git directory we need to run git rev-parse first! - if err := ensureValidGitRepository(ctx, repo.Path); err != nil { + if err := ensureValidGitRepository(ctx, repoPath); err != nil { return nil, err } var check Batch - check.Writer, check.Reader, check.cancel = catFileBatchCheck(ctx, repo.Path) + check.Writer, check.Reader, check.cancel = catFileBatchCheck(ctx, repoPath) return &check, nil } diff --git a/modules/git/batch_reader.go b/modules/git/batch_reader.go index 33e54fe75c..7bbab76bb8 100644 --- a/modules/git/batch_reader.go +++ b/modules/git/batch_reader.go @@ -29,8 +29,8 @@ type WriteCloserError interface { // This is needed otherwise the git cat-file will hang for invalid repositories. func ensureValidGitRepository(ctx context.Context, repoPath string) error { stderr := strings.Builder{} - err := NewCommand(ctx, "rev-parse"). - Run(&RunOpts{ + err := NewCommand("rev-parse"). + Run(ctx, &RunOpts{ Dir: repoPath, Stderr: &stderr, }) @@ -61,8 +61,8 @@ func catFileBatchCheck(ctx context.Context, repoPath string) (WriteCloserError, go func() { stderr := strings.Builder{} - err := NewCommand(ctx, "cat-file", "--batch-check"). - Run(&RunOpts{ + err := NewCommand("cat-file", "--batch-check"). + Run(ctx, &RunOpts{ Dir: repoPath, Stdin: batchStdinReader, Stdout: batchStdoutWriter, @@ -109,8 +109,8 @@ func catFileBatch(ctx context.Context, repoPath string) (WriteCloserError, *bufi go func() { stderr := strings.Builder{} - err := NewCommand(ctx, "cat-file", "--batch"). - Run(&RunOpts{ + err := NewCommand("cat-file", "--batch"). + Run(ctx, &RunOpts{ Dir: repoPath, Stdin: batchStdinReader, Stdout: batchStdoutWriter, diff --git a/modules/git/blame.go b/modules/git/blame.go index cad720edf4..659dec34a1 100644 --- a/modules/git/blame.go +++ b/modules/git/blame.go @@ -11,7 +11,7 @@ import ( "os" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/setting" ) // BlamePart represents block of blame - continuous lines with one sha @@ -29,12 +29,13 @@ type BlameReader struct { bufferedReader *bufio.Reader done chan error lastSha *string - ignoreRevsFile *string + ignoreRevsFile string objectFormat ObjectFormat + cleanupFuncs []func() } func (r *BlameReader) UsesIgnoreRevs() bool { - return r.ignoreRevsFile != nil + return r.ignoreRevsFile != "" } // NextPart returns next part of blame (sequential code lines with the same commit) @@ -122,40 +123,49 @@ func (r *BlameReader) Close() error { r.bufferedReader = nil _ = r.reader.Close() _ = r.output.Close() - if r.ignoreRevsFile != nil { - _ = util.Remove(*r.ignoreRevsFile) + for _, cleanup := range r.cleanupFuncs { + if cleanup != nil { + cleanup() + } } return err } // CreateBlameReader creates reader for given repository, commit and file -func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath string, commit *Commit, file string, bypassBlameIgnore bool) (*BlameReader, error) { - var ignoreRevsFile *string +func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath string, commit *Commit, file string, bypassBlameIgnore bool) (rd *BlameReader, err error) { + var ignoreRevsFileName string + var ignoreRevsFileCleanup func() + defer func() { + if err != nil && ignoreRevsFileCleanup != nil { + ignoreRevsFileCleanup() + } + }() + + cmd := NewCommandNoGlobals("blame", "--porcelain") + if DefaultFeatures().CheckVersionAtLeast("2.23") && !bypassBlameIgnore { - ignoreRevsFile = tryCreateBlameIgnoreRevsFile(commit) + ignoreRevsFileName, ignoreRevsFileCleanup, err = tryCreateBlameIgnoreRevsFile(commit) + if err != nil && !IsErrNotExist(err) { + return nil, err + } + if ignoreRevsFileName != "" { + // 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. + cmd.AddOptionValues("--ignore-revs-file", ignoreRevsFileName) + } } - cmd := NewCommandContextNoGlobals(ctx, "blame", "--porcelain") - if ignoreRevsFile != nil { - // 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. - cmd.AddOptionValues("--ignore-revs-file", *ignoreRevsFile) - } cmd.AddDynamicArguments(commit.ID.String()).AddDashesAndList(file) + + done := make(chan error, 1) reader, stdout, err := os.Pipe() if err != nil { - if ignoreRevsFile != nil { - _ = util.Remove(*ignoreRevsFile) - } return nil, err } - - done := make(chan error, 1) - go func() { stderr := bytes.Buffer{} // 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 := cmd.Run(&RunOpts{ + err := cmd.Run(ctx, &RunOpts{ UseContextTimeout: true, Dir: repoPath, Stdout: stdout, @@ -169,40 +179,40 @@ func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath }() bufferedReader := bufio.NewReader(reader) - return &BlameReader{ output: stdout, reader: reader, bufferedReader: bufferedReader, done: done, - ignoreRevsFile: ignoreRevsFile, + ignoreRevsFile: ignoreRevsFileName, objectFormat: objectFormat, + cleanupFuncs: []func(){ignoreRevsFileCleanup}, }, nil } -func tryCreateBlameIgnoreRevsFile(commit *Commit) *string { +func tryCreateBlameIgnoreRevsFile(commit *Commit) (string, func(), error) { entry, err := commit.GetTreeEntryByPath(".git-blame-ignore-revs") if err != nil { - return nil + return "", nil, err } r, err := entry.Blob().DataAsync() if err != nil { - return nil + return "", nil, err } defer r.Close() - f, err := os.CreateTemp("", "gitea_git-blame-ignore-revs") + f, cleanup, err := setting.AppDataTempDir("git-repo-content").CreateTempFileRandom("git-blame-ignore-revs") if err != nil { - return nil + return "", nil, err } - + filename := f.Name() _, err = io.Copy(f, r) _ = f.Close() if err != nil { - _ = util.Remove(f.Name()) - return nil + cleanup() + return "", nil, err } - return util.ToPointer(f.Name()) + return filename, cleanup, nil } diff --git a/modules/git/blame_sha256_test.go b/modules/git/blame_sha256_test.go index da451f22fc..c0a97bed3b 100644 --- a/modules/git/blame_sha256_test.go +++ b/modules/git/blame_sha256_test.go @@ -7,11 +7,14 @@ import ( "context" "testing" + "code.gitea.io/gitea/modules/setting" + "github.com/stretchr/testify/assert" ) func TestReadingBlameOutputSha256(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) + setting.AppDataPath = t.TempDir() + ctx, cancel := context.WithCancel(t.Context()) defer cancel() if isGogit { diff --git a/modules/git/blame_test.go b/modules/git/blame_test.go index 4220c85600..809d6fbcf7 100644 --- a/modules/git/blame_test.go +++ b/modules/git/blame_test.go @@ -7,11 +7,14 @@ import ( "context" "testing" + "code.gitea.io/gitea/modules/setting" + "github.com/stretchr/testify/assert" ) func TestReadingBlameOutput(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) + setting.AppDataPath = t.TempDir() + ctx, cancel := context.WithCancel(t.Context()) defer cancel() t.Run("Without .git-blame-ignore-revs", func(t *testing.T) { diff --git a/modules/git/blob.go b/modules/git/blob.go index bcecb42e16..40d8f44e79 100644 --- a/modules/git/blob.go +++ b/modules/git/blob.go @@ -7,7 +7,9 @@ package git import ( "bytes" "encoding/base64" + "errors" "io" + "strings" "code.gitea.io/gitea/modules/typesniffer" "code.gitea.io/gitea/modules/util" @@ -20,22 +22,28 @@ func (b *Blob) Name() string { return b.name } -// GetBlobContent Gets the limited content of the blob as raw text -func (b *Blob) GetBlobContent(limit int64) (string, error) { +// GetBlobBytes Gets the limited content of the blob +func (b *Blob) GetBlobBytes(limit int64) ([]byte, error) { if limit <= 0 { - return "", nil + return nil, nil } dataRc, err := b.DataAsync() if err != nil { - return "", err + return nil, err } defer dataRc.Close() - buf, err := util.ReadWithLimit(dataRc, int(limit)) + return util.ReadWithLimit(dataRc, int(limit)) +} + +// GetBlobContent Gets the limited content of the blob as raw text +func (b *Blob) GetBlobContent(limit int64) (string, error) { + buf, err := b.GetBlobBytes(limit) return string(buf), err } -// GetBlobLineCount gets line count of the blob -func (b *Blob) GetBlobLineCount() (int, error) { +// GetBlobLineCount gets line count of the blob. +// It will also try to write the content to w if it's not nil, then we could pre-fetch the content without reading it again. +func (b *Blob) GetBlobLineCount(w io.Writer) (int, error) { reader, err := b.DataAsync() if err != nil { return 0, err @@ -44,59 +52,61 @@ func (b *Blob) GetBlobLineCount() (int, error) { buf := make([]byte, 32*1024) count := 1 lineSep := []byte{'\n'} - - c, err := reader.Read(buf) - if c == 0 && err == io.EOF { - return 0, nil - } for { + c, err := reader.Read(buf) + if w != nil { + if _, err := w.Write(buf[:c]); err != nil { + return count, err + } + } count += bytes.Count(buf[:c], lineSep) switch { - case err == io.EOF: + case errors.Is(err, io.EOF): return count, nil case err != nil: return count, err } - c, err = reader.Read(buf) } } -// GetBlobContentBase64 Reads the content of the blob with a base64 encode and returns the encoded string -func (b *Blob) GetBlobContentBase64() (string, error) { +// GetBlobContentBase64 Reads the content of the blob with a base64 encoding and returns the encoded string +func (b *Blob) GetBlobContentBase64(originContent *strings.Builder) (string, error) { dataRc, err := b.DataAsync() if err != nil { return "", err } defer dataRc.Close() - pr, pw := io.Pipe() - encoder := base64.NewEncoder(base64.StdEncoding, pw) - - go func() { - _, err := io.Copy(encoder, dataRc) - _ = encoder.Close() - - if err != nil { - _ = pw.CloseWithError(err) - } else { - _ = pw.Close() + base64buf := &strings.Builder{} + encoder := base64.NewEncoder(base64.StdEncoding, base64buf) + buf := make([]byte, 32*1024) +loop: + for { + n, err := dataRc.Read(buf) + if n > 0 { + if originContent != nil { + _, _ = originContent.Write(buf[:n]) + } + if _, err := encoder.Write(buf[:n]); err != nil { + return "", err + } + } + switch { + case errors.Is(err, io.EOF): + break loop + case err != nil: + return "", err } - }() - - out, err := io.ReadAll(pr) - if err != nil { - return "", err } - return string(out), nil + _ = encoder.Close() + return base64buf.String(), nil } // GuessContentType guesses the content type of the blob. func (b *Blob) GuessContentType() (typesniffer.SniffedType, error) { - r, err := b.DataAsync() + buf, err := b.GetBlobBytes(typesniffer.SniffContentSize) if err != nil { return typesniffer.SniffedType{}, err } - defer r.Close() - - return typesniffer.DetectContentTypeFromReader(r) + return typesniffer.DetectContentType(buf), nil } diff --git a/modules/git/blob_test.go b/modules/git/blob_test.go index d0804350ed..f21e8d146d 100644 --- a/modules/git/blob_test.go +++ b/modules/git/blob_test.go @@ -47,7 +47,7 @@ func Benchmark_Blob_Data(b *testing.B) { b.Fatal(err) } - for i := 0; i < b.N; i++ { + for b.Loop() { r, err := testBlob.DataAsync() if err != nil { b.Fatal(err) diff --git a/modules/git/cmdverb.go b/modules/git/cmdverb.go new file mode 100644 index 0000000000..3d6f4ae0c6 --- /dev/null +++ b/modules/git/cmdverb.go @@ -0,0 +1,36 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +const ( + CmdVerbUploadPack = "git-upload-pack" + CmdVerbUploadArchive = "git-upload-archive" + CmdVerbReceivePack = "git-receive-pack" + CmdVerbLfsAuthenticate = "git-lfs-authenticate" + CmdVerbLfsTransfer = "git-lfs-transfer" + + CmdSubVerbLfsUpload = "upload" + CmdSubVerbLfsDownload = "download" +) + +func IsAllowedVerbForServe(verb string) bool { + switch verb { + case CmdVerbUploadPack, + CmdVerbUploadArchive, + CmdVerbReceivePack, + CmdVerbLfsAuthenticate, + CmdVerbLfsTransfer: + return true + } + return false +} + +func IsAllowedVerbForServeLfs(verb string) bool { + switch verb { + case CmdVerbLfsAuthenticate, + CmdVerbLfsTransfer: + return true + } + return false +} diff --git a/modules/git/command.go b/modules/git/command.go index 2584e3cc57..22f1d02339 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -18,6 +18,7 @@ import ( "time" "code.gitea.io/gitea/modules/git/internal" //nolint:depguard // only this file can use the internal type CmdArg, other files and packages should use AddXxx functions + "code.gitea.io/gitea/modules/gtprof" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/util" @@ -43,9 +44,10 @@ const DefaultLocale = "C" type Command struct { prog string args []string - parentContext context.Context globalArgsLength int brokenArgs []string + cmd *exec.Cmd // for debug purpose only + configArgs []string } func logArgSanitize(arg string) string { @@ -54,7 +56,7 @@ func logArgSanitize(arg string) string { } else if filepath.IsAbs(arg) { base := filepath.Base(arg) dir := filepath.Dir(arg) - return filepath.Join(filepath.Base(dir), base) + return ".../" + filepath.Join(filepath.Base(dir), base) } return arg } @@ -79,9 +81,16 @@ func (c *Command) LogString() string { return strings.Join(a, " ") } +func (c *Command) ProcessState() string { + if c.cmd == nil { + return "" + } + return c.cmd.ProcessState.String() +} + // NewCommand creates and returns a new Git Command based on given command and arguments. // Each argument should be safe to be trusted. User-provided arguments should be passed to AddDynamicArguments instead. -func NewCommand(ctx context.Context, args ...internal.CmdArg) *Command { +func NewCommand(args ...internal.CmdArg) *Command { // Make an explicit copy of globalCommandArgs, otherwise append might overwrite it cargs := make([]string, 0, len(globalCommandArgs)+len(args)) for _, arg := range globalCommandArgs { @@ -93,31 +102,23 @@ func NewCommand(ctx context.Context, args ...internal.CmdArg) *Command { return &Command{ prog: GitExecutable, args: cargs, - parentContext: ctx, globalArgsLength: len(globalCommandArgs), } } -// 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 +// NewCommandNoGlobals creates and returns a new Git Command based on given command and arguments only with the specified args and don't use global command args // Each argument should be safe to be trusted. User-provided arguments should be passed to AddDynamicArguments instead. -func NewCommandContextNoGlobals(ctx context.Context, args ...internal.CmdArg) *Command { +func NewCommandNoGlobals(args ...internal.CmdArg) *Command { cargs := make([]string, 0, len(args)) for _, arg := range args { cargs = append(cargs, string(arg)) } return &Command{ - prog: GitExecutable, - args: cargs, - parentContext: ctx, + prog: GitExecutable, + args: cargs, } } -// SetParentContext sets the parent context for this command -func (c *Command) SetParentContext(ctx context.Context) *Command { - c.parentContext = ctx - return c -} - // isSafeArgumentValue checks if the argument is safe to be used as a value (not an option) func isSafeArgumentValue(s string) bool { return s == "" || s[0] != '-' @@ -196,6 +197,16 @@ func (c *Command) AddDashesAndList(list ...string) *Command { return c } +func (c *Command) AddConfig(key, value string) *Command { + kv := key + "=" + value + if !isSafeArgumentValue(kv) { + c.brokenArgs = append(c.brokenArgs, key) + } else { + c.configArgs = append(c.configArgs, "-c", kv) + } + return c +} + // ToTrustedCmdArgs converts a list of strings (trusted as argument) to TrustedCmdArgs // In most cases, it shouldn't be used. Use NewCommand().AddXxx() function instead func ToTrustedCmdArgs(args []string) TrustedCmdArgs { @@ -276,11 +287,11 @@ func CommonCmdServEnvs() []string { var ErrBrokenCommand = errors.New("git command is broken") // Run runs the command with the RunOpts -func (c *Command) Run(opts *RunOpts) error { - return c.run(1, opts) +func (c *Command) Run(ctx context.Context, opts *RunOpts) error { + return c.run(ctx, 1, opts) } -func (c *Command) run(skip int, opts *RunOpts) error { +func (c *Command) run(ctx context.Context, skip int, opts *RunOpts) error { if len(c.brokenArgs) != 0 { log.Error("git command is broken: %s, broken args: %s", c.LogString(), strings.Join(c.brokenArgs, " ")) return ErrBrokenCommand @@ -295,29 +306,34 @@ func (c *Command) run(skip int, opts *RunOpts) error { timeout = defaultCommandExecutionTimeout } - var desc string + cmdLogString := c.LogString() callerInfo := util.CallerFuncName(1 /* util */ + 1 /* this */ + skip /* parent */) if pos := strings.LastIndex(callerInfo, "/"); pos >= 0 { callerInfo = callerInfo[pos+1:] } // these logs are for debugging purposes only, so no guarantee of correctness or stability - desc = fmt.Sprintf("git.Run(by:%s, repo:%s): %s", callerInfo, logArgSanitize(opts.Dir), c.LogString()) + desc := fmt.Sprintf("git.Run(by:%s, repo:%s): %s", callerInfo, logArgSanitize(opts.Dir), cmdLogString) log.Debug("git.Command: %s", desc) - var ctx context.Context + _, span := gtprof.GetTracer().Start(ctx, gtprof.TraceSpanGitRun) + defer span.End() + span.SetAttributeString(gtprof.TraceAttrFuncCaller, callerInfo) + span.SetAttributeString(gtprof.TraceAttrGitCommand, cmdLogString) + var cancel context.CancelFunc var finished context.CancelFunc if opts.UseContextTimeout { - ctx, cancel, finished = process.GetManager().AddContext(c.parentContext, desc) + ctx, cancel, finished = process.GetManager().AddContext(ctx, desc) } else { - ctx, cancel, finished = process.GetManager().AddContextTimeout(c.parentContext, timeout, desc) + ctx, cancel, finished = process.GetManager().AddContextTimeout(ctx, timeout, desc) } defer finished() startTime := time.Now() - cmd := exec.CommandContext(ctx, c.prog, c.args...) + cmd := exec.CommandContext(ctx, c.prog, append(c.configArgs, c.args...)...) + c.cmd = cmd // for debug purpose only if opts.Env == nil { cmd.Env = os.Environ() } else { @@ -352,9 +368,10 @@ func (c *Command) run(skip int, opts *RunOpts) error { // We need to check if the context is canceled by the program on Windows. // This is because Windows does not have signal checking when terminating the process. // It always returns exit code 1, unlike Linux, which has many exit codes for signals. + // `err.Error()` returns "exit status 1" when using the `git check-attr` command after the context is canceled. if runtime.GOOS == "windows" && err != nil && - err.Error() == "" && + (err.Error() == "" || err.Error() == "exit status 1") && cmd.ProcessState.ExitCode() == 1 && ctx.Err() == context.Canceled { return ctx.Err() @@ -404,8 +421,8 @@ func IsErrorExitCode(err error, code int) bool { } // 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) { - stdoutBytes, stderrBytes, err := c.runStdBytes(opts) +func (c *Command) RunStdString(ctx context.Context, opts *RunOpts) (stdout, stderr string, runErr RunStdError) { + stdoutBytes, stderrBytes, err := c.runStdBytes(ctx, opts) stdout = util.UnsafeBytesToString(stdoutBytes) stderr = util.UnsafeBytesToString(stderrBytes) if err != nil { @@ -416,11 +433,11 @@ 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) { - return c.runStdBytes(opts) +func (c *Command) RunStdBytes(ctx context.Context, opts *RunOpts) (stdout, stderr []byte, runErr RunStdError) { + return c.runStdBytes(ctx, opts) } -func (c *Command) runStdBytes(opts *RunOpts) (stdout, stderr []byte, runErr RunStdError) { +func (c *Command) runStdBytes(ctx context.Context, opts *RunOpts) (stdout, stderr []byte, runErr RunStdError) { if opts == nil { opts = &RunOpts{} } @@ -443,7 +460,7 @@ func (c *Command) runStdBytes(opts *RunOpts) (stdout, stderr []byte, runErr RunS PipelineFunc: opts.PipelineFunc, } - err := c.run(2, newOpts) + err := c.run(ctx, 2, newOpts) stderr = stderrBuf.Bytes() if err != nil { return nil, stderr, &runStdError{err: err, stderr: util.UnsafeBytesToString(stderr)} diff --git a/modules/git/command_race_test.go b/modules/git/command_race_test.go index f567406822..a6aa3a1580 100644 --- a/modules/git/command_race_test.go +++ b/modules/git/command_race_test.go @@ -15,9 +15,9 @@ func TestRunWithContextNoTimeout(t *testing.T) { maxLoops := 10 // 'git --version' does not block so it must be finished before the timeout triggered. - cmd := NewCommand(context.Background(), "--version") + cmd := NewCommand("--version") for i := 0; i < maxLoops; i++ { - if err := cmd.Run(&RunOpts{}); err != nil { + if err := cmd.Run(t.Context(), &RunOpts{}); err != nil { t.Fatal(err) } } @@ -27,9 +27,9 @@ func TestRunWithContextTimeout(t *testing.T) { maxLoops := 10 // 'git hash-object --stdin' blocks on stdin so we can have the timeout triggered. - cmd := NewCommand(context.Background(), "hash-object", "--stdin") + cmd := NewCommand("hash-object", "--stdin") for i := 0; i < maxLoops; i++ { - if err := cmd.Run(&RunOpts{Timeout: 1 * time.Millisecond}); err != nil { + if err := cmd.Run(t.Context(), &RunOpts{Timeout: 1 * time.Millisecond}); err != nil { if err != context.DeadlineExceeded { t.Fatalf("Testing %d/%d: %v", i, maxLoops, err) } diff --git a/modules/git/command_test.go b/modules/git/command_test.go index 0823afd7f7..eb112707e7 100644 --- a/modules/git/command_test.go +++ b/modules/git/command_test.go @@ -4,21 +4,20 @@ package git import ( - "context" "testing" "github.com/stretchr/testify/assert" ) func TestRunWithContextStd(t *testing.T) { - cmd := NewCommand(context.Background(), "--version") - stdout, stderr, err := cmd.RunStdString(&RunOpts{}) + cmd := NewCommand("--version") + stdout, stderr, err := cmd.RunStdString(t.Context(), &RunOpts{}) assert.NoError(t, err) assert.Empty(t, stderr) assert.Contains(t, stdout, "git version") - cmd = NewCommand(context.Background(), "--no-such-arg") - stdout, stderr, err = cmd.RunStdString(&RunOpts{}) + cmd = NewCommand("--no-such-arg") + stdout, stderr, err = cmd.RunStdString(t.Context(), &RunOpts{}) if assert.Error(t, err) { assert.Equal(t, stderr, err.Stderr()) assert.Contains(t, err.Stderr(), "unknown option:") @@ -26,17 +25,17 @@ func TestRunWithContextStd(t *testing.T) { assert.Empty(t, stdout) } - cmd = NewCommand(context.Background()) + cmd = NewCommand() cmd.AddDynamicArguments("-test") - assert.ErrorIs(t, cmd.Run(&RunOpts{}), ErrBrokenCommand) + assert.ErrorIs(t, cmd.Run(t.Context(), &RunOpts{}), ErrBrokenCommand) - cmd = NewCommand(context.Background()) + cmd = NewCommand() cmd.AddDynamicArguments("--test") - assert.ErrorIs(t, cmd.Run(&RunOpts{}), ErrBrokenCommand) + assert.ErrorIs(t, cmd.Run(t.Context(), &RunOpts{}), ErrBrokenCommand) subCmd := "version" - cmd = NewCommand(context.Background()).AddDynamicArguments(subCmd) // for test purpose only, the sub-command should never be dynamic for production - stdout, stderr, err = cmd.RunStdString(&RunOpts{}) + cmd = NewCommand().AddDynamicArguments(subCmd) // for test purpose only, the sub-command should never be dynamic for production + stdout, stderr, err = cmd.RunStdString(t.Context(), &RunOpts{}) assert.NoError(t, err) assert.Empty(t, stderr) assert.Contains(t, stdout, "git version") @@ -54,9 +53,9 @@ func TestGitArgument(t *testing.T) { } func TestCommandString(t *testing.T) { - cmd := NewCommandContextNoGlobals(context.Background(), "a", "-m msg", "it's a test", `say "hello"`) - assert.EqualValues(t, cmd.prog+` a "-m msg" "it's a test" "say \"hello\""`, cmd.LogString()) + cmd := NewCommandNoGlobals("a", "-m msg", "it's a test", `say "hello"`) + assert.Equal(t, cmd.prog+` a "-m msg" "it's a test" "say \"hello\""`, cmd.LogString()) - cmd = NewCommandContextNoGlobals(context.Background(), "url: https://a:b@c/", "/root/dir-a/dir-b") - assert.EqualValues(t, cmd.prog+` "url: https://sanitized-credential@c/" dir-a/dir-b`, cmd.LogString()) + cmd = NewCommandNoGlobals("url: https://a:b@c/", "/root/dir-a/dir-b") + assert.Equal(t, cmd.prog+` "url: https://sanitized-credential@c/" .../dir-a/dir-b`, cmd.LogString()) } diff --git a/modules/git/commit.go b/modules/git/commit.go index 0a50ba4356..ed4876e7b3 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -20,7 +20,8 @@ import ( // Commit represents a git commit. type Commit struct { - Tree + Tree // FIXME: bad design, this field can be nil if the commit is from "last commit cache" + ID ObjectID // The ID of this commit object Author *Signature Committer *Signature @@ -34,7 +35,7 @@ type Commit struct { // CommitSignature represents a git commit signature part. type CommitSignature struct { Signature string - Payload string // TODO check if can be reconstruct from the rest of commit information to not have duplicate data + Payload string } // Message returns the commit message. Same as retrieving CommitMessage directly. @@ -91,12 +92,12 @@ func AddChanges(repoPath string, all bool, files ...string) error { // AddChangesWithArgs marks local changes to be ready for commit. func AddChangesWithArgs(repoPath string, globalArgs TrustedCmdArgs, all bool, files ...string) error { - cmd := NewCommandContextNoGlobals(DefaultContext, globalArgs...).AddArguments("add") + cmd := NewCommandNoGlobals(globalArgs...).AddArguments("add") if all { cmd.AddArguments("--all") } cmd.AddDashesAndList(files...) - _, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}) + _, _, err := cmd.RunStdString(DefaultContext, &RunOpts{Dir: repoPath}) return err } @@ -118,7 +119,7 @@ func CommitChanges(repoPath string, opts CommitChangesOptions) error { // CommitChangesWithArgs commits local changes with given committer, author and message. // If author is nil, it will be the same as committer. func CommitChangesWithArgs(repoPath string, args TrustedCmdArgs, opts CommitChangesOptions) error { - cmd := NewCommandContextNoGlobals(DefaultContext, args...) + cmd := NewCommandNoGlobals(args...) if opts.Committer != nil { cmd.AddOptionValues("-c", "user.name="+opts.Committer.Name) cmd.AddOptionValues("-c", "user.email="+opts.Committer.Email) @@ -133,7 +134,7 @@ func CommitChangesWithArgs(repoPath string, args TrustedCmdArgs, opts CommitChan } cmd.AddOptionFormat("--message=%s", opts.Message) - _, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}) + _, _, err := cmd.RunStdString(DefaultContext, &RunOpts{Dir: repoPath}) // No stderr but exit status 1 means nothing to commit. if err != nil && err.Error() == "exit status 1" { return nil @@ -143,7 +144,7 @@ func CommitChangesWithArgs(repoPath string, args TrustedCmdArgs, opts CommitChan // AllCommitsCount returns count of all commits in repository func AllCommitsCount(ctx context.Context, repoPath string, hidePRRefs bool, files ...string) (int64, error) { - cmd := NewCommand(ctx, "rev-list") + cmd := NewCommand("rev-list") if hidePRRefs { cmd.AddArguments("--exclude=" + PullPrefix + "*") } @@ -152,7 +153,7 @@ func AllCommitsCount(ctx context.Context, repoPath string, hidePRRefs bool, file cmd.AddDashesAndList(files...) } - stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}) + stdout, _, err := cmd.RunStdString(ctx, &RunOpts{Dir: repoPath}) if err != nil { return 0, err } @@ -166,11 +167,13 @@ type CommitsCountOptions struct { Not string Revision []string RelPath []string + Since string + Until string } // CommitsCount returns number of total commits of until given revision. func CommitsCount(ctx context.Context, opts CommitsCountOptions) (int64, error) { - cmd := NewCommand(ctx, "rev-list", "--count") + cmd := NewCommand("rev-list", "--count") cmd.AddDynamicArguments(opts.Revision...) @@ -182,7 +185,7 @@ func CommitsCount(ctx context.Context, opts CommitsCountOptions) (int64, error) cmd.AddDashesAndList(opts.RelPath...) } - stdout, _, err := cmd.RunStdString(&RunOpts{Dir: opts.RepoPath}) + stdout, _, err := cmd.RunStdString(ctx, &RunOpts{Dir: opts.RepoPath}) if err != nil { return 0, err } @@ -199,8 +202,8 @@ func (c *Commit) CommitsCount() (int64, error) { } // CommitsByRange returns the specific page commits before current revision, every page's number default by CommitsRangeSize -func (c *Commit) CommitsByRange(page, pageSize int, not string) ([]*Commit, error) { - return c.repo.commitsByRange(c.ID, page, pageSize, not) +func (c *Commit) CommitsByRange(page, pageSize int, not, since, until string) ([]*Commit, error) { + return c.repo.commitsByRangeWithTime(c.ID, page, pageSize, not, since, until) } // CommitsBefore returns all the commits before current revision @@ -217,7 +220,7 @@ func (c *Commit) HasPreviousCommit(objectID ObjectID) (bool, error) { return false, nil } - _, _, err := NewCommand(c.repo.Ctx, "merge-base", "--is-ancestor").AddDynamicArguments(that, this).RunStdString(&RunOpts{Dir: c.repo.Path}) + _, _, err := NewCommand("merge-base", "--is-ancestor").AddDynamicArguments(that, this).RunStdString(c.repo.Ctx, &RunOpts{Dir: c.repo.Path}) if err == nil { return true, nil } @@ -275,8 +278,8 @@ func NewSearchCommitsOptions(searchString string, forAllRefs bool) SearchCommits var keywords, authors, committers []string var after, before string - fields := strings.Fields(searchString) - for _, k := range fields { + fields := strings.FieldsSeq(searchString) + for k := range fields { switch { case strings.HasPrefix(k, "author:"): authors = append(authors, strings.TrimPrefix(k, "author:")) @@ -358,12 +361,12 @@ func (c *Commit) GetFileContent(filename string, limit int) (string, error) { // GetBranchName gets the closest branch name (as returned by 'git name-rev --name-only') func (c *Commit) GetBranchName() (string, error) { - cmd := NewCommand(c.repo.Ctx, "name-rev") + cmd := NewCommand("name-rev") if DefaultFeatures().CheckVersionAtLeast("2.13.0") { cmd.AddArguments("--exclude", "refs/tags/*") } cmd.AddArguments("--name-only", "--no-undefined").AddDynamicArguments(c.ID.String()) - data, _, err := cmd.RunStdString(&RunOpts{Dir: c.repo.Path}) + data, _, err := cmd.RunStdString(c.repo.Ctx, &RunOpts{Dir: c.repo.Path}) if err != nil { // handle special case where git can not describe commit if strings.Contains(err.Error(), "cannot describe") { @@ -441,7 +444,7 @@ func GetCommitFileStatus(ctx context.Context, repoPath, commitID string) (*Commi }() stderr := new(bytes.Buffer) - err := NewCommand(ctx, "log", "--name-status", "-m", "--pretty=format:", "--first-parent", "--no-renames", "-z", "-1").AddDynamicArguments(commitID).Run(&RunOpts{ + err := NewCommand("log", "--name-status", "-m", "--pretty=format:", "--first-parent", "--no-renames", "-z", "-1").AddDynamicArguments(commitID).Run(ctx, &RunOpts{ Dir: repoPath, Stdout: w, Stderr: stderr, @@ -457,7 +460,7 @@ func GetCommitFileStatus(ctx context.Context, repoPath, commitID string) (*Commi // GetFullCommitID returns full length (40) of commit ID by given short SHA in a repository. func GetFullCommitID(ctx context.Context, repoPath, shortID string) (string, error) { - commitID, _, err := NewCommand(ctx, "rev-parse").AddDynamicArguments(shortID).RunStdString(&RunOpts{Dir: repoPath}) + commitID, _, err := NewCommand("rev-parse").AddDynamicArguments(shortID).RunStdString(ctx, &RunOpts{Dir: repoPath}) if err != nil { if strings.Contains(err.Error(), "exit status 128") { return "", ErrNotExist{shortID, ""} diff --git a/modules/git/commit_info_nogogit.go b/modules/git/commit_info_nogogit.go index ef2df0b133..1b45fc8a6c 100644 --- a/modules/git/commit_info_nogogit.go +++ b/modules/git/commit_info_nogogit.go @@ -7,8 +7,7 @@ package git import ( "context" - "fmt" - "io" + "maps" "path" "sort" @@ -40,9 +39,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath return nil, nil, err } - for pth, found := range commits { - revs[pth] = found - } + maps.Copy(revs, commits) } } else { sort.Strings(entryPaths) @@ -65,7 +62,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath log.Debug("missing commit for %s", entry.Name()) } - // If the entry if a submodule add a submodule file for this + // If the entry is a submodule add a submodule file for this if entry.IsSubModule() { subModuleURL := "" var fullPath string @@ -85,8 +82,8 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath } // Retrieve the commit for the treePath itself (see above). We basically - // get it for free during the tree traversal and it's used for listing - // pages to display information about newest commit for a given path. + // get it for free during the tree traversal, and it's used for listing + // pages to display information about the newest commit for a given path. var treeCommit *Commit var ok bool if treePath == "" { @@ -124,48 +121,25 @@ func GetLastCommitForPaths(ctx context.Context, commit *Commit, treePath string, return nil, err } - batchStdinWriter, batchReader, cancel, err := commit.repo.CatFileBatch(ctx) - if err != nil { - return nil, err - } - defer cancel() - commitsMap := map[string]*Commit{} commitsMap[commit.ID.String()] = commit commitCommits := map[string]*Commit{} for path, commitID := range revs { - c, ok := commitsMap[commitID] - if ok { - commitCommits[path] = c + if len(commitID) == 0 { continue } - if len(commitID) == 0 { + c, ok := commitsMap[commitID] + if ok { + commitCommits[path] = c continue } - _, err := batchStdinWriter.Write([]byte(commitID + "\n")) - if err != nil { - return nil, err - } - _, typ, size, err := ReadBatchLine(batchReader) + c, err := commit.repo.GetCommit(commitID) // Ensure the commit exists in the repository if err != nil { return nil, err } - if typ != "commit" { - if err := DiscardFull(batchReader, size+1); err != nil { - return nil, err - } - return nil, fmt.Errorf("unexpected type: %s for commit id: %s", typ, commitID) - } - c, err = CommitFromReader(commit.repo, MustIDFromString(commitID), io.LimitReader(batchReader, size)) - if err != nil { - return nil, err - } - if _, err := batchReader.Discard(1); err != nil { - return nil, err - } commitCommits[path] = c } diff --git a/modules/git/commit_info_test.go b/modules/git/commit_info_test.go index 1e331fac00..ba518ab245 100644 --- a/modules/git/commit_info_test.go +++ b/modules/git/commit_info_test.go @@ -4,7 +4,6 @@ package git import ( - "context" "path/filepath" "testing" "time" @@ -83,7 +82,7 @@ func testGetCommitsInfo(t *testing.T, repo1 *Repository) { } // FIXME: Context.TODO() - if graceful has started we should use its Shutdown context otherwise use install signals in TestMain. - commitsInfo, treeCommit, err := entries.GetCommitsInfo(context.TODO(), commit, testCase.Path) + commitsInfo, treeCommit, err := entries.GetCommitsInfo(t.Context(), commit, testCase.Path) assert.NoError(t, err, "Unable to get commit information for entries of subtree: %s in commit: %s from testcase due to error: %v", testCase.Path, testCase.CommitID, err) if err != nil { t.FailNow() @@ -159,8 +158,8 @@ func BenchmarkEntries_GetCommitsInfo(b *testing.B) { entries.Sort() b.ResetTimer() b.Run(benchmark.name, func(b *testing.B) { - for i := 0; i < b.N; i++ { - _, _, err := entries.GetCommitsInfo(context.Background(), commit, "") + for b.Loop() { + _, _, err := entries.GetCommitsInfo(b.Context(), commit, "") if err != nil { b.Fatal(err) } diff --git a/modules/git/commit_reader.go b/modules/git/commit_reader.go index 228bbaf314..eb8f4c6322 100644 --- a/modules/git/commit_reader.go +++ b/modules/git/commit_reader.go @@ -6,10 +6,44 @@ package git import ( "bufio" "bytes" + "fmt" "io" - "strings" ) +const ( + commitHeaderGpgsig = "gpgsig" + commitHeaderGpgsigSha256 = "gpgsig-sha256" +) + +func assignCommitFields(gitRepo *Repository, commit *Commit, headerKey string, headerValue []byte) error { + if len(headerValue) > 0 && headerValue[len(headerValue)-1] == '\n' { + headerValue = headerValue[:len(headerValue)-1] // remove trailing newline + } + switch headerKey { + case "tree": + objID, err := NewIDFromString(string(headerValue)) + if err != nil { + return fmt.Errorf("invalid tree ID %q: %w", string(headerValue), err) + } + commit.Tree = *NewTree(gitRepo, objID) + case "parent": + objID, err := NewIDFromString(string(headerValue)) + if err != nil { + return fmt.Errorf("invalid parent ID %q: %w", string(headerValue), err) + } + commit.Parents = append(commit.Parents, objID) + case "author": + commit.Author.Decode(headerValue) + case "committer": + commit.Committer.Decode(headerValue) + case commitHeaderGpgsig, commitHeaderGpgsigSha256: + // if there are duplicate "gpgsig" and "gpgsig-sha256" headers, then the signature must have already been invalid + // so we don't need to handle duplicate headers here + commit.Signature = &CommitSignature{Signature: string(headerValue)} + } + return nil +} + // CommitFromReader will generate a Commit from a provided reader // We need this to interpret commits from cat-file or cat-file --batch // @@ -21,90 +55,46 @@ func CommitFromReader(gitRepo *Repository, objectID ObjectID, reader io.Reader) Committer: &Signature{}, } - payloadSB := new(strings.Builder) - signatureSB := new(strings.Builder) - messageSB := new(strings.Builder) - message := false - pgpsig := false - - bufReader, ok := reader.(*bufio.Reader) - if !ok { - bufReader = bufio.NewReader(reader) - } - -readLoop: + bufReader := bufio.NewReader(reader) + inHeader := true + var payloadSB, messageSB bytes.Buffer + var headerKey string + var headerValue []byte for { line, err := bufReader.ReadBytes('\n') - if err != nil { - if err == io.EOF { - if message { - _, _ = messageSB.Write(line) - } - _, _ = payloadSB.Write(line) - break readLoop - } - return nil, err + if err != nil && err != io.EOF { + return nil, fmt.Errorf("unable to read commit %q: %w", objectID.String(), err) } - if pgpsig { - if len(line) > 0 && line[0] == ' ' { - _, _ = signatureSB.Write(line[1:]) - continue - } - pgpsig = false + if len(line) == 0 { + break } - if !message { - // This is probably not correct but is copied from go-gits interpretation... - trimmed := bytes.TrimSpace(line) - if len(trimmed) == 0 { - message = true - _, _ = payloadSB.Write(line) - continue - } - - split := bytes.SplitN(trimmed, []byte{' '}, 2) - var data []byte - if len(split) > 1 { - data = split[1] + if inHeader { + inHeader = !(len(line) == 1 && line[0] == '\n') // still in header if line is not just a newline + k, v, _ := bytes.Cut(line, []byte{' '}) + if len(k) != 0 || !inHeader { + if headerKey != "" { + if err = assignCommitFields(gitRepo, commit, headerKey, headerValue); err != nil { + return nil, fmt.Errorf("unable to parse commit %q: %w", objectID.String(), err) + } + } + headerKey = string(k) // it also resets the headerValue to empty string if not inHeader + headerValue = v + } else { + headerValue = append(headerValue, v...) } - - switch string(split[0]) { - case "tree": - commit.Tree = *NewTree(gitRepo, MustIDFromString(string(data))) + if headerKey != commitHeaderGpgsig && headerKey != commitHeaderGpgsigSha256 { _, _ = payloadSB.Write(line) - case "parent": - commit.Parents = append(commit.Parents, MustIDFromString(string(data))) - _, _ = payloadSB.Write(line) - case "author": - commit.Author = &Signature{} - commit.Author.Decode(data) - _, _ = payloadSB.Write(line) - case "committer": - commit.Committer = &Signature{} - commit.Committer.Decode(data) - _, _ = payloadSB.Write(line) - case "encoding": - _, _ = payloadSB.Write(line) - case "gpgsig": - fallthrough - case "gpgsig-sha256": // FIXME: no intertop, so only 1 exists at present. - _, _ = signatureSB.Write(data) - _ = signatureSB.WriteByte('\n') - pgpsig = true } } else { _, _ = messageSB.Write(line) _, _ = payloadSB.Write(line) } } + commit.CommitMessage = messageSB.String() - commit.Signature = &CommitSignature{ - Signature: signatureSB.String(), - Payload: payloadSB.String(), - } - if len(commit.Signature.Signature) == 0 { - commit.Signature = nil + if commit.Signature != nil { + commit.Signature.Payload = payloadSB.String() } - return commit, nil } diff --git a/modules/git/commit_sha256_test.go b/modules/git/commit_sha256_test.go index f6ca83c9ed..97ccecdacc 100644 --- a/modules/git/commit_sha256_test.go +++ b/modules/git/commit_sha256_test.go @@ -60,8 +60,7 @@ func TestGetFullCommitIDErrorSha256(t *testing.T) { } func TestCommitFromReaderSha256(t *testing.T) { - commitString := `9433b2a62b964c17a4485ae180f45f595d3e69d31b786087775e28c6b6399df0 commit 1114 -tree e7f9e96dd79c09b078cac8b303a7d3b9d65ff9b734e86060a4d20409fd379f9e + commitString := `tree e7f9e96dd79c09b078cac8b303a7d3b9d65ff9b734e86060a4d20409fd379f9e parent 26e9ccc29fad747e9c5d9f4c9ddeb7eff61cc45ef6a8dc258cbeb181afc055e8 author Adam Majer <amajer@suse.de> 1698676906 +0100 committer Adam Majer <amajer@suse.de> 1698676906 +0100 @@ -97,7 +96,7 @@ signed commit` assert.NoError(t, err) require.NotNil(t, commitFromReader) assert.EqualValues(t, sha, commitFromReader.ID) - assert.EqualValues(t, `-----BEGIN PGP SIGNATURE----- + assert.Equal(t, `-----BEGIN PGP SIGNATURE----- iQIrBAABCgAtFiEES+fB08xlgTrzSdQvhkUIsBsmec8FAmU/wKoPHGFtYWplckBz dXNlLmRlAAoJEIZFCLAbJnnP4s4PQIJATa++WPzR6/H4etT7bsOGoMyguEJYyWOd @@ -112,21 +111,20 @@ VAEUo6ecdDxSpyt2naeg9pKus/BRi7P6g4B1hkk/zZstUX/QP4IQuAJbXjkvsC+X HKRr3NlRM/DygzTyj0gN74uoa0goCIbyAQhiT42nm0cuhM7uN/W0ayrlZjGF1cbR 8NCJUL2Nwj0ywKIavC99Ipkb8AsFwpVT6U6effs6 =xybZ ------END PGP SIGNATURE----- -`, commitFromReader.Signature.Signature) - assert.EqualValues(t, `tree e7f9e96dd79c09b078cac8b303a7d3b9d65ff9b734e86060a4d20409fd379f9e +-----END PGP SIGNATURE-----`, commitFromReader.Signature.Signature) + assert.Equal(t, `tree e7f9e96dd79c09b078cac8b303a7d3b9d65ff9b734e86060a4d20409fd379f9e parent 26e9ccc29fad747e9c5d9f4c9ddeb7eff61cc45ef6a8dc258cbeb181afc055e8 author Adam Majer <amajer@suse.de> 1698676906 +0100 committer Adam Majer <amajer@suse.de> 1698676906 +0100 signed commit`, commitFromReader.Signature.Payload) - assert.EqualValues(t, "Adam Majer <amajer@suse.de>", commitFromReader.Author.String()) + assert.Equal(t, "Adam Majer <amajer@suse.de>", commitFromReader.Author.String()) commitFromReader2, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString+"\n\n")) assert.NoError(t, err) commitFromReader.CommitMessage += "\n\n" commitFromReader.Signature.Payload += "\n\n" - assert.EqualValues(t, commitFromReader, commitFromReader2) + assert.Equal(t, commitFromReader, commitFromReader2) } func TestHasPreviousCommitSha256(t *testing.T) { diff --git a/modules/git/commit_submodule_file.go b/modules/git/commit_submodule_file.go index 2ac744fbf6..729401f752 100644 --- a/modules/git/commit_submodule_file.go +++ b/modules/git/commit_submodule_file.go @@ -46,9 +46,9 @@ func (sf *CommitSubmoduleFile) SubmoduleWebLink(ctx context.Context, optCommitID if len(optCommitID) == 2 { commitLink = sf.repoLink + "/compare/" + optCommitID[0] + "..." + optCommitID[1] } else if len(optCommitID) == 1 { - commitLink = sf.repoLink + "/commit/" + optCommitID[0] + commitLink = sf.repoLink + "/tree/" + optCommitID[0] } else { - commitLink = sf.repoLink + "/commit/" + sf.refID + commitLink = sf.repoLink + "/tree/" + sf.refID } return &SubmoduleWebLink{RepoWebLink: sf.repoLink, CommitWebLink: commitLink} } diff --git a/modules/git/commit_submodule_file_test.go b/modules/git/commit_submodule_file_test.go index 4b5b767612..6581fa8712 100644 --- a/modules/git/commit_submodule_file_test.go +++ b/modules/git/commit_submodule_file_test.go @@ -4,7 +4,6 @@ package git import ( - "context" "testing" "github.com/stretchr/testify/assert" @@ -13,18 +12,18 @@ import ( func TestCommitSubmoduleLink(t *testing.T) { sf := NewCommitSubmoduleFile("git@github.com:user/repo.git", "aaaa") - wl := sf.SubmoduleWebLink(context.Background()) + wl := sf.SubmoduleWebLink(t.Context()) assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink) - assert.Equal(t, "https://github.com/user/repo/commit/aaaa", wl.CommitWebLink) + assert.Equal(t, "https://github.com/user/repo/tree/aaaa", wl.CommitWebLink) - wl = sf.SubmoduleWebLink(context.Background(), "1111") + wl = sf.SubmoduleWebLink(t.Context(), "1111") assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink) - assert.Equal(t, "https://github.com/user/repo/commit/1111", wl.CommitWebLink) + assert.Equal(t, "https://github.com/user/repo/tree/1111", wl.CommitWebLink) - wl = sf.SubmoduleWebLink(context.Background(), "1111", "2222") + wl = sf.SubmoduleWebLink(t.Context(), "1111", "2222") assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink) assert.Equal(t, "https://github.com/user/repo/compare/1111...2222", wl.CommitWebLink) - wl = (*CommitSubmoduleFile)(nil).SubmoduleWebLink(context.Background()) + wl = (*CommitSubmoduleFile)(nil).SubmoduleWebLink(t.Context()) assert.Nil(t, wl) } diff --git a/modules/git/commit_test.go b/modules/git/commit_test.go index 9560c2cd94..81fb91dfc6 100644 --- a/modules/git/commit_test.go +++ b/modules/git/commit_test.go @@ -4,7 +4,6 @@ package git import ( - "context" "os" "path/filepath" "strings" @@ -60,8 +59,7 @@ func TestGetFullCommitIDError(t *testing.T) { } func TestCommitFromReader(t *testing.T) { - commitString := `feaf4ba6bc635fec442f46ddd4512416ec43c2c2 commit 1074 -tree f1a6cb52b2d16773290cefe49ad0684b50a4f930 + commitString := `tree f1a6cb52b2d16773290cefe49ad0684b50a4f930 parent 37991dec2c8e592043f47155ce4808d4580f9123 author silverwind <me@silverwind.io> 1563741793 +0200 committer silverwind <me@silverwind.io> 1563741793 +0200 @@ -94,7 +92,7 @@ empty commit` assert.NoError(t, err) require.NotNil(t, commitFromReader) assert.EqualValues(t, sha, commitFromReader.ID) - assert.EqualValues(t, `-----BEGIN PGP SIGNATURE----- + assert.Equal(t, `-----BEGIN PGP SIGNATURE----- iQIzBAABCAAdFiEEWPb2jX6FS2mqyJRQLmK0HJOGlEMFAl00zmEACgkQLmK0HJOG lEMDFBAAhQKKqLD1VICygJMEB8t1gBmNLgvziOLfpX4KPWdPtBk3v/QJ7OrfMrVK @@ -109,26 +107,24 @@ sD53z/f0J+We4VZjY+pidvA9BGZPFVdR3wd3xGs8/oH6UWaLJAMGkLG6dDb3qDLm mfeFhT57UbE4qukTDIQ0Y0WM40UYRTakRaDY7ubhXgLgx09Cnp9XTVMsHgT6j9/i 1pxsB104XLWjQHTjr1JtiaBQEwFh9r2OKTcpvaLcbNtYpo7CzOs= =FRsO ------END PGP SIGNATURE----- -`, commitFromReader.Signature.Signature) - assert.EqualValues(t, `tree f1a6cb52b2d16773290cefe49ad0684b50a4f930 +-----END PGP SIGNATURE-----`, commitFromReader.Signature.Signature) + assert.Equal(t, `tree f1a6cb52b2d16773290cefe49ad0684b50a4f930 parent 37991dec2c8e592043f47155ce4808d4580f9123 author silverwind <me@silverwind.io> 1563741793 +0200 committer silverwind <me@silverwind.io> 1563741793 +0200 empty commit`, commitFromReader.Signature.Payload) - assert.EqualValues(t, "silverwind <me@silverwind.io>", commitFromReader.Author.String()) + assert.Equal(t, "silverwind <me@silverwind.io>", commitFromReader.Author.String()) commitFromReader2, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString+"\n\n")) assert.NoError(t, err) commitFromReader.CommitMessage += "\n\n" commitFromReader.Signature.Payload += "\n\n" - assert.EqualValues(t, commitFromReader, commitFromReader2) + assert.Equal(t, commitFromReader, commitFromReader2) } func TestCommitWithEncodingFromReader(t *testing.T) { - commitString := `feaf4ba6bc635fec442f46ddd4512416ec43c2c2 commit 1074 -tree ca3fad42080dd1a6d291b75acdfc46e5b9b307e5 + commitString := `tree ca3fad42080dd1a6d291b75acdfc46e5b9b307e5 parent 47b24e7ab977ed31c5a39989d570847d6d0052af author KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100 committer KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100 @@ -160,7 +156,7 @@ ISO-8859-1` assert.NoError(t, err) require.NotNil(t, commitFromReader) assert.EqualValues(t, sha, commitFromReader.ID) - assert.EqualValues(t, `-----BEGIN PGP SIGNATURE----- + assert.Equal(t, `-----BEGIN PGP SIGNATURE----- iQGzBAABCgAdFiEE9HRrbqvYxPT8PXbefPSEkrowAa8FAmYGg7IACgkQfPSEkrow Aa9olwv+P0HhtCM6CRvlUmPaqswRsDPNR4i66xyXGiSxdI9V5oJL7HLiQIM7KrFR @@ -173,22 +169,21 @@ SONRzusmu5n3DgV956REL7x62h7JuqmBz/12HZkr0z0zgXkcZ04q08pSJATX5N1F yN+tWxTsWg+zhDk96d5Esdo9JMjcFvPv0eioo30GAERaz1hoD7zCMT4jgUFTQwgz jw4YcO5u =r3UU ------END PGP SIGNATURE----- -`, commitFromReader.Signature.Signature) - assert.EqualValues(t, `tree ca3fad42080dd1a6d291b75acdfc46e5b9b307e5 +-----END PGP SIGNATURE-----`, commitFromReader.Signature.Signature) + assert.Equal(t, `tree ca3fad42080dd1a6d291b75acdfc46e5b9b307e5 parent 47b24e7ab977ed31c5a39989d570847d6d0052af author KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100 committer KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100 encoding ISO-8859-1 ISO-8859-1`, commitFromReader.Signature.Payload) - assert.EqualValues(t, "KN4CK3R <admin@oldschoolhack.me>", commitFromReader.Author.String()) + assert.Equal(t, "KN4CK3R <admin@oldschoolhack.me>", commitFromReader.Author.String()) commitFromReader2, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString+"\n\n")) assert.NoError(t, err) commitFromReader.CommitMessage += "\n\n" commitFromReader.Signature.Payload += "\n\n" - assert.EqualValues(t, commitFromReader, commitFromReader2) + assert.Equal(t, commitFromReader, commitFromReader2) } func TestHasPreviousCommit(t *testing.T) { @@ -347,15 +342,15 @@ func TestGetCommitFileStatusMerges(t *testing.T) { func Test_GetCommitBranchStart(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - repo, err := OpenRepository(context.Background(), bareRepo1Path) + repo, err := OpenRepository(t.Context(), bareRepo1Path) assert.NoError(t, err) defer repo.Close() commit, err := repo.GetBranchCommit("branch1") assert.NoError(t, err) - assert.EqualValues(t, "2839944139e0de9737a044f78b0e4b40d989a9e3", commit.ID.String()) + assert.Equal(t, "2839944139e0de9737a044f78b0e4b40d989a9e3", commit.ID.String()) startCommitID, err := repo.GetCommitBranchStart(os.Environ(), "branch1", commit.ID.String()) assert.NoError(t, err) assert.NotEmpty(t, startCommitID) - assert.EqualValues(t, "9c9aef8dd84e02bc7ec12641deb4c930a7c30185", startCommitID) + assert.Equal(t, "95bb4d39648ee7e325106df01a621c530863a653", startCommitID) } diff --git a/modules/git/config.go b/modules/git/config.go index 9c36cf1654..234be7b955 100644 --- a/modules/git/config.go +++ b/modules/git/config.go @@ -116,7 +116,7 @@ func syncGitConfig() (err error) { } func configSet(key, value string) error { - stdout, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil) + stdout, _, err := NewCommand("config", "--global", "--get").AddDynamicArguments(key).RunStdString(DefaultContext, nil) if err != nil && !IsErrorExitCode(err, 1) { return fmt.Errorf("failed to get git config %s, err: %w", key, err) } @@ -126,7 +126,7 @@ func configSet(key, value string) error { return nil } - _, _, err = NewCommand(DefaultContext, "config", "--global").AddDynamicArguments(key, value).RunStdString(nil) + _, _, err = NewCommand("config", "--global").AddDynamicArguments(key, value).RunStdString(DefaultContext, nil) if err != nil { return fmt.Errorf("failed to set git global config %s, err: %w", key, err) } @@ -135,14 +135,14 @@ func configSet(key, value string) error { } func configSetNonExist(key, value string) error { - _, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil) + _, _, err := NewCommand("config", "--global", "--get").AddDynamicArguments(key).RunStdString(DefaultContext, nil) if err == nil { // already exist return nil } if IsErrorExitCode(err, 1) { // not exist, set new config - _, _, err = NewCommand(DefaultContext, "config", "--global").AddDynamicArguments(key, value).RunStdString(nil) + _, _, err = NewCommand("config", "--global").AddDynamicArguments(key, value).RunStdString(DefaultContext, nil) if err != nil { return fmt.Errorf("failed to set git global config %s, err: %w", key, err) } @@ -153,14 +153,14 @@ func configSetNonExist(key, value string) error { } func configAddNonExist(key, value string) error { - _, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(nil) + _, _, err := NewCommand("config", "--global", "--get").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(DefaultContext, nil) if err == nil { // already exist return nil } if IsErrorExitCode(err, 1) { // not exist, add new config - _, _, err = NewCommand(DefaultContext, "config", "--global", "--add").AddDynamicArguments(key, value).RunStdString(nil) + _, _, err = NewCommand("config", "--global", "--add").AddDynamicArguments(key, value).RunStdString(DefaultContext, nil) if err != nil { return fmt.Errorf("failed to add git global config %s, err: %w", key, err) } @@ -170,10 +170,10 @@ func configAddNonExist(key, value string) error { } func configUnsetAll(key, value string) error { - _, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil) + _, _, err := NewCommand("config", "--global", "--get").AddDynamicArguments(key).RunStdString(DefaultContext, nil) if err == nil { // exist, need to remove - _, _, err = NewCommand(DefaultContext, "config", "--global", "--unset-all").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(nil) + _, _, err = NewCommand("config", "--global", "--unset-all").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(DefaultContext, nil) if err != nil { return fmt.Errorf("failed to unset git global config %s, err: %w", key, err) } diff --git a/modules/git/diff.go b/modules/git/diff.go index da0a2f26ba..c4df6b8063 100644 --- a/modules/git/diff.go +++ b/modules/git/diff.go @@ -34,8 +34,8 @@ func GetRawDiff(repo *Repository, commitID string, diffType RawDiffType, writer // GetReverseRawDiff dumps the reverse diff results of repository in given commit ID to io.Writer. func GetReverseRawDiff(ctx context.Context, repoPath, commitID string, writer io.Writer) error { stderr := new(bytes.Buffer) - cmd := NewCommand(ctx, "show", "--pretty=format:revert %H%n", "-R").AddDynamicArguments(commitID) - if err := cmd.Run(&RunOpts{ + cmd := NewCommand("show", "--pretty=format:revert %H%n", "-R").AddDynamicArguments(commitID) + if err := cmd.Run(ctx, &RunOpts{ Dir: repoPath, Stdout: writer, Stderr: stderr, @@ -56,7 +56,7 @@ func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diff files = append(files, file) } - cmd := NewCommand(repo.Ctx) + cmd := NewCommand() switch diffType { case RawDiffNormal: if len(startCommit) != 0 { @@ -89,7 +89,7 @@ func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diff } stderr := new(bytes.Buffer) - if err = cmd.Run(&RunOpts{ + if err = cmd.Run(repo.Ctx, &RunOpts{ Dir: repo.Path, Stdout: writer, Stderr: stderr, @@ -301,8 +301,8 @@ func GetAffectedFiles(repo *Repository, branchName, oldCommitID, newCommitID str affectedFiles := make([]string, 0, 32) // Run `git diff --name-only` to get the names of the changed files - err = NewCommand(repo.Ctx, "diff", "--name-only").AddDynamicArguments(oldCommitID, newCommitID). - Run(&RunOpts{ + err = NewCommand("diff", "--name-only").AddDynamicArguments(oldCommitID, newCommitID). + Run(repo.Ctx, &RunOpts{ Env: env, Dir: repo.Path, Stdout: stdoutWriter, diff --git a/modules/git/diff_test.go b/modules/git/diff_test.go index 0f865c52a8..7671fffcc1 100644 --- a/modules/git/diff_test.go +++ b/modules/git/diff_test.go @@ -154,7 +154,7 @@ func TestCutDiffAroundLine(t *testing.T) { } func BenchmarkCutDiffAroundLine(b *testing.B) { - for n := 0; n < b.N; n++ { + for b.Loop() { CutDiffAroundLine(strings.NewReader(exampleDiff), 3, true, 3) } } @@ -177,8 +177,8 @@ func ExampleCutDiffAroundLine() { func TestParseDiffHunkString(t *testing.T) { leftLine, leftHunk, rightLine, rightHunk := ParseDiffHunkString("@@ -19,3 +19,5 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER") - assert.EqualValues(t, 19, leftLine) - assert.EqualValues(t, 3, leftHunk) - assert.EqualValues(t, 19, rightLine) - assert.EqualValues(t, 5, rightHunk) + assert.Equal(t, 19, leftLine) + assert.Equal(t, 3, leftHunk) + assert.Equal(t, 19, rightLine) + assert.Equal(t, 5, rightHunk) } diff --git a/modules/git/error.go b/modules/git/error.go index 10fb37be07..7d131345d0 100644 --- a/modules/git/error.go +++ b/modules/git/error.go @@ -32,22 +32,6 @@ func (err ErrNotExist) Unwrap() error { return util.ErrNotExist } -// ErrBadLink entry.FollowLink error -type ErrBadLink struct { - Name string - Message string -} - -func (err ErrBadLink) Error() string { - return fmt.Sprintf("%s: %s", err.Name, err.Message) -} - -// IsErrBadLink if some error is ErrBadLink -func IsErrBadLink(err error) bool { - _, ok := err.(ErrBadLink) - return ok -} - // ErrBranchNotExist represents a "BranchNotExist" kind of error. type ErrBranchNotExist struct { Name string diff --git a/modules/git/foreachref/format.go b/modules/git/foreachref/format.go index 97e8ee4724..d9573a55d6 100644 --- a/modules/git/foreachref/format.go +++ b/modules/git/foreachref/format.go @@ -76,7 +76,7 @@ func (f Format) Parser(r io.Reader) *Parser { // would turn into "%0a%00". func (f Format) hexEscaped(delim []byte) string { escaped := "" - for i := 0; i < len(delim); i++ { + for i := range delim { escaped += "%" + hex.EncodeToString([]byte{delim[i]}) } return escaped diff --git a/modules/git/fsck.go b/modules/git/fsck.go index cec27f165b..a52684c84f 100644 --- a/modules/git/fsck.go +++ b/modules/git/fsck.go @@ -10,5 +10,5 @@ import ( // Fsck verifies the connectivity and validity of the objects in the database func Fsck(ctx context.Context, repoPath string, timeout time.Duration, args TrustedCmdArgs) error { - return NewCommand(ctx, "fsck").AddArguments(args...).Run(&RunOpts{Timeout: timeout, Dir: repoPath}) + return NewCommand("fsck").AddArguments(args...).Run(ctx, &RunOpts{Timeout: timeout, Dir: repoPath}) } diff --git a/modules/git/git.go b/modules/git/git.go index e3e5b83274..a2ffd6d289 100644 --- a/modules/git/git.go +++ b/modules/git/git.go @@ -30,6 +30,7 @@ type Features struct { SupportProcReceive bool // >= 2.29 SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’ SupportedObjectFormats []ObjectFormat // sha1, sha256 + SupportCheckAttrOnBare bool // >= 2.40 } var ( @@ -60,7 +61,7 @@ func DefaultFeatures() *Features { } func loadGitVersionFeatures() (*Features, error) { - stdout, _, runErr := NewCommand(DefaultContext, "version").RunStdString(nil) + stdout, _, runErr := NewCommand("version").RunStdString(DefaultContext, nil) if runErr != nil { return nil, runErr } @@ -77,6 +78,7 @@ func loadGitVersionFeatures() (*Features, error) { if features.SupportHashSha256 { features.SupportedObjectFormats = append(features.SupportedObjectFormats, Sha256ObjectFormat) } + features.SupportCheckAttrOnBare = features.CheckVersionAtLeast("2.40") return features, nil } diff --git a/modules/git/git_test.go b/modules/git/git_test.go index 5472842b76..58ba01cabc 100644 --- a/modules/git/git_test.go +++ b/modules/git/git_test.go @@ -10,18 +10,19 @@ import ( "testing" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/tempdir" "github.com/hashicorp/go-version" "github.com/stretchr/testify/assert" ) func testRun(m *testing.M) error { - gitHomePath, err := os.MkdirTemp(os.TempDir(), "git-home") + gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home") if err != nil { return fmt.Errorf("unable to create temp dir: %w", err) } - defer util.RemoveAll(gitHomePath) + defer cleanup() + setting.Git.HomePath = gitHomePath if err = InitFull(context.Background()); err != nil { diff --git a/modules/git/grep.go b/modules/git/grep.go index bf6b41a886..66711650c9 100644 --- a/modules/git/grep.go +++ b/modules/git/grep.go @@ -23,11 +23,19 @@ type GrepResult struct { LineCodes []string } +type GrepModeType string + +const ( + GrepModeExact GrepModeType = "exact" + GrepModeWords GrepModeType = "words" + GrepModeRegexp GrepModeType = "regexp" +) + type GrepOptions struct { RefName string MaxResultLimit int ContextLineNumber int - IsFuzzy bool + GrepMode GrepModeType MaxLineLength int // the maximum length of a line to parse, exceeding chars will be truncated PathspecList []string } @@ -52,21 +60,30 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO 2^@repo: go-gitea/gitea */ var results []*GrepResult - cmd := NewCommand(ctx, "grep", "--null", "--break", "--heading", "--fixed-strings", "--line-number", "--ignore-case", "--full-name") - cmd.AddOptionValues("--context", fmt.Sprint(opts.ContextLineNumber)) - if opts.IsFuzzy { + cmd := NewCommand("grep", "--null", "--break", "--heading", "--line-number", "--full-name") + cmd.AddOptionValues("--context", strconv.Itoa(opts.ContextLineNumber)) + switch opts.GrepMode { + case GrepModeExact: + cmd.AddArguments("--fixed-strings") + cmd.AddOptionValues("-e", strings.TrimLeft(search, "-")) + case GrepModeRegexp: + cmd.AddArguments("--perl-regexp") + cmd.AddOptionValues("-e", strings.TrimLeft(search, "-")) + default: /* words */ words := strings.Fields(search) - for _, word := range words { + cmd.AddArguments("--fixed-strings", "--ignore-case") + for i, word := range words { cmd.AddOptionValues("-e", strings.TrimLeft(word, "-")) + if i < len(words)-1 { + cmd.AddOptionValues("--and") + } } - } else { - cmd.AddOptionValues("-e", strings.TrimLeft(search, "-")) } cmd.AddDynamicArguments(util.IfZero(opts.RefName, "HEAD")) cmd.AddDashesAndList(opts.PathspecList...) opts.MaxResultLimit = util.IfZero(opts.MaxResultLimit, 50) stderr := bytes.Buffer{} - err = cmd.Run(&RunOpts{ + err = cmd.Run(ctx, &RunOpts{ Dir: repo.Path, Stdout: stdoutWriter, Stderr: &stderr, diff --git a/modules/git/grep_test.go b/modules/git/grep_test.go index 005d539726..0dce464b7c 100644 --- a/modules/git/grep_test.go +++ b/modules/git/grep_test.go @@ -4,7 +4,6 @@ package git import ( - "context" "path/filepath" "testing" @@ -16,7 +15,7 @@ func TestGrepSearch(t *testing.T) { assert.NoError(t, err) defer repo.Close() - res, err := GrepSearch(context.Background(), repo, "void", GrepOptions{}) + res, err := GrepSearch(t.Context(), repo, "void", GrepOptions{}) assert.NoError(t, err) assert.Equal(t, []*GrepResult{ { @@ -31,7 +30,7 @@ func TestGrepSearch(t *testing.T) { }, }, res) - res, err = GrepSearch(context.Background(), repo, "void", GrepOptions{PathspecList: []string{":(glob)java-hello/*"}}) + res, err = GrepSearch(t.Context(), repo, "void", GrepOptions{PathspecList: []string{":(glob)java-hello/*"}}) assert.NoError(t, err) assert.Equal(t, []*GrepResult{ { @@ -41,7 +40,7 @@ func TestGrepSearch(t *testing.T) { }, }, res) - res, err = GrepSearch(context.Background(), repo, "void", GrepOptions{PathspecList: []string{":(glob,exclude)java-hello/*"}}) + res, err = GrepSearch(t.Context(), repo, "void", GrepOptions{PathspecList: []string{":(glob,exclude)java-hello/*"}}) assert.NoError(t, err) assert.Equal(t, []*GrepResult{ { @@ -51,7 +50,7 @@ func TestGrepSearch(t *testing.T) { }, }, res) - res, err = GrepSearch(context.Background(), repo, "void", GrepOptions{MaxResultLimit: 1}) + res, err = GrepSearch(t.Context(), repo, "void", GrepOptions{MaxResultLimit: 1}) assert.NoError(t, err) assert.Equal(t, []*GrepResult{ { @@ -61,7 +60,7 @@ func TestGrepSearch(t *testing.T) { }, }, res) - res, err = GrepSearch(context.Background(), repo, "void", GrepOptions{MaxResultLimit: 1, MaxLineLength: 39}) + res, err = GrepSearch(t.Context(), repo, "void", GrepOptions{MaxResultLimit: 1, MaxLineLength: 39}) assert.NoError(t, err) assert.Equal(t, []*GrepResult{ { @@ -71,11 +70,11 @@ func TestGrepSearch(t *testing.T) { }, }, res) - res, err = GrepSearch(context.Background(), repo, "no-such-content", GrepOptions{}) + res, err = GrepSearch(t.Context(), repo, "no-such-content", GrepOptions{}) assert.NoError(t, err) assert.Empty(t, res) - res, err = GrepSearch(context.Background(), &Repository{Path: "no-such-git-repo"}, "no-such-content", GrepOptions{}) + res, err = GrepSearch(t.Context(), &Repository{Path: "no-such-git-repo"}, "no-such-content", GrepOptions{}) assert.Error(t, err) assert.Empty(t, res) } diff --git a/modules/git/hook.go b/modules/git/hook.go index 46f93ce13e..548a59971d 100644 --- a/modules/git/hook.go +++ b/modules/git/hook.go @@ -7,11 +7,10 @@ package git import ( "errors" "os" - "path" "path/filepath" + "slices" "strings" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" ) @@ -27,12 +26,7 @@ var ErrNotValidHook = errors.New("not a valid Git hook") // IsValidHookName returns true if given name is a valid Git hook. func IsValidHookName(name string) bool { - for _, hn := range hookNames { - if hn == name { - return true - } - } - return false + return slices.Contains(hookNames, name) } // Hook represents a Git hook. @@ -51,17 +45,28 @@ func GetHook(repoPath, name string) (*Hook, error) { } h := &Hook{ name: name, - path: path.Join(repoPath, "hooks", name+".d", name), + path: filepath.Join(repoPath, "hooks", name+".d", name), } - samplePath := filepath.Join(repoPath, "hooks", name+".sample") - if isFile(h.path) { + isFile, err := util.IsFile(h.path) + if err != nil { + return nil, err + } + if isFile { data, err := os.ReadFile(h.path) if err != nil { return nil, err } h.IsActive = true h.Content = string(data) - } else if isFile(samplePath) { + return h, nil + } + + samplePath := filepath.Join(repoPath, "hooks", name+".sample") + isFile, err = util.IsFile(samplePath) + if err != nil { + return nil, err + } + if isFile { data, err := os.ReadFile(samplePath) if err != nil { return nil, err @@ -79,7 +84,11 @@ func (h *Hook) Name() string { // Update updates hook settings. func (h *Hook) Update() error { if len(strings.TrimSpace(h.Content)) == 0 { - if isExist(h.path) { + exist, err := util.IsExist(h.path) + if err != nil { + return err + } + if exist { err := util.Remove(h.path) if err != nil { return err @@ -103,7 +112,10 @@ func (h *Hook) Update() error { // ListHooks returns a list of Git hooks of given repository. func ListHooks(repoPath string) (_ []*Hook, err error) { - if !isDir(path.Join(repoPath, "hooks")) { + exist, err := util.IsDir(filepath.Join(repoPath, "hooks")) + if err != nil { + return nil, err + } else if !exist { return nil, errors.New("hooks path does not exist") } @@ -116,28 +128,3 @@ func ListHooks(repoPath string) (_ []*Hook, err error) { } return hooks, nil } - -const ( - // HookPathUpdate hook update path - HookPathUpdate = "hooks/update" -) - -// SetUpdateHook writes given content to update hook of the repository. -func SetUpdateHook(repoPath, content string) (err error) { - log.Debug("Setting update hook: %s", repoPath) - hookPath := path.Join(repoPath, HookPathUpdate) - isExist, err := util.IsExist(hookPath) - if err != nil { - log.Debug("Unable to check if %s exists. Error: %v", hookPath, err) - return err - } - if isExist { - err = util.Remove(hookPath) - } else { - err = os.MkdirAll(path.Dir(hookPath), os.ModePerm) - } - if err != nil { - return err - } - return os.WriteFile(hookPath, []byte(content), 0o777) -} diff --git a/modules/git/key.go b/modules/git/key.go new file mode 100644 index 0000000000..2513c048b7 --- /dev/null +++ b/modules/git/key.go @@ -0,0 +1,15 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +// Based on https://git-scm.com/docs/git-config#Documentation/git-config.txt-gpgformat +const ( + SigningKeyFormatOpenPGP = "openpgp" // for GPG keys, the expected default of git cli + SigningKeyFormatSSH = "ssh" +) + +type SigningKey struct { + KeyID string + Format string +} diff --git a/modules/git/repo_language_stats.go b/modules/git/languagestats/language_stats.go index 8551ea9d24..a71284c3e4 100644 --- a/modules/git/repo_language_stats.go +++ b/modules/git/languagestats/language_stats.go @@ -1,13 +1,15 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package git +package languagestats import ( + "context" "strings" "unicode" - "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/git/attribute" ) const ( @@ -49,19 +51,15 @@ func mergeLanguageStats(stats map[string]int64) map[string]int64 { return res } -func TryReadLanguageAttribute(attrs map[string]string) optional.Option[string] { - language := AttributeToString(attrs, AttributeLinguistLanguage) - if language.Value() == "" { - language = AttributeToString(attrs, AttributeGitlabLanguage) - if language.Has() { - raw := language.Value() - // gitlab-language may have additional parameters after the language - // ignore them and just use the main language - // https://docs.gitlab.com/ee/user/project/highlighting.html#override-syntax-highlighting-for-a-file-type - if idx := strings.IndexByte(raw, '?'); idx >= 0 { - language = optional.Some(raw[:idx]) - } - } +// GetFileLanguage tries to get the (linguist) language of the file content +func GetFileLanguage(ctx context.Context, gitRepo *git.Repository, treeish, treePath string) (string, error) { + attributesMap, err := attribute.CheckAttributes(ctx, gitRepo, treeish, attribute.CheckAttributeOpts{ + Attributes: []string{attribute.LinguistLanguage, attribute.GitlabLanguage}, + Filenames: []string{treePath}, + }) + if err != nil { + return "", err } - return language + + return attributesMap[treePath].GetLanguage().Value(), nil } diff --git a/modules/git/repo_language_stats_gogit.go b/modules/git/languagestats/language_stats_gogit.go index a34c03c781..418c05b157 100644 --- a/modules/git/repo_language_stats_gogit.go +++ b/modules/git/languagestats/language_stats_gogit.go @@ -3,13 +3,15 @@ //go:build gogit -package git +package languagestats import ( "bytes" "io" "code.gitea.io/gitea/modules/analyze" + git_module "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/git/attribute" "code.gitea.io/gitea/modules/optional" "github.com/go-enry/go-enry/v2" @@ -19,7 +21,7 @@ import ( ) // GetLanguageStats calculates language stats for git repository at specified commit -func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, error) { +func GetLanguageStats(repo *git_module.Repository, commitID string) (map[string]int64, error) { r, err := git.PlainOpen(repo.Path) if err != nil { return nil, err @@ -40,8 +42,11 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err return nil, err } - checker, deferable := repo.CheckAttributeReader(commitID) - defer deferable() + checker, err := attribute.NewBatchChecker(repo, commitID, attribute.LinguistAttributes) + if err != nil { + return nil, err + } + defer checker.Close() // sizes contains the current calculated size of all files by language sizes := make(map[string]int64) @@ -62,43 +67,41 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err isDocumentation := optional.None[bool]() isDetectable := optional.None[bool]() - if checker != nil { - attrs, err := checker.CheckPath(f.Name) - if err == nil { - isVendored = AttributeToBool(attrs, AttributeLinguistVendored) - if isVendored.ValueOrDefault(false) { - return nil - } - - isGenerated = AttributeToBool(attrs, AttributeLinguistGenerated) - if isGenerated.ValueOrDefault(false) { - return nil - } + attrs, err := checker.CheckPath(f.Name) + if err == nil { + isVendored = attrs.GetVendored() + if isVendored.ValueOrDefault(false) { + return nil + } - isDocumentation = AttributeToBool(attrs, AttributeLinguistDocumentation) - if isDocumentation.ValueOrDefault(false) { - return nil - } + isGenerated = attrs.GetGenerated() + if isGenerated.ValueOrDefault(false) { + return nil + } - isDetectable = AttributeToBool(attrs, AttributeLinguistDetectable) - if !isDetectable.ValueOrDefault(true) { - return nil - } + isDocumentation = attrs.GetDocumentation() + if isDocumentation.ValueOrDefault(false) { + return nil + } - hasLanguage := TryReadLanguageAttribute(attrs) - if hasLanguage.Value() != "" { - language := hasLanguage.Value() + isDetectable = attrs.GetDetectable() + if !isDetectable.ValueOrDefault(true) { + return nil + } - // group languages, such as Pug -> HTML; SCSS -> CSS - group := enry.GetLanguageGroup(language) - if len(group) != 0 { - language = group - } + hasLanguage := attrs.GetLanguage() + if hasLanguage.Value() != "" { + language := hasLanguage.Value() - // this language will always be added to the size - sizes[language] += f.Size - return nil + // group languages, such as Pug -> HTML; SCSS -> CSS + group := enry.GetLanguageGroup(language) + if len(group) != 0 { + language = group } + + // this language will always be added to the size + sizes[language] += f.Size + return nil } } diff --git a/modules/git/repo_language_stats_nogogit.go b/modules/git/languagestats/language_stats_nogogit.go index de7707bd6c..94cf9fff8c 100644 --- a/modules/git/repo_language_stats_nogogit.go +++ b/modules/git/languagestats/language_stats_nogogit.go @@ -3,13 +3,15 @@ //go:build !gogit -package git +package languagestats import ( "bytes" "io" "code.gitea.io/gitea/modules/analyze" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/git/attribute" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" @@ -17,7 +19,7 @@ import ( ) // GetLanguageStats calculates language stats for git repository at specified commit -func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, error) { +func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64, error) { // We will feed the commit IDs in order into cat-file --batch, followed by blobs as necessary. // so let's create a batch stdin and stdout batchStdinWriter, batchReader, cancel, err := repo.CatFileBatch(repo.Ctx) @@ -34,19 +36,19 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err if err := writeID(commitID); err != nil { return nil, err } - shaBytes, typ, size, err := ReadBatchLine(batchReader) + shaBytes, typ, size, err := git.ReadBatchLine(batchReader) if typ != "commit" { log.Debug("Unable to get commit for: %s. Err: %v", commitID, err) - return nil, ErrNotExist{commitID, ""} + return nil, git.ErrNotExist{ID: commitID} } - sha, err := NewIDFromString(string(shaBytes)) + sha, err := git.NewIDFromString(string(shaBytes)) if err != nil { log.Debug("Unable to get commit for: %s. Err: %v", commitID, err) - return nil, ErrNotExist{commitID, ""} + return nil, git.ErrNotExist{ID: commitID} } - commit, err := CommitFromReader(repo, sha, io.LimitReader(batchReader, size)) + commit, err := git.CommitFromReader(repo, sha, io.LimitReader(batchReader, size)) if err != nil { log.Debug("Unable to get commit for: %s. Err: %v", commitID, err) return nil, err @@ -62,8 +64,11 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err return nil, err } - checker, deferable := repo.CheckAttributeReader(commitID) - defer deferable() + checker, err := attribute.NewBatchChecker(repo, commitID, attribute.LinguistAttributes) + if err != nil { + return nil, err + } + defer checker.Close() contentBuf := bytes.Buffer{} var content []byte @@ -92,47 +97,40 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err } isVendored := optional.None[bool]() - isGenerated := optional.None[bool]() isDocumentation := optional.None[bool]() isDetectable := optional.None[bool]() - if checker != nil { - attrs, err := checker.CheckPath(f.Name()) - if err == nil { - isVendored = AttributeToBool(attrs, AttributeLinguistVendored) - if isVendored.ValueOrDefault(false) { - continue - } - - isGenerated = AttributeToBool(attrs, AttributeLinguistGenerated) - if isGenerated.ValueOrDefault(false) { - continue - } + attrs, err := checker.CheckPath(f.Name()) + attrLinguistGenerated := optional.None[bool]() + if err == nil { + if isVendored = attrs.GetVendored(); isVendored.ValueOrDefault(false) { + continue + } - isDocumentation = AttributeToBool(attrs, AttributeLinguistDocumentation) - if isDocumentation.ValueOrDefault(false) { - continue - } + if attrLinguistGenerated = attrs.GetGenerated(); attrLinguistGenerated.ValueOrDefault(false) { + continue + } - isDetectable = AttributeToBool(attrs, AttributeLinguistDetectable) - if !isDetectable.ValueOrDefault(true) { - continue - } + if isDocumentation = attrs.GetDocumentation(); isDocumentation.ValueOrDefault(false) { + continue + } - hasLanguage := TryReadLanguageAttribute(attrs) - if hasLanguage.Value() != "" { - language := hasLanguage.Value() + if isDetectable = attrs.GetDetectable(); !isDetectable.ValueOrDefault(true) { + continue + } - // group languages, such as Pug -> HTML; SCSS -> CSS - group := enry.GetLanguageGroup(language) - if len(group) != 0 { - language = group - } + if hasLanguage := attrs.GetLanguage(); hasLanguage.Value() != "" { + language := hasLanguage.Value() - // this language will always be added to the size - sizes[language] += f.Size() - continue + // group languages, such as Pug -> HTML; SCSS -> CSS + group := enry.GetLanguageGroup(language) + if len(group) != 0 { + language = group } + + // this language will always be added to the size + sizes[language] += f.Size() + continue } } @@ -149,7 +147,7 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err if err := writeID(f.ID.String()); err != nil { return nil, err } - _, _, size, err := ReadBatchLine(batchReader) + _, _, size, err := git.ReadBatchLine(batchReader) if err != nil { log.Debug("Error reading blob: %s Err: %v", f.ID.String(), err) return nil, err @@ -167,11 +165,19 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err return nil, err } content = contentBuf.Bytes() - if err := DiscardFull(batchReader, discard); err != nil { + if err := git.DiscardFull(batchReader, discard); err != nil { return nil, err } } - if !isGenerated.Has() && enry.IsGenerated(f.Name(), content) { + + // if "generated" attribute is set, use it, otherwise use enry.IsGenerated to guess + var isGenerated bool + if attrLinguistGenerated.Has() { + isGenerated = attrLinguistGenerated.Value() + } else { + isGenerated = enry.IsGenerated(f.Name(), content) + } + if isGenerated { continue } diff --git a/modules/git/repo_language_stats_test.go b/modules/git/languagestats/language_stats_test.go index 1ee5f4c3af..b908ae6413 100644 --- a/modules/git/repo_language_stats_test.go +++ b/modules/git/languagestats/language_stats_test.go @@ -3,34 +3,36 @@ //go:build !gogit -package git +package languagestats import ( - "path/filepath" "testing" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestRepository_GetLanguageStats(t *testing.T) { - repoPath := filepath.Join(testReposDir, "language_stats_repo") - gitRepo, err := openRepositoryWithDefaultContext(repoPath) + setting.AppDataPath = t.TempDir() + repoPath := "../tests/repos/language_stats_repo" + gitRepo, err := git.OpenRepository(t.Context(), repoPath) require.NoError(t, err) - defer gitRepo.Close() - stats, err := gitRepo.GetLanguageStats("8fee858da5796dfb37704761701bb8e800ad9ef3") + stats, err := GetLanguageStats(gitRepo, "8fee858da5796dfb37704761701bb8e800ad9ef3") require.NoError(t, err) - assert.EqualValues(t, map[string]int64{ + assert.Equal(t, map[string]int64{ "Python": 134, "Java": 112, }, stats) } func TestMergeLanguageStats(t *testing.T) { - assert.EqualValues(t, map[string]int64{ + assert.Equal(t, map[string]int64{ "PHP": 1, "python": 10, "JAVA": 700, diff --git a/modules/git/languagestats/main_test.go b/modules/git/languagestats/main_test.go new file mode 100644 index 0000000000..707d268c81 --- /dev/null +++ b/modules/git/languagestats/main_test.go @@ -0,0 +1,41 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package languagestats + +import ( + "context" + "fmt" + "os" + "testing" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" +) + +func testRun(m *testing.M) error { + gitHomePath, err := os.MkdirTemp(os.TempDir(), "git-home") + if err != nil { + return fmt.Errorf("unable to create temp dir: %w", err) + } + defer util.RemoveAll(gitHomePath) + setting.Git.HomePath = gitHomePath + + if err = git.InitFull(context.Background()); err != nil { + return fmt.Errorf("failed to call Init: %w", err) + } + + exitCode := m.Run() + if exitCode != 0 { + return fmt.Errorf("run test failed, ExitCode=%d", exitCode) + } + return nil +} + +func TestMain(m *testing.M) { + if err := testRun(m); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Test failed: %v", err) + os.Exit(1) + } +} diff --git a/modules/git/last_commit_cache.go b/modules/git/last_commit_cache.go index cf9c10d7b4..cff2556083 100644 --- a/modules/git/last_commit_cache.go +++ b/modules/git/last_commit_cache.go @@ -13,7 +13,7 @@ import ( ) func getCacheKey(repoPath, commitID, entryPath string) string { - hashBytes := sha256.Sum256([]byte(fmt.Sprintf("%s:%s:%s", repoPath, commitID, entryPath))) + hashBytes := sha256.Sum256(fmt.Appendf(nil, "%s:%s:%s", repoPath, commitID, entryPath)) return fmt.Sprintf("last_commit:%x", hashBytes) } diff --git a/modules/git/log_name_status.go b/modules/git/log_name_status.go index 1fd58abfcd..dfdef38ef9 100644 --- a/modules/git/log_name_status.go +++ b/modules/git/log_name_status.go @@ -34,7 +34,7 @@ func LogNameStatusRepo(ctx context.Context, repository, head, treepath string, p _ = stdoutWriter.Close() } - cmd := NewCommand(ctx) + cmd := NewCommand() cmd.AddArguments("log", "--name-status", "-c", "--format=commit%x00%H %P%x00", "--parents", "--no-renames", "-t", "-z").AddDynamicArguments(head) var files []string @@ -64,7 +64,7 @@ func LogNameStatusRepo(ctx context.Context, repository, head, treepath string, p go func() { stderr := strings.Builder{} - err := cmd.Run(&RunOpts{ + err := cmd.Run(ctx, &RunOpts{ Dir: repository, Stdout: stdoutWriter, Stderr: &stderr, @@ -118,11 +118,12 @@ func (g *LogNameStatusRepoParser) Next(treepath string, paths2ids map[string]int g.buffull = false g.next, err = g.rd.ReadSlice('\x00') if err != nil { - if err == bufio.ErrBufferFull { + switch err { + case bufio.ErrBufferFull: g.buffull = true - } else if err == io.EOF { + case io.EOF: return nil, nil - } else { + default: return nil, err } } @@ -132,11 +133,12 @@ func (g *LogNameStatusRepoParser) Next(treepath string, paths2ids map[string]int if bytes.Equal(g.next, []byte("commit\000")) { g.next, err = g.rd.ReadSlice('\x00') if err != nil { - if err == bufio.ErrBufferFull { + switch err { + case bufio.ErrBufferFull: g.buffull = true - } else if err == io.EOF { + case io.EOF: return nil, nil - } else { + default: return nil, err } } @@ -214,11 +216,12 @@ diffloop: } g.next, err = g.rd.ReadSlice('\x00') if err != nil { - if err == bufio.ErrBufferFull { + switch err { + case bufio.ErrBufferFull: g.buffull = true - } else if err == io.EOF { + case io.EOF: return &ret, nil - } else { + default: return nil, err } } @@ -343,10 +346,7 @@ func WalkGitLog(ctx context.Context, repo *Repository, head *Commit, treepath st results := make([]string, len(paths)) remaining := len(paths) - nextRestart := (len(paths) * 3) / 4 - if nextRestart > 70 { - nextRestart = 70 - } + nextRestart := min((len(paths)*3)/4, 70) lastEmptyParent := head.ID.String() commitSinceLastEmptyParent := uint64(0) commitSinceNextRestart := uint64(0) diff --git a/modules/git/notes_test.go b/modules/git/notes_test.go index 267671d8fa..ca05a9e525 100644 --- a/modules/git/notes_test.go +++ b/modules/git/notes_test.go @@ -4,7 +4,6 @@ package git import ( - "context" "path/filepath" "testing" @@ -18,7 +17,7 @@ func TestGetNotes(t *testing.T) { defer bareRepo1.Close() note := Note{} - err = GetNote(context.Background(), bareRepo1, "95bb4d39648ee7e325106df01a621c530863a653", ¬e) + err = GetNote(t.Context(), bareRepo1, "95bb4d39648ee7e325106df01a621c530863a653", ¬e) assert.NoError(t, err) assert.Equal(t, []byte("Note contents\n"), note.Message) assert.Equal(t, "Vladimir Panteleev", note.Commit.Author.Name) @@ -31,10 +30,10 @@ func TestGetNestedNotes(t *testing.T) { defer repo.Close() note := Note{} - err = GetNote(context.Background(), repo, "3e668dbfac39cbc80a9ff9c61eb565d944453ba4", ¬e) + err = GetNote(t.Context(), repo, "3e668dbfac39cbc80a9ff9c61eb565d944453ba4", ¬e) assert.NoError(t, err) assert.Equal(t, []byte("Note 2"), note.Message) - err = GetNote(context.Background(), repo, "ba0a96fa63532d6c5087ecef070b0250ed72fa47", ¬e) + err = GetNote(t.Context(), repo, "ba0a96fa63532d6c5087ecef070b0250ed72fa47", ¬e) assert.NoError(t, err) assert.Equal(t, []byte("Note 1"), note.Message) } @@ -46,7 +45,7 @@ func TestGetNonExistentNotes(t *testing.T) { defer bareRepo1.Close() note := Note{} - err = GetNote(context.Background(), bareRepo1, "non_existent_sha", ¬e) + err = GetNote(t.Context(), bareRepo1, "non_existent_sha", ¬e) assert.Error(t, err) assert.IsType(t, ErrNotExist{}, err) } diff --git a/modules/git/object_id.go b/modules/git/object_id.go index 82d30184df..25dfef3ec5 100644 --- a/modules/git/object_id.go +++ b/modules/git/object_id.go @@ -99,5 +99,5 @@ type ErrInvalidSHA struct { } func (err ErrInvalidSHA) Error() string { - return fmt.Sprintf("invalid sha: %s", err.SHA) + return "invalid sha: " + err.SHA } diff --git a/modules/git/parse.go b/modules/git/parse.go index eb26632cc0..a7f5c58e89 100644 --- a/modules/git/parse.go +++ b/modules/git/parse.go @@ -46,19 +46,9 @@ func parseLsTreeLine(line []byte) (*LsTreeEntry, error) { entry.Size = optional.Some(size) } - switch string(entryMode) { - case "100644": - entry.EntryMode = EntryModeBlob - case "100755": - entry.EntryMode = EntryModeExec - case "120000": - entry.EntryMode = EntryModeSymlink - case "160000": - entry.EntryMode = EntryModeCommit - case "040000", "040755": // git uses 040000 for tree object, but some users may get 040755 for unknown reasons - entry.EntryMode = EntryModeTree - default: - return nil, fmt.Errorf("unknown type: %v", string(entryMode)) + entry.EntryMode, err = ParseEntryMode(string(entryMode)) + if err != nil || entry.EntryMode == EntryModeNoEntry { + return nil, fmt.Errorf("invalid ls-tree output (invalid mode): %q, err: %w", line, err) } entry.ID, err = NewIDFromString(string(entryObjectID)) diff --git a/modules/git/parse_nogogit.go b/modules/git/parse_nogogit.go index 676bb3c76c..78a0162889 100644 --- a/modules/git/parse_nogogit.go +++ b/modules/git/parse_nogogit.go @@ -19,7 +19,7 @@ func ParseTreeEntries(data []byte) ([]*TreeEntry, error) { return parseTreeEntries(data, nil) } -// parseTreeEntries FIXME this function's design is not right, it should make the caller read all data into memory +// parseTreeEntries FIXME this function's design is not right, it should not make the caller read all data into memory func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) { entries := make([]*TreeEntry, 0, bytes.Count(data, []byte{'\n'})+1) for pos := 0; pos < len(data); { diff --git a/modules/git/parse_nogogit_test.go b/modules/git/parse_nogogit_test.go index a4436ce499..6594c84269 100644 --- a/modules/git/parse_nogogit_test.go +++ b/modules/git/parse_nogogit_test.go @@ -58,7 +58,7 @@ func TestParseTreeEntriesLong(t *testing.T) { assert.NoError(t, err) assert.Len(t, entries, len(testCase.Expected)) for i, entry := range entries { - assert.EqualValues(t, testCase.Expected[i], entry) + assert.Equal(t, testCase.Expected[i], entry) } } } @@ -91,7 +91,7 @@ func TestParseTreeEntriesShort(t *testing.T) { assert.NoError(t, err) assert.Len(t, entries, len(testCase.Expected)) for i, entry := range entries { - assert.EqualValues(t, testCase.Expected[i], entry) + assert.Equal(t, testCase.Expected[i], entry) } } } diff --git a/modules/git/pipeline/catfile.go b/modules/git/pipeline/catfile.go index 4677218150..5ddc36cc01 100644 --- a/modules/git/pipeline/catfile.go +++ b/modules/git/pipeline/catfile.go @@ -25,8 +25,8 @@ func CatFileBatchCheck(ctx context.Context, shasToCheckReader *io.PipeReader, ca stderr := new(bytes.Buffer) var errbuf strings.Builder - cmd := git.NewCommand(ctx, "cat-file", "--batch-check") - if err := cmd.Run(&git.RunOpts{ + cmd := git.NewCommand("cat-file", "--batch-check") + if err := cmd.Run(ctx, &git.RunOpts{ Dir: tmpBasePath, Stdin: shasToCheckReader, Stdout: catFileCheckWriter, @@ -43,8 +43,8 @@ func CatFileBatchCheckAllObjects(ctx context.Context, catFileCheckWriter *io.Pip stderr := new(bytes.Buffer) var errbuf strings.Builder - cmd := git.NewCommand(ctx, "cat-file", "--batch-check", "--batch-all-objects") - if err := cmd.Run(&git.RunOpts{ + cmd := git.NewCommand("cat-file", "--batch-check", "--batch-all-objects") + if err := cmd.Run(ctx, &git.RunOpts{ Dir: tmpBasePath, Stdout: catFileCheckWriter, Stderr: stderr, @@ -64,7 +64,7 @@ func CatFileBatch(ctx context.Context, shasToBatchReader *io.PipeReader, catFile stderr := new(bytes.Buffer) var errbuf strings.Builder - if err := git.NewCommand(ctx, "cat-file", "--batch").Run(&git.RunOpts{ + if err := git.NewCommand("cat-file", "--batch").Run(ctx, &git.RunOpts{ Dir: tmpBasePath, Stdout: catFileBatchWriter, Stdin: shasToBatchReader, diff --git a/modules/git/pipeline/lfs_nogogit.go b/modules/git/pipeline/lfs_nogogit.go index 92e35c5a10..c5eed73701 100644 --- a/modules/git/pipeline/lfs_nogogit.go +++ b/modules/git/pipeline/lfs_nogogit.go @@ -32,7 +32,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err go func() { stderr := strings.Builder{} - err := git.NewCommand(repo.Ctx, "rev-list", "--all").Run(&git.RunOpts{ + err := git.NewCommand("rev-list", "--all").Run(repo.Ctx, &git.RunOpts{ Dir: repo.Path, Stdout: revListWriter, Stderr: &stderr, diff --git a/modules/git/pipeline/namerev.go b/modules/git/pipeline/namerev.go index ad583a7479..06731c5051 100644 --- a/modules/git/pipeline/namerev.go +++ b/modules/git/pipeline/namerev.go @@ -22,7 +22,7 @@ func NameRevStdin(ctx context.Context, shasToNameReader *io.PipeReader, nameRevS stderr := new(bytes.Buffer) var errbuf strings.Builder - if err := git.NewCommand(ctx, "name-rev", "--stdin", "--name-only", "--always").Run(&git.RunOpts{ + if err := git.NewCommand("name-rev", "--stdin", "--name-only", "--always").Run(ctx, &git.RunOpts{ Dir: tmpBasePath, Stdout: nameRevStdinWriter, Stdin: shasToNameReader, diff --git a/modules/git/pipeline/revlist.go b/modules/git/pipeline/revlist.go index d88ebe78ef..31627a0f3a 100644 --- a/modules/git/pipeline/revlist.go +++ b/modules/git/pipeline/revlist.go @@ -23,8 +23,8 @@ func RevListAllObjects(ctx context.Context, revListWriter *io.PipeWriter, wg *sy stderr := new(bytes.Buffer) var errbuf strings.Builder - cmd := git.NewCommand(ctx, "rev-list", "--objects", "--all") - if err := cmd.Run(&git.RunOpts{ + cmd := git.NewCommand("rev-list", "--objects", "--all") + if err := cmd.Run(ctx, &git.RunOpts{ Dir: basePath, Stdout: revListWriter, Stderr: stderr, @@ -42,11 +42,11 @@ func RevListObjects(ctx context.Context, revListWriter *io.PipeWriter, wg *sync. defer revListWriter.Close() stderr := new(bytes.Buffer) var errbuf strings.Builder - cmd := git.NewCommand(ctx, "rev-list", "--objects").AddDynamicArguments(headSHA) + cmd := git.NewCommand("rev-list", "--objects").AddDynamicArguments(headSHA) if baseSHA != "" { cmd = cmd.AddArguments("--not").AddDynamicArguments(baseSHA) } - if err := cmd.Run(&git.RunOpts{ + if err := cmd.Run(ctx, &git.RunOpts{ Dir: tmpBasePath, Stdout: revListWriter, Stderr: stderr, diff --git a/modules/git/ref.go b/modules/git/ref.go index f20a175e42..56b2db858a 100644 --- a/modules/git/ref.go +++ b/modules/git/ref.go @@ -109,8 +109,8 @@ func (ref RefName) IsFor() bool { } func (ref RefName) nameWithoutPrefix(prefix string) string { - if strings.HasPrefix(string(ref), prefix) { - return strings.TrimPrefix(string(ref), prefix) + if after, ok := strings.CutPrefix(string(ref), prefix); ok { + return after } return "" } diff --git a/modules/git/remote.go b/modules/git/remote.go index ff8c040eb1..876c3d6acb 100644 --- a/modules/git/remote.go +++ b/modules/git/remote.go @@ -17,12 +17,12 @@ import ( func GetRemoteAddress(ctx context.Context, repoPath, remoteName string) (string, error) { var cmd *Command if DefaultFeatures().CheckVersionAtLeast("2.7") { - cmd = NewCommand(ctx, "remote", "get-url").AddDynamicArguments(remoteName) + cmd = NewCommand("remote", "get-url").AddDynamicArguments(remoteName) } else { - cmd = NewCommand(ctx, "config", "--get").AddDynamicArguments("remote." + remoteName + ".url") + cmd = NewCommand("config", "--get").AddDynamicArguments("remote." + remoteName + ".url") } - result, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}) + result, _, err := cmd.RunStdString(ctx, &RunOpts{Dir: repoPath}) if err != nil { return "", err } diff --git a/modules/git/repo.go b/modules/git/repo.go index 0993a4ac37..f1f6902773 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -18,6 +18,7 @@ import ( "time" "code.gitea.io/gitea/modules/proxy" + "code.gitea.io/gitea/modules/setting" ) // GPGSettings represents the default GPG settings for this repository @@ -27,6 +28,7 @@ type GPGSettings struct { Email string Name string PublicKeyContent string + Format string } const prettyLogFormat = `--pretty=format:%H` @@ -42,9 +44,9 @@ func (repo *Repository) parsePrettyFormatLogToList(logs []byte) ([]*Commit, erro return commits, nil } - parts := bytes.Split(logs, []byte{'\n'}) + parts := bytes.SplitSeq(logs, []byte{'\n'}) - for _, commitID := range parts { + for commitID := range parts { commit, err := repo.GetCommit(string(commitID)) if err != nil { return nil, err @@ -57,7 +59,7 @@ func (repo *Repository) parsePrettyFormatLogToList(logs []byte) ([]*Commit, erro // IsRepoURLAccessible checks if given repository URL is accessible. func IsRepoURLAccessible(ctx context.Context, url string) bool { - _, _, err := NewCommand(ctx, "ls-remote", "-q", "-h").AddDynamicArguments(url, "HEAD").RunStdString(nil) + _, _, err := NewCommand("ls-remote", "-q", "-h").AddDynamicArguments(url, "HEAD").RunStdString(ctx, nil) return err == nil } @@ -68,7 +70,7 @@ func InitRepository(ctx context.Context, repoPath string, bare bool, objectForma return err } - cmd := NewCommand(ctx, "init") + cmd := NewCommand("init") if !IsValidObjectFormat(objectFormatName) { return fmt.Errorf("invalid object format: %s", objectFormatName) @@ -80,15 +82,15 @@ func InitRepository(ctx context.Context, repoPath string, bare bool, objectForma if bare { cmd.AddArguments("--bare") } - _, _, err = cmd.RunStdString(&RunOpts{Dir: repoPath}) + _, _, err = cmd.RunStdString(ctx, &RunOpts{Dir: repoPath}) return err } // IsEmpty Check if repository is empty. func (repo *Repository) IsEmpty() (bool, error) { var errbuf, output strings.Builder - if err := NewCommand(repo.Ctx).AddOptionFormat("--git-dir=%s", repo.Path).AddArguments("rev-list", "-n", "1", "--all"). - Run(&RunOpts{ + if err := NewCommand().AddOptionFormat("--git-dir=%s", repo.Path).AddArguments("rev-list", "-n", "1", "--all"). + Run(repo.Ctx, &RunOpts{ Dir: repo.Path, Stdout: &output, Stderr: &errbuf, @@ -129,7 +131,7 @@ func CloneWithArgs(ctx context.Context, args TrustedCmdArgs, from, to string, op return err } - cmd := NewCommandContextNoGlobals(ctx, args...).AddArguments("clone") + cmd := NewCommandNoGlobals(args...).AddArguments("clone") if opts.SkipTLSVerify { cmd.AddArguments("-c", "http.sslVerify=false") } @@ -170,7 +172,7 @@ func CloneWithArgs(ctx context.Context, args TrustedCmdArgs, from, to string, op } stderr := new(bytes.Buffer) - if err = cmd.Run(&RunOpts{ + if err = cmd.Run(ctx, &RunOpts{ Timeout: opts.Timeout, Env: envs, Stdout: io.Discard, @@ -193,7 +195,7 @@ type PushOptions struct { // Push pushs local commits to given remote branch. func Push(ctx context.Context, repoPath string, opts PushOptions) error { - cmd := NewCommand(ctx, "push") + cmd := NewCommand("push") if opts.Force { cmd.AddArguments("-f") } @@ -206,7 +208,7 @@ func Push(ctx context.Context, repoPath string, opts PushOptions) error { } cmd.AddDashesAndList(remoteBranchArgs...) - stdout, stderr, err := cmd.RunStdString(&RunOpts{Env: opts.Env, Timeout: opts.Timeout, Dir: repoPath}) + stdout, stderr, err := cmd.RunStdString(ctx, &RunOpts{Env: opts.Env, Timeout: opts.Timeout, Dir: repoPath}) if err != nil { if strings.Contains(stderr, "non-fast-forward") { return &ErrPushOutOfDate{StdOut: stdout, StdErr: stderr, Err: err} @@ -225,8 +227,8 @@ func Push(ctx context.Context, repoPath string, opts PushOptions) error { // GetLatestCommitTime returns time for latest commit in repository (across all branches) func GetLatestCommitTime(ctx context.Context, repoPath string) (time.Time, error) { - cmd := NewCommand(ctx, "for-each-ref", "--sort=-committerdate", BranchPrefix, "--count", "1", "--format=%(committerdate)") - stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}) + cmd := NewCommand("for-each-ref", "--sort=-committerdate", BranchPrefix, "--count", "1", "--format=%(committerdate)") + stdout, _, err := cmd.RunStdString(ctx, &RunOpts{Dir: repoPath}) if err != nil { return time.Time{}, err } @@ -242,9 +244,9 @@ type DivergeObject struct { // GetDivergingCommits returns the number of commits a targetBranch is ahead or behind a baseBranch func GetDivergingCommits(ctx context.Context, repoPath, baseBranch, targetBranch string) (do DivergeObject, err error) { - cmd := NewCommand(ctx, "rev-list", "--count", "--left-right"). + cmd := NewCommand("rev-list", "--count", "--left-right"). AddDynamicArguments(baseBranch + "..." + targetBranch).AddArguments("--") - stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}) + stdout, _, err := cmd.RunStdString(ctx, &RunOpts{Dir: repoPath}) if err != nil { return do, err } @@ -266,30 +268,30 @@ func GetDivergingCommits(ctx context.Context, repoPath, baseBranch, targetBranch // CreateBundle create bundle content to the target path func (repo *Repository) CreateBundle(ctx context.Context, commit string, out io.Writer) error { - tmp, err := os.MkdirTemp(os.TempDir(), "gitea-bundle") + tmp, cleanup, err := setting.AppDataTempDir("git-repo-content").MkdirTempRandom("gitea-bundle") if err != nil { return err } - defer os.RemoveAll(tmp) + defer cleanup() env := append(os.Environ(), "GIT_OBJECT_DIRECTORY="+filepath.Join(repo.Path, "objects")) - _, _, err = NewCommand(ctx, "init", "--bare").RunStdString(&RunOpts{Dir: tmp, Env: env}) + _, _, err = NewCommand("init", "--bare").RunStdString(ctx, &RunOpts{Dir: tmp, Env: env}) if err != nil { return err } - _, _, err = NewCommand(ctx, "reset", "--soft").AddDynamicArguments(commit).RunStdString(&RunOpts{Dir: tmp, Env: env}) + _, _, err = NewCommand("reset", "--soft").AddDynamicArguments(commit).RunStdString(ctx, &RunOpts{Dir: tmp, Env: env}) if err != nil { return err } - _, _, err = NewCommand(ctx, "branch", "-m", "bundle").RunStdString(&RunOpts{Dir: tmp, Env: env}) + _, _, err = NewCommand("branch", "-m", "bundle").RunStdString(ctx, &RunOpts{Dir: tmp, Env: env}) if err != nil { return err } tmpFile := filepath.Join(tmp, "bundle") - _, _, err = NewCommand(ctx, "bundle", "create").AddDynamicArguments(tmpFile, "bundle", "HEAD").RunStdString(&RunOpts{Dir: tmp, Env: env}) + _, _, err = NewCommand("bundle", "create").AddDynamicArguments(tmpFile, "bundle", "HEAD").RunStdString(ctx, &RunOpts{Dir: tmp, Env: env}) if err != nil { return err } diff --git a/modules/git/repo_archive.go b/modules/git/repo_archive.go index 92f3e88f7c..0b2f6f2a45 100644 --- a/modules/git/repo_archive.go +++ b/modules/git/repo_archive.go @@ -53,7 +53,7 @@ func (repo *Repository) CreateArchive(ctx context.Context, format ArchiveType, t return fmt.Errorf("unknown format: %v", format) } - cmd := NewCommand(ctx, "archive") + cmd := NewCommand("archive") if usePrefix { cmd.AddOptionFormat("--prefix=%s", filepath.Base(strings.TrimSuffix(repo.Path, ".git"))+"/") } @@ -61,7 +61,7 @@ func (repo *Repository) CreateArchive(ctx context.Context, format ArchiveType, t cmd.AddDynamicArguments(commitID) var stderr strings.Builder - err := cmd.Run(&RunOpts{ + err := cmd.Run(ctx, &RunOpts{ Dir: repo.Path, Stdout: target, Stderr: &stderr, diff --git a/modules/git/repo_attribute.go b/modules/git/repo_attribute.go deleted file mode 100644 index 90eb783fe8..0000000000 --- a/modules/git/repo_attribute.go +++ /dev/null @@ -1,323 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package git - -import ( - "bytes" - "context" - "fmt" - "io" - "os" - - "code.gitea.io/gitea/modules/log" -) - -// CheckAttributeOpts represents the possible options to CheckAttribute -type CheckAttributeOpts struct { - CachedOnly bool - AllAttributes bool - Attributes []string - Filenames []string - IndexFile string - WorkTree string -} - -// CheckAttribute return the Blame object of file -func (repo *Repository) CheckAttribute(opts CheckAttributeOpts) (map[string]map[string]string, error) { - env := []string{} - - if len(opts.IndexFile) > 0 { - env = append(env, "GIT_INDEX_FILE="+opts.IndexFile) - } - if len(opts.WorkTree) > 0 { - env = append(env, "GIT_WORK_TREE="+opts.WorkTree) - } - - if len(env) > 0 { - env = append(os.Environ(), env...) - } - - stdOut := new(bytes.Buffer) - stdErr := new(bytes.Buffer) - - cmd := NewCommand(repo.Ctx, "check-attr", "-z") - - if opts.AllAttributes { - cmd.AddArguments("-a") - } else { - for _, attribute := range opts.Attributes { - if attribute != "" { - cmd.AddDynamicArguments(attribute) - } - } - } - - if opts.CachedOnly { - cmd.AddArguments("--cached") - } - - cmd.AddDashesAndList(opts.Filenames...) - - if err := cmd.Run(&RunOpts{ - Env: env, - Dir: repo.Path, - Stdout: stdOut, - Stderr: stdErr, - }); err != nil { - return nil, fmt.Errorf("failed to run check-attr: %w\n%s\n%s", err, stdOut.String(), stdErr.String()) - } - - // FIXME: This is incorrect on versions < 1.8.5 - fields := bytes.Split(stdOut.Bytes(), []byte{'\000'}) - - if len(fields)%3 != 1 { - return nil, fmt.Errorf("wrong number of fields in return from check-attr") - } - - name2attribute2info := make(map[string]map[string]string) - - for i := 0; i < (len(fields) / 3); i++ { - filename := string(fields[3*i]) - attribute := string(fields[3*i+1]) - info := string(fields[3*i+2]) - attribute2info := name2attribute2info[filename] - if attribute2info == nil { - attribute2info = make(map[string]string) - } - attribute2info[attribute] = info - name2attribute2info[filename] = attribute2info - } - - return name2attribute2info, nil -} - -// CheckAttributeReader provides a reader for check-attribute content that can be long running -type CheckAttributeReader struct { - // params - Attributes []string - Repo *Repository - IndexFile string - WorkTree string - - stdinReader io.ReadCloser - stdinWriter *os.File - stdOut attributeWriter - cmd *Command - env []string - ctx context.Context - cancel context.CancelFunc -} - -// Init initializes the CheckAttributeReader -func (c *CheckAttributeReader) Init(ctx context.Context) error { - if len(c.Attributes) == 0 { - lw := new(nulSeparatedAttributeWriter) - lw.attributes = make(chan attributeTriple) - lw.closed = make(chan struct{}) - - c.stdOut = lw - c.stdOut.Close() - return fmt.Errorf("no provided Attributes to check") - } - - c.ctx, c.cancel = context.WithCancel(ctx) - c.cmd = NewCommand(c.ctx, "check-attr", "--stdin", "-z") - - if len(c.IndexFile) > 0 { - c.cmd.AddArguments("--cached") - c.env = append(c.env, "GIT_INDEX_FILE="+c.IndexFile) - } - - if len(c.WorkTree) > 0 { - c.env = append(c.env, "GIT_WORK_TREE="+c.WorkTree) - } - - c.env = append(c.env, "GIT_FLUSH=1") - - c.cmd.AddDynamicArguments(c.Attributes...) - - var err error - - c.stdinReader, c.stdinWriter, err = os.Pipe() - if err != nil { - c.cancel() - return err - } - - lw := new(nulSeparatedAttributeWriter) - lw.attributes = make(chan attributeTriple, 5) - lw.closed = make(chan struct{}) - c.stdOut = lw - return nil -} - -// Run run cmd -func (c *CheckAttributeReader) Run() error { - defer func() { - _ = c.stdinReader.Close() - _ = c.stdOut.Close() - }() - stdErr := new(bytes.Buffer) - err := c.cmd.Run(&RunOpts{ - Env: c.env, - Dir: c.Repo.Path, - Stdin: c.stdinReader, - Stdout: c.stdOut, - Stderr: stdErr, - }) - if err != nil && !IsErrCanceledOrKilled(err) { - return fmt.Errorf("failed to run attr-check. Error: %w\nStderr: %s", err, stdErr.String()) - } - return nil -} - -// CheckPath check attr for given path -func (c *CheckAttributeReader) CheckPath(path string) (rs map[string]string, err error) { - defer func() { - if err != nil && err != c.ctx.Err() { - log.Error("Unexpected error when checking path %s in %s. Error: %v", path, c.Repo.Path, err) - } - }() - - select { - case <-c.ctx.Done(): - return nil, c.ctx.Err() - default: - } - - if _, err = c.stdinWriter.Write([]byte(path + "\x00")); err != nil { - defer c.Close() - return nil, err - } - - rs = make(map[string]string) - for range c.Attributes { - select { - case attr, ok := <-c.stdOut.ReadAttribute(): - if !ok { - return nil, c.ctx.Err() - } - rs[attr.Attribute] = attr.Value - case <-c.ctx.Done(): - return nil, c.ctx.Err() - } - } - return rs, nil -} - -// Close close pip after use -func (c *CheckAttributeReader) Close() error { - c.cancel() - err := c.stdinWriter.Close() - return err -} - -type attributeWriter interface { - io.WriteCloser - ReadAttribute() <-chan attributeTriple -} - -type attributeTriple struct { - Filename string - Attribute string - Value string -} - -type nulSeparatedAttributeWriter struct { - tmp []byte - attributes chan attributeTriple - closed chan struct{} - working attributeTriple - pos int -} - -func (wr *nulSeparatedAttributeWriter) Write(p []byte) (n int, err error) { - l, read := len(p), 0 - - nulIdx := bytes.IndexByte(p, '\x00') - for nulIdx >= 0 { - wr.tmp = append(wr.tmp, p[:nulIdx]...) - switch wr.pos { - case 0: - wr.working = attributeTriple{ - Filename: string(wr.tmp), - } - case 1: - wr.working.Attribute = string(wr.tmp) - case 2: - wr.working.Value = string(wr.tmp) - } - wr.tmp = wr.tmp[:0] - wr.pos++ - if wr.pos > 2 { - wr.attributes <- wr.working - wr.pos = 0 - } - read += nulIdx + 1 - if l > read { - p = p[nulIdx+1:] - nulIdx = bytes.IndexByte(p, '\x00') - } else { - return l, nil - } - } - wr.tmp = append(wr.tmp, p...) - return len(p), nil -} - -func (wr *nulSeparatedAttributeWriter) ReadAttribute() <-chan attributeTriple { - return wr.attributes -} - -func (wr *nulSeparatedAttributeWriter) Close() error { - select { - case <-wr.closed: - return nil - default: - } - close(wr.attributes) - close(wr.closed) - return nil -} - -// Create a check attribute reader for the current repository and provided commit ID -func (repo *Repository) CheckAttributeReader(commitID string) (*CheckAttributeReader, context.CancelFunc) { - indexFilename, worktree, deleteTemporaryFile, err := repo.ReadTreeToTemporaryIndex(commitID) - if err != nil { - return nil, func() {} - } - - checker := &CheckAttributeReader{ - Attributes: []string{ - AttributeLinguistVendored, - AttributeLinguistGenerated, - AttributeLinguistDocumentation, - AttributeLinguistDetectable, - AttributeLinguistLanguage, - AttributeGitlabLanguage, - }, - Repo: repo, - IndexFile: indexFilename, - WorkTree: worktree, - } - ctx, cancel := context.WithCancel(repo.Ctx) - if err := checker.Init(ctx); err != nil { - log.Error("Unable to open checker for %s. Error: %v", commitID, err) - } else { - go func() { - err := checker.Run() - if err != nil && err != ctx.Err() { - log.Error("Unable to open checker for %s. Error: %v", commitID, err) - } - cancel() - }() - } - deferable := func() { - _ = checker.Close() - cancel() - deleteTemporaryFile() - } - - return checker, deferable -} diff --git a/modules/git/repo_attribute_test.go b/modules/git/repo_attribute_test.go deleted file mode 100644 index 0fcd94b4c7..0000000000 --- a/modules/git/repo_attribute_test.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2021 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package git - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) { - wr := &nulSeparatedAttributeWriter{ - attributes: make(chan attributeTriple, 5), - } - - testStr := ".gitignore\"\n\x00linguist-vendored\x00unspecified\x00" - - n, err := wr.Write([]byte(testStr)) - - assert.Len(t, testStr, n) - assert.NoError(t, err) - select { - case attr := <-wr.ReadAttribute(): - assert.Equal(t, ".gitignore\"\n", attr.Filename) - assert.Equal(t, AttributeLinguistVendored, attr.Attribute) - assert.Equal(t, "unspecified", attr.Value) - case <-time.After(100 * time.Millisecond): - assert.FailNow(t, "took too long to read an attribute from the list") - } - // Write a second attribute again - n, err = wr.Write([]byte(testStr)) - - assert.Len(t, testStr, n) - assert.NoError(t, err) - - select { - case attr := <-wr.ReadAttribute(): - assert.Equal(t, ".gitignore\"\n", attr.Filename) - assert.Equal(t, AttributeLinguistVendored, attr.Attribute) - assert.Equal(t, "unspecified", attr.Value) - case <-time.After(100 * time.Millisecond): - assert.FailNow(t, "took too long to read an attribute from the list") - } - - // Write a partial attribute - _, err = wr.Write([]byte("incomplete-file")) - assert.NoError(t, err) - _, err = wr.Write([]byte("name\x00")) - assert.NoError(t, err) - - select { - case <-wr.ReadAttribute(): - assert.FailNow(t, "There should not be an attribute ready to read") - case <-time.After(100 * time.Millisecond): - } - _, err = wr.Write([]byte("attribute\x00")) - assert.NoError(t, err) - select { - case <-wr.ReadAttribute(): - assert.FailNow(t, "There should not be an attribute ready to read") - case <-time.After(100 * time.Millisecond): - } - - _, err = wr.Write([]byte("value\x00")) - assert.NoError(t, err) - - attr := <-wr.ReadAttribute() - assert.Equal(t, "incomplete-filename", attr.Filename) - assert.Equal(t, "attribute", attr.Attribute) - assert.Equal(t, "value", attr.Value) - - _, err = wr.Write([]byte("shouldbe.vendor\x00linguist-vendored\x00set\x00shouldbe.vendor\x00linguist-generated\x00unspecified\x00shouldbe.vendor\x00linguist-language\x00unspecified\x00")) - assert.NoError(t, err) - attr = <-wr.ReadAttribute() - assert.NoError(t, err) - assert.EqualValues(t, attributeTriple{ - Filename: "shouldbe.vendor", - Attribute: AttributeLinguistVendored, - Value: "set", - }, attr) - attr = <-wr.ReadAttribute() - assert.NoError(t, err) - assert.EqualValues(t, attributeTriple{ - Filename: "shouldbe.vendor", - Attribute: AttributeLinguistGenerated, - Value: "unspecified", - }, attr) - attr = <-wr.ReadAttribute() - assert.NoError(t, err) - assert.EqualValues(t, attributeTriple{ - Filename: "shouldbe.vendor", - Attribute: AttributeLinguistLanguage, - Value: "unspecified", - }, attr) -} diff --git a/modules/git/repo_base_gogit.go b/modules/git/repo_base_gogit.go index 0ca1ea79c2..293aca159c 100644 --- a/modules/git/repo_base_gogit.go +++ b/modules/git/repo_base_gogit.go @@ -49,7 +49,12 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) { repoPath, err := filepath.Abs(repoPath) if err != nil { return nil, err - } else if !isDir(repoPath) { + } + exist, err := util.IsDir(repoPath) + if err != nil { + return nil, err + } + if !exist { return nil, util.NewNotExistErrorf("no such file or directory") } diff --git a/modules/git/repo_base_nogogit.go b/modules/git/repo_base_nogogit.go index 477e3b8742..6f9bfd4b43 100644 --- a/modules/git/repo_base_nogogit.go +++ b/modules/git/repo_base_nogogit.go @@ -47,7 +47,12 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) { repoPath, err := filepath.Abs(repoPath) if err != nil { return nil, err - } else if !isDir(repoPath) { + } + exist, err := util.IsDir(repoPath) + if err != nil { + return nil, err + } + if !exist { return nil, util.NewNotExistErrorf("no such file or directory") } @@ -62,7 +67,7 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) { func (repo *Repository) CatFileBatch(ctx context.Context) (WriteCloserError, *bufio.Reader, func(), error) { if repo.batch == nil { var err error - repo.batch, err = repo.NewBatch(ctx) + repo.batch, err = NewBatch(ctx, repo.Path) if err != nil { return nil, nil, nil, err } @@ -76,7 +81,7 @@ func (repo *Repository) CatFileBatch(ctx context.Context) (WriteCloserError, *bu } log.Debug("Opening temporary cat file batch for: %s", repo.Path) - tempBatch, err := repo.NewBatch(ctx) + tempBatch, err := NewBatch(ctx, repo.Path) if err != nil { return nil, nil, nil, err } @@ -87,7 +92,7 @@ func (repo *Repository) CatFileBatch(ctx context.Context) (WriteCloserError, *bu func (repo *Repository) CatFileBatchCheck(ctx context.Context) (WriteCloserError, *bufio.Reader, func(), error) { if repo.check == nil { var err error - repo.check, err = repo.NewBatchCheck(ctx) + repo.check, err = NewBatchCheck(ctx, repo.Path) if err != nil { return nil, nil, nil, err } @@ -101,7 +106,7 @@ func (repo *Repository) CatFileBatchCheck(ctx context.Context) (WriteCloserError } log.Debug("Opening temporary cat file batch-check for: %s", repo.Path) - tempBatchCheck, err := repo.NewBatchCheck(ctx) + tempBatchCheck, err := NewBatchCheck(ctx, repo.Path) if err != nil { return nil, nil, nil, err } diff --git a/modules/git/repo_blame.go b/modules/git/repo_blame.go index 139cdd7be9..6941a76c42 100644 --- a/modules/git/repo_blame.go +++ b/modules/git/repo_blame.go @@ -9,10 +9,10 @@ import ( // LineBlame returns the latest commit at the given line func (repo *Repository) LineBlame(revision, path, file string, line uint) (*Commit, error) { - res, _, err := NewCommand(repo.Ctx, "blame"). + res, _, err := NewCommand("blame"). AddOptionFormat("-L %d,%d", line, line). AddOptionValues("-p", revision). - AddDashesAndList(file).RunStdString(&RunOpts{Dir: path}) + AddDashesAndList(file).RunStdString(repo.Ctx, &RunOpts{Dir: path}) if err != nil { return nil, err } diff --git a/modules/git/repo_branch.go b/modules/git/repo_branch.go index 552ae2bb8c..e7ecf53f51 100644 --- a/modules/git/repo_branch.go +++ b/modules/git/repo_branch.go @@ -7,7 +7,6 @@ package git import ( "context" "errors" - "fmt" "strings" ) @@ -16,7 +15,7 @@ const BranchPrefix = "refs/heads/" // IsReferenceExist returns true if given reference exists in the repository. func IsReferenceExist(ctx context.Context, repoPath, name string) bool { - _, _, err := NewCommand(ctx, "show-ref", "--verify").AddDashesAndList(name).RunStdString(&RunOpts{Dir: repoPath}) + _, _, err := NewCommand("show-ref", "--verify").AddDashesAndList(name).RunStdString(ctx, &RunOpts{Dir: repoPath}) return err == nil } @@ -25,38 +24,8 @@ func IsBranchExist(ctx context.Context, repoPath, name string) bool { return IsReferenceExist(ctx, repoPath, BranchPrefix+name) } -// Branch represents a Git branch. -type Branch struct { - Name string - Path string - - gitRepo *Repository -} - -// GetHEADBranch returns corresponding branch of HEAD. -func (repo *Repository) GetHEADBranch() (*Branch, error) { - if repo == nil { - return nil, fmt.Errorf("nil repo") - } - stdout, _, err := NewCommand(repo.Ctx, "symbolic-ref", "HEAD").RunStdString(&RunOpts{Dir: repo.Path}) - if err != nil { - return nil, err - } - stdout = strings.TrimSpace(stdout) - - if !strings.HasPrefix(stdout, BranchPrefix) { - return nil, fmt.Errorf("invalid HEAD branch: %v", stdout) - } - - return &Branch{ - Name: stdout[len(BranchPrefix):], - Path: stdout, - gitRepo: repo, - }, nil -} - func GetDefaultBranch(ctx context.Context, repoPath string) (string, error) { - stdout, _, err := NewCommand(ctx, "symbolic-ref", "HEAD").RunStdString(&RunOpts{Dir: repoPath}) + stdout, _, err := NewCommand("symbolic-ref", "HEAD").RunStdString(ctx, &RunOpts{Dir: repoPath}) if err != nil { return "", err } @@ -67,37 +36,6 @@ func GetDefaultBranch(ctx context.Context, repoPath string) (string, error) { return strings.TrimPrefix(stdout, BranchPrefix), nil } -// GetBranch returns a branch by it's name -func (repo *Repository) GetBranch(branch string) (*Branch, error) { - if !repo.IsBranchExist(branch) { - return nil, ErrBranchNotExist{branch} - } - return &Branch{ - Path: repo.Path, - Name: branch, - gitRepo: repo, - }, nil -} - -// GetBranches returns a slice of *git.Branch -func (repo *Repository) GetBranches(skip, limit int) ([]*Branch, int, error) { - brs, countAll, err := repo.GetBranchNames(skip, limit) - if err != nil { - return nil, 0, err - } - - branches := make([]*Branch, len(brs)) - for i := range brs { - branches[i] = &Branch{ - Path: repo.Path, - Name: brs[i], - gitRepo: repo, - } - } - - return branches, countAll, nil -} - // DeleteBranchOptions Option(s) for delete branch type DeleteBranchOptions struct { Force bool @@ -105,7 +43,7 @@ type DeleteBranchOptions struct { // DeleteBranch delete a branch by name on repository. func (repo *Repository) DeleteBranch(name string, opts DeleteBranchOptions) error { - cmd := NewCommand(repo.Ctx, "branch") + cmd := NewCommand("branch") if opts.Force { cmd.AddArguments("-D") @@ -114,46 +52,41 @@ func (repo *Repository) DeleteBranch(name string, opts DeleteBranchOptions) erro } cmd.AddDashesAndList(name) - _, _, err := cmd.RunStdString(&RunOpts{Dir: repo.Path}) + _, _, err := cmd.RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) return err } // CreateBranch create a new branch func (repo *Repository) CreateBranch(branch, oldbranchOrCommit string) error { - cmd := NewCommand(repo.Ctx, "branch") + cmd := NewCommand("branch") cmd.AddDashesAndList(branch, oldbranchOrCommit) - _, _, err := cmd.RunStdString(&RunOpts{Dir: repo.Path}) + _, _, err := cmd.RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) return err } // AddRemote adds a new remote to repository. func (repo *Repository) AddRemote(name, url string, fetch bool) error { - cmd := NewCommand(repo.Ctx, "remote", "add") + cmd := NewCommand("remote", "add") if fetch { cmd.AddArguments("-f") } cmd.AddDynamicArguments(name, url) - _, _, err := cmd.RunStdString(&RunOpts{Dir: repo.Path}) + _, _, err := cmd.RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) return err } // RemoveRemote removes a remote from repository. func (repo *Repository) RemoveRemote(name string) error { - _, _, err := NewCommand(repo.Ctx, "remote", "rm").AddDynamicArguments(name).RunStdString(&RunOpts{Dir: repo.Path}) + _, _, err := NewCommand("remote", "rm").AddDynamicArguments(name).RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) return err } -// GetCommit returns the head commit of a branch -func (branch *Branch) GetCommit() (*Commit, error) { - return branch.gitRepo.GetBranchCommit(branch.Name) -} - // RenameBranch rename a branch func (repo *Repository) RenameBranch(from, to string) error { - _, _, err := NewCommand(repo.Ctx, "branch", "-m").AddDynamicArguments(from, to).RunStdString(&RunOpts{Dir: repo.Path}) + _, _, err := NewCommand("branch", "-m").AddDynamicArguments(from, to).RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) return err } diff --git a/modules/git/repo_branch_gogit.go b/modules/git/repo_branch_gogit.go index dbc4a5fedc..77aecb21eb 100644 --- a/modules/git/repo_branch_gogit.go +++ b/modules/git/repo_branch_gogit.go @@ -57,7 +57,7 @@ func (repo *Repository) IsBranchExist(name string) bool { // GetBranches returns branches from the repository, skipping "skip" initial branches and // returning at most "limit" branches, or all branches if "limit" is 0. -// Branches are returned with sort of `-commiterdate` as the nogogit +// Branches are returned with sort of `-committerdate` as the nogogit // implementation. This requires full fetch, sort and then the // skip/limit applies later as gogit returns in undefined order. func (repo *Repository) GetBranchNames(skip, limit int) ([]string, int, error) { diff --git a/modules/git/repo_branch_nogogit.go b/modules/git/repo_branch_nogogit.go index 0d2efd4a6b..0d11198523 100644 --- a/modules/git/repo_branch_nogogit.go +++ b/modules/git/repo_branch_nogogit.go @@ -109,7 +109,7 @@ func WalkShowRef(ctx context.Context, repoPath string, extraArgs TrustedCmdArgs, stderrBuilder := &strings.Builder{} args := TrustedCmdArgs{"for-each-ref", "--format=%(objectname) %(refname)"} args = append(args, extraArgs...) - err := NewCommand(ctx, args...).Run(&RunOpts{ + err := NewCommand(args...).Run(ctx, &RunOpts{ Dir: repoPath, Stdout: stdoutWriter, Stderr: stderrBuilder, diff --git a/modules/git/repo_branch_test.go b/modules/git/repo_branch_test.go index 5d3b8abb3a..8e8ea16fcd 100644 --- a/modules/git/repo_branch_test.go +++ b/modules/git/repo_branch_test.go @@ -21,21 +21,21 @@ func TestRepository_GetBranches(t *testing.T) { assert.NoError(t, err) assert.Len(t, branches, 2) - assert.EqualValues(t, 3, countAll) + assert.Equal(t, 3, countAll) assert.ElementsMatch(t, []string{"master", "branch2"}, branches) branches, countAll, err = bareRepo1.GetBranchNames(0, 0) assert.NoError(t, err) assert.Len(t, branches, 3) - assert.EqualValues(t, 3, countAll) + assert.Equal(t, 3, countAll) assert.ElementsMatch(t, []string{"master", "branch2", "branch1"}, branches) branches, countAll, err = bareRepo1.GetBranchNames(5, 1) assert.NoError(t, err) assert.Empty(t, branches) - assert.EqualValues(t, 3, countAll) + assert.Equal(t, 3, countAll) assert.ElementsMatch(t, []string{}, branches) } @@ -47,7 +47,7 @@ func BenchmarkRepository_GetBranches(b *testing.B) { } defer bareRepo1.Close() - for i := 0; i < b.N; i++ { + for b.Loop() { _, _, err := bareRepo1.GetBranchNames(0, 0) if err != nil { b.Fatal(err) @@ -71,15 +71,15 @@ func TestGetRefsBySha(t *testing.T) { // refs/pull/1/head branches, err = bareRepo5.GetRefsBySha("c83380d7056593c51a699d12b9c00627bd5743e9", PullPrefix) assert.NoError(t, err) - assert.EqualValues(t, []string{"refs/pull/1/head"}, branches) + assert.Equal(t, []string{"refs/pull/1/head"}, branches) branches, err = bareRepo5.GetRefsBySha("d8e0bbb45f200e67d9a784ce55bd90821af45ebd", BranchPrefix) assert.NoError(t, err) - assert.EqualValues(t, []string{"refs/heads/master", "refs/heads/master-clone"}, branches) + assert.Equal(t, []string{"refs/heads/master", "refs/heads/master-clone"}, branches) branches, err = bareRepo5.GetRefsBySha("58a4bcc53ac13e7ff76127e0fb518b5262bf09af", BranchPrefix) assert.NoError(t, err) - assert.EqualValues(t, []string{"refs/heads/test-patch-1"}, branches) + assert.Equal(t, []string{"refs/heads/test-patch-1"}, branches) } func BenchmarkGetRefsBySha(b *testing.B) { diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index 647894bb21..4066a1ca7b 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -59,7 +59,7 @@ func (repo *Repository) getCommitByPathWithID(id ObjectID, relpath string) (*Com relpath = `\` + relpath } - stdout, _, runErr := NewCommand(repo.Ctx, "log", "-1", prettyLogFormat).AddDynamicArguments(id.String()).AddDashesAndList(relpath).RunStdString(&RunOpts{Dir: repo.Path}) + stdout, _, runErr := NewCommand("log", "-1", prettyLogFormat).AddDynamicArguments(id.String()).AddDashesAndList(relpath).RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) if runErr != nil { return nil, runErr } @@ -74,7 +74,7 @@ func (repo *Repository) getCommitByPathWithID(id ObjectID, relpath string) (*Com // GetCommitByPath returns the last commit of relative path. func (repo *Repository) GetCommitByPath(relpath string) (*Commit, error) { - stdout, _, runErr := NewCommand(repo.Ctx, "log", "-1", prettyLogFormat).AddDashesAndList(relpath).RunStdBytes(&RunOpts{Dir: repo.Path}) + stdout, _, runErr := NewCommand("log", "-1", prettyLogFormat).AddDashesAndList(relpath).RunStdBytes(repo.Ctx, &RunOpts{Dir: repo.Path}) if runErr != nil { return nil, runErr } @@ -89,8 +89,9 @@ func (repo *Repository) GetCommitByPath(relpath string) (*Commit, error) { return commits[0], nil } -func (repo *Repository) commitsByRange(id ObjectID, page, pageSize int, not string) ([]*Commit, error) { - cmd := NewCommand(repo.Ctx, "log"). +// commitsByRangeWithTime returns the specific page commits before current revision, with not, since, until support +func (repo *Repository) commitsByRangeWithTime(id ObjectID, page, pageSize int, not, since, until string) ([]*Commit, error) { + cmd := NewCommand("log"). AddOptionFormat("--skip=%d", (page-1)*pageSize). AddOptionFormat("--max-count=%d", pageSize). AddArguments(prettyLogFormat). @@ -99,8 +100,14 @@ func (repo *Repository) commitsByRange(id ObjectID, page, pageSize int, not stri if not != "" { cmd.AddOptionValues("--not", not) } + if since != "" { + cmd.AddOptionFormat("--since=%s", since) + } + if until != "" { + cmd.AddOptionFormat("--until=%s", until) + } - stdout, _, err := cmd.RunStdBytes(&RunOpts{Dir: repo.Path}) + stdout, _, err := cmd.RunStdBytes(repo.Ctx, &RunOpts{Dir: repo.Path}) if err != nil { return nil, err } @@ -134,7 +141,7 @@ func (repo *Repository) searchCommits(id ObjectID, opts SearchCommitsOptions) ([ } // create new git log command with limit of 100 commits - cmd := NewCommand(repo.Ctx, "log", "-100", prettyLogFormat).AddDynamicArguments(id.String()) + cmd := NewCommand("log", "-100", prettyLogFormat).AddDynamicArguments(id.String()) // pretend that all refs along with HEAD were listed on command line as <commis> // https://git-scm.com/docs/git-log#Documentation/git-log.txt---all @@ -154,7 +161,7 @@ func (repo *Repository) searchCommits(id ObjectID, opts SearchCommitsOptions) ([ // search for commits matching given constraints and keywords in commit msg addCommonSearchArgs(cmd) - stdout, _, err := cmd.RunStdBytes(&RunOpts{Dir: repo.Path}) + stdout, _, err := cmd.RunStdBytes(repo.Ctx, &RunOpts{Dir: repo.Path}) if err != nil { return nil, err } @@ -168,14 +175,14 @@ func (repo *Repository) searchCommits(id ObjectID, opts SearchCommitsOptions) ([ // ignore anything not matching a valid sha pattern if id.Type().IsValid(v) { // create new git log command with 1 commit limit - hashCmd := NewCommand(repo.Ctx, "log", "-1", prettyLogFormat) + hashCmd := NewCommand("log", "-1", prettyLogFormat) // add previous arguments except for --grep and --all addCommonSearchArgs(hashCmd) // add keyword as <commit> hashCmd.AddDynamicArguments(v) // search with given constraints for commit matching sha hash of v - hashMatching, _, err := hashCmd.RunStdBytes(&RunOpts{Dir: repo.Path}) + hashMatching, _, err := hashCmd.RunStdBytes(repo.Ctx, &RunOpts{Dir: repo.Path}) if err != nil || bytes.Contains(stdout, hashMatching) { continue } @@ -190,7 +197,7 @@ func (repo *Repository) searchCommits(id ObjectID, opts SearchCommitsOptions) ([ // FileChangedBetweenCommits Returns true if the file changed between commit IDs id1 and id2 // You must ensure that id1 and id2 are valid commit ids. func (repo *Repository) FileChangedBetweenCommits(filename, id1, id2 string) (bool, error) { - stdout, _, err := NewCommand(repo.Ctx, "diff", "--name-only", "-z").AddDynamicArguments(id1, id2).AddDashesAndList(filename).RunStdBytes(&RunOpts{Dir: repo.Path}) + stdout, _, err := NewCommand("diff", "--name-only", "-z").AddDynamicArguments(id1, id2).AddDashesAndList(filename).RunStdBytes(repo.Ctx, &RunOpts{Dir: repo.Path}) if err != nil { return false, err } @@ -212,6 +219,8 @@ type CommitsByFileAndRangeOptions struct { File string Not string Page int + Since string + Until string } // CommitsByFileAndRange return the commits according revision file and the page @@ -223,7 +232,7 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) }() go func() { stderr := strings.Builder{} - gitCmd := NewCommand(repo.Ctx, "rev-list"). + gitCmd := NewCommand("rev-list"). AddOptionFormat("--max-count=%d", setting.Git.CommitsRangeSize). AddOptionFormat("--skip=%d", (opts.Page-1)*setting.Git.CommitsRangeSize) gitCmd.AddDynamicArguments(opts.Revision) @@ -231,9 +240,15 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) if opts.Not != "" { gitCmd.AddOptionValues("--not", opts.Not) } + if opts.Since != "" { + gitCmd.AddOptionFormat("--since=%s", opts.Since) + } + if opts.Until != "" { + gitCmd.AddOptionFormat("--until=%s", opts.Until) + } gitCmd.AddDashesAndList(opts.File) - err := gitCmd.Run(&RunOpts{ + err := gitCmd.Run(repo.Ctx, &RunOpts{ Dir: repo.Path, Stdout: stdoutWriter, Stderr: &stderr, @@ -275,11 +290,11 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) // FilesCountBetween return the number of files changed between two commits func (repo *Repository) FilesCountBetween(startCommitID, endCommitID string) (int, error) { - stdout, _, err := NewCommand(repo.Ctx, "diff", "--name-only").AddDynamicArguments(startCommitID + "..." + endCommitID).RunStdString(&RunOpts{Dir: repo.Path}) + stdout, _, err := NewCommand("diff", "--name-only").AddDynamicArguments(startCommitID+"..."+endCommitID).RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) if err != nil && strings.Contains(err.Error(), "no merge base") { // git >= 2.28 now returns an error if startCommitID and endCommitID have become unrelated. // previously it would return the results of git diff --name-only startCommitID endCommitID so let's try that... - stdout, _, err = NewCommand(repo.Ctx, "diff", "--name-only").AddDynamicArguments(startCommitID, endCommitID).RunStdString(&RunOpts{Dir: repo.Path}) + stdout, _, err = NewCommand("diff", "--name-only").AddDynamicArguments(startCommitID, endCommitID).RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) } if err != nil { return 0, err @@ -293,13 +308,13 @@ func (repo *Repository) CommitsBetween(last, before *Commit) ([]*Commit, error) var stdout []byte var err error if before == nil { - stdout, _, err = NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path}) + stdout, _, err = NewCommand("rev-list").AddDynamicArguments(last.ID.String()).RunStdBytes(repo.Ctx, &RunOpts{Dir: repo.Path}) } else { - stdout, _, err = NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(before.ID.String() + ".." + last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path}) + stdout, _, err = NewCommand("rev-list").AddDynamicArguments(before.ID.String()+".."+last.ID.String()).RunStdBytes(repo.Ctx, &RunOpts{Dir: repo.Path}) if err != nil && strings.Contains(err.Error(), "no merge base") { // future versions of git >= 2.28 are likely to return an error if before and last have become unrelated. // previously it would return the results of git rev-list before last so let's try that... - stdout, _, err = NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(before.ID.String(), last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path}) + stdout, _, err = NewCommand("rev-list").AddDynamicArguments(before.ID.String(), last.ID.String()).RunStdBytes(repo.Ctx, &RunOpts{Dir: repo.Path}) } } if err != nil { @@ -313,22 +328,22 @@ func (repo *Repository) CommitsBetweenLimit(last, before *Commit, limit, skip in var stdout []byte var err error if before == nil { - stdout, _, err = NewCommand(repo.Ctx, "rev-list"). + stdout, _, err = NewCommand("rev-list"). AddOptionValues("--max-count", strconv.Itoa(limit)). AddOptionValues("--skip", strconv.Itoa(skip)). - AddDynamicArguments(last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path}) + AddDynamicArguments(last.ID.String()).RunStdBytes(repo.Ctx, &RunOpts{Dir: repo.Path}) } else { - stdout, _, err = NewCommand(repo.Ctx, "rev-list"). + stdout, _, err = NewCommand("rev-list"). AddOptionValues("--max-count", strconv.Itoa(limit)). AddOptionValues("--skip", strconv.Itoa(skip)). - AddDynamicArguments(before.ID.String() + ".." + last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path}) + AddDynamicArguments(before.ID.String()+".."+last.ID.String()).RunStdBytes(repo.Ctx, &RunOpts{Dir: repo.Path}) if err != nil && strings.Contains(err.Error(), "no merge base") { // future versions of git >= 2.28 are likely to return an error if before and last have become unrelated. // previously it would return the results of git rev-list --max-count n before last so let's try that... - stdout, _, err = NewCommand(repo.Ctx, "rev-list"). + stdout, _, err = NewCommand("rev-list"). AddOptionValues("--max-count", strconv.Itoa(limit)). AddOptionValues("--skip", strconv.Itoa(skip)). - AddDynamicArguments(before.ID.String(), last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path}) + AddDynamicArguments(before.ID.String(), last.ID.String()).RunStdBytes(repo.Ctx, &RunOpts{Dir: repo.Path}) } } if err != nil { @@ -343,13 +358,13 @@ func (repo *Repository) CommitsBetweenNotBase(last, before *Commit, baseBranch s var stdout []byte var err error if before == nil { - stdout, _, err = NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(last.ID.String()).AddOptionValues("--not", baseBranch).RunStdBytes(&RunOpts{Dir: repo.Path}) + stdout, _, err = NewCommand("rev-list").AddDynamicArguments(last.ID.String()).AddOptionValues("--not", baseBranch).RunStdBytes(repo.Ctx, &RunOpts{Dir: repo.Path}) } else { - stdout, _, err = NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(before.ID.String()+".."+last.ID.String()).AddOptionValues("--not", baseBranch).RunStdBytes(&RunOpts{Dir: repo.Path}) + stdout, _, err = NewCommand("rev-list").AddDynamicArguments(before.ID.String()+".."+last.ID.String()).AddOptionValues("--not", baseBranch).RunStdBytes(repo.Ctx, &RunOpts{Dir: repo.Path}) if err != nil && strings.Contains(err.Error(), "no merge base") { // future versions of git >= 2.28 are likely to return an error if before and last have become unrelated. // previously it would return the results of git rev-list before last so let's try that... - stdout, _, err = NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(before.ID.String(), last.ID.String()).AddOptionValues("--not", baseBranch).RunStdBytes(&RunOpts{Dir: repo.Path}) + stdout, _, err = NewCommand("rev-list").AddDynamicArguments(before.ID.String(), last.ID.String()).AddOptionValues("--not", baseBranch).RunStdBytes(repo.Ctx, &RunOpts{Dir: repo.Path}) } } if err != nil { @@ -395,13 +410,13 @@ func (repo *Repository) CommitsCountBetween(start, end string) (int64, error) { // commitsBefore the limit is depth, not total number of returned commits. func (repo *Repository) commitsBefore(id ObjectID, limit int) ([]*Commit, error) { - cmd := NewCommand(repo.Ctx, "log", prettyLogFormat) + cmd := NewCommand("log", prettyLogFormat) if limit > 0 { cmd.AddOptionFormat("-%d", limit) } cmd.AddDynamicArguments(id.String()) - stdout, _, runErr := cmd.RunStdBytes(&RunOpts{Dir: repo.Path}) + stdout, _, runErr := cmd.RunStdBytes(repo.Ctx, &RunOpts{Dir: repo.Path}) if runErr != nil { return nil, runErr } @@ -438,10 +453,10 @@ func (repo *Repository) getCommitsBeforeLimit(id ObjectID, num int) ([]*Commit, func (repo *Repository) getBranches(env []string, commitID string, limit int) ([]string, error) { if DefaultFeatures().CheckVersionAtLeast("2.7.0") { - stdout, _, err := NewCommand(repo.Ctx, "for-each-ref", "--format=%(refname:strip=2)"). + stdout, _, err := NewCommand("for-each-ref", "--format=%(refname:strip=2)"). AddOptionFormat("--count=%d", limit). AddOptionValues("--contains", commitID, BranchPrefix). - RunStdString(&RunOpts{ + RunStdString(repo.Ctx, &RunOpts{ Dir: repo.Path, Env: env, }) @@ -453,7 +468,7 @@ func (repo *Repository) getBranches(env []string, commitID string, limit int) ([ return branches, nil } - stdout, _, err := NewCommand(repo.Ctx, "branch").AddOptionValues("--contains", commitID).RunStdString(&RunOpts{ + stdout, _, err := NewCommand("branch").AddOptionValues("--contains", commitID).RunStdString(repo.Ctx, &RunOpts{ Dir: repo.Path, Env: env, }) @@ -495,7 +510,7 @@ func (repo *Repository) GetCommitsFromIDs(commitIDs []string) []*Commit { // IsCommitInBranch check if the commit is on the branch func (repo *Repository) IsCommitInBranch(commitID, branch string) (r bool, err error) { - stdout, _, err := NewCommand(repo.Ctx, "branch", "--contains").AddDynamicArguments(commitID, branch).RunStdString(&RunOpts{Dir: repo.Path}) + stdout, _, err := NewCommand("branch", "--contains").AddDynamicArguments(commitID, branch).RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) if err != nil { return false, err } @@ -519,11 +534,12 @@ func (repo *Repository) AddLastCommitCache(cacheKey, fullName, sha string) error return nil } +// GetCommitBranchStart returns the commit where the branch diverged func (repo *Repository) GetCommitBranchStart(env []string, branch, endCommitID string) (string, error) { - cmd := NewCommand(repo.Ctx, "log", prettyLogFormat) + cmd := NewCommand("log", prettyLogFormat) cmd.AddDynamicArguments(endCommitID) - stdout, _, runErr := cmd.RunStdBytes(&RunOpts{ + stdout, _, runErr := cmd.RunStdBytes(repo.Ctx, &RunOpts{ Dir: repo.Path, Env: env, }) @@ -531,21 +547,20 @@ func (repo *Repository) GetCommitBranchStart(env []string, branch, endCommitID s return "", runErr } - parts := bytes.Split(bytes.TrimSpace(stdout), []byte{'\n'}) + parts := bytes.SplitSeq(bytes.TrimSpace(stdout), []byte{'\n'}) - var startCommitID string - for _, commitID := range parts { + // check the commits one by one until we find a commit contained by another branch + // and we think this commit is the divergence point + for commitID := range parts { branches, err := repo.getBranches(env, string(commitID), 2) if err != nil { return "", err } for _, b := range branches { if b != branch { - return startCommitID, nil + return string(commitID), nil } } - - startCommitID = string(commitID) } return "", nil diff --git a/modules/git/repo_commit_gogit.go b/modules/git/repo_commit_gogit.go index 993013eef7..a88902e209 100644 --- a/modules/git/repo_commit_gogit.go +++ b/modules/git/repo_commit_gogit.go @@ -59,7 +59,7 @@ func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) { } } - actualCommitID, _, err := NewCommand(repo.Ctx, "rev-parse", "--verify").AddDynamicArguments(commitID).RunStdString(&RunOpts{Dir: repo.Path}) + actualCommitID, _, err := NewCommand("rev-parse", "--verify").AddDynamicArguments(commitID).RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) actualCommitID = strings.TrimSpace(actualCommitID) if err != nil { if strings.Contains(err.Error(), "unknown revision or path") || diff --git a/modules/git/repo_commit_nogogit.go b/modules/git/repo_commit_nogogit.go index f5ed282a45..3ead3e2216 100644 --- a/modules/git/repo_commit_nogogit.go +++ b/modules/git/repo_commit_nogogit.go @@ -16,7 +16,7 @@ import ( // ResolveReference resolves a name to a reference func (repo *Repository) ResolveReference(name string) (string, error) { - stdout, _, err := NewCommand(repo.Ctx, "show-ref", "--hash").AddDynamicArguments(name).RunStdString(&RunOpts{Dir: repo.Path}) + stdout, _, err := NewCommand("show-ref", "--hash").AddDynamicArguments(name).RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) if err != nil { if strings.Contains(err.Error(), "not a valid ref") { return "", ErrNotExist{name, ""} @@ -52,13 +52,13 @@ func (repo *Repository) GetRefCommitID(name string) (string, error) { // SetReference sets the commit ID string of given reference (e.g. branch or tag). func (repo *Repository) SetReference(name, commitID string) error { - _, _, err := NewCommand(repo.Ctx, "update-ref").AddDynamicArguments(name, commitID).RunStdString(&RunOpts{Dir: repo.Path}) + _, _, err := NewCommand("update-ref").AddDynamicArguments(name, commitID).RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) return err } // RemoveReference removes the given reference (e.g. branch or tag). func (repo *Repository) RemoveReference(name string) error { - _, _, err := NewCommand(repo.Ctx, "update-ref", "--no-deref", "-d").AddDynamicArguments(name).RunStdString(&RunOpts{Dir: repo.Path}) + _, _, err := NewCommand("update-ref", "--no-deref", "-d").AddDynamicArguments(name).RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) return err } @@ -68,7 +68,7 @@ func (repo *Repository) IsCommitExist(name string) bool { log.Error("IsCommitExist: %v", err) return false } - _, _, err := NewCommand(repo.Ctx, "cat-file", "-e").AddDynamicArguments(name).RunStdString(&RunOpts{Dir: repo.Path}) + _, _, err := NewCommand("cat-file", "-e").AddDynamicArguments(name).RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) return err == nil } @@ -81,10 +81,10 @@ func (repo *Repository) getCommit(id ObjectID) (*Commit, error) { _, _ = wr.Write([]byte(id.String() + "\n")) - return repo.getCommitFromBatchReader(rd, id) + return repo.getCommitFromBatchReader(wr, rd, id) } -func (repo *Repository) getCommitFromBatchReader(rd *bufio.Reader, id ObjectID) (*Commit, error) { +func (repo *Repository) getCommitFromBatchReader(wr WriteCloserError, rd *bufio.Reader, id ObjectID) (*Commit, error) { _, typ, size, err := ReadBatchLine(rd) if err != nil { if errors.Is(err, io.EOF) || IsErrNotExist(err) { @@ -112,7 +112,11 @@ func (repo *Repository) getCommitFromBatchReader(rd *bufio.Reader, id ObjectID) return nil, err } - commit, err := tag.Commit(repo) + if _, err := wr.Write([]byte(tag.Object.String() + "\n")); err != nil { + return nil, err + } + + commit, err := repo.getCommitFromBatchReader(wr, rd, tag.Object) if err != nil { return nil, err } diff --git a/modules/git/repo_commitgraph.go b/modules/git/repo_commitgraph.go index 087d5bcec4..62c6378054 100644 --- a/modules/git/repo_commitgraph.go +++ b/modules/git/repo_commitgraph.go @@ -12,7 +12,7 @@ import ( // this requires git v2.18 to be installed func WriteCommitGraph(ctx context.Context, repoPath string) error { if DefaultFeatures().CheckVersionAtLeast("2.18") { - if _, _, err := NewCommand(ctx, "commit-graph", "write").RunStdString(&RunOpts{Dir: repoPath}); err != nil { + if _, _, err := NewCommand("commit-graph", "write").RunStdString(ctx, &RunOpts{Dir: repoPath}); err != nil { return fmt.Errorf("unable to write commit-graph for '%s' : %w", repoPath, err) } } diff --git a/modules/git/repo_commitgraph_gogit.go b/modules/git/repo_commitgraph_gogit.go index d3182f15c6..c0082b62c8 100644 --- a/modules/git/repo_commitgraph_gogit.go +++ b/modules/git/repo_commitgraph_gogit.go @@ -8,7 +8,7 @@ package git import ( "os" - "path" + "path/filepath" gitealog "code.gitea.io/gitea/modules/log" @@ -18,7 +18,7 @@ import ( // CommitNodeIndex returns the index for walking commit graph func (r *Repository) CommitNodeIndex() (cgobject.CommitNodeIndex, *os.File) { - indexPath := path.Join(r.Path, "objects", "info", "commit-graph") + indexPath := filepath.Join(r.Path, "objects", "info", "commit-graph") file, err := os.Open(indexPath) if err == nil { diff --git a/modules/git/repo_compare.go b/modules/git/repo_compare.go index 877a7ff3b8..ff44506e13 100644 --- a/modules/git/repo_compare.go +++ b/modules/git/repo_compare.go @@ -39,13 +39,13 @@ func (repo *Repository) GetMergeBase(tmpRemote, base, head string) (string, stri if tmpRemote != "origin" { tmpBaseName := RemotePrefix + tmpRemote + "/tmp_" + base // Fetch commit into a temporary branch in order to be able to handle commits and tags - _, _, err := NewCommand(repo.Ctx, "fetch", "--no-tags").AddDynamicArguments(tmpRemote).AddDashesAndList(base + ":" + tmpBaseName).RunStdString(&RunOpts{Dir: repo.Path}) + _, _, err := NewCommand("fetch", "--no-tags").AddDynamicArguments(tmpRemote).AddDashesAndList(base+":"+tmpBaseName).RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) if err == nil { base = tmpBaseName } } - stdout, _, err := NewCommand(repo.Ctx, "merge-base").AddDashesAndList(base, head).RunStdString(&RunOpts{Dir: repo.Path}) + stdout, _, err := NewCommand("merge-base").AddDashesAndList(base, head).RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) return strings.TrimSpace(stdout), base, err } @@ -94,9 +94,9 @@ func (repo *Repository) GetCompareInfo(basePath, baseBranch, headBranch string, if !fileOnly { // avoid: ambiguous argument 'refs/a...refs/b': unknown revision or path not in the working tree. Use '--': 'git <command> [<revision>...] -- [<file>...]' var logs []byte - logs, _, err = NewCommand(repo.Ctx, "log").AddArguments(prettyLogFormat). - AddDynamicArguments(baseCommitID + separator + headBranch).AddArguments("--"). - RunStdBytes(&RunOpts{Dir: repo.Path}) + logs, _, err = NewCommand("log").AddArguments(prettyLogFormat). + AddDynamicArguments(baseCommitID+separator+headBranch).AddArguments("--"). + RunStdBytes(repo.Ctx, &RunOpts{Dir: repo.Path}) if err != nil { return nil, err } @@ -150,8 +150,8 @@ func (repo *Repository) GetDiffNumChangedFiles(base, head string, directComparis } // avoid: ambiguous argument 'refs/a...refs/b': unknown revision or path not in the working tree. Use '--': 'git <command> [<revision>...] -- [<file>...]' - if err := NewCommand(repo.Ctx, "diff", "-z", "--name-only").AddDynamicArguments(base + separator + head).AddArguments("--"). - Run(&RunOpts{ + if err := NewCommand("diff", "-z", "--name-only").AddDynamicArguments(base+separator+head).AddArguments("--"). + Run(repo.Ctx, &RunOpts{ Dir: repo.Path, Stdout: w, Stderr: stderr, @@ -161,7 +161,7 @@ func (repo *Repository) GetDiffNumChangedFiles(base, head string, directComparis // previously it would return the results of git diff -z --name-only base head so let's try that... w = &lineCountWriter{} stderr.Reset() - if err = NewCommand(repo.Ctx, "diff", "-z", "--name-only").AddDynamicArguments(base, head).AddArguments("--").Run(&RunOpts{ + if err = NewCommand("diff", "-z", "--name-only").AddDynamicArguments(base, head).AddArguments("--").Run(repo.Ctx, &RunOpts{ Dir: repo.Path, Stdout: w, Stderr: stderr, @@ -174,23 +174,15 @@ func (repo *Repository) GetDiffNumChangedFiles(base, head string, directComparis return w.numLines, nil } -// GetDiffShortStat counts number of changed files, number of additions and deletions -func (repo *Repository) GetDiffShortStat(base, head string) (numFiles, totalAdditions, totalDeletions int, err error) { - numFiles, totalAdditions, totalDeletions, err = GetDiffShortStat(repo.Ctx, repo.Path, nil, base+"..."+head) - if err != nil && strings.Contains(err.Error(), "no merge base") { - return GetDiffShortStat(repo.Ctx, repo.Path, nil, base, head) - } - return numFiles, totalAdditions, totalDeletions, err -} - -// GetDiffShortStat counts number of changed files, number of additions and deletions -func GetDiffShortStat(ctx context.Context, repoPath string, trustedArgs TrustedCmdArgs, dynamicArgs ...string) (numFiles, totalAdditions, totalDeletions int, err error) { +// GetDiffShortStatByCmdArgs counts number of changed files, number of additions and deletions +// TODO: it can be merged with another "GetDiffShortStat" in the future +func GetDiffShortStatByCmdArgs(ctx context.Context, repoPath string, trustedArgs TrustedCmdArgs, dynamicArgs ...string) (numFiles, totalAdditions, totalDeletions int, err error) { // Now if we call: // $ git diff --shortstat 1ebb35b98889ff77299f24d82da426b434b0cca0...788b8b1440462d477f45b0088875 // we get: // " 9902 files changed, 2034198 insertions(+), 298800 deletions(-)\n" - cmd := NewCommand(ctx, "diff", "--shortstat").AddArguments(trustedArgs...).AddDynamicArguments(dynamicArgs...) - stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}) + cmd := NewCommand("diff", "--shortstat").AddArguments(trustedArgs...).AddDynamicArguments(dynamicArgs...) + stdout, _, err := cmd.RunStdString(ctx, &RunOpts{Dir: repoPath}) if err != nil { return 0, 0, 0, err } @@ -236,8 +228,8 @@ func parseDiffStat(stdout string) (numFiles, totalAdditions, totalDeletions int, // GetDiff generates and returns patch data between given revisions, optimized for human readability func (repo *Repository) GetDiff(compareArg string, w io.Writer) error { stderr := new(bytes.Buffer) - return NewCommand(repo.Ctx, "diff", "-p").AddDynamicArguments(compareArg). - Run(&RunOpts{ + return NewCommand("diff", "-p").AddDynamicArguments(compareArg). + Run(repo.Ctx, &RunOpts{ Dir: repo.Path, Stdout: w, Stderr: stderr, @@ -246,7 +238,7 @@ func (repo *Repository) GetDiff(compareArg string, w io.Writer) error { // GetDiffBinary generates and returns patch data between given revisions, including binary diffs. func (repo *Repository) GetDiffBinary(compareArg string, w io.Writer) error { - return NewCommand(repo.Ctx, "diff", "-p", "--binary", "--histogram").AddDynamicArguments(compareArg).Run(&RunOpts{ + return NewCommand("diff", "-p", "--binary", "--histogram").AddDynamicArguments(compareArg).Run(repo.Ctx, &RunOpts{ Dir: repo.Path, Stdout: w, }) @@ -255,8 +247,8 @@ func (repo *Repository) GetDiffBinary(compareArg string, w io.Writer) error { // GetPatch generates and returns format-patch data between given revisions, able to be used with `git apply` func (repo *Repository) GetPatch(compareArg string, w io.Writer) error { stderr := new(bytes.Buffer) - return NewCommand(repo.Ctx, "format-patch", "--binary", "--stdout").AddDynamicArguments(compareArg). - Run(&RunOpts{ + return NewCommand("format-patch", "--binary", "--stdout").AddDynamicArguments(compareArg). + Run(repo.Ctx, &RunOpts{ Dir: repo.Path, Stdout: w, Stderr: stderr, @@ -271,13 +263,13 @@ func (repo *Repository) GetFilesChangedBetween(base, head string) ([]string, err if err != nil { return nil, err } - cmd := NewCommand(repo.Ctx, "diff-tree", "--name-only", "--root", "--no-commit-id", "-r", "-z") + cmd := NewCommand("diff-tree", "--name-only", "--root", "--no-commit-id", "-r", "-z") if base == objectFormat.EmptyObjectID().String() { cmd.AddDynamicArguments(head) } else { cmd.AddDynamicArguments(base, head) } - stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repo.Path}) + stdout, _, err := cmd.RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) if err != nil { return nil, err } diff --git a/modules/git/repo_gpg.go b/modules/git/repo_gpg.go index e2b45064fd..0021a7bda7 100644 --- a/modules/git/repo_gpg.go +++ b/modules/git/repo_gpg.go @@ -6,6 +6,7 @@ package git import ( "fmt" + "os" "strings" "code.gitea.io/gitea/modules/process" @@ -13,6 +14,14 @@ import ( // LoadPublicKeyContent will load the key from gpg func (gpgSettings *GPGSettings) LoadPublicKeyContent() error { + if gpgSettings.Format == SigningKeyFormatSSH { + content, err := os.ReadFile(gpgSettings.KeyID) + if err != nil { + return fmt.Errorf("unable to read SSH public key file: %s, %w", gpgSettings.KeyID, err) + } + gpgSettings.PublicKeyContent = string(content) + return nil + } content, stderr, err := process.GetManager().Exec( "gpg -a --export", "gpg", "-a", "--export", gpgSettings.KeyID) @@ -33,7 +42,7 @@ func (repo *Repository) GetDefaultPublicGPGKey(forceUpdate bool) (*GPGSettings, Sign: true, } - value, _, _ := NewCommand(repo.Ctx, "config", "--get", "commit.gpgsign").RunStdString(&RunOpts{Dir: repo.Path}) + value, _, _ := NewCommand("config", "--get", "commit.gpgsign").RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) sign, valid := ParseBool(strings.TrimSpace(value)) if !sign || !valid { gpgSettings.Sign = false @@ -41,13 +50,16 @@ func (repo *Repository) GetDefaultPublicGPGKey(forceUpdate bool) (*GPGSettings, return gpgSettings, nil } - signingKey, _, _ := NewCommand(repo.Ctx, "config", "--get", "user.signingkey").RunStdString(&RunOpts{Dir: repo.Path}) + signingKey, _, _ := NewCommand("config", "--get", "user.signingkey").RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) gpgSettings.KeyID = strings.TrimSpace(signingKey) - defaultEmail, _, _ := NewCommand(repo.Ctx, "config", "--get", "user.email").RunStdString(&RunOpts{Dir: repo.Path}) + format, _, _ := NewCommand("config", "--default", SigningKeyFormatOpenPGP, "--get", "gpg.format").RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) + gpgSettings.Format = strings.TrimSpace(format) + + defaultEmail, _, _ := NewCommand("config", "--get", "user.email").RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) gpgSettings.Email = strings.TrimSpace(defaultEmail) - defaultName, _, _ := NewCommand(repo.Ctx, "config", "--get", "user.name").RunStdString(&RunOpts{Dir: repo.Path}) + defaultName, _, _ := NewCommand("config", "--get", "user.name").RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) gpgSettings.Name = strings.TrimSpace(defaultName) if err := gpgSettings.LoadPublicKeyContent(); err != nil { diff --git a/modules/git/repo_index.go b/modules/git/repo_index.go index f45b6e6191..4879121a41 100644 --- a/modules/git/repo_index.go +++ b/modules/git/repo_index.go @@ -10,8 +10,7 @@ import ( "path/filepath" "strings" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/setting" ) // ReadTreeToIndex reads a treeish to the index @@ -22,7 +21,7 @@ func (repo *Repository) ReadTreeToIndex(treeish string, indexFilename ...string) } if len(treeish) != objectFormat.FullLength() { - res, _, err := NewCommand(repo.Ctx, "rev-parse", "--verify").AddDynamicArguments(treeish).RunStdString(&RunOpts{Dir: repo.Path}) + res, _, err := NewCommand("rev-parse", "--verify").AddDynamicArguments(treeish).RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) if err != nil { return err } @@ -42,7 +41,7 @@ func (repo *Repository) readTreeToIndex(id ObjectID, indexFilename ...string) er if len(indexFilename) > 0 { env = append(os.Environ(), "GIT_INDEX_FILE="+indexFilename[0]) } - _, _, err := NewCommand(repo.Ctx, "read-tree").AddDynamicArguments(id.String()).RunStdString(&RunOpts{Dir: repo.Path, Env: env}) + _, _, err := NewCommand("read-tree").AddDynamicArguments(id.String()).RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path, Env: env}) if err != nil { return err } @@ -59,43 +58,35 @@ func (repo *Repository) ReadTreeToTemporaryIndex(treeish string) (tmpIndexFilena } }() - removeDirFn := func(dir string) func() { // it can't use the return value "tmpDir" directly because it is empty when error occurs - return func() { - if err := util.RemoveAll(dir); err != nil { - log.Error("failed to remove tmp index dir: %v", err) - } - } - } - - tmpDir, err = os.MkdirTemp("", "index") + tmpDir, cancel, err = setting.AppDataTempDir("git-repo-content").MkdirTempRandom("index") if err != nil { return "", "", nil, err } tmpIndexFilename = filepath.Join(tmpDir, ".tmp-index") - cancel = removeDirFn(tmpDir) + err = repo.ReadTreeToIndex(treeish, tmpIndexFilename) if err != nil { return "", "", cancel, err } - return tmpIndexFilename, tmpDir, cancel, err + return tmpIndexFilename, tmpDir, cancel, nil } // EmptyIndex empties the index func (repo *Repository) EmptyIndex() error { - _, _, err := NewCommand(repo.Ctx, "read-tree", "--empty").RunStdString(&RunOpts{Dir: repo.Path}) + _, _, err := NewCommand("read-tree", "--empty").RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) return err } // LsFiles checks if the given filenames are in the index func (repo *Repository) LsFiles(filenames ...string) ([]string, error) { - cmd := NewCommand(repo.Ctx, "ls-files", "-z").AddDashesAndList(filenames...) - res, _, err := cmd.RunStdBytes(&RunOpts{Dir: repo.Path}) + cmd := NewCommand("ls-files", "-z").AddDashesAndList(filenames...) + res, _, err := cmd.RunStdBytes(repo.Ctx, &RunOpts{Dir: repo.Path}) if err != nil { return nil, err } filelist := make([]string, 0, len(filenames)) - for _, line := range bytes.Split(res, []byte{'\000'}) { + for line := range bytes.SplitSeq(res, []byte{'\000'}) { filelist = append(filelist, string(line)) } @@ -108,7 +99,7 @@ func (repo *Repository) RemoveFilesFromIndex(filenames ...string) error { if err != nil { return err } - cmd := NewCommand(repo.Ctx, "update-index", "--remove", "-z", "--index-info") + cmd := NewCommand("update-index", "--remove", "-z", "--index-info") stdout := new(bytes.Buffer) stderr := new(bytes.Buffer) buffer := new(bytes.Buffer) @@ -118,7 +109,7 @@ func (repo *Repository) RemoveFilesFromIndex(filenames ...string) error { buffer.WriteString("0 blob " + objectFormat.EmptyObjectID().String() + "\t" + file + "\000") } } - return cmd.Run(&RunOpts{ + return cmd.Run(repo.Ctx, &RunOpts{ Dir: repo.Path, Stdin: bytes.NewReader(buffer.Bytes()), Stdout: stdout, @@ -134,7 +125,7 @@ type IndexObjectInfo struct { // AddObjectsToIndex adds the provided object hashes to the index at the provided filenames func (repo *Repository) AddObjectsToIndex(objects ...IndexObjectInfo) error { - cmd := NewCommand(repo.Ctx, "update-index", "--add", "--replace", "-z", "--index-info") + cmd := NewCommand("update-index", "--add", "--replace", "-z", "--index-info") stdout := new(bytes.Buffer) stderr := new(bytes.Buffer) buffer := new(bytes.Buffer) @@ -142,7 +133,7 @@ func (repo *Repository) AddObjectsToIndex(objects ...IndexObjectInfo) error { // using format: mode SP type SP sha1 TAB path buffer.WriteString(object.Mode + " blob " + object.Object.String() + "\t" + object.Filename + "\000") } - return cmd.Run(&RunOpts{ + return cmd.Run(repo.Ctx, &RunOpts{ Dir: repo.Path, Stdin: bytes.NewReader(buffer.Bytes()), Stdout: stdout, @@ -157,7 +148,7 @@ func (repo *Repository) AddObjectToIndex(mode string, object ObjectID, filename // WriteTree writes the current index as a tree to the object db and returns its hash func (repo *Repository) WriteTree() (*Tree, error) { - stdout, _, runErr := NewCommand(repo.Ctx, "write-tree").RunStdString(&RunOpts{Dir: repo.Path}) + stdout, _, runErr := NewCommand("write-tree").RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) if runErr != nil { return nil, runErr } diff --git a/modules/git/repo_object.go b/modules/git/repo_object.go index 3d48b91c6d..08e0413311 100644 --- a/modules/git/repo_object.go +++ b/modules/git/repo_object.go @@ -68,13 +68,13 @@ func (repo *Repository) HashObject(reader io.Reader) (ObjectID, error) { func (repo *Repository) hashObject(reader io.Reader, save bool) (string, error) { var cmd *Command if save { - cmd = NewCommand(repo.Ctx, "hash-object", "-w", "--stdin") + cmd = NewCommand("hash-object", "-w", "--stdin") } else { - cmd = NewCommand(repo.Ctx, "hash-object", "--stdin") + cmd = NewCommand("hash-object", "--stdin") } stdout := new(bytes.Buffer) stderr := new(bytes.Buffer) - err := cmd.Run(&RunOpts{ + err := cmd.Run(repo.Ctx, &RunOpts{ Dir: repo.Path, Stdin: reader, Stdout: stdout, @@ -85,17 +85,3 @@ func (repo *Repository) hashObject(reader io.Reader, save bool) (string, error) } return strings.TrimSpace(stdout.String()), nil } - -// GetRefType gets the type of the ref based on the string -func (repo *Repository) GetRefType(ref string) ObjectType { - if repo.IsTagExist(ref) { - return ObjectTag - } else if repo.IsBranchExist(ref) { - return ObjectBranch - } else if repo.IsCommitExist(ref) { - return ObjectCommit - } else if _, err := repo.GetBlob(ref); err == nil { - return ObjectBlob - } - return ObjectType("invalid") -} diff --git a/modules/git/repo_ref.go b/modules/git/repo_ref.go index 850ec65502..554f9f73e1 100644 --- a/modules/git/repo_ref.go +++ b/modules/git/repo_ref.go @@ -18,15 +18,16 @@ func (repo *Repository) GetRefs() ([]*Reference, error) { // ListOccurrences lists all refs of the given refType the given commit appears in sorted by creation date DESC // refType should only be a literal "branch" or "tag" and nothing else func (repo *Repository) ListOccurrences(ctx context.Context, refType, commitSHA string) ([]string, error) { - cmd := NewCommand(ctx) - if refType == "branch" { + cmd := NewCommand() + switch refType { + case "branch": cmd.AddArguments("branch") - } else if refType == "tag" { + case "tag": cmd.AddArguments("tag") - } else { + default: return nil, util.NewInvalidArgumentErrorf(`can only use "branch" or "tag" for refType, but got %q`, refType) } - stdout, _, err := cmd.AddArguments("--no-color", "--sort=-creatordate", "--contains").AddDynamicArguments(commitSHA).RunStdString(&RunOpts{Dir: repo.Path}) + stdout, _, err := cmd.AddArguments("--no-color", "--sort=-creatordate", "--contains").AddDynamicArguments(commitSHA).RunStdString(ctx, &RunOpts{Dir: repo.Path}) if err != nil { return nil, err } diff --git a/modules/git/repo_ref_nogogit.go b/modules/git/repo_ref_nogogit.go index ac53d661b5..8d34713eaf 100644 --- a/modules/git/repo_ref_nogogit.go +++ b/modules/git/repo_ref_nogogit.go @@ -21,7 +21,7 @@ func (repo *Repository) GetRefsFiltered(pattern string) ([]*Reference, error) { go func() { stderrBuilder := &strings.Builder{} - err := NewCommand(repo.Ctx, "for-each-ref").Run(&RunOpts{ + err := NewCommand("for-each-ref").Run(repo.Ctx, &RunOpts{ Dir: repo.Path, Stdout: stdoutWriter, Stderr: stderrBuilder, diff --git a/modules/git/repo_stats.go b/modules/git/repo_stats.go index 83220104bd..8c6f31c38c 100644 --- a/modules/git/repo_stats.go +++ b/modules/git/repo_stats.go @@ -40,7 +40,9 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string) since := fromTime.Format(time.RFC3339) - stdout, _, runErr := NewCommand(repo.Ctx, "rev-list", "--count", "--no-merges", "--branches=*", "--date=iso").AddOptionFormat("--since='%s'", since).RunStdString(&RunOpts{Dir: repo.Path}) + stdout, _, runErr := NewCommand("rev-list", "--count", "--no-merges", "--branches=*", "--date=iso"). + AddOptionFormat("--since=%s", since). + RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) if runErr != nil { return nil, runErr } @@ -60,7 +62,8 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string) _ = stdoutWriter.Close() }() - gitCmd := NewCommand(repo.Ctx, "log", "--numstat", "--no-merges", "--pretty=format:---%n%h%n%aN%n%aE%n", "--date=iso").AddOptionFormat("--since='%s'", since) + gitCmd := NewCommand("log", "--numstat", "--no-merges", "--pretty=format:---%n%h%n%aN%n%aE%n", "--date=iso"). + AddOptionFormat("--since=%s", since) if len(branch) == 0 { gitCmd.AddArguments("--branches=*") } else { @@ -68,7 +71,7 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string) } stderr := new(strings.Builder) - err = gitCmd.Run(&RunOpts{ + err = gitCmd.Run(repo.Ctx, &RunOpts{ Env: []string{}, Dir: repo.Path, Stdout: stdoutWriter, diff --git a/modules/git/repo_stats_test.go b/modules/git/repo_stats_test.go index 3d032385ee..85d8807a6e 100644 --- a/modules/git/repo_stats_test.go +++ b/modules/git/repo_stats_test.go @@ -30,7 +30,7 @@ func TestRepository_GetCodeActivityStats(t *testing.T) { assert.EqualValues(t, 10, code.Additions) assert.EqualValues(t, 1, code.Deletions) assert.Len(t, code.Authors, 3) - assert.EqualValues(t, "tris.git@shoddynet.org", code.Authors[1].Email) + assert.Equal(t, "tris.git@shoddynet.org", code.Authors[1].Email) assert.EqualValues(t, 3, code.Authors[1].Commits) assert.EqualValues(t, 5, code.Authors[0].Commits) } diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go index 2026a4c9f5..c8d72eee02 100644 --- a/modules/git/repo_tag.go +++ b/modules/git/repo_tag.go @@ -5,7 +5,6 @@ package git import ( - "context" "fmt" "io" "strings" @@ -17,20 +16,15 @@ import ( // TagPrefix tags prefix path on the repository const TagPrefix = "refs/tags/" -// IsTagExist returns true if given tag exists in the repository. -func IsTagExist(ctx context.Context, repoPath, name string) bool { - return IsReferenceExist(ctx, repoPath, TagPrefix+name) -} - // CreateTag create one tag in the repository func (repo *Repository) CreateTag(name, revision string) error { - _, _, err := NewCommand(repo.Ctx, "tag").AddDashesAndList(name, revision).RunStdString(&RunOpts{Dir: repo.Path}) + _, _, err := NewCommand("tag").AddDashesAndList(name, revision).RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) return err } // CreateAnnotatedTag create one annotated tag in the repository func (repo *Repository) CreateAnnotatedTag(name, message, revision string) error { - _, _, err := NewCommand(repo.Ctx, "tag", "-a", "-m").AddDynamicArguments(message).AddDashesAndList(name, revision).RunStdString(&RunOpts{Dir: repo.Path}) + _, _, err := NewCommand("tag", "-a", "-m").AddDynamicArguments(message).AddDashesAndList(name, revision).RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) return err } @@ -40,13 +34,13 @@ func (repo *Repository) GetTagNameBySHA(sha string) (string, error) { return "", fmt.Errorf("SHA is too short: %s", sha) } - stdout, _, err := NewCommand(repo.Ctx, "show-ref", "--tags", "-d").RunStdString(&RunOpts{Dir: repo.Path}) + stdout, _, err := NewCommand("show-ref", "--tags", "-d").RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) if err != nil { return "", err } - tagRefs := strings.Split(stdout, "\n") - for _, tagRef := range tagRefs { + tagRefs := strings.SplitSeq(stdout, "\n") + for tagRef := range tagRefs { if len(strings.TrimSpace(tagRef)) > 0 { fields := strings.Fields(tagRef) if strings.HasPrefix(fields[0], sha) && strings.HasPrefix(fields[1], TagPrefix) { @@ -63,12 +57,12 @@ func (repo *Repository) GetTagNameBySHA(sha string) (string, error) { // GetTagID returns the object ID for a tag (annotated tags have both an object SHA AND a commit SHA) func (repo *Repository) GetTagID(name string) (string, error) { - stdout, _, err := NewCommand(repo.Ctx, "show-ref", "--tags").AddDashesAndList(name).RunStdString(&RunOpts{Dir: repo.Path}) + stdout, _, err := NewCommand("show-ref", "--tags").AddDashesAndList(name).RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) if err != nil { return "", err } // Make sure exact match is used: "v1" != "release/v1" - for _, line := range strings.Split(stdout, "\n") { + for line := range strings.SplitSeq(stdout, "\n") { fields := strings.Fields(line) if len(fields) == 2 && fields[1] == "refs/tags/"+name { return fields[0], nil @@ -123,9 +117,9 @@ func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) { rc := &RunOpts{Dir: repo.Path, Stdout: stdoutWriter, Stderr: &stderr} go func() { - err := NewCommand(repo.Ctx, "for-each-ref"). + err := NewCommand("for-each-ref"). AddOptionFormat("--format=%s", forEachRefFmt.Flag()). - AddArguments("--sort", "-*creatordate", "refs/tags").Run(rc) + AddArguments("--sort", "-*creatordate", "refs/tags").Run(repo.Ctx, rc) if err != nil { _ = stdoutWriter.CloseWithError(ConcatenateError(err, stderr.String())) } else { diff --git a/modules/git/repo_tag_nogogit.go b/modules/git/repo_tag_nogogit.go index e0a3104249..3d2b4f52bd 100644 --- a/modules/git/repo_tag_nogogit.go +++ b/modules/git/repo_tag_nogogit.go @@ -41,8 +41,11 @@ func (repo *Repository) GetTagType(id ObjectID) (string, error) { return "", err } _, typ, _, err := ReadBatchLine(rd) - if IsErrNotExist(err) { - return "", ErrNotExist{ID: id.String()} + if err != nil { + if IsErrNotExist(err) { + return "", ErrNotExist{ID: id.String()} + } + return "", err } return typ, nil } diff --git a/modules/git/repo_tag_test.go b/modules/git/repo_tag_test.go index f1f081680a..f1f5ff6664 100644 --- a/modules/git/repo_tag_test.go +++ b/modules/git/repo_tag_test.go @@ -27,12 +27,12 @@ func TestRepository_GetTags(t *testing.T) { } assert.Len(t, tags, 2) assert.Len(t, tags, total) - assert.EqualValues(t, "signed-tag", tags[0].Name) - assert.EqualValues(t, "36f97d9a96457e2bab511db30fe2db03893ebc64", tags[0].ID.String()) - assert.EqualValues(t, "tag", tags[0].Type) - assert.EqualValues(t, "test", tags[1].Name) - assert.EqualValues(t, "3ad28a9149a2864384548f3d17ed7f38014c9e8a", tags[1].ID.String()) - assert.EqualValues(t, "tag", tags[1].Type) + assert.Equal(t, "signed-tag", tags[0].Name) + assert.Equal(t, "36f97d9a96457e2bab511db30fe2db03893ebc64", tags[0].ID.String()) + assert.Equal(t, "tag", tags[0].Type) + assert.Equal(t, "test", tags[1].Name) + assert.Equal(t, "3ad28a9149a2864384548f3d17ed7f38014c9e8a", tags[1].ID.String()) + assert.Equal(t, "tag", tags[1].Type) } func TestRepository_GetTag(t *testing.T) { @@ -64,18 +64,13 @@ func TestRepository_GetTag(t *testing.T) { // and try to get the Tag for lightweight tag lTag, err := bareRepo1.GetTag(lTagName) - if err != nil { - assert.NoError(t, err) - return - } - if lTag == nil { - assert.NotNil(t, lTag) - assert.FailNow(t, "nil lTag: %s", lTagName) - } - assert.EqualValues(t, lTagName, lTag.Name) - assert.EqualValues(t, lTagCommitID, lTag.ID.String()) - assert.EqualValues(t, lTagCommitID, lTag.Object.String()) - assert.EqualValues(t, "commit", lTag.Type) + require.NoError(t, err) + require.NotNil(t, lTag, "nil lTag: %s", lTagName) + + assert.Equal(t, lTagName, lTag.Name) + assert.Equal(t, lTagCommitID, lTag.ID.String()) + assert.Equal(t, lTagCommitID, lTag.Object.String()) + assert.Equal(t, "commit", lTag.Type) // ANNOTATED TAGS aTagCommitID := "8006ff9adbf0cb94da7dad9e537e53817f9fa5c0" @@ -97,19 +92,14 @@ func TestRepository_GetTag(t *testing.T) { } aTag, err := bareRepo1.GetTag(aTagName) - if err != nil { - assert.NoError(t, err) - return - } - if aTag == nil { - assert.NotNil(t, aTag) - assert.FailNow(t, "nil aTag: %s", aTagName) - } - assert.EqualValues(t, aTagName, aTag.Name) - assert.EqualValues(t, aTagID, aTag.ID.String()) + require.NoError(t, err) + require.NotNil(t, aTag, "nil aTag: %s", aTagName) + + assert.Equal(t, aTagName, aTag.Name) + assert.Equal(t, aTagID, aTag.ID.String()) assert.NotEqual(t, aTagID, aTag.Object.String()) - assert.EqualValues(t, aTagCommitID, aTag.Object.String()) - assert.EqualValues(t, "tag", aTag.Type) + assert.Equal(t, aTagCommitID, aTag.Object.String()) + assert.Equal(t, "tag", aTag.Type) // RELEASE TAGS @@ -127,14 +117,14 @@ func TestRepository_GetTag(t *testing.T) { assert.NoError(t, err) return } - assert.EqualValues(t, rTagCommitID, rTagID) + assert.Equal(t, rTagCommitID, rTagID) oTagID, err := bareRepo1.GetTagID(lTagName) if err != nil { assert.NoError(t, err) return } - assert.EqualValues(t, lTagCommitID, oTagID) + assert.Equal(t, lTagCommitID, oTagID) } func TestRepository_GetAnnotatedTag(t *testing.T) { @@ -170,9 +160,9 @@ func TestRepository_GetAnnotatedTag(t *testing.T) { return } assert.NotNil(t, tag) - assert.EqualValues(t, aTagName, tag.Name) - assert.EqualValues(t, aTagID, tag.ID.String()) - assert.EqualValues(t, "tag", tag.Type) + assert.Equal(t, aTagName, tag.Name) + assert.Equal(t, aTagID, tag.ID.String()) + assert.Equal(t, "tag", tag.Type) // Annotated tag's Commit ID should fail tag2, err := bareRepo1.GetAnnotatedTag(aTagCommitID) diff --git a/modules/git/repo_test.go b/modules/git/repo_test.go index 9db78153a1..4638bdac1f 100644 --- a/modules/git/repo_test.go +++ b/modules/git/repo_test.go @@ -4,7 +4,6 @@ package git import ( - "context" "path/filepath" "testing" @@ -33,21 +32,21 @@ func TestRepoIsEmpty(t *testing.T) { func TestRepoGetDivergingCommits(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - do, err := GetDivergingCommits(context.Background(), bareRepo1Path, "master", "branch2") + do, err := GetDivergingCommits(t.Context(), bareRepo1Path, "master", "branch2") assert.NoError(t, err) assert.Equal(t, DivergeObject{ Ahead: 1, Behind: 5, }, do) - do, err = GetDivergingCommits(context.Background(), bareRepo1Path, "master", "master") + do, err = GetDivergingCommits(t.Context(), bareRepo1Path, "master", "master") assert.NoError(t, err) assert.Equal(t, DivergeObject{ Ahead: 0, Behind: 0, }, do) - do, err = GetDivergingCommits(context.Background(), bareRepo1Path, "master", "test") + do, err = GetDivergingCommits(t.Context(), bareRepo1Path, "master", "test") assert.NoError(t, err) assert.Equal(t, DivergeObject{ Ahead: 0, diff --git a/modules/git/repo_tree.go b/modules/git/repo_tree.go index ab48d47d13..309a73d759 100644 --- a/modules/git/repo_tree.go +++ b/modules/git/repo_tree.go @@ -15,7 +15,7 @@ import ( type CommitTreeOpts struct { Parents []string Message string - KeyID string + Key *SigningKey NoGPGSign bool AlwaysSign bool } @@ -33,7 +33,7 @@ func (repo *Repository) CommitTree(author, committer *Signature, tree *Tree, opt "GIT_COMMITTER_EMAIL="+committer.Email, "GIT_COMMITTER_DATE="+commitTimeStr, ) - cmd := NewCommand(repo.Ctx, "commit-tree").AddDynamicArguments(tree.ID.String()) + cmd := NewCommand("commit-tree").AddDynamicArguments(tree.ID.String()) for _, parent := range opts.Parents { cmd.AddArguments("-p").AddDynamicArguments(parent) @@ -43,8 +43,13 @@ func (repo *Repository) CommitTree(author, committer *Signature, tree *Tree, opt _, _ = messageBytes.WriteString(opts.Message) _, _ = messageBytes.WriteString("\n") - if opts.KeyID != "" || opts.AlwaysSign { - cmd.AddOptionFormat("-S%s", opts.KeyID) + if opts.Key != nil { + if opts.Key.Format != "" { + cmd.AddConfig("gpg.format", opts.Key.Format) + } + cmd.AddOptionFormat("-S%s", opts.Key.KeyID) + } else if opts.AlwaysSign { + cmd.AddOptionFormat("-S") } if opts.NoGPGSign { @@ -53,7 +58,7 @@ func (repo *Repository) CommitTree(author, committer *Signature, tree *Tree, opt stdout := new(bytes.Buffer) stderr := new(bytes.Buffer) - err := cmd.Run(&RunOpts{ + err := cmd.Run(repo.Ctx, &RunOpts{ Env: env, Dir: repo.Path, Stdin: messageBytes, diff --git a/modules/git/repo_tree_gogit.go b/modules/git/repo_tree_gogit.go index 651794a5aa..f77cd83612 100644 --- a/modules/git/repo_tree_gogit.go +++ b/modules/git/repo_tree_gogit.go @@ -36,7 +36,7 @@ func (repo *Repository) GetTree(idStr string) (*Tree, error) { } if len(idStr) != objectFormat.FullLength() { - res, _, err := NewCommand(repo.Ctx, "rev-parse", "--verify").AddDynamicArguments(idStr).RunStdString(&RunOpts{Dir: repo.Path}) + res, _, err := NewCommand("rev-parse", "--verify").AddDynamicArguments(idStr).RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) if err != nil { return nil, err } diff --git a/modules/git/repo_tree_nogogit.go b/modules/git/repo_tree_nogogit.go index d74769ccb2..1954f85162 100644 --- a/modules/git/repo_tree_nogogit.go +++ b/modules/git/repo_tree_nogogit.go @@ -35,7 +35,11 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) { if err != nil { return nil, err } - commit, err := tag.Commit(repo) + + if _, err := wr.Write([]byte(tag.Object.String() + "\n")); err != nil { + return nil, err + } + commit, err := repo.getCommitFromBatchReader(wr, rd, tag.Object) if err != nil { return nil, err } diff --git a/modules/git/signature_test.go b/modules/git/signature_test.go index 92681feea9..b9b5aff61b 100644 --- a/modules/git/signature_test.go +++ b/modules/git/signature_test.go @@ -42,6 +42,6 @@ func TestParseSignatureFromCommitLine(t *testing.T) { } for _, test := range tests { got := parseSignatureFromCommitLine(test.line) - assert.EqualValues(t, test.want, got) + assert.Equal(t, test.want, got) } } diff --git a/modules/git/submodule.go b/modules/git/submodule.go index 017b644052..31a32f1a9e 100644 --- a/modules/git/submodule.go +++ b/modules/git/submodule.go @@ -45,7 +45,7 @@ func GetTemplateSubmoduleCommits(ctx context.Context, repoPath string) (submodul return scanner.Err() }, } - err = NewCommand(ctx, "ls-tree", "-r", "--", "HEAD").Run(opts) + err = NewCommand("ls-tree", "-r", "--", "HEAD").Run(ctx, opts) if err != nil { return nil, fmt.Errorf("GetTemplateSubmoduleCommits: error running git ls-tree: %v", err) } @@ -56,8 +56,8 @@ func GetTemplateSubmoduleCommits(ctx context.Context, repoPath string) (submodul // It is only for generating new repos based on existing template, requires the .gitmodules file to be already present in the work dir. func AddTemplateSubmoduleIndexes(ctx context.Context, repoPath string, submodules []TemplateSubmoduleCommit) error { for _, submodule := range submodules { - cmd := NewCommand(ctx, "update-index", "--add", "--cacheinfo", "160000").AddDynamicArguments(submodule.Commit, submodule.Path) - if stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}); err != nil { + cmd := NewCommand("update-index", "--add", "--cacheinfo", "160000").AddDynamicArguments(submodule.Commit, submodule.Path) + if stdout, _, err := cmd.RunStdString(ctx, &RunOpts{Dir: repoPath}); err != nil { log.Error("Unable to add %s as submodule to repo %s: stdout %s\nError: %v", submodule.Path, repoPath, stdout, err) return err } diff --git a/modules/git/submodule_test.go b/modules/git/submodule_test.go index d53946a27d..7893b95e3a 100644 --- a/modules/git/submodule_test.go +++ b/modules/git/submodule_test.go @@ -4,7 +4,6 @@ package git import ( - "context" "os" "path/filepath" "testing" @@ -20,29 +19,29 @@ func TestGetTemplateSubmoduleCommits(t *testing.T) { assert.Len(t, submodules, 2) - assert.EqualValues(t, "<°)))><", submodules[0].Path) - assert.EqualValues(t, "d2932de67963f23d43e1c7ecf20173e92ee6c43c", submodules[0].Commit) + assert.Equal(t, "<°)))><", submodules[0].Path) + assert.Equal(t, "d2932de67963f23d43e1c7ecf20173e92ee6c43c", submodules[0].Commit) - assert.EqualValues(t, "libtest", submodules[1].Path) - assert.EqualValues(t, "1234567890123456789012345678901234567890", submodules[1].Commit) + assert.Equal(t, "libtest", submodules[1].Path) + assert.Equal(t, "1234567890123456789012345678901234567890", submodules[1].Commit) } func TestAddTemplateSubmoduleIndexes(t *testing.T) { - ctx := context.Background() + ctx := t.Context() tmpDir := t.TempDir() var err error - _, _, err = NewCommand(ctx, "init").RunStdString(&RunOpts{Dir: tmpDir}) + _, _, err = NewCommand("init").RunStdString(ctx, &RunOpts{Dir: tmpDir}) require.NoError(t, err) _ = os.Mkdir(filepath.Join(tmpDir, "new-dir"), 0o755) err = AddTemplateSubmoduleIndexes(ctx, tmpDir, []TemplateSubmoduleCommit{{Path: "new-dir", Commit: "1234567890123456789012345678901234567890"}}) require.NoError(t, err) - _, _, err = NewCommand(ctx, "add", "--all").RunStdString(&RunOpts{Dir: tmpDir}) + _, _, err = NewCommand("add", "--all").RunStdString(ctx, &RunOpts{Dir: tmpDir}) require.NoError(t, err) - _, _, err = NewCommand(ctx, "-c", "user.name=a", "-c", "user.email=b", "commit", "-m=test").RunStdString(&RunOpts{Dir: tmpDir}) + _, _, err = NewCommand("-c", "user.name=a", "-c", "user.email=b", "commit", "-m=test").RunStdString(ctx, &RunOpts{Dir: tmpDir}) require.NoError(t, err) submodules, err := GetTemplateSubmoduleCommits(DefaultContext, tmpDir) require.NoError(t, err) assert.Len(t, submodules, 1) - assert.EqualValues(t, "new-dir", submodules[0].Path) - assert.EqualValues(t, "1234567890123456789012345678901234567890", submodules[0].Commit) + assert.Equal(t, "new-dir", submodules[0].Path) + assert.Equal(t, "1234567890123456789012345678901234567890", submodules[0].Commit) } diff --git a/modules/git/tag.go b/modules/git/tag.go index f7666aa89b..8bf3658d62 100644 --- a/modules/git/tag.go +++ b/modules/git/tag.go @@ -21,11 +21,6 @@ type Tag struct { Signature *CommitSignature } -// Commit return the commit of the tag reference -func (tag *Tag) Commit(gitRepo *Repository) (*Commit, error) { - return gitRepo.getCommit(tag.Object) -} - func parsePayloadSignature(data []byte, messageStart int) (payload, msg, sign string) { pos := messageStart signStart, signEnd := -1, -1 diff --git a/modules/git/tests/repos/language_stats_repo/config b/modules/git/tests/repos/language_stats_repo/config index 515f483629..a4ef456cbc 100644 --- a/modules/git/tests/repos/language_stats_repo/config +++ b/modules/git/tests/repos/language_stats_repo/config @@ -1,5 +1,5 @@ [core] repositoryformatversion = 0 filemode = true - bare = false + bare = true logallrefupdates = true diff --git a/modules/git/tests/repos/repo3_notes/config b/modules/git/tests/repos/repo3_notes/config index d545cdabdb..5ed22e23d1 100644 --- a/modules/git/tests/repos/repo3_notes/config +++ b/modules/git/tests/repos/repo3_notes/config @@ -1,7 +1,7 @@ [core] repositoryformatversion = 0 filemode = false - bare = false + bare = true logallrefupdates = true symlinks = false ignorecase = true diff --git a/modules/git/tests/repos/repo4_commitsbetween/config b/modules/git/tests/repos/repo4_commitsbetween/config index d545cdabdb..5ed22e23d1 100644 --- a/modules/git/tests/repos/repo4_commitsbetween/config +++ b/modules/git/tests/repos/repo4_commitsbetween/config @@ -1,7 +1,7 @@ [core] repositoryformatversion = 0 filemode = false - bare = false + bare = true logallrefupdates = true symlinks = false ignorecase = true diff --git a/modules/git/tree.go b/modules/git/tree.go index 5a644f6c87..38fb45f3b1 100644 --- a/modules/git/tree.go +++ b/modules/git/tree.go @@ -48,15 +48,15 @@ func (t *Tree) SubTree(rpath string) (*Tree, error) { // LsTree checks if the given filenames are in the tree func (repo *Repository) LsTree(ref string, filenames ...string) ([]string, error) { - cmd := NewCommand(repo.Ctx, "ls-tree", "-z", "--name-only"). + cmd := NewCommand("ls-tree", "-z", "--name-only"). AddDashesAndList(append([]string{ref}, filenames...)...) - res, _, err := cmd.RunStdBytes(&RunOpts{Dir: repo.Path}) + res, _, err := cmd.RunStdBytes(repo.Ctx, &RunOpts{Dir: repo.Path}) if err != nil { return nil, err } filelist := make([]string, 0, len(filenames)) - for _, line := range bytes.Split(res, []byte{'\000'}) { + for line := range bytes.SplitSeq(res, []byte{'\000'}) { filelist = append(filelist, string(line)) } @@ -65,9 +65,9 @@ func (repo *Repository) LsTree(ref string, filenames ...string) ([]string, error // GetTreePathLatestCommit returns the latest commit of a tree path func (repo *Repository) GetTreePathLatestCommit(refName, treePath string) (*Commit, error) { - stdout, _, err := NewCommand(repo.Ctx, "rev-list", "-1"). + stdout, _, err := NewCommand("rev-list", "-1"). AddDynamicArguments(refName).AddDashesAndList(treePath). - RunStdString(&RunOpts{Dir: repo.Path}) + RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) if err != nil { return nil, err } diff --git a/modules/git/tree_blob_gogit.go b/modules/git/tree_blob_gogit.go index 92c25cb92c..f29e8f8b9e 100644 --- a/modules/git/tree_blob_gogit.go +++ b/modules/git/tree_blob_gogit.go @@ -21,6 +21,7 @@ func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) { return &TreeEntry{ ID: t.ID, // Type: ObjectTree, + ptree: t, gogitTreeEntry: &object.TreeEntry{ Name: "", Mode: filemode.Dir, diff --git a/modules/git/tree_blob_nogogit.go b/modules/git/tree_blob_nogogit.go index b7bcf40edd..b18d0fa05e 100644 --- a/modules/git/tree_blob_nogogit.go +++ b/modules/git/tree_blob_nogogit.go @@ -11,7 +11,7 @@ import ( ) // GetTreeEntryByPath get the tree entries according the sub dir -func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) { +func (t *Tree) GetTreeEntryByPath(relpath string) (_ *TreeEntry, err error) { if len(relpath) == 0 { return &TreeEntry{ ptree: t, @@ -21,27 +21,25 @@ func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) { }, nil } - // FIXME: This should probably use git cat-file --batch to be a bit more efficient relpath = path.Clean(relpath) parts := strings.Split(relpath, "/") - var err error + tree := t - for i, name := range parts { - if i == len(parts)-1 { - entries, err := tree.ListEntries() - if err != nil { - return nil, err - } - for _, v := range entries { - if v.Name() == name { - return v, nil - } - } - } else { - tree, err = tree.SubTree(name) - if err != nil { - return nil, err - } + for _, name := range parts[:len(parts)-1] { + tree, err = tree.SubTree(name) + if err != nil { + return nil, err + } + } + + name := parts[len(parts)-1] + entries, err := tree.ListEntries() + if err != nil { + return nil, err + } + for _, v := range entries { + if v.Name() == name { + return v, nil } } return nil, ErrNotExist{"", relpath} diff --git a/modules/git/tree_entry.go b/modules/git/tree_entry.go index 9513121487..5099d8ee79 100644 --- a/modules/git/tree_entry.go +++ b/modules/git/tree_entry.go @@ -5,9 +5,11 @@ package git import ( - "io" + "path" "sort" "strings" + + "code.gitea.io/gitea/modules/util" ) // Type returns the type of the entry (commit, tree, blob) @@ -22,83 +24,57 @@ func (te *TreeEntry) Type() string { } } -// FollowLink returns the entry pointed to by a symlink -func (te *TreeEntry) FollowLink() (*TreeEntry, error) { +type EntryFollowResult struct { + SymlinkContent string + TargetFullPath string + TargetEntry *TreeEntry +} + +func EntryFollowLink(commit *Commit, fullPath string, te *TreeEntry) (*EntryFollowResult, error) { if !te.IsLink() { - return nil, ErrBadLink{te.Name(), "not a symlink"} + return nil, util.ErrorWrap(util.ErrUnprocessableContent, "%q is not a symlink", fullPath) } - // read the link - r, err := te.Blob().DataAsync() - if err != nil { - return nil, err + // git's filename max length is 4096, hopefully a link won't be longer than multiple of that + const maxSymlinkSize = 20 * 4096 + if te.Blob().Size() > maxSymlinkSize { + return nil, util.ErrorWrap(util.ErrUnprocessableContent, "%q content exceeds symlink limit", fullPath) } - closed := false - defer func() { - if !closed { - _ = r.Close() - } - }() - buf := make([]byte, te.Size()) - _, err = io.ReadFull(r, buf) + + link, err := te.Blob().GetBlobContent(maxSymlinkSize) if err != nil { return nil, err } - _ = r.Close() - closed = true - - lnk := string(buf) - t := te.ptree - - // traverse up directories - for ; t != nil && strings.HasPrefix(lnk, "../"); lnk = lnk[3:] { - t = t.ptree + if strings.HasPrefix(link, "/") { + // It's said that absolute path will be stored as is in Git + return &EntryFollowResult{SymlinkContent: link}, util.ErrorWrap(util.ErrUnprocessableContent, "%q is an absolute symlink", fullPath) } - if t == nil { - return nil, ErrBadLink{te.Name(), "points outside of repo"} - } - - target, err := t.GetTreeEntryByPath(lnk) + targetFullPath := path.Join(path.Dir(fullPath), link) + targetEntry, err := commit.GetTreeEntryByPath(targetFullPath) if err != nil { - if IsErrNotExist(err) { - return nil, ErrBadLink{te.Name(), "broken link"} - } - return nil, err + return &EntryFollowResult{SymlinkContent: link}, err } - return target, nil + return &EntryFollowResult{SymlinkContent: link, TargetFullPath: targetFullPath, TargetEntry: targetEntry}, nil } -// FollowLinks returns the entry ultimately pointed to by a symlink -func (te *TreeEntry) FollowLinks() (*TreeEntry, error) { - if !te.IsLink() { - return nil, ErrBadLink{te.Name(), "not a symlink"} - } - entry := te - for i := 0; i < 999; i++ { - if entry.IsLink() { - next, err := entry.FollowLink() - if err != nil { - return nil, err - } - if next.ID == entry.ID { - return nil, ErrBadLink{ - entry.Name(), - "recursive link", - } - } - entry = next - } else { +func EntryFollowLinks(commit *Commit, firstFullPath string, firstTreeEntry *TreeEntry, optLimit ...int) (res *EntryFollowResult, err error) { + limit := util.OptionalArg(optLimit, 10) + treeEntry, fullPath := firstTreeEntry, firstFullPath + for range limit { + res, err = EntryFollowLink(commit, fullPath, treeEntry) + if err != nil { + return res, err + } + treeEntry, fullPath = res.TargetEntry, res.TargetFullPath + if !treeEntry.IsLink() { break } } - if entry.IsLink() { - return nil, ErrBadLink{ - te.Name(), - "too many levels of symbolic links", - } + if treeEntry.IsLink() { + return res, util.ErrorWrap(util.ErrUnprocessableContent, "%q has too many links", firstFullPath) } - return entry, nil + return res, nil } // returns the Tree pointed to by this TreeEntry, or nil if this is not a tree diff --git a/modules/git/tree_entry_common_test.go b/modules/git/tree_entry_common_test.go new file mode 100644 index 0000000000..8b63bbb993 --- /dev/null +++ b/modules/git/tree_entry_common_test.go @@ -0,0 +1,76 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "testing" + + "code.gitea.io/gitea/modules/util" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFollowLink(t *testing.T) { + r, err := openRepositoryWithDefaultContext("tests/repos/repo1_bare") + require.NoError(t, err) + defer r.Close() + + commit, err := r.GetCommit("37991dec2c8e592043f47155ce4808d4580f9123") + require.NoError(t, err) + + // get the symlink + { + lnkFullPath := "foo/bar/link_to_hello" + lnk, err := commit.Tree.GetTreeEntryByPath("foo/bar/link_to_hello") + require.NoError(t, err) + assert.True(t, lnk.IsLink()) + + // should be able to dereference to target + res, err := EntryFollowLink(commit, lnkFullPath, lnk) + require.NoError(t, err) + assert.Equal(t, "hello", res.TargetEntry.Name()) + assert.Equal(t, "foo/nar/hello", res.TargetFullPath) + assert.False(t, res.TargetEntry.IsLink()) + assert.Equal(t, "b14df6442ea5a1b382985a6549b85d435376c351", res.TargetEntry.ID.String()) + } + + { + // should error when called on a normal file + entry, err := commit.Tree.GetTreeEntryByPath("file1.txt") + require.NoError(t, err) + res, err := EntryFollowLink(commit, "file1.txt", entry) + assert.ErrorIs(t, err, util.ErrUnprocessableContent) + assert.Nil(t, res) + } + + { + // should error for broken links + entry, err := commit.Tree.GetTreeEntryByPath("foo/broken_link") + require.NoError(t, err) + assert.True(t, entry.IsLink()) + res, err := EntryFollowLink(commit, "foo/broken_link", entry) + assert.ErrorIs(t, err, util.ErrNotExist) + assert.Equal(t, "nar/broken_link", res.SymlinkContent) + } + + { + // should error for external links + entry, err := commit.Tree.GetTreeEntryByPath("foo/outside_repo") + require.NoError(t, err) + assert.True(t, entry.IsLink()) + res, err := EntryFollowLink(commit, "foo/outside_repo", entry) + assert.ErrorIs(t, err, util.ErrNotExist) + assert.Equal(t, "../../outside_repo", res.SymlinkContent) + } + + { + // testing fix for short link bug + entry, err := commit.Tree.GetTreeEntryByPath("foo/link_short") + require.NoError(t, err) + res, err := EntryFollowLink(commit, "foo/link_short", entry) + assert.ErrorIs(t, err, util.ErrNotExist) + assert.Equal(t, "a", res.SymlinkContent) + } +} diff --git a/modules/git/tree_entry_gogit.go b/modules/git/tree_entry_gogit.go index eb9b012681..e6845f1c77 100644 --- a/modules/git/tree_entry_gogit.go +++ b/modules/git/tree_entry_gogit.go @@ -19,16 +19,12 @@ type TreeEntry struct { gogitTreeEntry *object.TreeEntry ptree *Tree - size int64 - sized bool - fullName string + size int64 + sized bool } // Name returns the name of the entry func (te *TreeEntry) Name() string { - if te.fullName != "" { - return te.fullName - } return te.gogitTreeEntry.Name } @@ -55,7 +51,7 @@ func (te *TreeEntry) Size() int64 { return te.size } -// IsSubModule if the entry is a sub module +// IsSubModule if the entry is a submodule func (te *TreeEntry) IsSubModule() bool { return te.gogitTreeEntry.Mode == filemode.Submodule } diff --git a/modules/git/tree_entry_mode.go b/modules/git/tree_entry_mode.go index a399118cf8..f36c07bc2a 100644 --- a/modules/git/tree_entry_mode.go +++ b/modules/git/tree_entry_mode.go @@ -3,7 +3,10 @@ package git -import "strconv" +import ( + "fmt" + "strconv" +) // EntryMode the type of the object in the git tree type EntryMode int @@ -11,16 +14,15 @@ type EntryMode int // There are only a few file modes in Git. They look like unix file modes, but they can only be // one of these. const ( - // EntryModeBlob - EntryModeBlob EntryMode = 0o100644 - // EntryModeExec - EntryModeExec EntryMode = 0o100755 - // EntryModeSymlink + // EntryModeNoEntry is possible if the file was added or removed in a commit. In the case of + // when adding the base commit doesn't have the file in its tree, a mode of 0o000000 is used. + EntryModeNoEntry EntryMode = 0o000000 + + EntryModeBlob EntryMode = 0o100644 + EntryModeExec EntryMode = 0o100755 EntryModeSymlink EntryMode = 0o120000 - // EntryModeCommit - EntryModeCommit EntryMode = 0o160000 - // EntryModeTree - EntryModeTree EntryMode = 0o040000 + EntryModeCommit EntryMode = 0o160000 + EntryModeTree EntryMode = 0o040000 ) // String converts an EntryMode to a string @@ -28,8 +30,46 @@ func (e EntryMode) String() string { return strconv.FormatInt(int64(e), 8) } -// ToEntryMode converts a string to an EntryMode -func ToEntryMode(value string) EntryMode { - v, _ := strconv.ParseInt(value, 8, 32) - return EntryMode(v) +// IsSubModule if the entry is a submodule +func (e EntryMode) IsSubModule() bool { + return e == EntryModeCommit +} + +// IsDir if the entry is a sub dir +func (e EntryMode) IsDir() bool { + return e == EntryModeTree +} + +// IsLink if the entry is a symlink +func (e EntryMode) IsLink() bool { + return e == EntryModeSymlink +} + +// IsRegular if the entry is a regular file +func (e EntryMode) IsRegular() bool { + return e == EntryModeBlob +} + +// IsExecutable if the entry is an executable file (not necessarily binary) +func (e EntryMode) IsExecutable() bool { + return e == EntryModeExec +} + +func ParseEntryMode(mode string) (EntryMode, error) { + switch mode { + case "000000": + return EntryModeNoEntry, nil + case "100644": + return EntryModeBlob, nil + case "100755": + return EntryModeExec, nil + case "120000": + return EntryModeSymlink, nil + case "160000": + return EntryModeCommit, nil + case "040000", "040755": // git uses 040000 for tree object, but some users may get 040755 for unknown reasons + return EntryModeTree, nil + default: + return 0, fmt.Errorf("unparsable entry mode: %s", mode) + } } diff --git a/modules/git/tree_entry_nogogit.go b/modules/git/tree_entry_nogogit.go index 81fb638d56..8fad96cdf8 100644 --- a/modules/git/tree_entry_nogogit.go +++ b/modules/git/tree_entry_nogogit.go @@ -18,7 +18,7 @@ type TreeEntry struct { sized bool } -// Name returns the name of the entry +// Name returns the name of the entry (base name) func (te *TreeEntry) Name() string { return te.name } @@ -57,29 +57,29 @@ func (te *TreeEntry) Size() int64 { return te.size } -// IsSubModule if the entry is a sub module +// IsSubModule if the entry is a submodule func (te *TreeEntry) IsSubModule() bool { - return te.entryMode == EntryModeCommit + return te.entryMode.IsSubModule() } // IsDir if the entry is a sub dir func (te *TreeEntry) IsDir() bool { - return te.entryMode == EntryModeTree + return te.entryMode.IsDir() } // IsLink if the entry is a symlink func (te *TreeEntry) IsLink() bool { - return te.entryMode == EntryModeSymlink + return te.entryMode.IsLink() } // IsRegular if the entry is a regular file func (te *TreeEntry) IsRegular() bool { - return te.entryMode == EntryModeBlob + return te.entryMode.IsRegular() } // IsExecutable if the entry is an executable file (not necessarily binary) func (te *TreeEntry) IsExecutable() bool { - return te.entryMode == EntryModeExec + return te.entryMode.IsExecutable() } // Blob returns the blob object the entry diff --git a/modules/git/tree_entry_test.go b/modules/git/tree_entry_test.go index 30eee13669..9ca82675e0 100644 --- a/modules/git/tree_entry_test.go +++ b/modules/git/tree_entry_test.go @@ -53,50 +53,3 @@ func TestEntriesCustomSort(t *testing.T) { assert.Equal(t, "bcd", entries[6].Name()) assert.Equal(t, "abc", entries[7].Name()) } - -func TestFollowLink(t *testing.T) { - r, err := openRepositoryWithDefaultContext("tests/repos/repo1_bare") - assert.NoError(t, err) - defer r.Close() - - commit, err := r.GetCommit("37991dec2c8e592043f47155ce4808d4580f9123") - assert.NoError(t, err) - - // get the symlink - lnk, err := commit.Tree.GetTreeEntryByPath("foo/bar/link_to_hello") - assert.NoError(t, err) - assert.True(t, lnk.IsLink()) - - // should be able to dereference to target - target, err := lnk.FollowLink() - assert.NoError(t, err) - assert.Equal(t, "hello", target.Name()) - assert.False(t, target.IsLink()) - assert.Equal(t, "b14df6442ea5a1b382985a6549b85d435376c351", target.ID.String()) - - // should error when called on normal file - target, err = commit.Tree.GetTreeEntryByPath("file1.txt") - assert.NoError(t, err) - _, err = target.FollowLink() - assert.EqualError(t, err, "file1.txt: not a symlink") - - // should error for broken links - target, err = commit.Tree.GetTreeEntryByPath("foo/broken_link") - assert.NoError(t, err) - assert.True(t, target.IsLink()) - _, err = target.FollowLink() - assert.EqualError(t, err, "broken_link: broken link") - - // should error for external links - target, err = commit.Tree.GetTreeEntryByPath("foo/outside_repo") - assert.NoError(t, err) - assert.True(t, target.IsLink()) - _, err = target.FollowLink() - assert.EqualError(t, err, "outside_repo: points outside of repo") - - // testing fix for short link bug - target, err = commit.Tree.GetTreeEntryByPath("foo/link_short") - assert.NoError(t, err) - _, err = target.FollowLink() - assert.EqualError(t, err, "link_short: broken link") -} diff --git a/modules/git/tree_gogit.go b/modules/git/tree_gogit.go index 421b0ecb0f..272b018ffd 100644 --- a/modules/git/tree_gogit.go +++ b/modules/git/tree_gogit.go @@ -69,7 +69,7 @@ func (t *Tree) ListEntriesRecursiveWithSize() (Entries, error) { seen := map[plumbing.Hash]bool{} walker := object.NewTreeWalker(t.gogitTree, true, seen) for { - fullName, entry, err := walker.Next() + _, entry, err := walker.Next() if err == io.EOF { break } @@ -84,7 +84,6 @@ func (t *Tree) ListEntriesRecursiveWithSize() (Entries, error) { ID: ParseGogitHash(entry.Hash), gogitTreeEntry: &entry, ptree: t, - fullName: fullName, } entries = append(entries, convertedEntry) } diff --git a/modules/git/tree_nogogit.go b/modules/git/tree_nogogit.go index 993b98edc2..f88788418e 100644 --- a/modules/git/tree_nogogit.go +++ b/modules/git/tree_nogogit.go @@ -70,7 +70,7 @@ func (t *Tree) ListEntries() (Entries, error) { } } - stdout, _, runErr := NewCommand(t.repo.Ctx, "ls-tree", "-l").AddDynamicArguments(t.ID.String()).RunStdBytes(&RunOpts{Dir: t.repo.Path}) + stdout, _, runErr := NewCommand("ls-tree", "-l").AddDynamicArguments(t.ID.String()).RunStdBytes(t.repo.Ctx, &RunOpts{Dir: t.repo.Path}) if runErr != nil { if strings.Contains(runErr.Error(), "fatal: Not a valid object name") || strings.Contains(runErr.Error(), "fatal: not a tree object") { return nil, ErrNotExist{ @@ -96,10 +96,10 @@ func (t *Tree) listEntriesRecursive(extraArgs TrustedCmdArgs) (Entries, error) { return t.entriesRecursive, nil } - stdout, _, runErr := NewCommand(t.repo.Ctx, "ls-tree", "-t", "-r"). + stdout, _, runErr := NewCommand("ls-tree", "-t", "-r"). AddArguments(extraArgs...). AddDynamicArguments(t.ID.String()). - RunStdBytes(&RunOpts{Dir: t.repo.Path}) + RunStdBytes(t.repo.Ctx, &RunOpts{Dir: t.repo.Path}) if runErr != nil { return nil, runErr } diff --git a/modules/git/tree_test.go b/modules/git/tree_test.go index 5fee64b038..cae11c4b1b 100644 --- a/modules/git/tree_test.go +++ b/modules/git/tree_test.go @@ -19,7 +19,7 @@ func TestSubTree_Issue29101(t *testing.T) { assert.NoError(t, err) // old code could produce a different error if called multiple times - for i := 0; i < 10; i++ { + for range 10 { _, err = commit.SubTree("file1.txt") assert.Error(t, err) assert.True(t, IsErrNotExist(err)) @@ -33,10 +33,10 @@ func Test_GetTreePathLatestCommit(t *testing.T) { commitID, err := repo.GetBranchCommitID("master") assert.NoError(t, err) - assert.EqualValues(t, "544d8f7a3b15927cddf2299b4b562d6ebd71b6a7", commitID) + assert.Equal(t, "544d8f7a3b15927cddf2299b4b562d6ebd71b6a7", commitID) commit, err := repo.GetTreePathLatestCommit("master", "blame.txt") assert.NoError(t, err) assert.NotNil(t, commit) - assert.EqualValues(t, "45fb6cbc12f970b04eacd5cd4165edd11c8d7376", commit.ID.String()) + assert.Equal(t, "45fb6cbc12f970b04eacd5cd4165edd11c8d7376", commit.ID.String()) } diff --git a/modules/git/url/url.go b/modules/git/url/url.go index 1c5e8377a6..aa6fa31c5e 100644 --- a/modules/git/url/url.go +++ b/modules/git/url/url.go @@ -133,12 +133,13 @@ func ParseRepositoryURL(ctx context.Context, repoURL string) (*RepositoryURL, er } } - if parsed.URL.Scheme == "http" || parsed.URL.Scheme == "https" { + switch parsed.URL.Scheme { + case "http", "https": if !httplib.IsCurrentGiteaSiteURL(ctx, repoURL) { return ret, nil } fillPathParts(strings.TrimPrefix(parsed.URL.Path, setting.AppSubURL)) - } else if parsed.URL.Scheme == "ssh" || parsed.URL.Scheme == "git+ssh" { + case "ssh", "git+ssh": domainSSH := setting.SSH.Domain domainCur := httplib.GuessCurrentHostDomain(ctx) urlDomain, _, _ := net.SplitHostPort(parsed.URL.Host) @@ -166,9 +167,10 @@ func MakeRepositoryWebLink(repoURL *RepositoryURL) string { // now, let's guess, for example: // * git@github.com:owner/submodule.git // * https://github.com/example/submodule1.git - if repoURL.GitURL.Scheme == "http" || repoURL.GitURL.Scheme == "https" { + switch repoURL.GitURL.Scheme { + case "http", "https": return strings.TrimSuffix(repoURL.GitURL.String(), ".git") - } else if repoURL.GitURL.Scheme == "ssh" || repoURL.GitURL.Scheme == "git+ssh" { + case "ssh", "git+ssh": hostname, _, _ := net.SplitHostPort(repoURL.GitURL.Host) hostname = util.IfZero(hostname, repoURL.GitURL.Host) urlPath := strings.TrimSuffix(repoURL.GitURL.Path, ".git") diff --git a/modules/git/url/url_test.go b/modules/git/url/url_test.go index 9c020adb4d..6655c20be3 100644 --- a/modules/git/url/url_test.go +++ b/modules/git/url/url_test.go @@ -165,8 +165,8 @@ func TestParseGitURLs(t *testing.T) { t.Run(kase.kase, func(t *testing.T) { u, err := ParseGitURL(kase.kase) assert.NoError(t, err) - assert.EqualValues(t, kase.expected.extraMark, u.extraMark) - assert.EqualValues(t, *kase.expected, *u) + assert.Equal(t, kase.expected.extraMark, u.extraMark) + assert.Equal(t, *kase.expected, *u) }) } } @@ -179,7 +179,7 @@ func TestParseRepositoryURL(t *testing.T) { ctxReq := &http.Request{URL: ctxURL, Header: http.Header{}} ctxReq.Host = ctxURL.Host ctxReq.Header.Add("X-Forwarded-Proto", ctxURL.Scheme) - ctx := context.WithValue(context.Background(), httplib.RequestContextKey, ctxReq) + ctx := context.WithValue(t.Context(), httplib.RequestContextKey, ctxReq) cases := []struct { input string ownerName, repoName, remaining string @@ -249,19 +249,19 @@ func TestMakeRepositoryBaseLink(t *testing.T) { defer test.MockVariableValue(&setting.AppURL, "https://localhost:3000/subpath")() defer test.MockVariableValue(&setting.AppSubURL, "/subpath")() - u, err := ParseRepositoryURL(context.Background(), "https://localhost:3000/subpath/user/repo.git") + u, err := ParseRepositoryURL(t.Context(), "https://localhost:3000/subpath/user/repo.git") assert.NoError(t, err) assert.Equal(t, "/subpath/user/repo", MakeRepositoryWebLink(u)) - u, err = ParseRepositoryURL(context.Background(), "https://github.com/owner/repo.git") + u, err = ParseRepositoryURL(t.Context(), "https://github.com/owner/repo.git") assert.NoError(t, err) assert.Equal(t, "https://github.com/owner/repo", MakeRepositoryWebLink(u)) - u, err = ParseRepositoryURL(context.Background(), "git@github.com:owner/repo.git") + u, err = ParseRepositoryURL(t.Context(), "git@github.com:owner/repo.git") assert.NoError(t, err) assert.Equal(t, "https://github.com/owner/repo", MakeRepositoryWebLink(u)) - u, err = ParseRepositoryURL(context.Background(), "git+ssh://other:123/owner/repo.git") + u, err = ParseRepositoryURL(t.Context(), "git+ssh://other:123/owner/repo.git") assert.NoError(t, err) assert.Equal(t, "https://other/owner/repo", MakeRepositoryWebLink(u)) } diff --git a/modules/git/utils.go b/modules/git/utils.go index 56cba9087a..897306efd0 100644 --- a/modules/git/utils.go +++ b/modules/git/utils.go @@ -8,7 +8,6 @@ import ( "encoding/hex" "fmt" "io" - "os" "strconv" "strings" "sync" @@ -41,33 +40,6 @@ func (oc *ObjectCache[T]) Get(id string) (T, bool) { return obj, has } -// isDir returns true if given path is a directory, -// or returns false when it's a file or does not exist. -func isDir(dir string) bool { - f, e := os.Stat(dir) - if e != nil { - return false - } - return f.IsDir() -} - -// isFile returns true if given path is a file, -// or returns false when it's a directory or does not exist. -func isFile(filePath string) bool { - f, e := os.Stat(filePath) - if e != nil { - return false - } - return !f.IsDir() -} - -// isExist checks whether a file or directory exists. -// It returns false when the file or directory does not exist. -func isExist(path string) bool { - _, err := os.Stat(path) - return err == nil || os.IsExist(err) -} - // ConcatenateError concatenats an error with stderr string func ConcatenateError(err error, stderr string) error { if len(stderr) == 0 { |