mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-05 01:18:39 +00:00
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:
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
|
||||
+2
-3
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 (
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 (
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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"})
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 (
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user