refactor!: remove go-git backend (#39487)

Removes the go-git backend so every build uses the git CLI backend. Its
Windows performance advantage is gone, it lacks SHA-256 support, and it
breaks repositories on Windows.

The performance changes moved to
https://github.com/go-gitea/gitea/pull/39526.

Fixes https://github.com/go-gitea/gitea/issues/38359
Fixes https://github.com/go-gitea/gitea/issues/34694

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
silverwind
2026-10-03 12:04:24 +02:00
committed by GitHub
parent 3932624947
commit ad38b60983
69 changed files with 995 additions and 2741 deletions
-5
View File
@@ -15,11 +15,6 @@ func TestReadingBlameOutputSha256(t *testing.T) {
setting.AppDataPath = t.TempDir()
ctx := t.Context()
if DefaultFeatures().UsingGogit {
t.Skip("Skipping test since gogit does not support sha256")
return
}
t.Run("Without .git-blame-ignore-revs", func(t *testing.T) {
storage := mockRepository("repo5_pulls_sha256")
repo, err := OpenRepository(ctx, storage)
+96 -2
View File
@@ -12,11 +12,10 @@ import (
"io"
"strings"
"gitea.dev/modules/log"
"gitea.dev/modules/util"
)
// This file contains common functions between the gogit and !gogit variants for git Blobs
// Name returns name of the tree entry this blob object was created from (or empty string)
func (b *Blob) Name() string {
return b.name
@@ -114,3 +113,98 @@ loop:
_ = encoder.Close()
return base64buf.String(), nil
}
// Blob represents a Git object.
type Blob struct {
ID ObjectID
gotSize bool
size int64
name string
repo *Repository
}
// DataAsync gets a ReadCloser for the contents of a blob without reading it all.
// Calling the Close function on the result will discard all unread output.
func (b *Blob) DataAsync(ctx context.Context) (_ io.ReadCloser, retErr error) {
batch, cancel, err := b.repo.CatFileBatch()
if err != nil {
return nil, err
}
defer func() {
// if there was an error, cancel the batch right away,
// otherwise let the caller close it
if retErr != nil {
cancel()
}
}()
info, contentReader, err := batch.QueryContent(b.ID.String())
if err != nil {
return nil, err
}
b.gotSize = true
b.size = info.Size
return &blobReader{
rd: contentReader,
n: info.Size,
cancel: cancel,
}, nil
}
// Size returns the uncompressed size of the blob
func (b *Blob) Size(ctx context.Context) int64 {
if b.gotSize {
return b.size
}
batch, cancel, err := b.repo.CatFileBatch()
if err != nil {
log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.LogString(), err)
return 0
}
defer cancel()
info, err := batch.QueryInfo(b.ID.String())
if err != nil {
log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.LogString(), err)
return 0
}
b.gotSize = true
b.size = info.Size
return b.size
}
type blobReader struct {
rd BufferedReader
n int64
cancel func()
}
func (b *blobReader) Read(p []byte) (n int, err error) {
if b.n <= 0 {
return 0, io.EOF
}
if int64(len(p)) > b.n {
p = p[0:b.n]
}
n, err = b.rd.Read(p)
b.n -= int64(n)
return n, err
}
// Close implements io.Closer
func (b *blobReader) Close() error {
if b.rd == nil {
return nil
}
defer b.cancel()
if err := DiscardFull(b.rd, b.n+1); err != nil {
return err
}
b.rd = nil
return nil
}
-47
View File
@@ -1,47 +0,0 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"context"
"io"
"gitea.dev/modules/log"
"github.com/go-git/go-git/v5/plumbing"
)
// Blob represents a Git object.
type Blob struct {
ID ObjectID
repo *Repository
name string
}
func (b *Blob) gogitEncodedObj() (plumbing.EncodedObject, error) {
return b.repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, plumbing.Hash(b.ID.RawValue()))
}
// DataAsync gets a ReadCloser for the contents of a blob without reading it all.
// Calling the Close function on the result will discard all unread output.
func (b *Blob) DataAsync(_ context.Context) (io.ReadCloser, error) {
obj, err := b.gogitEncodedObj()
if err != nil {
return nil, err
}
return obj.Reader()
}
// Size returns the uncompressed size of the blob
func (b *Blob) Size(_ context.Context) int64 {
obj, err := b.gogitEncodedObj()
if err != nil {
log.Error("Error getting gogit encoded object for blob %s(%s): %v", b.name, b.ID.String(), err)
return 0
}
return obj.Size()
}
-108
View File
@@ -1,108 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (
"context"
"io"
"gitea.dev/modules/log"
)
// Blob represents a Git object.
type Blob struct {
ID ObjectID
gotSize bool
size int64
name string
repo *Repository
}
// DataAsync gets a ReadCloser for the contents of a blob without reading it all.
// Calling the Close function on the result will discard all unread output.
func (b *Blob) DataAsync(ctx context.Context) (_ io.ReadCloser, retErr error) {
batch, cancel, err := b.repo.CatFileBatch()
if err != nil {
return nil, err
}
defer func() {
// if there was an error, cancel the batch right away,
// otherwise let the caller close it
if retErr != nil {
cancel()
}
}()
info, contentReader, err := batch.QueryContent(b.ID.String())
if err != nil {
return nil, err
}
b.gotSize = true
b.size = info.Size
return &blobReader{
rd: contentReader,
n: info.Size,
cancel: cancel,
}, nil
}
// Size returns the uncompressed size of the blob
func (b *Blob) Size(ctx context.Context) int64 {
if b.gotSize {
return b.size
}
batch, cancel, err := b.repo.CatFileBatch()
if err != nil {
log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.LogString(), err)
return 0
}
defer cancel()
info, err := batch.QueryInfo(b.ID.String())
if err != nil {
log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.LogString(), err)
return 0
}
b.gotSize = true
b.size = info.Size
return b.size
}
type blobReader struct {
rd BufferedReader
n int64
cancel func()
}
func (b *blobReader) Read(p []byte) (n int, err error) {
if b.n <= 0 {
return 0, io.EOF
}
if int64(len(p)) > b.n {
p = p[0:b.n]
}
n, err = b.rd.Read(p)
b.n -= int64(n)
return n, err
}
// Close implements io.Closer
func (b *blobReader) Close() error {
if b.rd == nil {
return nil
}
defer b.cancel()
if err := DiscardFull(b.rd, b.n+1); err != nil {
return err
}
b.rd = nil
return nil
}
-76
View File
@@ -1,76 +0,0 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"fmt"
"strings"
"github.com/go-git/go-git/v5/plumbing/object"
)
func convertPGPSignature(c *object.Commit) *CommitSignature {
if c.PGPSignature == "" {
return nil
}
var w strings.Builder
var err error
if _, err = fmt.Fprintf(&w, "tree %s\n", c.TreeHash.String()); err != nil {
return nil
}
for _, parent := range c.ParentHashes {
if _, err = fmt.Fprintf(&w, "parent %s\n", parent.String()); err != nil {
return nil
}
}
if _, err = fmt.Fprint(&w, "author "); err != nil {
return nil
}
if err = c.Author.Encode(&w); err != nil {
return nil
}
if _, err = fmt.Fprint(&w, "\ncommitter "); err != nil {
return nil
}
if err = c.Committer.Encode(&w); err != nil {
return nil
}
if c.Encoding != "" && c.Encoding != "UTF-8" {
if _, err = fmt.Fprintf(&w, "\nencoding %s\n", c.Encoding); err != nil {
return nil
}
}
if _, err = fmt.Fprintf(&w, "\n\n%s", c.Message); err != nil {
return nil
}
return &CommitSignature{
Signature: c.PGPSignature,
Payload: w.String(),
}
}
func convertCommit(c *object.Commit) *Commit {
return &Commit{
ID: ParseGogitHash(c.Hash),
TreeID: ParseGogitHash(c.TreeHash),
CommitMessage: CommitMessage{MessageRaw: c.Message},
Committer: &c.Committer,
Author: &c.Author,
Signature: convertPGPSignature(c),
Parents: ParseGogitHashArray(c.ParentHashes),
}
}
+33
View File
@@ -113,3 +113,36 @@ func getLastCommitForPathsByCache(ctx context.Context, commitID, treePath string
return results, unHitEntryPaths, nil
}
// GetLastCommitForPaths returns last commit information
func GetLastCommitForPaths(ctx context.Context, gitRepo *Repository, commit *Commit, treePath string, paths []string) (map[string]*Commit, error) {
// We read backwards from the commit to obtain all of the commits
revs, err := walkGitLog(ctx, gitRepo, commit, treePath, paths...)
if err != nil {
return nil, err
}
commitsMap := map[string]*Commit{}
commitsMap[commit.ID.String()] = commit
commitCommits := map[string]*Commit{}
for path, commitID := range revs {
if len(commitID) == 0 {
continue
}
c, ok := commitsMap[commitID]
if ok {
commitCommits[path] = c
continue
}
c, err := gitRepo.GetCommit(ctx, commitID) // Ensure the commit exists in the repository
if err != nil {
return nil, err
}
commitCommits[path] = c
}
return commitCommits, nil
}
-207
View File
@@ -1,207 +0,0 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"context"
"path"
"github.com/emirpasic/gods/trees/binaryheap"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
cgobject "github.com/go-git/go-git/v5/plumbing/object/commitgraph"
)
type commitAndPaths struct {
commit cgobject.CommitNode
// Paths that are still on the branch represented by commit
paths []string
// Set of hashes for the paths
hashes map[string]plumbing.Hash
}
func getCommitTree(c cgobject.CommitNode, treePath string) (*object.Tree, error) {
tree, err := c.Tree()
if err != nil {
return nil, err
}
// Optimize deep traversals by focusing only on the specific tree
if treePath != "" {
tree, err = tree.Tree(treePath)
if err != nil {
return nil, err
}
}
return tree, nil
}
func getFileHashes(c cgobject.CommitNode, treePath string, paths []string) (map[string]plumbing.Hash, error) {
tree, err := getCommitTree(c, treePath)
if err == object.ErrDirectoryNotFound {
// The whole tree didn't exist, so return empty map
return make(map[string]plumbing.Hash), nil
}
if err != nil {
return nil, err
}
hashes := make(map[string]plumbing.Hash)
for _, path := range paths {
if path != "" {
entry, err := tree.FindEntry(path)
if err == nil {
hashes[path] = entry.Hash
}
} else {
hashes[path] = tree.Hash
}
}
return hashes, nil
}
// GetLastCommitForPaths returns last commit information
func GetLastCommitForPaths(ctx context.Context, gitRepo *Repository, commit *Commit, treePath string, paths []string) (map[string]*Commit, error) {
commitNodeIndex, closer := gitRepo.CommitNodeIndex()
defer closer()
c, err := commitNodeIndex.Get(plumbing.Hash(commit.ID.RawValue()))
if err != nil {
return nil, err
}
return getLastCommitForPathsByCommitNode(ctx, gitRepo, c, treePath, paths)
}
func getLastCommitForPathsByCommitNode(ctx context.Context, gitRepo *Repository, c cgobject.CommitNode, treePath string, paths []string) (map[string]*Commit, error) {
refSha := c.ID().String()
// We do a tree traversal with nodes sorted by commit time
heap := binaryheap.NewWith(func(a, b any) int {
if a.(*commitAndPaths).commit.CommitTime().Before(b.(*commitAndPaths).commit.CommitTime()) { //nolint:forcetypeassert // this heap only ever holds *commitAndPaths
return 1
}
return -1
})
resultNodes := make(map[string]cgobject.CommitNode)
initialHashes, err := getFileHashes(c, treePath, paths)
if err != nil {
return nil, err
}
// Start search from the root commit and with full set of paths
heap.Push(&commitAndPaths{c, paths, initialHashes})
heaploop:
for {
select {
case <-ctx.Done():
if ctx.Err() == context.DeadlineExceeded {
break heaploop
}
return nil, ctx.Err()
default:
}
cIn, ok := heap.Pop()
if !ok {
break
}
current := cIn.(*commitAndPaths) //nolint:forcetypeassert // this heap only ever holds *commitAndPaths
// Load the parent commits for the one we are currently examining
numParents := current.commit.NumParents()
var parents []cgobject.CommitNode
for i := range numParents {
parent, err := current.commit.ParentNode(i)
if err != nil {
break
}
parents = append(parents, parent)
}
// Examine the current commit and set of interesting paths
pathUnchanged := make([]bool, len(current.paths))
parentHashes := make([]map[string]plumbing.Hash, len(parents))
for j, parent := range parents {
parentHashes[j], err = getFileHashes(parent, treePath, current.paths)
if err != nil {
break
}
for i, path := range current.paths {
if parentHashes[j][path] == current.hashes[path] {
pathUnchanged[i] = true
}
}
}
var remainingPaths []string
for i, pth := range current.paths {
// The results could already contain some newer change for the same path,
// so don't override that and bail out on the file early.
if resultNodes[pth] == nil {
if pathUnchanged[i] {
// The path existed with the same hash in at least one parent so it could
// not have been changed in this commit directly.
remainingPaths = append(remainingPaths, pth)
} else {
// There are few possible cases how can we get here:
// - The path didn't exist in any parent, so it must have been created by
// this commit.
// - The path did exist in the parent commit, but the hash of the file has
// changed.
// - We are looking at a merge commit and the hash of the file doesn't
// match any of the hashes being merged. This is more common for directories,
// but it can also happen if a file is changed through conflict resolution.
resultNodes[pth] = current.commit
if err := gitRepo.LastCommitCache.Put(refSha, path.Join(treePath, pth), current.commit.ID().String()); err != nil {
return nil, err
}
}
}
}
if len(remainingPaths) > 0 {
// Add the parent nodes along with remaining paths to the heap for further
// processing.
for j, parent := range parents {
// Combine remainingPath with paths available on the parent branch
// and make union of them
remainingPathsForParent := make([]string, 0, len(remainingPaths))
newRemainingPaths := make([]string, 0, len(remainingPaths))
for _, path := range remainingPaths {
if parentHashes[j][path] == current.hashes[path] {
remainingPathsForParent = append(remainingPathsForParent, path)
} else {
newRemainingPaths = append(newRemainingPaths, path)
}
}
if remainingPathsForParent != nil {
heap.Push(&commitAndPaths{parent, remainingPathsForParent, parentHashes[j]})
}
if len(newRemainingPaths) == 0 {
break
}
remainingPaths = newRemainingPaths
}
}
}
// Post-processing
result := make(map[string]*Commit)
for path, commitNode := range resultNodes {
commit, err := commitNode.Commit()
if err != nil {
return nil, err
}
result[path] = convertCommit(commit)
}
return result, nil
}
-43
View File
@@ -1,43 +0,0 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (
"context"
)
// GetLastCommitForPaths returns last commit information
func GetLastCommitForPaths(ctx context.Context, gitRepo *Repository, commit *Commit, treePath string, paths []string) (map[string]*Commit, error) {
// We read backwards from the commit to obtain all of the commits
revs, err := walkGitLog(ctx, gitRepo, commit, treePath, paths...)
if err != nil {
return nil, err
}
commitsMap := map[string]*Commit{}
commitsMap[commit.ID.String()] = commit
commitCommits := map[string]*Commit{}
for path, commitID := range revs {
if len(commitID) == 0 {
continue
}
c, ok := commitsMap[commitID]
if ok {
commitCommits[path] = c
continue
}
c, err := gitRepo.GetCommit(ctx, commitID) // Ensure the commit exists in the repository
if err != nil {
return nil, err
}
commitCommits[path] = c
}
return commitCommits, nil
}
-57
View File
@@ -1,57 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (
"context"
"path/filepath"
"testing"
"time"
"gitea.dev/modules/git/gitrepo"
"gitea.dev/modules/test"
"gitea.dev/modules/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEntries_GetCommitsInfo_ContextErr(t *testing.T) {
repoPath, _ := filepath.Abs(filepath.Join(testReposDir, "repo1_bare"))
repo, err := OpenRepository(t.Context(), gitrepo.RepositoryManaged("dummy", repoPath))
require.NoError(t, err)
defer repo.Close()
commit, err := repo.GetCommit(t.Context(), "feaf4ba6bc635fec442f46ddd4512416ec43c2c2")
require.NoError(t, err)
entries, err := commit.Tree().ListEntries(t.Context(), repo)
require.NoError(t, err)
countCommitInfosCommit := func(infos []CommitInfo) (nilCommits, nonNilCommits int) {
for _, info := range infos {
nilCommits += util.Iif(info.Commit == nil, 1, 0)
nonNilCommits += util.Iif(info.Commit != nil, 1, 0)
}
return nilCommits, nonNilCommits
}
ctx, cancel := context.WithCancel(t.Context())
defer test.MockVariableValue(&walkGitLogDebugBeforeNext)()
walkGitLogDebugBeforeNext = cancel
commitInfos, _, err := entries.GetCommitsInfo(ctx, time.Second, "/any/repo-link", repo, commit, "")
assert.NoError(t, err)
nilCommits, nonNilCommits := countCommitInfosCommit(commitInfos)
assert.Equal(t, 0, nonNilCommits) // no commit info due to canceled (or deadline-exceeded) context
assert.Equal(t, 3, nilCommits)
walkGitLogDebugBeforeNext = nil
commitInfos, _, err = entries.GetCommitsInfo(t.Context(), time.Second, "/any/repo-link", repo, commit, "")
assert.NoError(t, err)
nilCommits, nonNilCommits = countCommitInfosCommit(commitInfos)
assert.Equal(t, 3, nonNilCommits)
assert.Equal(t, 0, nilCommits)
}
+40
View File
@@ -4,11 +4,14 @@
package git
import (
"context"
"path/filepath"
"testing"
"time"
"gitea.dev/modules/git/gitrepo"
"gitea.dev/modules/test"
"gitea.dev/modules/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -164,3 +167,40 @@ func TestEntries_GetCommitsInfo(t *testing.T) {
assert.Nil(t, cisf.SubmoduleWebLinkTree(t.Context()))
})
}
func TestEntries_GetCommitsInfo_ContextErr(t *testing.T) {
repoPath, _ := filepath.Abs(filepath.Join(testReposDir, "repo1_bare"))
repo, err := OpenRepository(t.Context(), gitrepo.RepositoryManaged("dummy", repoPath))
require.NoError(t, err)
defer repo.Close()
commit, err := repo.GetCommit(t.Context(), "feaf4ba6bc635fec442f46ddd4512416ec43c2c2")
require.NoError(t, err)
entries, err := commit.Tree().ListEntries(t.Context(), repo)
require.NoError(t, err)
countCommitInfosCommit := func(infos []CommitInfo) (nilCommits, nonNilCommits int) {
for _, info := range infos {
nilCommits += util.Iif(info.Commit == nil, 1, 0)
nonNilCommits += util.Iif(info.Commit != nil, 1, 0)
}
return nilCommits, nonNilCommits
}
ctx, cancel := context.WithCancel(t.Context())
defer test.MockVariableValue(&walkGitLogDebugBeforeNext)()
walkGitLogDebugBeforeNext = cancel
commitInfos, _, err := entries.GetCommitsInfo(ctx, time.Second, "/any/repo-link", repo, commit, "")
assert.NoError(t, err)
nilCommits, nonNilCommits := countCommitInfosCommit(commitInfos)
assert.Equal(t, 0, nonNilCommits) // no commit info due to canceled (or deadline-exceeded) context
assert.Equal(t, 3, nilCommits)
walkGitLogDebugBeforeNext = nil
commitInfos, _, err = entries.GetCommitsInfo(t.Context(), time.Second, "/any/repo-link", repo, commit, "")
assert.NoError(t, err)
nilCommits, nonNilCommits = countCommitInfosCommit(commitInfos)
assert.Equal(t, 3, nonNilCommits)
assert.Equal(t, 0, nilCommits)
}
-2
View File
@@ -1,8 +1,6 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (
+2 -3
View File
@@ -29,7 +29,6 @@ const RequiredVersion = "2.25.0" // the minimum Git version required
type Features struct {
gitVersion *version.Version
UsingGogit bool
SupportProcReceive bool // >= 2.29
SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’
SupportedObjectFormats []ObjectFormat // sha1, sha256
@@ -79,9 +78,9 @@ func loadGitVersionFeatures() (*Features, error) {
return nil, err
}
features := &Features{gitVersion: ver, UsingGogit: isGogit}
features := &Features{gitVersion: ver}
features.SupportProcReceive = features.CheckVersionAtLeast("2.29")
features.SupportHashSha256 = features.CheckVersionAtLeast("2.42") && !isGogit
features.SupportHashSha256 = features.CheckVersionAtLeast("2.42")
features.SupportedObjectFormats = []ObjectFormat{Sha1ObjectFormat}
if features.SupportHashSha256 {
features.SupportedObjectFormats = append(features.SupportedObjectFormats, Sha256ObjectFormat)
+40
View File
@@ -78,3 +78,43 @@ func (c *LastCommitCache) GetCommitByPath(ctx context.Context, entryCommitID Obj
return lastCommit, nil
}
// CacheCommit will cache the commit from the gitRepository
func (c *Commit) CacheCommit(ctx context.Context, gitRepo *Repository) error {
return c.recursiveCache(ctx, gitRepo, c.Tree(), "", 1)
}
func (c *Commit) recursiveCache(ctx context.Context, gitRepo *Repository, tree *Tree, treePath string, level int) error {
if level == 0 {
return nil
}
entries, err := tree.ListEntries(ctx, gitRepo)
if err != nil {
return err
}
entryPaths := make([]string, len(entries))
for i, entry := range entries {
entryPaths[i] = entry.Name()
}
_, err = walkGitLog(ctx, gitRepo, c, treePath, entryPaths...)
if err != nil {
return err
}
for _, treeEntry := range entries {
// entryMap won't contain "" therefore skip this.
if treeEntry.IsDir() {
subTree, err := tree.SubTree(ctx, gitRepo, treeEntry.Name())
if err != nil {
return err
}
if err := c.recursiveCache(ctx, gitRepo, subTree, treeEntry.Name(), level-1); err != nil {
return err
}
}
}
return nil
}
-63
View File
@@ -1,63 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"context"
"github.com/go-git/go-git/v5/plumbing"
cgobject "github.com/go-git/go-git/v5/plumbing/object/commitgraph"
)
// CacheCommit will cache the commit from the gitRepository
func (c *Commit) CacheCommit(ctx context.Context, gitRepo *Repository) error {
commitNodeIndex, closer := gitRepo.CommitNodeIndex()
defer closer()
index, err := commitNodeIndex.Get(plumbing.Hash(c.ID.RawValue()))
if err != nil {
return err
}
return c.recursiveCache(ctx, gitRepo, index, c.Tree(), "", 1)
}
func (c *Commit) recursiveCache(ctx context.Context, gitRepo *Repository, index cgobject.CommitNode, tree *Tree, treePath string, level int) error {
if level == 0 {
return nil
}
entries, err := tree.ListEntries(ctx, gitRepo)
if err != nil {
return err
}
entryPaths := make([]string, len(entries))
entryMap := make(map[string]*TreeEntry)
for i, entry := range entries {
entryPaths[i] = entry.Name()
entryMap[entry.Name()] = entry
}
commits, err := getLastCommitForPathsByCommitNode(ctx, gitRepo, index, treePath, entryPaths)
if err != nil {
return err
}
for entry := range commits {
if entryMap[entry].IsDir() {
subTree, err := tree.SubTree(ctx, gitRepo, entry)
if err != nil {
return err
}
if err := c.recursiveCache(ctx, gitRepo, index, subTree, entry, level-1); err != nil {
return err
}
}
}
return nil
}
-50
View File
@@ -1,50 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (
"context"
)
// CacheCommit will cache the commit from the gitRepository
func (c *Commit) CacheCommit(ctx context.Context, gitRepo *Repository) error {
return c.recursiveCache(ctx, gitRepo, c.Tree(), "", 1)
}
func (c *Commit) recursiveCache(ctx context.Context, gitRepo *Repository, tree *Tree, treePath string, level int) error {
if level == 0 {
return nil
}
entries, err := tree.ListEntries(ctx, gitRepo)
if err != nil {
return err
}
entryPaths := make([]string, len(entries))
for i, entry := range entries {
entryPaths[i] = entry.Name()
}
_, err = walkGitLog(ctx, gitRepo, c, treePath, entryPaths...)
if err != nil {
return err
}
for _, treeEntry := range entries {
// entryMap won't contain "" therefore skip this.
if treeEntry.IsDir() {
subTree, err := tree.SubTree(ctx, gitRepo, treeEntry.Name())
if err != nil {
return err
}
if err := c.recursiveCache(ctx, gitRepo, subTree, treeEntry.Name(), level-1); err != nil {
return err
}
}
}
return nil
}
@@ -1,8 +1,6 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (
-30
View File
@@ -1,30 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/hash"
)
func ParseGogitHash(h plumbing.Hash) ObjectID {
switch hash.Size {
case 20:
return Sha1ObjectFormat.MustID(h[:])
case 32:
return Sha256ObjectFormat.MustID(h[:])
}
return nil
}
func ParseGogitHashArray(objectIDs []plumbing.Hash) []ObjectID {
ret := make([]ObjectID, len(objectIDs))
for i, h := range objectIDs {
ret[i] = ParseGogitHash(h)
}
return ret
}
-2
View File
@@ -43,8 +43,6 @@ func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) {
return entries, nil
}
var _ = catBatchParseTreeEntries // bypass "unused" lint because it is only used by "nogogit"
func catBatchParseTreeEntries(objectFormat ObjectFormat, ptree *Tree, rd BufferedReader, sz int64) ([]*TreeEntry, error) {
entries := make([]*TreeEntry, 0, 10)
-1
View File
@@ -16,7 +16,6 @@ import (
)
func fillResultNameRev(ctx context.Context, repo git.RepositoryFacade, results []*LFSResult) error {
// Should really use a go-git function here but name-rev is not completed and recapitulating it is not simple
wg := errgroup.Group{}
cmd := gitcmd.NewCommand("name-rev", "--stdin", "--name-only", "--always").WithRepo(repo)
stdin, stdinClose := cmd.MakeStdinPipe()
@@ -2,12 +2,8 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
const isGogit = false
type Repository struct {
RepositoryBase
}
-175
View File
@@ -1,175 +0,0 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"errors"
"io"
"os"
"path/filepath"
"slices"
"strings"
"gitea.dev/modules/container"
"gitea.dev/modules/git/gitrepo"
"gitea.dev/modules/setting"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/osfs"
gogit "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/cache"
"github.com/go-git/go-git/v5/storage/filesystem"
"github.com/go-git/go-git/v5/storage/filesystem/dotgit"
)
const isGogit = true
type Repository struct {
RepositoryBase
gogitRepo *gogit.Repository
gogitStorage *reindexingStorage
}
// reindexingStorage reloads the pack index when git added or removed packs after go-git loaded it
// https://github.com/go-git/go-git/issues/2439 https://github.com/go-git/go-git/issues/1623
// FIXME: gogit workaround, remove with the gogit build
type reindexingStorage struct {
*filesystem.Storage
packs []plumbing.Hash
}
func isRepackError(err error) bool {
return errors.Is(err, plumbing.ErrObjectNotFound) || errors.Is(err, dotgit.ErrPackfileNotFound) || errors.Is(err, os.ErrNotExist)
}
// retry reruns fn while a concurrent repack keeps changing the packs
func (s *reindexingStorage) retry(fn func() error) error {
for {
err := fn()
if !isRepackError(err) {
return err
}
packs, _ := s.ObjectPacks()
if slices.Equal(packs, s.packs) {
return err
}
s.packs = packs
s.Reindex()
}
}
func (s *reindexingStorage) EncodedObject(t plumbing.ObjectType, h plumbing.Hash) (obj plumbing.EncodedObject, err error) {
err = s.retry(func() (err error) {
obj, err = s.Storage.EncodedObject(t, h)
return err
})
if err != nil {
return nil, err
}
if _, ok := obj.(*plumbing.MemoryObject); ok {
return obj, nil
}
return &lazyObject{EncodedObject: obj, storage: s}, nil
}
// lazyObject looks up a large object again when its file got removed before Reader reopened it
// FIXME: gogit workaround, remove with the gogit build
type lazyObject struct {
plumbing.EncodedObject
storage *reindexingStorage
}
func (o *lazyObject) Reader() (rc io.ReadCloser, err error) {
rc, err = o.EncodedObject.Reader()
if !isRepackError(err) {
return rc, err
}
err = o.storage.retry(func() error {
obj, err := o.storage.Storage.EncodedObject(o.Type(), o.Hash())
if err == nil {
o.EncodedObject = obj
rc, err = obj.Reader()
}
return err
})
return rc, err
}
// packIdxFS lists packs like git, only while their .idx exists
// FIXME: gogit workaround, remove with the gogit build
type packIdxFS struct {
billy.Filesystem
}
func (f packIdxFS) ReadDir(dir string) ([]os.FileInfo, error) {
if dir != f.Join("objects", "pack") {
return f.Filesystem.ReadDir(dir)
}
dirFile, err := os.Open(filepath.Join(f.Root(), dir))
if err != nil {
return nil, err
}
defer dirFile.Close()
infos, err := dirFile.Readdir(-1) // skips files removed before their lstat, unlike billy's ReadDir
if err != nil {
return nil, err
}
names := make(container.Set[string], len(infos))
for _, info := range infos {
names.Add(info.Name())
}
return slices.DeleteFunc(infos, func(info os.FileInfo) bool {
base, isPack := strings.CutSuffix(info.Name(), ".pack")
return isPack && !names.Contains(base+".idx")
}), nil
}
func openRepositoryInternal(gitRepo *Repository) error {
repoPath := gitrepo.RepoLocalPath(gitRepo)
fs := osfs.New(repoPath)
_, err := fs.Stat(".git")
if err == nil {
fs, err = fs.Chroot(".git")
if err != nil {
return err
}
}
// the "clone --shared" repo doesn't work well with go-git AlternativeFS, https://github.com/go-git/go-git/issues/1006
// so use "/" for AlternatesFS, I guess it is the same behavior as current nogogit (no limitation or check for the "objects/info/alternates" paths), trust the "clone" command executed by the server.
var altFs billy.Filesystem
if setting.IsWindows {
altFs = osfs.New(filepath.VolumeName(setting.RepoRootPath) + "\\") // TODO: does it really work for Windows? Need some time to check.
} else {
altFs = osfs.New("/")
}
gitRepo.objectFormatCache = ParseGogitHash(plumbing.ZeroHash).Type()
storage := filesystem.NewStorageWithOptions(packIdxFS{fs}, cache.NewObjectLRUDefault(), filesystem.Options{KeepDescriptors: true, LargeObjectThreshold: setting.Git.LargeObjectThreshold, AlternatesFS: altFs})
packs, _ := storage.ObjectPacks()
gitRepo.gogitStorage = &reindexingStorage{Storage: storage, packs: packs}
gitRepo.gogitRepo, err = gogit.Open(gitRepo.gogitStorage, fs)
if err != nil {
_ = gitRepo.gogitStorage.Close()
return err
}
return nil
}
func (repo *Repository) closeInternal() error {
if repo.gogitStorage == nil {
return nil
}
err := repo.gogitStorage.Close()
repo.gogitStorage = nil
return err
}
// GoGitRepo gets the go-git repo representation
func (repo *Repository) GoGitRepo() *gogit.Repository {
return repo.gogitRepo
}
@@ -1,8 +1,6 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (
+171
View File
@@ -5,9 +5,13 @@
package git
import (
"bufio"
"context"
"io"
"strings"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/log"
)
// BranchPrefix base dir of the branch information file store on git
@@ -24,3 +28,170 @@ func (repo *Repository) AddRemote(ctx context.Context, name, url string, fetch b
RunStdString(ctx)
return err
}
// IsObjectExist returns true if the given object exists in the repository.
// FIXME: this function doesn't seem right, it is only used by GarbageCollectLFSMetaObjectsForRepo
func (repo *Repository) IsObjectExist(ctx context.Context, name string) bool {
if name == "" {
return false
}
batch, cancel, err := repo.CatFileBatch()
if err != nil {
log.Debug("Error opening CatFileBatch %v", err)
return false
}
defer cancel()
info, err := batch.QueryInfo(name)
if err != nil {
log.Debug("Error checking object info %v", err)
return false
}
return strings.HasPrefix(info.ID, name) // FIXME: this logic doesn't seem right, why "HasPrefix"
}
// IsReferenceExist returns true if given reference exists in the repository.
func (repo *Repository) IsReferenceExist(ctx context.Context, name string) bool {
if name == "" {
return false
}
batch, cancel, err := repo.CatFileBatch()
if err != nil {
log.Error("Error opening CatFileBatch %v", err)
return false
}
defer cancel()
_, err = batch.QueryInfo(name)
return err == nil
}
// IsBranchExist returns true if given branch exists in current repository.
func (repo *Repository) IsBranchExist(ctx context.Context, name string) bool {
if repo == nil || name == "" {
return false
}
return repo.IsReferenceExist(ctx, BranchPrefix+name)
}
// GetBranchNames returns branches from the repository, skipping "skip" initial branches and
// returning at most "limit" branches, or all branches if "limit" is 0.
func (repo *Repository) GetBranchNames(ctx context.Context, skip, limit int) ([]string, int, error) {
return callShowRef(ctx, repo, BranchPrefix, gitcmd.TrustedCmdArgs{BranchPrefix, "--sort=-committerdate"}, skip, limit)
}
// WalkReferences walks all the references from the repository
// refType should be empty, ObjectTag or ObjectBranch. All other values are equivalent to empty.
func (repo *Repository) WalkReferences(ctx context.Context, refType ObjectType, skip, limit int, walkfn func(sha1, refname string) error) (int, error) {
var args gitcmd.TrustedCmdArgs
switch refType {
case ObjectTag:
args = gitcmd.TrustedCmdArgs{TagPrefix, "--sort=-taggerdate"}
case ObjectBranch:
args = gitcmd.TrustedCmdArgs{BranchPrefix, "--sort=-committerdate"}
}
return WalkShowRef(ctx, repo, args, skip, limit, walkfn)
}
// callShowRef return refs, if limit = 0 it will not limit
func callShowRef(ctx context.Context, repo RepositoryFacade, trimPrefix string, extraArgs gitcmd.TrustedCmdArgs, skip, limit int) (branchNames []string, countAll int, err error) {
countAll, err = WalkShowRef(ctx, repo, extraArgs, skip, limit, func(_, branchName string) error {
branchName = strings.TrimPrefix(branchName, trimPrefix)
branchNames = append(branchNames, branchName)
return nil
})
return branchNames, countAll, err
}
func WalkShowRef(ctx context.Context, repo RepositoryFacade, extraArgs gitcmd.TrustedCmdArgs, skip, limit int, walkfn func(sha1, refname string) error) (countAll int, err error) {
i := 0
args := gitcmd.TrustedCmdArgs{"for-each-ref", "--format=%(objectname) %(refname)"}
args = append(args, extraArgs...)
cmd := gitcmd.NewCommand(args...)
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
defer stdoutReaderClose()
cmd.WithRepo(repo).
WithPipelineFunc(func(gitcmd.Context) error {
bufReader := bufio.NewReader(stdoutReader)
for i < skip {
_, isPrefix, err := bufReader.ReadLine()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
if !isPrefix {
i++
}
}
for limit == 0 || i < skip+limit {
// The output of show-ref is simply a list:
// <sha> SP <ref> LF
sha, err := bufReader.ReadString(' ')
if err == io.EOF {
return nil
}
if err != nil {
return err
}
branchName, err := bufReader.ReadString('\n')
if err == io.EOF {
// This shouldn't happen... but we'll tolerate it for the sake of peace
return nil
}
if err != nil {
return err
}
if len(branchName) > 0 {
branchName = branchName[:len(branchName)-1]
}
if len(sha) > 0 {
sha = sha[:len(sha)-1]
}
err = walkfn(sha, branchName)
if err != nil {
return err
}
i++
}
// count all refs
for limit != 0 {
_, isPrefix, err := bufReader.ReadLine()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
if !isPrefix {
i++
}
}
return nil
})
err = cmd.RunWithStderr(ctx)
if errPipeline, ok := gitcmd.UnwrapPipelineError(err); ok {
return i, errPipeline // keep the old behavior: return pipeline error directly
}
return i, err
}
// GetRefsBySha returns all references filtered with prefix that belong to a sha commit hash
func (repo *Repository) GetRefsBySha(ctx context.Context, sha, prefix string) ([]string, error) {
var revList []string
_, err := WalkShowRef(ctx, repo, nil, 0, 0, func(walkSha, refname string) error {
if walkSha == sha && strings.HasPrefix(refname, prefix) {
revList = append(revList, refname)
}
return nil
})
return revList, err
}
-153
View File
@@ -1,153 +0,0 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"context"
"sort"
"strings"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/storer"
)
// IsObjectExist returns true if the given object exists in the repository.
// FIXME: Inconsistent behavior with nogogit edition
// Unlike the implementation of IsObjectExist in nogogit edition, it does not support short hashes here.
// For example, IsObjectExist("153f451") will return false, but it will return true in nogogit edition.
// To fix this, the solution could be adding support for short hashes in gogit edition if it's really needed.
func (repo *Repository) IsObjectExist(_ context.Context, name string) bool {
if name == "" {
return false
}
_, err := repo.gogitRepo.Object(plumbing.AnyObject, plumbing.NewHash(name))
return err == nil
}
// IsReferenceExist returns true if given reference exists in the repository.
// FIXME: Inconsistent behavior with nogogit edition
// Unlike the implementation of IsObjectExist in nogogit edition, it does not support blob hashes here.
// For example, IsObjectExist([existing_blob_hash]) will return false, but it will return true in nogogit edition.
// To fix this, the solution could be refusing to support blob hashes in nogogit edition since a blob hash is not a reference.
func (repo *Repository) IsReferenceExist(_ context.Context, name string) bool {
if name == "" {
return false
}
_, err := repo.gogitRepo.ResolveRevision(plumbing.Revision(name))
return err == nil
}
// IsBranchExist returns true if given branch exists in current repository.
func (repo *Repository) IsBranchExist(_ context.Context, name string) bool {
if name == "" {
return false
}
reference, err := repo.gogitRepo.Reference(plumbing.ReferenceName(BranchPrefix+name), true)
if err != nil {
return false
}
return reference.Type() != plumbing.InvalidReference
}
// 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 `-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(_ context.Context, skip, limit int) ([]string, int, error) {
type BranchData struct {
name string
committerDate int64
}
var branchData []BranchData
branchIter, err := repo.gogitRepo.Branches()
if err != nil {
return nil, 0, err
}
_ = branchIter.ForEach(func(branch *plumbing.Reference) error {
obj, err := repo.gogitRepo.CommitObject(branch.Hash())
if err != nil {
// skip branch if can't find commit
return nil
}
branchData = append(branchData, BranchData{strings.TrimPrefix(branch.Name().String(), BranchPrefix), obj.Committer.When.Unix()})
return nil
})
sort.Slice(branchData, func(i, j int) bool {
return !(branchData[i].committerDate < branchData[j].committerDate)
})
var branchNames []string
maxPos := len(branchData)
if limit > 0 {
maxPos = min(skip+limit, maxPos)
}
for i := skip; i < maxPos; i++ {
branchNames = append(branchNames, branchData[i].name)
}
return branchNames, len(branchData), nil
}
// WalkReferences walks all the references from the repository
func (repo *Repository) WalkReferences(ctx context.Context, arg ObjectType, skip, limit int, walkfn func(sha1, refname string) error) (int, error) {
i := 0
var iter storer.ReferenceIter
var err error
switch arg {
case ObjectTag:
iter, err = repo.gogitRepo.Tags()
case ObjectBranch:
iter, err = repo.gogitRepo.Branches()
default:
iter, err = repo.gogitRepo.References()
}
if err != nil {
return i, err
}
defer iter.Close()
err = iter.ForEach(func(ref *plumbing.Reference) error {
if i < skip {
i++
return nil
}
err := walkfn(ref.Hash().String(), string(ref.Name()))
i++
if err != nil {
return err
}
if limit != 0 && i >= skip+limit {
return storer.ErrStop
}
return ctx.Err()
})
return i, err
}
// GetRefsBySha returns all references filtered with prefix that belong to a sha commit hash
func (repo *Repository) GetRefsBySha(ctx context.Context, sha, prefix string) ([]string, error) {
var revList []string
iter, err := repo.gogitRepo.References()
if err != nil {
return nil, err
}
err = iter.ForEach(func(ref *plumbing.Reference) error {
if ref.Hash().String() == sha && strings.HasPrefix(string(ref.Name()), prefix) {
revList = append(revList, string(ref.Name()))
}
return ctx.Err()
})
return revList, err
}
-184
View File
@@ -1,184 +0,0 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (
"bufio"
"context"
"io"
"strings"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/log"
)
// IsObjectExist returns true if the given object exists in the repository.
// FIXME: this function doesn't seem right, it is only used by GarbageCollectLFSMetaObjectsForRepo
func (repo *Repository) IsObjectExist(ctx context.Context, name string) bool {
if name == "" {
return false
}
batch, cancel, err := repo.CatFileBatch()
if err != nil {
log.Debug("Error opening CatFileBatch %v", err)
return false
}
defer cancel()
info, err := batch.QueryInfo(name)
if err != nil {
log.Debug("Error checking object info %v", err)
return false
}
return strings.HasPrefix(info.ID, name) // FIXME: this logic doesn't seem right, why "HasPrefix"
}
// IsReferenceExist returns true if given reference exists in the repository.
func (repo *Repository) IsReferenceExist(ctx context.Context, name string) bool {
if name == "" {
return false
}
batch, cancel, err := repo.CatFileBatch()
if err != nil {
log.Error("Error opening CatFileBatch %v", err)
return false
}
defer cancel()
_, err = batch.QueryInfo(name)
return err == nil
}
// IsBranchExist returns true if given branch exists in current repository.
func (repo *Repository) IsBranchExist(ctx context.Context, name string) bool {
if repo == nil || name == "" {
return false
}
return repo.IsReferenceExist(ctx, BranchPrefix+name)
}
// GetBranchNames returns branches from the repository, skipping "skip" initial branches and
// returning at most "limit" branches, or all branches if "limit" is 0.
func (repo *Repository) GetBranchNames(ctx context.Context, skip, limit int) ([]string, int, error) {
return callShowRef(ctx, repo, BranchPrefix, gitcmd.TrustedCmdArgs{BranchPrefix, "--sort=-committerdate"}, skip, limit)
}
// WalkReferences walks all the references from the repository
// refType should be empty, ObjectTag or ObjectBranch. All other values are equivalent to empty.
func (repo *Repository) WalkReferences(ctx context.Context, refType ObjectType, skip, limit int, walkfn func(sha1, refname string) error) (int, error) {
var args gitcmd.TrustedCmdArgs
switch refType {
case ObjectTag:
args = gitcmd.TrustedCmdArgs{TagPrefix, "--sort=-taggerdate"}
case ObjectBranch:
args = gitcmd.TrustedCmdArgs{BranchPrefix, "--sort=-committerdate"}
}
return WalkShowRef(ctx, repo, args, skip, limit, walkfn)
}
// callShowRef return refs, if limit = 0 it will not limit
func callShowRef(ctx context.Context, repo RepositoryFacade, trimPrefix string, extraArgs gitcmd.TrustedCmdArgs, skip, limit int) (branchNames []string, countAll int, err error) {
countAll, err = WalkShowRef(ctx, repo, extraArgs, skip, limit, func(_, branchName string) error {
branchName = strings.TrimPrefix(branchName, trimPrefix)
branchNames = append(branchNames, branchName)
return nil
})
return branchNames, countAll, err
}
func WalkShowRef(ctx context.Context, repo RepositoryFacade, extraArgs gitcmd.TrustedCmdArgs, skip, limit int, walkfn func(sha1, refname string) error) (countAll int, err error) {
i := 0
args := gitcmd.TrustedCmdArgs{"for-each-ref", "--format=%(objectname) %(refname)"}
args = append(args, extraArgs...)
cmd := gitcmd.NewCommand(args...)
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
defer stdoutReaderClose()
cmd.WithRepo(repo).
WithPipelineFunc(func(gitcmd.Context) error {
bufReader := bufio.NewReader(stdoutReader)
for i < skip {
_, isPrefix, err := bufReader.ReadLine()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
if !isPrefix {
i++
}
}
for limit == 0 || i < skip+limit {
// The output of show-ref is simply a list:
// <sha> SP <ref> LF
sha, err := bufReader.ReadString(' ')
if err == io.EOF {
return nil
}
if err != nil {
return err
}
branchName, err := bufReader.ReadString('\n')
if err == io.EOF {
// This shouldn't happen... but we'll tolerate it for the sake of peace
return nil
}
if err != nil {
return err
}
if len(branchName) > 0 {
branchName = branchName[:len(branchName)-1]
}
if len(sha) > 0 {
sha = sha[:len(sha)-1]
}
err = walkfn(sha, branchName)
if err != nil {
return err
}
i++
}
// count all refs
for limit != 0 {
_, isPrefix, err := bufReader.ReadLine()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
if !isPrefix {
i++
}
}
return nil
})
err = cmd.RunWithStderr(ctx)
if errPipeline, ok := gitcmd.UnwrapPipelineError(err); ok {
return i, errPipeline // keep the old behavior: return pipeline error directly
}
return i, err
}
// GetRefsBySha returns all references filtered with prefix that belong to a sha commit hash
func (repo *Repository) GetRefsBySha(ctx context.Context, sha, prefix string) ([]string, error) {
var revList []string
_, err := WalkShowRef(ctx, repo, nil, 0, 0, func(walkSha, refname string) error {
if walkSha == sha && strings.HasPrefix(refname, prefix) {
revList = append(revList, refname)
}
return nil
})
return revList, err
}
+4 -53
View File
@@ -4,15 +4,9 @@
package git
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -45,41 +39,6 @@ func TestRepository_GetBranches(t *testing.T) {
assert.ElementsMatch(t, []string{}, branches)
}
// FIXME: covers the gogit workarounds in repo_base_gogit.go, remove with the gogit build
func TestReadsAfterConcurrentRepack(t *testing.T) {
repoDir := t.TempDir()
require.NoError(t, gitcmd.NewCommand("init", "--bare").AddDynamicArguments(repoDir).Run(t.Context()))
content := strings.Repeat("a", int(setting.Git.LargeObjectThreshold)+1)
for _, from := range []string{"", "from refs/heads/main^0\n"} {
stdin := fmt.Sprintf("commit refs/heads/main\ncommitter a <a@a> 0 +0000\ndata 0\n%sM 100644 inline f\ndata %d\n%s\n", from, len(content), content)
require.NoError(t, gitcmd.NewCommand("fast-import").WithDir(repoDir).WithStdinBytes([]byte(stdin)).Run(t.Context()))
require.NoError(t, gitcmd.NewCommand("repack", "-d").WithDir(repoDir).Run(t.Context()))
}
repo, err := OpenRepositoryLocal(t.Context(), repoDir)
require.NoError(t, err)
defer repo.Close()
require.False(t, repo.IsObjectExist(t.Context(), "0000000000000000000000000000000000000001"))
blobRepo, err := OpenRepositoryLocal(t.Context(), repoDir)
require.NoError(t, err)
defer blobRepo.Close()
commit, err := blobRepo.GetBranchCommit(t.Context(), "main")
require.NoError(t, err)
readBlob := func() string {
data, err := commit.GetFileContent(t.Context(), blobRepo, "f", len(content))
require.NoError(t, err)
return data
}
require.Equal(t, content, readBlob())
require.NoError(t, gitcmd.NewCommand("repack", "-a", "-d").WithDir(repoDir).Run(t.Context()))
require.NoError(t, os.WriteFile(filepath.Join(repoDir, "objects", "pack", "pack-"+strings.Repeat("1", 40)+".pack"), nil, 0o644))
branches, _, err := repo.GetBranchNames(t.Context(), 0, 0)
require.NoError(t, err)
assert.Equal(t, []string{"main"}, branches)
assert.Equal(t, content, readBlob())
}
func BenchmarkRepository_GetBranches(b *testing.B) {
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
bareRepo1, err := OpenRepositoryLocal(b.Context(), bareRepo1Path)
@@ -143,10 +102,6 @@ func TestRepository_IsObjectExist(t *testing.T) {
require.NoError(t, err)
defer repo.Close()
// FIXME: Inconsistent behavior between gogit and nogogit editions
// See the comment of IsObjectExist in gogit edition for more details.
supportShortHash := !isGogit
tests := []struct {
name string
arg string
@@ -170,7 +125,7 @@ func TestRepository_IsObjectExist(t *testing.T) {
{
name: "short commit hash",
arg: "ce06481",
want: supportShortHash,
want: true,
},
{
name: "blob hash",
@@ -180,7 +135,7 @@ func TestRepository_IsObjectExist(t *testing.T) {
{
name: "short blob hash",
arg: "153f451",
want: supportShortHash,
want: true,
},
}
for _, tt := range tests {
@@ -196,10 +151,6 @@ func TestRepository_IsReferenceExist(t *testing.T) {
require.NoError(t, err)
defer repo.Close()
// FIXME: Inconsistent behavior between gogit and nogogit editions
// See the comment of IsReferenceExist in gogit edition for more details.
supportBlobHash := !isGogit
tests := []struct {
name string
arg string
@@ -228,12 +179,12 @@ func TestRepository_IsReferenceExist(t *testing.T) {
{
name: "blob hash",
arg: "153f451b9ee7fa1da317ab17a127e9fd9d384310",
want: supportBlobHash,
want: true,
},
{
name: "short blob hash",
arg: "153f451",
want: supportBlobHash,
want: true,
},
}
for _, tt := range tests {
+122
View File
@@ -7,6 +7,7 @@ package git
import (
"bytes"
"context"
"errors"
"io"
"strconv"
"strings"
@@ -458,3 +459,124 @@ func (repo *Repository) GetCommitBranchStart(ctx context.Context, env []string,
return "", nil
}
// GetRefCommitID returns the last commit ID string of given reference (branch or tag).
func (repo *Repository) GetRefCommitID(ctx context.Context, name string) (string, error) {
batch, cancel, err := repo.CatFileBatch()
if err != nil {
return "", err
}
defer cancel()
info, err := batch.QueryInfo(name)
if IsErrNotExist(err) {
return "", ErrNotExist{name, ""}
} else if err != nil {
return "", err
}
return info.ID, nil
}
func (repo *Repository) getCommit(_ context.Context, id ObjectID) (*Commit, error) {
batch, cancel, err := repo.CatFileBatch()
if err != nil {
return nil, err
}
defer cancel()
return repo.getCommitWithBatch(batch, id)
}
func limitDiscardReader(rd BufferedReader, full, limit int64) (io.Reader, func() error) {
return io.LimitReader(rd, min(full, limit)), func() error {
if full > limit {
return DiscardFull(rd, full-limit)
}
return nil
}
}
func (repo *Repository) getCommitWithBatch(batch CatFileBatch, id ObjectID) (*Commit, error) {
info, rd, err := batch.QueryContent(id.String())
if err != nil {
if errors.Is(err, io.EOF) || IsErrNotExist(err) {
return nil, ErrNotExist{ID: id.String()}
}
return nil, err
}
switch info.Type {
case "missing":
return nil, ErrNotExist{ID: id.String()}
case "tag":
limitReader, limitDiscard := limitDiscardReader(rd, info.Size, MaxGitObjectSize)
data, err := io.ReadAll(limitReader)
if err != nil {
return nil, err
}
if err = limitDiscard(); err != nil {
return nil, err
}
_, err = rd.Discard(1)
if err != nil {
return nil, err
}
tag, err := parseTagData(id.Type(), data)
if err != nil {
return nil, err
}
return repo.getCommitWithBatch(batch, tag.Object)
case "commit":
limitReader, limitDiscard := limitDiscardReader(rd, info.Size, MaxGitObjectSize)
commit, err := CommitFromReader(id, limitReader)
if err != nil {
return nil, err
}
if err = limitDiscard(); err != nil {
return nil, err
}
_, err = rd.Discard(1)
if err != nil {
return nil, err
}
return commit, nil
default:
if info.Type != "blob" && info.Type != "tree" {
setting.PanicInDevOrTesting("Unknown cat-file object type %s for object %s in repo %s", info.Type, id.String(), repo.LogString())
}
if err := DiscardFull(rd, info.Size+1); err != nil {
return nil, err
}
return nil, ErrNotExist{
ID: id.String(),
}
}
}
// ConvertToGitID returns a git object ID from the git ref, it doesn't guarantee the returned ID really exists
func (repo *Repository) ConvertToGitID(ctx context.Context, ref string) (ObjectID, error) {
objectFormat, err := repo.GetObjectFormat(ctx)
if err != nil {
return nil, err
}
if IsStringValidObjectID(objectFormat, ref) {
id, err := NewIDFromString(ref)
if err == nil {
return id, nil
}
}
batch, cancel, err := repo.CatFileBatch()
if err != nil {
return nil, err
}
defer cancel()
info, err := batch.QueryInfo(ref)
if err != nil {
if IsErrNotExist(err) {
return nil, ErrNotExist{ref, ""}
}
return nil, err
}
return MustIDFromString(info.ID), nil
}
-105
View File
@@ -1,105 +0,0 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"context"
"strings"
"gitea.dev/modules/git/gitcmd"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
)
// GetRefCommitID returns the last commit ID string of given reference.
func (repo *Repository) GetRefCommitID(_ context.Context, name string) (string, error) {
if plumbing.IsHash(name) {
return name, nil
}
refName := plumbing.ReferenceName(name)
if err := refName.Validate(); err != nil {
// Match the nogogit behavior: an unresolvable/invalid ref name
// is reported as not-existing rather than a generic validation error,
// so callers can rely on IsErrNotExist regardless of build tag.
return "", ErrNotExist{ID: name}
}
ref, err := repo.gogitRepo.Reference(refName, true)
if err != nil {
if err == plumbing.ErrReferenceNotFound {
return "", ErrNotExist{
ID: name,
}
}
return "", err
}
return ref.Hash().String(), nil
}
// ConvertToHash returns a Hash object from a potential ID string
func (repo *Repository) ConvertToGitID(ctx context.Context, commitID string) (ObjectID, error) {
objectFormat, err := repo.GetObjectFormat(ctx)
if err != nil {
return nil, err
}
if IsStringValidObjectID(objectFormat, commitID) {
ID, err := NewIDFromString(commitID)
if err == nil {
return ID, nil
}
}
actualCommitID, _, err := gitcmd.NewCommand("rev-parse", "--verify").
AddDynamicArguments(commitID).
WithRepo(repo).
RunStdString(ctx)
actualCommitID = strings.TrimSpace(actualCommitID)
if err != nil {
if strings.Contains(err.Error(), "unknown revision or path") ||
strings.Contains(err.Error(), "fatal: Needed a single revision") {
return objectFormat.EmptyObjectID(), ErrNotExist{commitID, ""}
}
return objectFormat.EmptyObjectID(), err
}
return NewIDFromString(actualCommitID)
}
func (repo *Repository) getCommit(_ context.Context, id ObjectID) (*Commit, error) {
var tagObject *object.Tag
commitID := plumbing.Hash(id.RawValue())
gogitCommit, err := repo.gogitRepo.CommitObject(commitID)
if err == plumbing.ErrObjectNotFound {
tagObject, err = repo.gogitRepo.TagObject(commitID)
if err == plumbing.ErrObjectNotFound {
return nil, ErrNotExist{
ID: id.String(),
}
}
if err == nil {
gogitCommit, err = repo.gogitRepo.CommitObject(tagObject.Target)
}
// if we get a plumbing.ErrObjectNotFound here then the repository is broken and it should be 500
}
if err != nil {
return nil, err
}
commit := convertCommit(gogitCommit)
tree, err := gogitCommit.Tree()
if err != nil {
return nil, err
}
commit.TreeID = ParseGogitHash(tree.Hash)
commit.Tree().resolvedGogitTreeObject = tree
return commit, nil
}
-135
View File
@@ -1,135 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (
"context"
"errors"
"io"
"gitea.dev/modules/setting"
)
// GetRefCommitID returns the last commit ID string of given reference (branch or tag).
func (repo *Repository) GetRefCommitID(ctx context.Context, name string) (string, error) {
batch, cancel, err := repo.CatFileBatch()
if err != nil {
return "", err
}
defer cancel()
info, err := batch.QueryInfo(name)
if IsErrNotExist(err) {
return "", ErrNotExist{name, ""}
} else if err != nil {
return "", err
}
return info.ID, nil
}
func (repo *Repository) getCommit(ctx context.Context, id ObjectID) (*Commit, error) {
batch, cancel, err := repo.CatFileBatch()
if err != nil {
return nil, err
}
defer cancel()
return repo.getCommitWithBatch(batch, id)
}
func limitDiscardReader(rd BufferedReader, full, limit int64) (io.Reader, func() error) {
return io.LimitReader(rd, min(full, limit)), func() error {
if full > limit {
return DiscardFull(rd, full-limit)
}
return nil
}
}
func (repo *Repository) getCommitWithBatch(batch CatFileBatch, id ObjectID) (*Commit, error) {
info, rd, err := batch.QueryContent(id.String())
if err != nil {
if errors.Is(err, io.EOF) || IsErrNotExist(err) {
return nil, ErrNotExist{ID: id.String()}
}
return nil, err
}
switch info.Type {
case "missing":
return nil, ErrNotExist{ID: id.String()}
case "tag":
limitReader, limitDiscard := limitDiscardReader(rd, info.Size, MaxGitObjectSize)
data, err := io.ReadAll(limitReader)
if err != nil {
return nil, err
}
if err = limitDiscard(); err != nil {
return nil, err
}
_, err = rd.Discard(1)
if err != nil {
return nil, err
}
tag, err := parseTagData(id.Type(), data)
if err != nil {
return nil, err
}
return repo.getCommitWithBatch(batch, tag.Object)
case "commit":
limitReader, limitDiscard := limitDiscardReader(rd, info.Size, MaxGitObjectSize)
commit, err := CommitFromReader(id, limitReader)
if err != nil {
return nil, err
}
if err = limitDiscard(); err != nil {
return nil, err
}
_, err = rd.Discard(1)
if err != nil {
return nil, err
}
return commit, nil
default:
if info.Type != "blob" && info.Type != "tree" {
setting.PanicInDevOrTesting("Unknown cat-file object type %s for object %s in repo %s", info.Type, id.String(), repo.LogString())
}
if err := DiscardFull(rd, info.Size+1); err != nil {
return nil, err
}
return nil, ErrNotExist{
ID: id.String(),
}
}
}
// ConvertToGitID returns a git object ID from the git ref, it doesn't guarantee the returned ID really exists
func (repo *Repository) ConvertToGitID(ctx context.Context, ref string) (ObjectID, error) {
objectFormat, err := repo.GetObjectFormat(ctx)
if err != nil {
return nil, err
}
if IsStringValidObjectID(objectFormat, ref) {
id, err := NewIDFromString(ref)
if err == nil {
return id, nil
}
}
batch, cancel, err := repo.CatFileBatch()
if err != nil {
return nil, err
}
defer cancel()
info, err := batch.QueryInfo(ref)
if err != nil {
if IsErrNotExist(err) {
return nil, ErrNotExist{ref, ""}
}
return nil, err
}
return MustIDFromString(info.ID), nil
}
-38
View File
@@ -1,38 +0,0 @@
// Copyright 2019 The Gitea Authors.
// All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"os"
"path/filepath"
"gitea.dev/modules/git/gitrepo"
"gitea.dev/modules/log"
commitgraph "github.com/go-git/go-git/v5/plumbing/format/commitgraph/v2"
cgobject "github.com/go-git/go-git/v5/plumbing/object/commitgraph"
)
// CommitNodeIndex returns the index for walking commit graph
func (repo *Repository) CommitNodeIndex() (_ cgobject.CommitNodeIndex, closer func()) {
indexPath := filepath.Join(gitrepo.RepoLocalPath(repo), "objects", "info", "commit-graph")
file, err := os.Open(indexPath)
if err == nil {
var index commitgraph.Index
index, err = commitgraph.OpenFileIndex(file)
if err == nil {
return cgobject.NewGraphCommitNodeIndex(index, repo.gogitRepo.Storer), func() { _ = file.Close() }
}
_ = file.Close()
}
if !os.IsNotExist(err) {
log.Warn("Unable to read commit-graph for %s: %v", repo.LogString(), err)
}
return cgobject.NewObjectCommitNodeIndex(repo.gogitRepo.Storer), func() {}
}
+63
View File
@@ -4,7 +4,9 @@
package git
import (
"bufio"
"context"
"io"
"strings"
"gitea.dev/modules/git/gitcmd"
@@ -96,3 +98,64 @@ func (repo *Repository) UnstableGuessRefByShortName(ctx context.Context, shortNa
}
return ""
}
// GetRefsFiltered returns all references of the repository that matches patterm exactly or starting with.
func (repo *Repository) GetRefsFiltered(ctx context.Context, pattern string) ([]*Reference, error) {
refs := make([]*Reference, 0)
cmd := gitcmd.NewCommand("for-each-ref")
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
defer stdoutReaderClose()
err := cmd.WithRepo(repo).
WithPipelineFunc(func(context gitcmd.Context) error {
bufReader := bufio.NewReader(stdoutReader)
for {
// The output of for-each-ref is simply a list:
// <sha> SP <type> TAB <ref> LF
sha, err := bufReader.ReadString(' ')
if err == io.EOF {
break
}
if err != nil {
return err
}
sha = sha[:len(sha)-1]
typ, err := bufReader.ReadString('\t')
if err == io.EOF {
// This should not happen, but we'll tolerate it
break
}
if err != nil {
return err
}
typ = typ[:len(typ)-1]
refName, err := bufReader.ReadString('\n')
if err == io.EOF {
// This should not happen, but we'll tolerate it
break
}
if err != nil {
return err
}
refName = refName[:len(refName)-1]
// refName cannot be HEAD but can be remotes or stash
if strings.HasPrefix(refName, RemotePrefix) || refName == "/refs/stash" {
continue
}
if pattern == "" || strings.HasPrefix(refName, pattern) {
r := &Reference{
Name: refName,
Object: MustIDFromString(sha),
Type: typ,
repo: repo,
}
refs = append(refs, r)
}
}
return nil
}).RunWithStderr(ctx)
return refs, err
}
-48
View File
@@ -1,48 +0,0 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"context"
"strings"
"github.com/go-git/go-git/v5/plumbing"
)
// GetRefsFiltered returns all references of the repository that matches patterm exactly or starting with.
func (repo *Repository) GetRefsFiltered(ctx context.Context, pattern string) ([]*Reference, error) {
r := repo.gogitRepo
refsIter, err := r.References()
if err != nil {
return nil, err
}
refs := make([]*Reference, 0)
if err = refsIter.ForEach(func(ref *plumbing.Reference) error {
if ref.Name() != plumbing.HEAD && !ref.Name().IsRemote() &&
(pattern == "" || strings.HasPrefix(ref.Name().String(), pattern)) {
refType := string(ObjectCommit)
if ref.Name().IsTag() {
// tags can be of type `commit` (lightweight) or `tag` (annotated)
if tagType, _ := repo.GetTagType(ctx, ParseGogitHash(ref.Hash())); err == nil {
refType = tagType
}
}
r := &Reference{
Name: ref.Name().String(),
Object: ParseGogitHash(ref.Hash()),
Type: refType,
repo: repo,
}
refs = append(refs, r)
}
return nil
}); err != nil {
return nil, err
}
return refs, nil
}
-76
View File
@@ -1,76 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (
"bufio"
"context"
"io"
"strings"
"gitea.dev/modules/git/gitcmd"
)
// GetRefsFiltered returns all references of the repository that matches patterm exactly or starting with.
func (repo *Repository) GetRefsFiltered(ctx context.Context, pattern string) ([]*Reference, error) {
refs := make([]*Reference, 0)
cmd := gitcmd.NewCommand("for-each-ref")
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
defer stdoutReaderClose()
err := cmd.WithRepo(repo).
WithPipelineFunc(func(context gitcmd.Context) error {
bufReader := bufio.NewReader(stdoutReader)
for {
// The output of for-each-ref is simply a list:
// <sha> SP <type> TAB <ref> LF
sha, err := bufReader.ReadString(' ')
if err == io.EOF {
break
}
if err != nil {
return err
}
sha = sha[:len(sha)-1]
typ, err := bufReader.ReadString('\t')
if err == io.EOF {
// This should not happen, but we'll tolerate it
break
}
if err != nil {
return err
}
typ = typ[:len(typ)-1]
refName, err := bufReader.ReadString('\n')
if err == io.EOF {
// This should not happen, but we'll tolerate it
break
}
if err != nil {
return err
}
refName = refName[:len(refName)-1]
// refName cannot be HEAD but can be remotes or stash
if strings.HasPrefix(refName, RemotePrefix) || refName == "/refs/stash" {
continue
}
if pattern == "" || strings.HasPrefix(refName, pattern) {
r := &Reference{
Name: refName,
Object: MustIDFromString(sha),
Type: typ,
repo: repo,
}
refs = append(refs, r)
}
}
return nil
}).RunWithStderr(ctx)
return refs, err
}
+122
View File
@@ -6,13 +6,16 @@ package git
import (
"context"
"errors"
"fmt"
"io"
"strconv"
"strings"
"time"
"gitea.dev/modules/git/foreachref"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/log"
"gitea.dev/modules/util"
)
@@ -232,3 +235,122 @@ func (repo *Repository) GetAnnotatedTag(ctx context.Context, sha string) (*Tag,
}
return tag, nil
}
// IsTagExist returns true if given tag exists in the repository.
func (repo *Repository) IsTagExist(ctx context.Context, name string) bool {
if repo == nil || name == "" {
return false
}
return repo.IsReferenceExist(ctx, TagPrefix+name)
}
// GetTagType gets the type of the tag, either commit (simple) or tag (annotated)
func (repo *Repository) GetTagType(ctx context.Context, id ObjectID) (string, error) {
batch, cancel, err := repo.CatFileBatch()
if err != nil {
return "", err
}
defer cancel()
info, err := batch.QueryInfo(id.String())
if err != nil {
if IsErrNotExist(err) {
return "", ErrNotExist{ID: id.String()}
}
return "", err
}
return info.Type, nil
}
func (repo *Repository) getTag(ctx context.Context, tagID ObjectID, name string) (*Tag, error) {
t, ok := repo.tagCache.Get(tagID.String())
if ok {
log.Debug("Hit cache: %s", tagID)
tagClone := *t
tagClone.Name = name // This is necessary because lightweight tags may have same id
return &tagClone, nil
}
tp, err := repo.GetTagType(ctx, tagID)
if err != nil {
return nil, err
}
// Get the commit ID and tag ID (may be different for annotated tag) for the returned tag object
commitIDStr, err := repo.GetTagCommitID(ctx, name)
if err != nil {
// every tag should have a commit ID so return all errors
return nil, err
}
commitID, err := NewIDFromString(commitIDStr)
if err != nil {
return nil, err
}
// If type is "commit, the tag is a lightweight tag
if ObjectType(tp) == ObjectCommit {
commit, err := repo.GetCommit(ctx, commitIDStr)
if err != nil {
return nil, err
}
tag := &Tag{
Name: name,
ID: tagID,
Object: commitID,
Type: tp,
Tagger: commit.Committer,
CommitMessage: commit.CommitMessage,
}
repo.tagCache.Set(tagID.String(), tag)
return tag, nil
}
// The tag is an annotated tag with a message.
batch, cancel, err := repo.CatFileBatch()
if err != nil {
return nil, err
}
defer cancel()
info, rd, err := batch.QueryContent(tagID.String())
if err != nil {
if errors.Is(err, io.EOF) || IsErrNotExist(err) {
return nil, ErrNotExist{ID: tagID.String()}
}
return nil, err
}
typ, size := info.Type, info.Size
if typ != "tag" {
if err := DiscardFull(rd, size+1); err != nil {
return nil, err
}
return nil, ErrNotExist{ID: tagID.String()}
}
// then we need to parse the tag and load the commit
limitReader, limitDiscard := limitDiscardReader(rd, info.Size, MaxGitObjectSize)
data, err := io.ReadAll(limitReader)
if err != nil {
return nil, err
}
if err = limitDiscard(); err != nil {
return nil, err
}
_, err = rd.Discard(1)
if err != nil {
return nil, err
}
tag, err := parseTagData(tagID.Type(), data)
if err != nil {
return nil, err
}
tag.Name = name
tag.ID = tagID
tag.Type = tp
repo.tagCache.Set(tagID.String(), tag)
return tag, nil
}
-101
View File
@@ -1,101 +0,0 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"context"
"gitea.dev/modules/log"
"github.com/go-git/go-git/v5/plumbing"
)
// IsTagExist returns true if given tag exists in the repository.
func (repo *Repository) IsTagExist(_ context.Context, name string) bool {
_, err := repo.gogitRepo.Reference(plumbing.ReferenceName(TagPrefix+name), true)
return err == nil
}
// GetTagType gets the type of the tag, either commit (simple) or tag (annotated)
func (repo *Repository) GetTagType(_ context.Context, id ObjectID) (string, error) {
// Get tag type
obj, err := repo.gogitRepo.Object(plumbing.AnyObject, plumbing.Hash(id.RawValue()))
if err != nil {
if err == plumbing.ErrReferenceNotFound {
return "", &ErrNotExist{ID: id.String()}
}
return "", err
}
return obj.Type().String(), nil
}
func (repo *Repository) getTag(ctx context.Context, tagID ObjectID, name string) (*Tag, error) {
t, ok := repo.tagCache.Get(tagID.String())
if ok {
log.Debug("Hit cache: %s", tagID)
tagClone := *t
tagClone.Name = name // This is necessary because lightweight tags may have same id
return &tagClone, nil
}
tp, err := repo.GetTagType(ctx, tagID)
if err != nil {
return nil, err
}
// Get the commit ID and tag ID (may be different for annotated tag) for the returned tag object
commitIDStr, err := repo.GetTagCommitID(ctx, name)
if err != nil {
// every tag should have a commit ID so return all errors
return nil, err
}
commitID, err := NewIDFromString(commitIDStr)
if err != nil {
return nil, err
}
// If type is "commit, the tag is a lightweight tag
if ObjectType(tp) == ObjectCommit {
commit, err := repo.GetCommit(ctx, commitIDStr)
if err != nil {
return nil, err
}
tag := &Tag{
Name: name,
ID: tagID,
Object: commitID,
Type: tp,
Tagger: commit.Committer,
CommitMessage: CommitMessage{MessageRaw: commit.CommitMessage.MessageRaw},
}
repo.tagCache.Set(tagID.String(), tag)
return tag, nil
}
gogitTag, err := repo.gogitRepo.TagObject(plumbing.Hash(tagID.RawValue()))
if err != nil {
if err == plumbing.ErrReferenceNotFound {
return nil, &ErrNotExist{ID: tagID.String()}
}
return nil, err
}
tag := &Tag{
Name: name,
ID: tagID,
Object: commitID.Type().MustID(gogitTag.Target[:]),
Type: tp,
Tagger: &gogitTag.Tagger,
CommitMessage: CommitMessage{MessageRaw: gogitTag.Message},
}
repo.tagCache.Set(tagID.String(), tag)
return tag, nil
}
-134
View File
@@ -1,134 +0,0 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (
"context"
"errors"
"io"
"gitea.dev/modules/log"
)
// IsTagExist returns true if given tag exists in the repository.
func (repo *Repository) IsTagExist(ctx context.Context, name string) bool {
if repo == nil || name == "" {
return false
}
return repo.IsReferenceExist(ctx, TagPrefix+name)
}
// GetTagType gets the type of the tag, either commit (simple) or tag (annotated)
func (repo *Repository) GetTagType(ctx context.Context, id ObjectID) (string, error) {
batch, cancel, err := repo.CatFileBatch()
if err != nil {
return "", err
}
defer cancel()
info, err := batch.QueryInfo(id.String())
if err != nil {
if IsErrNotExist(err) {
return "", ErrNotExist{ID: id.String()}
}
return "", err
}
return info.Type, nil
}
func (repo *Repository) getTag(ctx context.Context, tagID ObjectID, name string) (*Tag, error) {
t, ok := repo.tagCache.Get(tagID.String())
if ok {
log.Debug("Hit cache: %s", tagID)
tagClone := *t
tagClone.Name = name // This is necessary because lightweight tags may have same id
return &tagClone, nil
}
tp, err := repo.GetTagType(ctx, tagID)
if err != nil {
return nil, err
}
// Get the commit ID and tag ID (may be different for annotated tag) for the returned tag object
commitIDStr, err := repo.GetTagCommitID(ctx, name)
if err != nil {
// every tag should have a commit ID so return all errors
return nil, err
}
commitID, err := NewIDFromString(commitIDStr)
if err != nil {
return nil, err
}
// If type is "commit, the tag is a lightweight tag
if ObjectType(tp) == ObjectCommit {
commit, err := repo.GetCommit(ctx, commitIDStr)
if err != nil {
return nil, err
}
tag := &Tag{
Name: name,
ID: tagID,
Object: commitID,
Type: tp,
Tagger: commit.Committer,
CommitMessage: commit.CommitMessage,
}
repo.tagCache.Set(tagID.String(), tag)
return tag, nil
}
// The tag is an annotated tag with a message.
batch, cancel, err := repo.CatFileBatch()
if err != nil {
return nil, err
}
defer cancel()
info, rd, err := batch.QueryContent(tagID.String())
if err != nil {
if errors.Is(err, io.EOF) || IsErrNotExist(err) {
return nil, ErrNotExist{ID: tagID.String()}
}
return nil, err
}
typ, size := info.Type, info.Size
if typ != "tag" {
if err := DiscardFull(rd, size+1); err != nil {
return nil, err
}
return nil, ErrNotExist{ID: tagID.String()}
}
// then we need to parse the tag and load the commit
limitReader, limitDiscard := limitDiscardReader(rd, info.Size, MaxGitObjectSize)
data, err := io.ReadAll(limitReader)
if err != nil {
return nil, err
}
if err = limitDiscard(); err != nil {
return nil, err
}
_, err = rd.Discard(1)
if err != nil {
return nil, err
}
tag, err := parseTagData(tagID.Type(), data)
if err != nil {
return nil, err
}
tag.Name = name
tag.ID = tagID
tag.Type = tp
repo.tagCache.Set(tagID.String(), tag)
return tag, nil
}
+85
View File
@@ -7,6 +7,7 @@ package git
import (
"bytes"
"context"
"io"
"os"
"strings"
"time"
@@ -68,3 +69,87 @@ func (repo *Repository) CommitTree(ctx context.Context, author, committer *Signa
}
return NewIDFromString(strings.TrimSpace(stdout))
}
func (repo *Repository) getTree(ctx context.Context, id ObjectID) (*Tree, error) {
batch, cancel, err := repo.CatFileBatch()
if err != nil {
return nil, err
}
defer cancel()
info, rd, err := batch.QueryContent(id.String())
if err != nil {
return nil, err
}
switch info.Type {
case "tag":
data, err := io.ReadAll(io.LimitReader(rd, info.Size))
if err != nil {
return nil, err
}
tag, err := parseTagData(id.Type(), data)
if err != nil {
return nil, err
}
commit, err := repo.getCommitWithBatch(batch, tag.Object)
if err != nil {
return nil, err
}
tree := commit.Tree()
return tree, nil
case "commit":
commit, err := CommitFromReader(id, io.LimitReader(rd, info.Size))
if err != nil {
return nil, err
}
if _, err := rd.Discard(1); err != nil {
return nil, err
}
tree := commit.Tree()
return tree, nil
case "tree":
tree := newTree(id)
objectFormat, err := repo.GetObjectFormat(ctx)
if err != nil {
return nil, err
}
tree.entries, err = catBatchParseTreeEntries(objectFormat, tree, rd, info.Size)
if err != nil {
return nil, err
}
tree.entriesParsed = true
return tree, nil
default:
if err := DiscardFull(rd, info.Size+1); err != nil {
return nil, err
}
return nil, ErrNotExist{
ID: id.String(),
}
}
}
// GetTree find the tree object in the repository.
func (repo *Repository) GetTree(ctx context.Context, idStr string) (*Tree, error) {
objectFormat, err := repo.GetObjectFormat(ctx)
if err != nil {
return nil, err
}
if len(idStr) != objectFormat.FullLength() {
res, err := repo.GetRefCommitID(ctx, idStr)
if err != nil {
return nil, err
}
if len(res) > 0 {
idStr = res
}
}
id, err := NewIDFromString(idStr)
if err != nil {
return nil, err
}
return repo.getTree(ctx, id)
}
-66
View File
@@ -1,66 +0,0 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"context"
"errors"
"gitea.dev/modules/git/gitcmd"
"github.com/go-git/go-git/v5/plumbing"
)
func (repo *Repository) getTree(_ context.Context, id ObjectID) (*Tree, error) {
gogitTree, err := repo.gogitRepo.TreeObject(plumbing.Hash(id.RawValue()))
if err != nil {
if errors.Is(err, plumbing.ErrObjectNotFound) {
return nil, ErrNotExist{
ID: id.String(),
}
}
return nil, err
}
tree := newTree(id)
tree.resolvedGogitTreeObject = gogitTree
return tree, nil
}
// GetTree find the tree object in the repository.
func (repo *Repository) GetTree(ctx context.Context, idStr string) (*Tree, error) {
objectFormat, err := repo.GetObjectFormat(ctx)
if err != nil {
return nil, err
}
if len(idStr) != objectFormat.FullLength() {
res, _, err := gitcmd.NewCommand("rev-parse", "--verify").
AddDynamicArguments(idStr).
WithRepo(repo).
RunStdString(ctx)
if err != nil {
return nil, err
}
if len(res) > 0 {
idStr = res[:len(res)-1]
}
}
id, err := NewIDFromString(idStr)
if err != nil {
return nil, err
}
commitObject, err := repo.gogitRepo.CommitObject(plumbing.Hash(id.RawValue()))
if err == nil {
id = ParseGogitHash(commitObject.TreeHash)
}
treeObject, err := repo.getTree(ctx, id)
if err != nil {
return nil, err
}
return treeObject, nil
}
-95
View File
@@ -1,95 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (
"context"
"io"
)
func (repo *Repository) getTree(ctx context.Context, id ObjectID) (*Tree, error) {
batch, cancel, err := repo.CatFileBatch()
if err != nil {
return nil, err
}
defer cancel()
info, rd, err := batch.QueryContent(id.String())
if err != nil {
return nil, err
}
switch info.Type {
case "tag":
data, err := io.ReadAll(io.LimitReader(rd, info.Size))
if err != nil {
return nil, err
}
tag, err := parseTagData(id.Type(), data)
if err != nil {
return nil, err
}
commit, err := repo.getCommitWithBatch(batch, tag.Object)
if err != nil {
return nil, err
}
tree := commit.Tree()
return tree, nil
case "commit":
commit, err := CommitFromReader(id, io.LimitReader(rd, info.Size))
if err != nil {
return nil, err
}
if _, err := rd.Discard(1); err != nil {
return nil, err
}
tree := commit.Tree()
return tree, nil
case "tree":
tree := newTree(id)
objectFormat, err := repo.GetObjectFormat(ctx)
if err != nil {
return nil, err
}
tree.entries, err = catBatchParseTreeEntries(objectFormat, tree, rd, info.Size)
if err != nil {
return nil, err
}
tree.entriesParsed = true
return tree, nil
default:
if err := DiscardFull(rd, info.Size+1); err != nil {
return nil, err
}
return nil, ErrNotExist{
ID: id.String(),
}
}
}
// GetTree find the tree object in the repository.
func (repo *Repository) GetTree(ctx context.Context, idStr string) (*Tree, error) {
objectFormat, err := repo.GetObjectFormat(ctx)
if err != nil {
return nil, err
}
if len(idStr) != objectFormat.FullLength() {
res, err := repo.GetRefCommitID(ctx, idStr)
if err != nil {
return nil, err
}
if len(res) > 0 {
idStr = res
}
}
id, err := NewIDFromString(idStr)
if err != nil {
return nil, err
}
return repo.getTree(ctx, id)
}
+25
View File
@@ -5,11 +5,14 @@
package git
import (
"fmt"
"io"
"strconv"
"strings"
"time"
"gitea.dev/modules/log"
"gitea.dev/modules/util"
)
// Helper to get a signature from the commit line, which looks like:
@@ -47,3 +50,25 @@ func parseSignatureFromCommitLine(line string) *Signature {
}
return sig
}
// Signature represents the Author, Committer or Tagger information.
type Signature struct {
Name string // the committer name, it can be anything
Email string // the committer email, it can be anything
When time.Time // the timestamp of the signature
}
func (s *Signature) String() string {
return fmt.Sprintf("%s <%s>", s.Name, s.Email)
}
// Encode writes the signature for git commit object
func (s *Signature) Encode(w io.Writer) error {
_, err := fmt.Fprintf(w, "%s <%s> %d %s", s.Name, s.Email, max(0, s.When.Unix()), s.When.Format("-0700"))
return err
}
// Decode parses the signature for git commit object
func (s *Signature) Decode(b []byte) {
*s = *parseSignatureFromCommitLine(util.UnsafeBytesToString(b))
}
-14
View File
@@ -1,14 +0,0 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"github.com/go-git/go-git/v5/plumbing/object"
)
// Signature represents the Author or Committer information.
type Signature = object.Signature
-37
View File
@@ -1,37 +0,0 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (
"fmt"
"io"
"time"
"gitea.dev/modules/util"
)
// Signature represents the Author, Committer or Tagger information.
type Signature struct {
Name string // the committer name, it can be anything
Email string // the committer email, it can be anything
When time.Time // the timestamp of the signature
}
func (s *Signature) String() string {
return fmt.Sprintf("%s <%s>", s.Name, s.Email)
}
// Encode writes the signature for git commit object (same as gogit's object.Signature Encode method)
func (s *Signature) Encode(w io.Writer) error {
_, err := fmt.Fprintf(w, "%s <%s> %d %s", s.Name, s.Email, max(0, s.When.Unix()), s.When.Format("-0700"))
return err
}
// Decode parses the signature for git commit object (same as gogit's object.Signature Decode method)
func (s *Signature) Decode(b []byte) {
*s = *parseSignatureFromCommitLine(util.UnsafeBytesToString(b))
}
+95
View File
@@ -7,6 +7,7 @@ package git
import (
"bytes"
"context"
"io"
"strings"
"gitea.dev/modules/git/gitcmd"
@@ -76,3 +77,97 @@ func (repo *Repository) GetTreePathLatestCommit(ctx context.Context, refName, tr
}
return repo.GetCommit(ctx, strings.TrimSpace(stdout))
}
// Tree represents a flat directory listing.
type Tree struct {
TreeCommon
entries Entries
entriesParsed bool
}
// ListEntries returns all entries of current tree.
func (t *Tree) ListEntries(ctx context.Context, gitRepo *Repository) (Entries, error) {
if t.entriesParsed {
return t.entries, nil
}
batch, cancel, err := gitRepo.CatFileBatch()
if err != nil {
return nil, err
}
defer cancel()
info, rd, err := batch.QueryContent(t.ID.String())
if err != nil {
return nil, err
}
if info.Type == "commit" {
treeID, err := ReadTreeID(rd, info.Size)
if err != nil && err != io.EOF {
return nil, err
}
info, rd, err = batch.QueryContent(treeID)
if err != nil {
return nil, err
}
}
if info.Type == "tree" {
t.entries, err = catBatchParseTreeEntries(t.ID.Type(), t, rd, info.Size)
if err != nil {
return nil, err
}
t.entriesParsed = true
return t.entries, nil
}
// Not a tree just use ls-tree instead
if err := DiscardFull(rd, info.Size+1); err != nil {
return nil, err
}
stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-l").AddDynamicArguments(t.ID.String()).WithRepo(gitRepo).RunStdBytes(ctx)
if runErr != nil {
if gitcmd.IsStderr(runErr, gitcmd.StderrNotValidObjectName) || gitcmd.IsStderr(runErr, gitcmd.StderrNotTreeObject) {
return nil, ErrNotExist{
ID: t.ID.String(),
}
}
return nil, runErr
}
t.entries, err = parseTreeEntries(stdout, t)
if err == nil {
t.entriesParsed = true
}
return t.entries, err
}
// listEntriesRecursive returns all entries of current tree recursively including all subtrees
// extraArgs could be "-l" to get the size, which is slower
func (t *Tree) listEntriesRecursive(ctx context.Context, gitRepo *Repository, extraArgs gitcmd.TrustedCmdArgs) (Entries, error) {
stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-t", "-r").
AddArguments(extraArgs...).
AddDynamicArguments(t.ID.String()).
WithRepo(gitRepo).
RunStdBytes(ctx)
if runErr != nil {
return nil, runErr
}
// FIXME: the "name" field is abused, here it is a full path
// FIXME: this ptree is not right, fortunately it isn't really used
return parseTreeEntries(stdout, t)
}
// ListEntriesRecursiveFast returns all entries of current tree recursively including all subtrees, no size
func (t *Tree) ListEntriesRecursiveFast(ctx context.Context, gitRepo *Repository) (Entries, error) {
return t.listEntriesRecursive(ctx, gitRepo, nil)
}
// ListEntriesRecursiveWithSize returns all entries of current tree recursively including all subtrees, with size
func (t *Tree) ListEntriesRecursiveWithSize(ctx context.Context, gitRepo *Repository) (Entries, error) {
return t.listEntriesRecursive(ctx, gitRepo, gitcmd.TrustedCmdArgs{"--long"})
}
+40 -1
View File
@@ -4,7 +4,11 @@
package git
import "context"
import (
"context"
"path"
"strings"
)
// GetBlobByPath get the blob object according the path
func (t *Tree) GetBlobByPath(ctx context.Context, gitRepo *Repository, relpath string) (*Blob, error) {
@@ -19,3 +23,38 @@ func (t *Tree) GetBlobByPath(ctx context.Context, gitRepo *Repository, relpath s
return nil, ErrNotExist{"", relpath}
}
// GetTreeEntryByPath get the tree entries according the sub dir
func (t *Tree) GetTreeEntryByPath(ctx context.Context, gitRepo *Repository, relpath string) (_ *TreeEntry, err error) {
if len(relpath) == 0 {
return &TreeEntry{
ptree: t,
ID: t.ID,
name: "",
entryMode: EntryModeTree,
}, nil
}
relpath = path.Clean(relpath)
parts := strings.Split(relpath, "/")
tree := t
for _, name := range parts[:len(parts)-1] {
tree, err = tree.SubTree(ctx, gitRepo, name)
if err != nil {
return nil, err
}
}
name := parts[len(parts)-1]
entries, err := tree.ListEntries(ctx, gitRepo)
if err != nil {
return nil, err
}
for _, v := range entries {
if v.Name() == name {
return v, nil
}
}
return nil, ErrNotExist{"", relpath}
}
-61
View File
@@ -1,61 +0,0 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"context"
"path"
"strings"
"github.com/go-git/go-git/v5/plumbing"
)
// GetTreeEntryByPath get the tree entries according the sub dir
func (t *Tree) GetTreeEntryByPath(ctx context.Context, gitRepo *Repository, relpath string) (*TreeEntry, error) {
if len(relpath) == 0 {
return &TreeEntry{
ID: t.ID,
ptree: t,
name: "",
entryMode: EntryModeTree,
}, nil
}
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(ctx, gitRepo)
if err != nil {
if err == plumbing.ErrObjectNotFound {
return nil, ErrNotExist{
RelPath: relpath,
}
}
return nil, err
}
for _, v := range entries {
if v.Name() == name {
return v, nil
}
}
} else {
tree, err = tree.SubTree(ctx, gitRepo, name)
if err != nil {
if err == plumbing.ErrObjectNotFound {
return nil, ErrNotExist{
RelPath: relpath,
}
}
return nil, err
}
}
}
return nil, ErrNotExist{"", relpath}
}
-47
View File
@@ -1,47 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (
"context"
"path"
"strings"
)
// GetTreeEntryByPath get the tree entries according the sub dir
func (t *Tree) GetTreeEntryByPath(ctx context.Context, gitRepo *Repository, relpath string) (_ *TreeEntry, err error) {
if len(relpath) == 0 {
return &TreeEntry{
ptree: t,
ID: t.ID,
name: "",
entryMode: EntryModeTree,
}, nil
}
relpath = path.Clean(relpath)
parts := strings.Split(relpath, "/")
tree := t
for _, name := range parts[:len(parts)-1] {
tree, err = tree.SubTree(ctx, gitRepo, name)
if err != nil {
return nil, err
}
}
name := parts[len(parts)-1]
entries, err := tree.ListEntries(ctx, gitRepo)
if err != nil {
return nil, err
}
for _, v := range entries {
if v.Name() == name {
return v, nil
}
}
return nil, ErrNotExist{"", relpath}
}
+36
View File
@@ -10,6 +10,7 @@ import (
"slices"
"strings"
"gitea.dev/modules/log"
"gitea.dev/modules/util"
)
@@ -169,3 +170,38 @@ func (tes Entries) CustomSort(cmp func(s1, s2 string) int) {
return cmp(a.Name(), b.Name())
})
}
func (te *TreeEntry) GetSize(ctx context.Context, gitRepo *Repository) int64 {
if te.IsDir() {
return 0
} else if te.sized {
return te.size
}
batch, cancel, err := gitRepo.CatFileBatch()
if err != nil {
log.Debug("error whilst reading size for %s in %s. Error: %v", te.ID.String(), gitRepo.LogString(), err)
return 0
}
defer cancel()
info, err := batch.QueryInfo(te.ID.String())
if err != nil {
log.Debug("error whilst reading size for %s in %s. Error: %v", te.ID.String(), gitRepo.LogString(), err)
return 0
}
te.size = info.Size
te.sized = true
return te.size
}
// Blob returns the blob object the entry
func (te *TreeEntry) Blob(gitRepo *Repository) *Blob {
return &Blob{
ID: te.ID,
name: te.Name(),
size: te.size,
gotSize: te.sized,
repo: gitRepo,
}
}
-63
View File
@@ -1,63 +0,0 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"context"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/filemode"
"github.com/go-git/go-git/v5/plumbing/object"
)
// gogitFileModeToEntryMode converts go-git filemode to EntryMode
func gogitFileModeToEntryMode(mode filemode.FileMode) EntryMode {
return EntryMode(mode)
}
func entryModeToGogitFileMode(mode EntryMode) filemode.FileMode {
return filemode.FileMode(mode)
}
func (te *TreeEntry) toGogitTreeEntry() *object.TreeEntry {
return &object.TreeEntry{
Name: te.name,
Mode: entryModeToGogitFileMode(te.entryMode),
Hash: plumbing.Hash(te.ID.RawValue()),
}
}
// GetSize returns the size of the entry
func (te *TreeEntry) GetSize(ctx context.Context, gitRepo *Repository) int64 {
if te.IsDir() {
return 0
} else if te.sized {
return te.size
}
ptreeGogitTree, err := te.ptree.gogitTreeObject(gitRepo)
if err != nil {
return 0
}
file, err := ptreeGogitTree.TreeEntryFile(te.toGogitTreeEntry())
if err != nil {
return 0
}
te.sized = true
te.size = file.Size
return te.size
}
// Blob returns the blob object the entry
func (te *TreeEntry) Blob(gitRepo *Repository) *Blob {
return &Blob{
ID: te.ID,
repo: gitRepo,
name: te.Name(),
}
}
-27
View File
@@ -1,27 +0,0 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"testing"
"github.com/go-git/go-git/v5/plumbing/filemode"
"github.com/stretchr/testify/assert"
)
func TestEntryGogit(t *testing.T) {
cases := map[EntryMode]filemode.FileMode{
EntryModeBlob: filemode.Regular,
EntryModeCommit: filemode.Submodule,
EntryModeExec: filemode.Executable,
EntryModeSymlink: filemode.Symlink,
EntryModeTree: filemode.Dir,
}
for emode, fmode := range cases {
assert.Equal(t, fmode, entryModeToGogitFileMode(emode))
assert.Equal(t, emode, gogitFileModeToEntryMode(fmode))
}
}
-47
View File
@@ -1,47 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (
"context"
"gitea.dev/modules/log"
)
func (te *TreeEntry) GetSize(ctx context.Context, gitRepo *Repository) int64 {
if te.IsDir() {
return 0
} else if te.sized {
return te.size
}
batch, cancel, err := gitRepo.CatFileBatch()
if err != nil {
log.Debug("error whilst reading size for %s in %s. Error: %v", te.ID.String(), gitRepo.LogString(), err)
return 0
}
defer cancel()
info, err := batch.QueryInfo(te.ID.String())
if err != nil {
log.Debug("error whilst reading size for %s in %s. Error: %v", te.ID.String(), gitRepo.LogString(), err)
return 0
}
te.size = info.Size
te.sized = true
return te.size
}
// Blob returns the blob object the entry
func (te *TreeEntry) Blob(gitRepo *Repository) *Blob {
return &Blob{
ID: te.ID,
name: te.Name(),
size: te.size,
gotSize: te.sized,
repo: gitRepo,
}
}
-82
View File
@@ -1,82 +0,0 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"context"
"io"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
)
// Tree represents a flat directory listing.
type Tree struct {
TreeCommon
resolvedGogitTreeObject *object.Tree
}
func (t *Tree) gogitTreeObject(gitRepo *Repository) (_ *object.Tree, err error) {
if t.resolvedGogitTreeObject == nil {
t.resolvedGogitTreeObject, err = gitRepo.gogitRepo.TreeObject(plumbing.Hash(t.ID.RawValue()))
if err != nil {
return nil, err
}
}
return t.resolvedGogitTreeObject, nil
}
// ListEntries returns all entries of current tree.
func (t *Tree) ListEntries(_ context.Context, gitRepo *Repository) (Entries, error) {
gogitTree, err := t.gogitTreeObject(gitRepo)
if err != nil {
return nil, err
}
entries := make([]*TreeEntry, len(gogitTree.Entries))
for i, gogitTreeEntry := range gogitTree.Entries {
entries[i] = &TreeEntry{
ID: ParseGogitHash(gogitTreeEntry.Hash),
ptree: t,
name: gogitTreeEntry.Name,
entryMode: gogitFileModeToEntryMode(gogitTreeEntry.Mode),
}
}
return entries, nil
}
// ListEntriesRecursiveWithSize returns all entries of current tree recursively including all subtrees
func (t *Tree) ListEntriesRecursiveWithSize(_ context.Context, gitRepo *Repository) (entries Entries, _ error) {
gogitTree, err := t.gogitTreeObject(gitRepo)
if err != nil {
return nil, err
}
walker := object.NewTreeWalker(gogitTree, true, nil)
for {
fullName, gogitTreeEntry, err := walker.Next()
if err == io.EOF {
break
} else if err != nil {
return nil, err
}
convertedEntry := &TreeEntry{
ID: ParseGogitHash(gogitTreeEntry.Hash),
name: fullName, // FIXME: the "name" field is abused, here it is a full path
ptree: t, // FIXME: this ptree is not right, fortunately it isn't really used
entryMode: gogitFileModeToEntryMode(gogitTreeEntry.Mode),
}
entries = append(entries, convertedEntry)
}
return entries, nil
}
// ListEntriesRecursiveFast is the alias of ListEntriesRecursiveWithSize for the gogit version
func (t *Tree) ListEntriesRecursiveFast(ctx context.Context, gitRepo *Repository) (Entries, error) {
return t.ListEntriesRecursiveWithSize(ctx, gitRepo)
}
-107
View File
@@ -1,107 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (
"context"
"io"
"gitea.dev/modules/git/gitcmd"
)
// Tree represents a flat directory listing.
type Tree struct {
TreeCommon
entries Entries
entriesParsed bool
}
// ListEntries returns all entries of current tree.
func (t *Tree) ListEntries(ctx context.Context, gitRepo *Repository) (Entries, error) {
if t.entriesParsed {
return t.entries, nil
}
batch, cancel, err := gitRepo.CatFileBatch()
if err != nil {
return nil, err
}
defer cancel()
info, rd, err := batch.QueryContent(t.ID.String())
if err != nil {
return nil, err
}
if info.Type == "commit" {
treeID, err := ReadTreeID(rd, info.Size)
if err != nil && err != io.EOF {
return nil, err
}
info, rd, err = batch.QueryContent(treeID)
if err != nil {
return nil, err
}
}
if info.Type == "tree" {
t.entries, err = catBatchParseTreeEntries(t.ID.Type(), t, rd, info.Size)
if err != nil {
return nil, err
}
t.entriesParsed = true
return t.entries, nil
}
// Not a tree just use ls-tree instead
if err := DiscardFull(rd, info.Size+1); err != nil {
return nil, err
}
stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-l").AddDynamicArguments(t.ID.String()).WithRepo(gitRepo).RunStdBytes(ctx)
if runErr != nil {
if gitcmd.IsStderr(runErr, gitcmd.StderrNotValidObjectName) || gitcmd.IsStderr(runErr, gitcmd.StderrNotTreeObject) {
return nil, ErrNotExist{
ID: t.ID.String(),
}
}
return nil, runErr
}
t.entries, err = parseTreeEntries(stdout, t)
if err == nil {
t.entriesParsed = true
}
return t.entries, err
}
// listEntriesRecursive returns all entries of current tree recursively including all subtrees
// extraArgs could be "-l" to get the size, which is slower
func (t *Tree) listEntriesRecursive(ctx context.Context, gitRepo *Repository, extraArgs gitcmd.TrustedCmdArgs) (Entries, error) {
stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-t", "-r").
AddArguments(extraArgs...).
AddDynamicArguments(t.ID.String()).
WithRepo(gitRepo).
RunStdBytes(ctx)
if runErr != nil {
return nil, runErr
}
// FIXME: the "name" field is abused, here it is a full path
// FIXME: this ptree is not right, fortunately it isn't really used
return parseTreeEntries(stdout, t)
}
// ListEntriesRecursiveFast returns all entries of current tree recursively including all subtrees, no size
func (t *Tree) ListEntriesRecursiveFast(ctx context.Context, gitRepo *Repository) (Entries, error) {
return t.listEntriesRecursive(ctx, gitRepo, nil)
}
// ListEntriesRecursiveWithSize returns all entries of current tree recursively including all subtrees, with size
func (t *Tree) ListEntriesRecursiveWithSize(ctx context.Context, gitRepo *Repository) (Entries, error) {
return t.listEntriesRecursive(ctx, gitRepo, gitcmd.TrustedCmdArgs{"--long"})
}
@@ -1,8 +1,6 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package lfs
import (
-55
View File
@@ -1,55 +0,0 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package lfs
import (
"context"
"fmt"
"gitea.dev/modules/git"
"github.com/go-git/go-git/v5/plumbing/object"
)
// SearchPointerBlobs scans the whole repository for LFS pointer files
func SearchPointerBlobs(ctx context.Context, repo *git.Repository, pointerChan chan<- PointerBlob) error {
gitRepo := repo.GoGitRepo()
err := func() error {
blobs, err := gitRepo.BlobObjects()
if err != nil {
return fmt.Errorf("lfs.SearchPointerBlobs BlobObjects: %w", err)
}
return blobs.ForEach(func(blob *object.Blob) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
if blob.Size > MetaFileMaxSize {
return nil
}
reader, err := blob.Reader()
if err != nil {
return fmt.Errorf("lfs.SearchPointerBlobs blob.Reader: %w", err)
}
defer reader.Close()
pointer, _ := ReadPointer(reader)
if pointer.IsValid() {
pointerChan <- PointerBlob{Hash: blob.Hash.String(), Pointer: pointer}
}
return nil
})
}()
close(pointerChan)
return err
}
-2
View File
@@ -28,7 +28,6 @@ var Git = struct {
GCArgs []string `ini:"GC_ARGS" delim:" "`
EnableAutoGitWireProtocol bool
PullRequestPushMessage bool
LargeObjectThreshold int64
DisableCoreProtectNTFS bool
DisablePartialClone bool
DiffRenameSimilarityThreshold string
@@ -49,7 +48,6 @@ var Git = struct {
GCArgs: []string{},
EnableAutoGitWireProtocol: true,
PullRequestPushMessage: true,
LargeObjectThreshold: 1024 * 1024,
DisablePartialClone: false,
DiffRenameSimilarityThreshold: "50%",
Timeout: struct {