[CHORE] Drop go-git
support
See https://codeberg.org/forgejo/discussions/issues/164 for the rationale and discussion of this change. Everything related to the `go-git` dependency is dropped (Only a single instance is left in a test file to test for an XSS, it requires crafting an commit that Git itself refuses to craft). `_gogit` files have been removed entirely, `go:build: !gogit` is removed, `XXX_nogogit.go` files either have been renamed or had their code being merged into the `XXX.go` file.
This commit is contained in:
parent
4132b18e59
commit
a21128a734
70 changed files with 1632 additions and 4069 deletions
3
Makefile
3
Makefile
|
@ -836,9 +836,6 @@ $(DIST_DIRS):
|
||||||
.PHONY: release-windows
|
.PHONY: release-windows
|
||||||
release-windows: | $(DIST_DIRS)
|
release-windows: | $(DIST_DIRS)
|
||||||
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) .
|
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) .
|
||||||
ifeq (,$(findstring gogit,$(TAGS)))
|
|
||||||
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo gogit $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION)-gogit .
|
|
||||||
endif
|
|
||||||
|
|
||||||
.PHONY: release-linux
|
.PHONY: release-linux
|
||||||
release-linux: | $(DIST_DIRS)
|
release-linux: | $(DIST_DIRS)
|
||||||
|
|
4
go.mod
4
go.mod
|
@ -31,7 +31,6 @@ require (
|
||||||
github.com/dustin/go-humanize v1.0.1
|
github.com/dustin/go-humanize v1.0.1
|
||||||
github.com/editorconfig/editorconfig-core-go/v2 v2.6.2
|
github.com/editorconfig/editorconfig-core-go/v2 v2.6.2
|
||||||
github.com/emersion/go-imap v1.2.1
|
github.com/emersion/go-imap v1.2.1
|
||||||
github.com/emirpasic/gods v1.18.1
|
|
||||||
github.com/felixge/fgprof v0.9.4
|
github.com/felixge/fgprof v0.9.4
|
||||||
github.com/fsnotify/fsnotify v1.7.0
|
github.com/fsnotify/fsnotify v1.7.0
|
||||||
github.com/gliderlabs/ssh v0.3.7
|
github.com/gliderlabs/ssh v0.3.7
|
||||||
|
@ -42,7 +41,6 @@ require (
|
||||||
github.com/go-co-op/gocron v1.37.0
|
github.com/go-co-op/gocron v1.37.0
|
||||||
github.com/go-enry/go-enry/v2 v2.8.8
|
github.com/go-enry/go-enry/v2 v2.8.8
|
||||||
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e
|
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e
|
||||||
github.com/go-git/go-billy/v5 v5.5.0
|
|
||||||
github.com/go-git/go-git/v5 v5.11.0
|
github.com/go-git/go-git/v5 v5.11.0
|
||||||
github.com/go-ldap/ldap/v3 v3.4.6
|
github.com/go-ldap/ldap/v3 v3.4.6
|
||||||
github.com/go-sql-driver/mysql v1.8.1
|
github.com/go-sql-driver/mysql v1.8.1
|
||||||
|
@ -172,6 +170,7 @@ require (
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.0 // indirect
|
github.com/dlclark/regexp2 v1.11.0 // indirect
|
||||||
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect
|
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect
|
||||||
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
github.com/fatih/color v1.16.0 // indirect
|
github.com/fatih/color v1.16.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.5.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.5.0 // indirect
|
||||||
|
@ -181,6 +180,7 @@ require (
|
||||||
github.com/go-faster/city v1.0.1 // indirect
|
github.com/go-faster/city v1.0.1 // indirect
|
||||||
github.com/go-faster/errors v0.7.1 // indirect
|
github.com/go-faster/errors v0.7.1 // indirect
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
|
github.com/go-git/go-billy/v5 v5.5.0 // indirect
|
||||||
github.com/go-ini/ini v1.67.0 // indirect
|
github.com/go-ini/ini v1.67.0 // indirect
|
||||||
github.com/go-openapi/analysis v0.22.2 // indirect
|
github.com/go-openapi/analysis v0.22.2 // indirect
|
||||||
github.com/go-openapi/errors v0.21.0 // indirect
|
github.com/go-openapi/errors v0.21.0 // indirect
|
||||||
|
|
|
@ -5,15 +5,119 @@
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/typesniffer"
|
"code.gitea.io/gitea/modules/typesniffer"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This file contains common functions between the gogit and !gogit variants for git Blobs
|
// 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() (io.ReadCloser, error) {
|
||||||
|
wr, rd, cancel := b.repo.CatFileBatch(b.repo.Ctx)
|
||||||
|
|
||||||
|
_, err := wr.Write([]byte(b.ID.String() + "\n"))
|
||||||
|
if err != nil {
|
||||||
|
cancel()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, _, size, err := ReadBatchLine(rd)
|
||||||
|
if err != nil {
|
||||||
|
cancel()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
b.gotSize = true
|
||||||
|
b.size = size
|
||||||
|
|
||||||
|
if size < 4096 {
|
||||||
|
bs, err := io.ReadAll(io.LimitReader(rd, size))
|
||||||
|
defer cancel()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = rd.Discard(1)
|
||||||
|
return io.NopCloser(bytes.NewReader(bs)), err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &blobReader{
|
||||||
|
rd: rd,
|
||||||
|
n: size,
|
||||||
|
cancel: cancel,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size returns the uncompressed size of the blob
|
||||||
|
func (b *Blob) Size() int64 {
|
||||||
|
if b.gotSize {
|
||||||
|
return b.size
|
||||||
|
}
|
||||||
|
|
||||||
|
wr, rd, cancel := b.repo.CatFileBatchCheck(b.repo.Ctx)
|
||||||
|
defer cancel()
|
||||||
|
_, err := wr.Write([]byte(b.ID.String() + "\n"))
|
||||||
|
if err != nil {
|
||||||
|
log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.Path, err)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
_, _, b.size, err = ReadBatchLine(rd)
|
||||||
|
if err != nil {
|
||||||
|
log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.Path, err)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
b.gotSize = true
|
||||||
|
|
||||||
|
return b.size
|
||||||
|
}
|
||||||
|
|
||||||
|
type blobReader struct {
|
||||||
|
rd *bufio.Reader
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
// Name returns name of the tree entry this blob object was created from (or empty string)
|
// Name returns name of the tree entry this blob object was created from (or empty string)
|
||||||
func (b *Blob) Name() string {
|
func (b *Blob) Name() string {
|
||||||
|
@ -100,3 +204,18 @@ func (b *Blob) GuessContentType() (typesniffer.SniffedType, error) {
|
||||||
|
|
||||||
return typesniffer.DetectContentTypeFromReader(r)
|
return typesniffer.DetectContentTypeFromReader(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetBlob finds the blob object in the repository.
|
||||||
|
func (repo *Repository) GetBlob(idStr string) (*Blob, error) {
|
||||||
|
id, err := NewIDFromString(idStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if id.IsZero() {
|
||||||
|
return nil, ErrNotExist{id.String(), ""}
|
||||||
|
}
|
||||||
|
return &Blob{
|
||||||
|
ID: id,
|
||||||
|
repo: repo,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,32 +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 (
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Blob represents a Git object.
|
|
||||||
type Blob struct {
|
|
||||||
ID ObjectID
|
|
||||||
|
|
||||||
gogitEncodedObj plumbing.EncodedObject
|
|
||||||
name string
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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() (io.ReadCloser, error) {
|
|
||||||
return b.gogitEncodedObj.Reader()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Size returns the uncompressed size of the blob
|
|
||||||
func (b *Blob) Size() int64 {
|
|
||||||
return b.gogitEncodedObj.Size()
|
|
||||||
}
|
|
|
@ -1,118 +0,0 @@
|
||||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build !gogit
|
|
||||||
|
|
||||||
package git
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/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() (io.ReadCloser, error) {
|
|
||||||
wr, rd, cancel := b.repo.CatFileBatch(b.repo.Ctx)
|
|
||||||
|
|
||||||
_, err := wr.Write([]byte(b.ID.String() + "\n"))
|
|
||||||
if err != nil {
|
|
||||||
cancel()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, _, size, err := ReadBatchLine(rd)
|
|
||||||
if err != nil {
|
|
||||||
cancel()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
b.gotSize = true
|
|
||||||
b.size = size
|
|
||||||
|
|
||||||
if size < 4096 {
|
|
||||||
bs, err := io.ReadAll(io.LimitReader(rd, size))
|
|
||||||
defer cancel()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = rd.Discard(1)
|
|
||||||
return io.NopCloser(bytes.NewReader(bs)), err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &blobReader{
|
|
||||||
rd: rd,
|
|
||||||
n: size,
|
|
||||||
cancel: cancel,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Size returns the uncompressed size of the blob
|
|
||||||
func (b *Blob) Size() int64 {
|
|
||||||
if b.gotSize {
|
|
||||||
return b.size
|
|
||||||
}
|
|
||||||
|
|
||||||
wr, rd, cancel := b.repo.CatFileBatchCheck(b.repo.Ctx)
|
|
||||||
defer cancel()
|
|
||||||
_, err := wr.Write([]byte(b.ID.String() + "\n"))
|
|
||||||
if err != nil {
|
|
||||||
log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.Path, err)
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
_, _, b.size, err = ReadBatchLine(rd)
|
|
||||||
if err != nil {
|
|
||||||
log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.Path, err)
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
b.gotSize = true
|
|
||||||
|
|
||||||
return b.size
|
|
||||||
}
|
|
||||||
|
|
||||||
type blobReader struct {
|
|
||||||
rd *bufio.Reader
|
|
||||||
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,75 +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) *ObjectSignature {
|
|
||||||
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 &ObjectSignature{
|
|
||||||
Signature: c.PGPSignature,
|
|
||||||
Payload: w.String(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertCommit(c *object.Commit) *Commit {
|
|
||||||
return &Commit{
|
|
||||||
ID: ParseGogitHash(c.Hash),
|
|
||||||
CommitMessage: c.Message,
|
|
||||||
Committer: &c.Committer,
|
|
||||||
Author: &c.Author,
|
|
||||||
Signature: convertPGPSignature(c),
|
|
||||||
Parents: ParseGogitHashArray(c.ParentHashes),
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,9 +3,173 @@
|
||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"path"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
)
|
||||||
|
|
||||||
// CommitInfo describes the first commit with the provided entry
|
// CommitInfo describes the first commit with the provided entry
|
||||||
type CommitInfo struct {
|
type CommitInfo struct {
|
||||||
Entry *TreeEntry
|
Entry *TreeEntry
|
||||||
Commit *Commit
|
Commit *Commit
|
||||||
SubModuleFile *SubModuleFile
|
SubModuleFile *SubModuleFile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCommitsInfo gets information of all commits that are corresponding to these entries
|
||||||
|
func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) {
|
||||||
|
entryPaths := make([]string, len(tes)+1)
|
||||||
|
// Get the commit for the treePath itself
|
||||||
|
entryPaths[0] = ""
|
||||||
|
for i, entry := range tes {
|
||||||
|
entryPaths[i+1] = entry.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
var revs map[string]*Commit
|
||||||
|
if commit.repo.LastCommitCache != nil {
|
||||||
|
var unHitPaths []string
|
||||||
|
revs, unHitPaths, err = getLastCommitForPathsByCache(commit.ID.String(), treePath, entryPaths, commit.repo.LastCommitCache)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if len(unHitPaths) > 0 {
|
||||||
|
sort.Strings(unHitPaths)
|
||||||
|
commits, err := GetLastCommitForPaths(ctx, commit, treePath, unHitPaths)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for pth, found := range commits {
|
||||||
|
revs[pth] = found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sort.Strings(entryPaths)
|
||||||
|
revs, err = GetLastCommitForPaths(ctx, commit, treePath, entryPaths)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
commitsInfo := make([]CommitInfo, len(tes))
|
||||||
|
for i, entry := range tes {
|
||||||
|
commitsInfo[i] = CommitInfo{
|
||||||
|
Entry: entry,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have found a commit for this entry in time
|
||||||
|
if entryCommit, ok := revs[entry.Name()]; ok {
|
||||||
|
commitsInfo[i].Commit = entryCommit
|
||||||
|
} else {
|
||||||
|
log.Debug("missing commit for %s", entry.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the entry if a submodule add a submodule file for this
|
||||||
|
if entry.IsSubModule() {
|
||||||
|
subModuleURL := ""
|
||||||
|
var fullPath string
|
||||||
|
if len(treePath) > 0 {
|
||||||
|
fullPath = treePath + "/" + entry.Name()
|
||||||
|
} else {
|
||||||
|
fullPath = entry.Name()
|
||||||
|
}
|
||||||
|
if subModule, err := commit.GetSubModule(fullPath); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
} else if subModule != nil {
|
||||||
|
subModuleURL = subModule.URL
|
||||||
|
}
|
||||||
|
subModuleFile := NewSubModuleFile(commitsInfo[i].Commit, subModuleURL, entry.ID.String())
|
||||||
|
commitsInfo[i].SubModuleFile = subModuleFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the commit for the treePath itself (see above). We basically
|
||||||
|
// get it for free during the tree traversal and it's used for listing
|
||||||
|
// pages to display information about newest commit for a given path.
|
||||||
|
var treeCommit *Commit
|
||||||
|
var ok bool
|
||||||
|
if treePath == "" {
|
||||||
|
treeCommit = commit
|
||||||
|
} else if treeCommit, ok = revs[""]; ok {
|
||||||
|
treeCommit.repo = commit.repo
|
||||||
|
}
|
||||||
|
return commitsInfo, treeCommit, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLastCommitForPathsByCache(commitID, treePath string, paths []string, cache *LastCommitCache) (map[string]*Commit, []string, error) {
|
||||||
|
var unHitEntryPaths []string
|
||||||
|
results := make(map[string]*Commit)
|
||||||
|
for _, p := range paths {
|
||||||
|
lastCommit, err := cache.Get(commitID, path.Join(treePath, p))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if lastCommit != nil {
|
||||||
|
results[p] = lastCommit
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
unHitEntryPaths = append(unHitEntryPaths, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, unHitEntryPaths, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLastCommitForPaths returns last commit information
|
||||||
|
func GetLastCommitForPaths(ctx context.Context, 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, commit.repo, commit, treePath, paths...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
batchStdinWriter, batchReader, cancel := commit.repo.CatFileBatch(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
commitsMap := map[string]*Commit{}
|
||||||
|
commitsMap[commit.ID.String()] = commit
|
||||||
|
|
||||||
|
commitCommits := map[string]*Commit{}
|
||||||
|
for path, commitID := range revs {
|
||||||
|
c, ok := commitsMap[commitID]
|
||||||
|
if ok {
|
||||||
|
commitCommits[path] = c
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(commitID) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := batchStdinWriter.Write([]byte(commitID + "\n"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, typ, size, err := ReadBatchLine(batchReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if typ != "commit" {
|
||||||
|
if err := DiscardFull(batchReader, size+1); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unexpected type: %s for commit id: %s", typ, commitID)
|
||||||
|
}
|
||||||
|
c, err = CommitFromReader(commit.repo, MustIDFromString(commitID), io.LimitReader(batchReader, size))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := batchReader.Discard(1); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
commitCommits[path] = c
|
||||||
|
}
|
||||||
|
|
||||||
|
return commitCommits, nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,304 +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"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetCommitsInfo gets information of all commits that are corresponding to these entries
|
|
||||||
func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) {
|
|
||||||
entryPaths := make([]string, len(tes)+1)
|
|
||||||
// Get the commit for the treePath itself
|
|
||||||
entryPaths[0] = ""
|
|
||||||
for i, entry := range tes {
|
|
||||||
entryPaths[i+1] = entry.Name()
|
|
||||||
}
|
|
||||||
|
|
||||||
commitNodeIndex, commitGraphFile := commit.repo.CommitNodeIndex()
|
|
||||||
if commitGraphFile != nil {
|
|
||||||
defer commitGraphFile.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := commitNodeIndex.Get(plumbing.Hash(commit.ID.RawValue()))
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var revs map[string]*Commit
|
|
||||||
if commit.repo.LastCommitCache != nil {
|
|
||||||
var unHitPaths []string
|
|
||||||
revs, unHitPaths, err = getLastCommitForPathsByCache(commit.ID.String(), treePath, entryPaths, commit.repo.LastCommitCache)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
if len(unHitPaths) > 0 {
|
|
||||||
revs2, err := GetLastCommitForPaths(ctx, commit.repo.LastCommitCache, c, treePath, unHitPaths)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range revs2 {
|
|
||||||
revs[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
revs, err = GetLastCommitForPaths(ctx, nil, c, treePath, entryPaths)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
commit.repo.gogitStorage.Close()
|
|
||||||
|
|
||||||
commitsInfo := make([]CommitInfo, len(tes))
|
|
||||||
for i, entry := range tes {
|
|
||||||
commitsInfo[i] = CommitInfo{
|
|
||||||
Entry: entry,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we have found a commit for this entry in time
|
|
||||||
if entryCommit, ok := revs[entry.Name()]; ok {
|
|
||||||
commitsInfo[i].Commit = entryCommit
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the entry if a submodule add a submodule file for this
|
|
||||||
if entry.IsSubModule() {
|
|
||||||
subModuleURL := ""
|
|
||||||
var fullPath string
|
|
||||||
if len(treePath) > 0 {
|
|
||||||
fullPath = treePath + "/" + entry.Name()
|
|
||||||
} else {
|
|
||||||
fullPath = entry.Name()
|
|
||||||
}
|
|
||||||
if subModule, err := commit.GetSubModule(fullPath); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
} else if subModule != nil {
|
|
||||||
subModuleURL = subModule.URL
|
|
||||||
}
|
|
||||||
subModuleFile := NewSubModuleFile(commitsInfo[i].Commit, subModuleURL, entry.ID.String())
|
|
||||||
commitsInfo[i].SubModuleFile = subModuleFile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve the commit for the treePath itself (see above). We basically
|
|
||||||
// get it for free during the tree traversal and it's used for listing
|
|
||||||
// pages to display information about newest commit for a given path.
|
|
||||||
var treeCommit *Commit
|
|
||||||
var ok bool
|
|
||||||
if treePath == "" {
|
|
||||||
treeCommit = commit
|
|
||||||
} else if treeCommit, ok = revs[""]; ok {
|
|
||||||
treeCommit.repo = commit.repo
|
|
||||||
}
|
|
||||||
return commitsInfo, treeCommit, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func getLastCommitForPathsByCache(commitID, treePath string, paths []string, cache *LastCommitCache) (map[string]*Commit, []string, error) {
|
|
||||||
var unHitEntryPaths []string
|
|
||||||
results := make(map[string]*Commit)
|
|
||||||
for _, p := range paths {
|
|
||||||
lastCommit, err := cache.Get(commitID, path.Join(treePath, p))
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
if lastCommit != nil {
|
|
||||||
results[p] = lastCommit
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
unHitEntryPaths = append(unHitEntryPaths, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
return results, unHitEntryPaths, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLastCommitForPaths returns last commit information
|
|
||||||
func GetLastCommitForPaths(ctx context.Context, cache *LastCommitCache, 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()) {
|
|
||||||
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)
|
|
||||||
|
|
||||||
// Load the parent commits for the one we are currently examining
|
|
||||||
numParents := current.commit.NumParents()
|
|
||||||
var parents []cgobject.CommitNode
|
|
||||||
for i := 0; i < numParents; i++ {
|
|
||||||
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 := cache.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
|
|
||||||
} else {
|
|
||||||
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,170 +0,0 @@
|
||||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build !gogit
|
|
||||||
|
|
||||||
package git
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"path"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetCommitsInfo gets information of all commits that are corresponding to these entries
|
|
||||||
func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) {
|
|
||||||
entryPaths := make([]string, len(tes)+1)
|
|
||||||
// Get the commit for the treePath itself
|
|
||||||
entryPaths[0] = ""
|
|
||||||
for i, entry := range tes {
|
|
||||||
entryPaths[i+1] = entry.Name()
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
|
|
||||||
var revs map[string]*Commit
|
|
||||||
if commit.repo.LastCommitCache != nil {
|
|
||||||
var unHitPaths []string
|
|
||||||
revs, unHitPaths, err = getLastCommitForPathsByCache(commit.ID.String(), treePath, entryPaths, commit.repo.LastCommitCache)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
if len(unHitPaths) > 0 {
|
|
||||||
sort.Strings(unHitPaths)
|
|
||||||
commits, err := GetLastCommitForPaths(ctx, commit, treePath, unHitPaths)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for pth, found := range commits {
|
|
||||||
revs[pth] = found
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
sort.Strings(entryPaths)
|
|
||||||
revs, err = GetLastCommitForPaths(ctx, commit, treePath, entryPaths)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
commitsInfo := make([]CommitInfo, len(tes))
|
|
||||||
for i, entry := range tes {
|
|
||||||
commitsInfo[i] = CommitInfo{
|
|
||||||
Entry: entry,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we have found a commit for this entry in time
|
|
||||||
if entryCommit, ok := revs[entry.Name()]; ok {
|
|
||||||
commitsInfo[i].Commit = entryCommit
|
|
||||||
} else {
|
|
||||||
log.Debug("missing commit for %s", entry.Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the entry if a submodule add a submodule file for this
|
|
||||||
if entry.IsSubModule() {
|
|
||||||
subModuleURL := ""
|
|
||||||
var fullPath string
|
|
||||||
if len(treePath) > 0 {
|
|
||||||
fullPath = treePath + "/" + entry.Name()
|
|
||||||
} else {
|
|
||||||
fullPath = entry.Name()
|
|
||||||
}
|
|
||||||
if subModule, err := commit.GetSubModule(fullPath); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
} else if subModule != nil {
|
|
||||||
subModuleURL = subModule.URL
|
|
||||||
}
|
|
||||||
subModuleFile := NewSubModuleFile(commitsInfo[i].Commit, subModuleURL, entry.ID.String())
|
|
||||||
commitsInfo[i].SubModuleFile = subModuleFile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve the commit for the treePath itself (see above). We basically
|
|
||||||
// get it for free during the tree traversal and it's used for listing
|
|
||||||
// pages to display information about newest commit for a given path.
|
|
||||||
var treeCommit *Commit
|
|
||||||
var ok bool
|
|
||||||
if treePath == "" {
|
|
||||||
treeCommit = commit
|
|
||||||
} else if treeCommit, ok = revs[""]; ok {
|
|
||||||
treeCommit.repo = commit.repo
|
|
||||||
}
|
|
||||||
return commitsInfo, treeCommit, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getLastCommitForPathsByCache(commitID, treePath string, paths []string, cache *LastCommitCache) (map[string]*Commit, []string, error) {
|
|
||||||
var unHitEntryPaths []string
|
|
||||||
results := make(map[string]*Commit)
|
|
||||||
for _, p := range paths {
|
|
||||||
lastCommit, err := cache.Get(commitID, path.Join(treePath, p))
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
if lastCommit != nil {
|
|
||||||
results[p] = lastCommit
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
unHitEntryPaths = append(unHitEntryPaths, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
return results, unHitEntryPaths, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLastCommitForPaths returns last commit information
|
|
||||||
func GetLastCommitForPaths(ctx context.Context, 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, commit.repo, commit, treePath, paths...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
batchStdinWriter, batchReader, cancel := commit.repo.CatFileBatch(ctx)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
commitsMap := map[string]*Commit{}
|
|
||||||
commitsMap[commit.ID.String()] = commit
|
|
||||||
|
|
||||||
commitCommits := map[string]*Commit{}
|
|
||||||
for path, commitID := range revs {
|
|
||||||
c, ok := commitsMap[commitID]
|
|
||||||
if ok {
|
|
||||||
commitCommits[path] = c
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(commitID) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := batchStdinWriter.Write([]byte(commitID + "\n"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, typ, size, err := ReadBatchLine(batchReader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if typ != "commit" {
|
|
||||||
if err := DiscardFull(batchReader, size+1); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("unexpected type: %s for commit id: %s", typ, commitID)
|
|
||||||
}
|
|
||||||
c, err = CommitFromReader(commit.repo, MustIDFromString(commitID), io.LimitReader(batchReader, size))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if _, err := batchReader.Discard(1); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
commitCommits[path] = c
|
|
||||||
}
|
|
||||||
|
|
||||||
return commitCommits, nil
|
|
||||||
}
|
|
|
@ -1,8 +1,6 @@
|
||||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
//go:build !gogit
|
|
||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
@ -186,12 +186,12 @@ func InitFull(ctx context.Context) (err error) {
|
||||||
globalCommandArgs = append(globalCommandArgs, "-c", "credential.helper=")
|
globalCommandArgs = append(globalCommandArgs, "-c", "credential.helper=")
|
||||||
}
|
}
|
||||||
SupportProcReceive = CheckGitVersionAtLeast("2.29") == nil
|
SupportProcReceive = CheckGitVersionAtLeast("2.29") == nil
|
||||||
SupportHashSha256 = CheckGitVersionAtLeast("2.42") == nil && !isGogit
|
SupportHashSha256 = CheckGitVersionAtLeast("2.42") == nil
|
||||||
SupportCheckAttrOnBare = CheckGitVersionAtLeast("2.40") == nil
|
SupportCheckAttrOnBare = CheckGitVersionAtLeast("2.40") == nil
|
||||||
if SupportHashSha256 {
|
if SupportHashSha256 {
|
||||||
SupportedObjectFormats = append(SupportedObjectFormats, Sha256ObjectFormat)
|
SupportedObjectFormats = append(SupportedObjectFormats, Sha256ObjectFormat)
|
||||||
} else {
|
} else {
|
||||||
log.Warn("sha256 hash support is disabled - requires Git >= 2.42. Gogit is currently unsupported")
|
log.Warn("sha256 hash support is disabled - requires Git >= 2.42")
|
||||||
}
|
}
|
||||||
|
|
||||||
InvertedGitFlushEnv = CheckGitVersionEqual("2.43.1") == nil
|
InvertedGitFlushEnv = CheckGitVersionEqual("2.43.1") == nil
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
@ -112,3 +113,47 @@ func (c *LastCommitCache) GetCommitByPath(commitID, entryPath string) (*Commit,
|
||||||
|
|
||||||
return lastCommit, nil
|
return lastCommit, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CacheCommit will cache the commit from the gitRepository
|
||||||
|
func (c *Commit) CacheCommit(ctx context.Context) error {
|
||||||
|
if c.repo.LastCommitCache == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c.recursiveCache(ctx, &c.Tree, "", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commit) recursiveCache(ctx context.Context, tree *Tree, treePath string, level int) error {
|
||||||
|
if level == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := tree.ListEntries()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
entryPaths := make([]string, len(entries))
|
||||||
|
for i, entry := range entries {
|
||||||
|
entryPaths[i] = entry.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = WalkGitLog(ctx, c.repo, 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(treeEntry.Name())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.recursiveCache(ctx, subTree, treeEntry.Name(), level-1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,65 +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) error {
|
|
||||||
if c.repo.LastCommitCache == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
commitNodeIndex, _ := c.repo.CommitNodeIndex()
|
|
||||||
|
|
||||||
index, err := commitNodeIndex.Get(plumbing.Hash(c.ID.RawValue()))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.recursiveCache(ctx, index, &c.Tree, "", 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Commit) recursiveCache(ctx context.Context, index cgobject.CommitNode, tree *Tree, treePath string, level int) error {
|
|
||||||
if level == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
entries, err := tree.ListEntries()
|
|
||||||
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 := GetLastCommitForPaths(ctx, c.repo.LastCommitCache, index, treePath, entryPaths)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for entry := range commits {
|
|
||||||
if entryMap[entry].IsDir() {
|
|
||||||
subTree, err := tree.SubTree(entry)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := c.recursiveCache(ctx, index, subTree, entry, level-1); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,54 +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) error {
|
|
||||||
if c.repo.LastCommitCache == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return c.recursiveCache(ctx, &c.Tree, "", 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Commit) recursiveCache(ctx context.Context, tree *Tree, treePath string, level int) error {
|
|
||||||
if level == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
entries, err := tree.ListEntries()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
entryPaths := make([]string, len(entries))
|
|
||||||
for i, entry := range entries {
|
|
||||||
entryPaths[i] = entry.Name()
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = WalkGitLog(ctx, c.repo, 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(treeEntry.Name())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := c.recursiveCache(ctx, subTree, treeEntry.Name(), level-1); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -3,6 +3,14 @@
|
||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
)
|
||||||
|
|
||||||
// NotesRef is the git ref where Gitea will look for git-notes data.
|
// NotesRef is the git ref where Gitea will look for git-notes data.
|
||||||
// The value ("refs/notes/commits") is the default ref used by git-notes.
|
// The value ("refs/notes/commits") is the default ref used by git-notes.
|
||||||
const NotesRef = "refs/notes/commits"
|
const NotesRef = "refs/notes/commits"
|
||||||
|
@ -12,3 +20,80 @@ type Note struct {
|
||||||
Message []byte
|
Message []byte
|
||||||
Commit *Commit
|
Commit *Commit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetNote retrieves the git-notes data for a given commit.
|
||||||
|
// FIXME: Add LastCommitCache support
|
||||||
|
func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note) error {
|
||||||
|
log.Trace("Searching for git note corresponding to the commit %q in the repository %q", commitID, repo.Path)
|
||||||
|
notes, err := repo.GetCommit(NotesRef)
|
||||||
|
if err != nil {
|
||||||
|
if IsErrNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Error("Unable to get commit from ref %q. Error: %v", NotesRef, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
path := ""
|
||||||
|
|
||||||
|
tree := ¬es.Tree
|
||||||
|
log.Trace("Found tree with ID %q while searching for git note corresponding to the commit %q", tree.ID, commitID)
|
||||||
|
|
||||||
|
var entry *TreeEntry
|
||||||
|
originalCommitID := commitID
|
||||||
|
for len(commitID) > 2 {
|
||||||
|
entry, err = tree.GetTreeEntryByPath(commitID)
|
||||||
|
if err == nil {
|
||||||
|
path += commitID
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if IsErrNotExist(err) {
|
||||||
|
tree, err = tree.SubTree(commitID[0:2])
|
||||||
|
path += commitID[0:2] + "/"
|
||||||
|
commitID = commitID[2:]
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
// Err may have been updated by the SubTree we need to recheck if it's again an ErrNotExist
|
||||||
|
if !IsErrNotExist(err) {
|
||||||
|
log.Error("Unable to find git note corresponding to the commit %q. Error: %v", originalCommitID, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
blob := entry.Blob()
|
||||||
|
dataRc, err := blob.DataAsync()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Unable to read blob with ID %q. Error: %v", blob.ID, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
closed := false
|
||||||
|
defer func() {
|
||||||
|
if !closed {
|
||||||
|
_ = dataRc.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
d, err := io.ReadAll(dataRc)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Unable to read blob with ID %q. Error: %v", blob.ID, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = dataRc.Close()
|
||||||
|
closed = true
|
||||||
|
note.Message = d
|
||||||
|
|
||||||
|
treePath := ""
|
||||||
|
if idx := strings.LastIndex(path, "/"); idx > -1 {
|
||||||
|
treePath = path[:idx]
|
||||||
|
path = path[idx+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
lastCommits, err := GetLastCommitForPaths(ctx, notes, treePath, []string{path})
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Unable to get the commit for the path %q. Error: %v", treePath, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
note.Commit = lastCommits[path]
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,89 +0,0 @@
|
||||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build gogit
|
|
||||||
|
|
||||||
package git
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetNote retrieves the git-notes data for a given commit.
|
|
||||||
// FIXME: Add LastCommitCache support
|
|
||||||
func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note) error {
|
|
||||||
log.Trace("Searching for git note corresponding to the commit %q in the repository %q", commitID, repo.Path)
|
|
||||||
notes, err := repo.GetCommit(NotesRef)
|
|
||||||
if err != nil {
|
|
||||||
if IsErrNotExist(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Error("Unable to get commit from ref %q. Error: %v", NotesRef, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
remainingCommitID := commitID
|
|
||||||
path := ""
|
|
||||||
currentTree := notes.Tree.gogitTree
|
|
||||||
log.Trace("Found tree with ID %q while searching for git note corresponding to the commit %q", currentTree.Entries[0].Name, commitID)
|
|
||||||
var file *object.File
|
|
||||||
for len(remainingCommitID) > 2 {
|
|
||||||
file, err = currentTree.File(remainingCommitID)
|
|
||||||
if err == nil {
|
|
||||||
path += remainingCommitID
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if err == object.ErrFileNotFound {
|
|
||||||
currentTree, err = currentTree.Tree(remainingCommitID[0:2])
|
|
||||||
path += remainingCommitID[0:2] + "/"
|
|
||||||
remainingCommitID = remainingCommitID[2:]
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
if err == object.ErrDirectoryNotFound {
|
|
||||||
return ErrNotExist{ID: remainingCommitID, RelPath: path}
|
|
||||||
}
|
|
||||||
log.Error("Unable to find git note corresponding to the commit %q. Error: %v", commitID, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
blob := file.Blob
|
|
||||||
dataRc, err := blob.Reader()
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Unable to read blob with ID %q. Error: %v", blob.ID, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer dataRc.Close()
|
|
||||||
d, err := io.ReadAll(dataRc)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Unable to read blob with ID %q. Error: %v", blob.ID, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
note.Message = d
|
|
||||||
|
|
||||||
commitNodeIndex, commitGraphFile := repo.CommitNodeIndex()
|
|
||||||
if commitGraphFile != nil {
|
|
||||||
defer commitGraphFile.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
commitNode, err := commitNodeIndex.Get(plumbing.Hash(notes.ID.RawValue()))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
lastCommits, err := GetLastCommitForPaths(ctx, nil, commitNode, "", []string{path})
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Unable to get the commit for the path %q. Error: %v", path, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
note.Commit = lastCommits[path]
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,91 +0,0 @@
|
||||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build !gogit
|
|
||||||
|
|
||||||
package git
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetNote retrieves the git-notes data for a given commit.
|
|
||||||
// FIXME: Add LastCommitCache support
|
|
||||||
func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note) error {
|
|
||||||
log.Trace("Searching for git note corresponding to the commit %q in the repository %q", commitID, repo.Path)
|
|
||||||
notes, err := repo.GetCommit(NotesRef)
|
|
||||||
if err != nil {
|
|
||||||
if IsErrNotExist(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Error("Unable to get commit from ref %q. Error: %v", NotesRef, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
path := ""
|
|
||||||
|
|
||||||
tree := ¬es.Tree
|
|
||||||
log.Trace("Found tree with ID %q while searching for git note corresponding to the commit %q", tree.ID, commitID)
|
|
||||||
|
|
||||||
var entry *TreeEntry
|
|
||||||
originalCommitID := commitID
|
|
||||||
for len(commitID) > 2 {
|
|
||||||
entry, err = tree.GetTreeEntryByPath(commitID)
|
|
||||||
if err == nil {
|
|
||||||
path += commitID
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if IsErrNotExist(err) {
|
|
||||||
tree, err = tree.SubTree(commitID[0:2])
|
|
||||||
path += commitID[0:2] + "/"
|
|
||||||
commitID = commitID[2:]
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
// Err may have been updated by the SubTree we need to recheck if it's again an ErrNotExist
|
|
||||||
if !IsErrNotExist(err) {
|
|
||||||
log.Error("Unable to find git note corresponding to the commit %q. Error: %v", originalCommitID, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
blob := entry.Blob()
|
|
||||||
dataRc, err := blob.DataAsync()
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Unable to read blob with ID %q. Error: %v", blob.ID, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
closed := false
|
|
||||||
defer func() {
|
|
||||||
if !closed {
|
|
||||||
_ = dataRc.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
d, err := io.ReadAll(dataRc)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Unable to read blob with ID %q. Error: %v", blob.ID, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_ = dataRc.Close()
|
|
||||||
closed = true
|
|
||||||
note.Message = d
|
|
||||||
|
|
||||||
treePath := ""
|
|
||||||
if idx := strings.LastIndex(path, "/"); idx > -1 {
|
|
||||||
treePath = path[:idx]
|
|
||||||
path = path[idx+1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
lastCommits, err := GetLastCommitForPaths(ctx, notes, treePath, []string{path})
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Unable to get the commit for the path %q. Error: %v", treePath, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
note.Commit = lastCommits[path]
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -1,8 +1,6 @@
|
||||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
//go:build !gogit
|
|
||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
|
@ -1,96 +0,0 @@
|
||||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build gogit
|
|
||||||
|
|
||||||
package git
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"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/hash"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ParseTreeEntries parses the output of a `git ls-tree -l` command.
|
|
||||||
func ParseTreeEntries(data []byte) ([]*TreeEntry, error) {
|
|
||||||
return parseTreeEntries(data, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) {
|
|
||||||
entries := make([]*TreeEntry, 0, 10)
|
|
||||||
for pos := 0; pos < len(data); {
|
|
||||||
// expect line to be of the form "<mode> <type> <sha> <space-padded-size>\t<filename>"
|
|
||||||
entry := new(TreeEntry)
|
|
||||||
entry.gogitTreeEntry = &object.TreeEntry{}
|
|
||||||
entry.ptree = ptree
|
|
||||||
if pos+6 > len(data) {
|
|
||||||
return nil, fmt.Errorf("Invalid ls-tree output: %s", string(data))
|
|
||||||
}
|
|
||||||
switch string(data[pos : pos+6]) {
|
|
||||||
case "100644":
|
|
||||||
entry.gogitTreeEntry.Mode = filemode.Regular
|
|
||||||
pos += 12 // skip over "100644 blob "
|
|
||||||
case "100755":
|
|
||||||
entry.gogitTreeEntry.Mode = filemode.Executable
|
|
||||||
pos += 12 // skip over "100755 blob "
|
|
||||||
case "120000":
|
|
||||||
entry.gogitTreeEntry.Mode = filemode.Symlink
|
|
||||||
pos += 12 // skip over "120000 blob "
|
|
||||||
case "160000":
|
|
||||||
entry.gogitTreeEntry.Mode = filemode.Submodule
|
|
||||||
pos += 14 // skip over "160000 object "
|
|
||||||
case "040000":
|
|
||||||
entry.gogitTreeEntry.Mode = filemode.Dir
|
|
||||||
pos += 12 // skip over "040000 tree "
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unknown type: %v", string(data[pos:pos+6]))
|
|
||||||
}
|
|
||||||
|
|
||||||
// in hex format, not byte format ....
|
|
||||||
if pos+hash.Size*2 > len(data) {
|
|
||||||
return nil, fmt.Errorf("Invalid ls-tree output: %s", string(data))
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
entry.ID, err = NewIDFromString(string(data[pos : pos+hash.Size*2]))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid ls-tree output: %w", err)
|
|
||||||
}
|
|
||||||
entry.gogitTreeEntry.Hash = plumbing.Hash(entry.ID.RawValue())
|
|
||||||
pos += 41 // skip over sha and trailing space
|
|
||||||
|
|
||||||
end := pos + bytes.IndexByte(data[pos:], '\t')
|
|
||||||
if end < pos {
|
|
||||||
return nil, fmt.Errorf("Invalid ls-tree -l output: %s", string(data))
|
|
||||||
}
|
|
||||||
entry.size, _ = strconv.ParseInt(strings.TrimSpace(string(data[pos:end])), 10, 64)
|
|
||||||
entry.sized = true
|
|
||||||
|
|
||||||
pos = end + 1
|
|
||||||
|
|
||||||
end = pos + bytes.IndexByte(data[pos:], '\n')
|
|
||||||
if end < pos {
|
|
||||||
return nil, fmt.Errorf("Invalid ls-tree output: %s", string(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
// In case entry name is surrounded by double quotes(it happens only in git-shell).
|
|
||||||
if data[pos] == '"' {
|
|
||||||
var err error
|
|
||||||
entry.gogitTreeEntry.Name, err = strconv.Unquote(string(data[pos:end]))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Invalid ls-tree output: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
entry.gogitTreeEntry.Name = string(data[pos:end])
|
|
||||||
}
|
|
||||||
|
|
||||||
pos = end + 1
|
|
||||||
entries = append(entries, entry)
|
|
||||||
}
|
|
||||||
return entries, nil
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build gogit
|
|
||||||
|
|
||||||
package git
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"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"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestParseTreeEntries(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
Input string
|
|
||||||
Expected []*TreeEntry
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Input: "",
|
|
||||||
Expected: []*TreeEntry{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Input: "100644 blob 61ab7345a1a3bbc590068ccae37b8515cfc5843c 1022\texample/file2.txt\n",
|
|
||||||
Expected: []*TreeEntry{
|
|
||||||
{
|
|
||||||
ID: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
|
|
||||||
gogitTreeEntry: &object.TreeEntry{
|
|
||||||
Hash: plumbing.Hash(MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c").RawValue()),
|
|
||||||
Name: "example/file2.txt",
|
|
||||||
Mode: filemode.Regular,
|
|
||||||
},
|
|
||||||
size: 1022,
|
|
||||||
sized: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Input: "120000 blob 61ab7345a1a3bbc590068ccae37b8515cfc5843c 234131\t\"example/\\n.txt\"\n" +
|
|
||||||
"040000 tree 1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8 -\texample\n",
|
|
||||||
Expected: []*TreeEntry{
|
|
||||||
{
|
|
||||||
ID: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
|
|
||||||
gogitTreeEntry: &object.TreeEntry{
|
|
||||||
Hash: plumbing.Hash(MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c").RawValue()),
|
|
||||||
Name: "example/\n.txt",
|
|
||||||
Mode: filemode.Symlink,
|
|
||||||
},
|
|
||||||
size: 234131,
|
|
||||||
sized: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: MustIDFromString("1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8"),
|
|
||||||
sized: true,
|
|
||||||
gogitTreeEntry: &object.TreeEntry{
|
|
||||||
Hash: plumbing.Hash(MustIDFromString("1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8").RawValue()),
|
|
||||||
Name: "example",
|
|
||||||
Mode: filemode.Dir,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
|
||||||
entries, err := ParseTreeEntries([]byte(testCase.Input))
|
|
||||||
require.NoError(t, err)
|
|
||||||
if len(entries) > 1 {
|
|
||||||
fmt.Println(testCase.Expected[0].ID)
|
|
||||||
fmt.Println(entries[0].ID)
|
|
||||||
}
|
|
||||||
assert.EqualValues(t, testCase.Expected, entries)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +1,6 @@
|
||||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
//go:build !gogit
|
|
||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
|
@ -1,21 +1,42 @@
|
||||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
//go:build !gogit
|
|
||||||
|
|
||||||
package pipeline
|
package pipeline
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// LFSResult represents commits found using a provided pointer file hash
|
||||||
|
type LFSResult struct {
|
||||||
|
Name string
|
||||||
|
SHA string
|
||||||
|
Summary string
|
||||||
|
When time.Time
|
||||||
|
ParentHashes []git.ObjectID
|
||||||
|
BranchName string
|
||||||
|
FullCommitName string
|
||||||
|
}
|
||||||
|
|
||||||
|
type lfsResultSlice []*LFSResult
|
||||||
|
|
||||||
|
func (a lfsResultSlice) Len() int { return len(a) }
|
||||||
|
func (a lfsResultSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
|
func (a lfsResultSlice) Less(i, j int) bool { return a[j].When.After(a[i].When) }
|
||||||
|
|
||||||
|
func lfsError(msg string, err error) error {
|
||||||
|
return fmt.Errorf("LFS error occurred, %s: err: %w", msg, err)
|
||||||
|
}
|
||||||
|
|
||||||
// FindLFSFile finds commits that contain a provided pointer file hash
|
// FindLFSFile finds commits that contain a provided pointer file hash
|
||||||
func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, error) {
|
func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, error) {
|
||||||
resultsMap := map[string]*LFSResult{}
|
resultsMap := map[string]*LFSResult{}
|
|
@ -1,32 +0,0 @@
|
||||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package pipeline
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LFSResult represents commits found using a provided pointer file hash
|
|
||||||
type LFSResult struct {
|
|
||||||
Name string
|
|
||||||
SHA string
|
|
||||||
Summary string
|
|
||||||
When time.Time
|
|
||||||
ParentHashes []git.ObjectID
|
|
||||||
BranchName string
|
|
||||||
FullCommitName string
|
|
||||||
}
|
|
||||||
|
|
||||||
type lfsResultSlice []*LFSResult
|
|
||||||
|
|
||||||
func (a lfsResultSlice) Len() int { return len(a) }
|
|
||||||
func (a lfsResultSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
||||||
func (a lfsResultSlice) Less(i, j int) bool { return a[j].When.After(a[i].When) }
|
|
||||||
|
|
||||||
func lfsError(msg string, err error) error {
|
|
||||||
return fmt.Errorf("LFS error occurred, %s: err: %w", msg, err)
|
|
||||||
}
|
|
|
@ -1,146 +0,0 @@
|
||||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build gogit
|
|
||||||
|
|
||||||
package pipeline
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"io"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
|
||||||
|
|
||||||
gogit "github.com/go-git/go-git/v5"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FindLFSFile finds commits that contain a provided pointer file hash
|
|
||||||
func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, error) {
|
|
||||||
resultsMap := map[string]*LFSResult{}
|
|
||||||
results := make([]*LFSResult, 0)
|
|
||||||
|
|
||||||
basePath := repo.Path
|
|
||||||
gogitRepo := repo.GoGitRepo()
|
|
||||||
|
|
||||||
commitsIter, err := gogitRepo.Log(&gogit.LogOptions{
|
|
||||||
Order: gogit.LogOrderCommitterTime,
|
|
||||||
All: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, lfsError("failed to get GoGit CommitsIter", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = commitsIter.ForEach(func(gitCommit *object.Commit) error {
|
|
||||||
tree, err := gitCommit.Tree()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
treeWalker := object.NewTreeWalker(tree, true, nil)
|
|
||||||
defer treeWalker.Close()
|
|
||||||
for {
|
|
||||||
name, entry, err := treeWalker.Next()
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if entry.Hash == plumbing.Hash(objectID.RawValue()) {
|
|
||||||
parents := make([]git.ObjectID, len(gitCommit.ParentHashes))
|
|
||||||
for i, parentCommitID := range gitCommit.ParentHashes {
|
|
||||||
parents[i] = git.ParseGogitHash(parentCommitID)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := LFSResult{
|
|
||||||
Name: name,
|
|
||||||
SHA: gitCommit.Hash.String(),
|
|
||||||
Summary: strings.Split(strings.TrimSpace(gitCommit.Message), "\n")[0],
|
|
||||||
When: gitCommit.Author.When,
|
|
||||||
ParentHashes: parents,
|
|
||||||
}
|
|
||||||
resultsMap[gitCommit.Hash.String()+":"+name] = &result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
return nil, lfsError("failure in CommitIter.ForEach", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, result := range resultsMap {
|
|
||||||
hasParent := false
|
|
||||||
for _, parentHash := range result.ParentHashes {
|
|
||||||
if _, hasParent = resultsMap[parentHash.String()+":"+result.Name]; hasParent {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !hasParent {
|
|
||||||
results = append(results, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Sort(lfsResultSlice(results))
|
|
||||||
|
|
||||||
// Should really use a go-git function here but name-rev is not completed and recapitulating it is not simple
|
|
||||||
shasToNameReader, shasToNameWriter := io.Pipe()
|
|
||||||
nameRevStdinReader, nameRevStdinWriter := io.Pipe()
|
|
||||||
errChan := make(chan error, 1)
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
wg.Add(3)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
scanner := bufio.NewScanner(nameRevStdinReader)
|
|
||||||
i := 0
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
if len(line) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
result := results[i]
|
|
||||||
result.FullCommitName = line
|
|
||||||
result.BranchName = strings.Split(line, "~")[0]
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
go NameRevStdin(repo.Ctx, shasToNameReader, nameRevStdinWriter, &wg, basePath)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
defer shasToNameWriter.Close()
|
|
||||||
for _, result := range results {
|
|
||||||
i := 0
|
|
||||||
if i < len(result.SHA) {
|
|
||||||
n, err := shasToNameWriter.Write([]byte(result.SHA)[i:])
|
|
||||||
if err != nil {
|
|
||||||
errChan <- err
|
|
||||||
break
|
|
||||||
}
|
|
||||||
i += n
|
|
||||||
}
|
|
||||||
n := 0
|
|
||||||
for n < 1 {
|
|
||||||
n, err = shasToNameWriter.Write([]byte{'\n'})
|
|
||||||
if err != nil {
|
|
||||||
errChan <- err
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case err, has := <-errChan:
|
|
||||||
if has {
|
|
||||||
return nil, lfsError("unable to obtain name for LFS files", err)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
return results, nil
|
|
||||||
}
|
|
|
@ -1,6 +1,116 @@
|
||||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||||
|
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
var isGogit bool
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Repository represents a Git repository.
|
||||||
|
type Repository struct {
|
||||||
|
Path string
|
||||||
|
|
||||||
|
tagCache *ObjectCache
|
||||||
|
|
||||||
|
gpgSettings *GPGSettings
|
||||||
|
|
||||||
|
batchInUse bool
|
||||||
|
batchCancel context.CancelFunc
|
||||||
|
batchReader *bufio.Reader
|
||||||
|
batchWriter WriteCloserError
|
||||||
|
|
||||||
|
checkInUse bool
|
||||||
|
checkCancel context.CancelFunc
|
||||||
|
checkReader *bufio.Reader
|
||||||
|
checkWriter WriteCloserError
|
||||||
|
|
||||||
|
Ctx context.Context
|
||||||
|
LastCommitCache *LastCommitCache
|
||||||
|
|
||||||
|
objectFormat ObjectFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
// openRepositoryWithDefaultContext opens the repository at the given path with DefaultContext.
|
||||||
|
func openRepositoryWithDefaultContext(repoPath string) (*Repository, error) {
|
||||||
|
return OpenRepository(DefaultContext, repoPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenRepository opens the repository at the given path with the provided context.
|
||||||
|
func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) {
|
||||||
|
repoPath, err := filepath.Abs(repoPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if !isDir(repoPath) {
|
||||||
|
return nil, errors.New("no such file or directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now because of some insanity with git cat-file not immediately failing if not run in a valid git directory we need to run git rev-parse first!
|
||||||
|
if err := EnsureValidGitRepository(ctx, repoPath); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := &Repository{
|
||||||
|
Path: repoPath,
|
||||||
|
tagCache: newObjectCache(),
|
||||||
|
Ctx: ctx,
|
||||||
|
}
|
||||||
|
|
||||||
|
repo.batchWriter, repo.batchReader, repo.batchCancel = CatFileBatch(ctx, repoPath)
|
||||||
|
repo.checkWriter, repo.checkReader, repo.checkCancel = CatFileBatchCheck(ctx, repoPath)
|
||||||
|
|
||||||
|
return repo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CatFileBatch obtains a CatFileBatch for this repository
|
||||||
|
func (repo *Repository) CatFileBatch(ctx context.Context) (WriteCloserError, *bufio.Reader, func()) {
|
||||||
|
if repo.batchCancel == nil || repo.batchInUse {
|
||||||
|
log.Debug("Opening temporary cat file batch for: %s", repo.Path)
|
||||||
|
return CatFileBatch(ctx, repo.Path)
|
||||||
|
}
|
||||||
|
repo.batchInUse = true
|
||||||
|
return repo.batchWriter, repo.batchReader, func() {
|
||||||
|
repo.batchInUse = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CatFileBatchCheck obtains a CatFileBatchCheck for this repository
|
||||||
|
func (repo *Repository) CatFileBatchCheck(ctx context.Context) (WriteCloserError, *bufio.Reader, func()) {
|
||||||
|
if repo.checkCancel == nil || repo.checkInUse {
|
||||||
|
log.Debug("Opening temporary cat file batch-check for: %s", repo.Path)
|
||||||
|
return CatFileBatchCheck(ctx, repo.Path)
|
||||||
|
}
|
||||||
|
repo.checkInUse = true
|
||||||
|
return repo.checkWriter, repo.checkReader, func() {
|
||||||
|
repo.checkInUse = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *Repository) Close() error {
|
||||||
|
if repo == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if repo.batchCancel != nil {
|
||||||
|
repo.batchCancel()
|
||||||
|
repo.batchReader = nil
|
||||||
|
repo.batchWriter = nil
|
||||||
|
repo.batchCancel = nil
|
||||||
|
repo.batchInUse = false
|
||||||
|
}
|
||||||
|
if repo.checkCancel != nil {
|
||||||
|
repo.checkCancel()
|
||||||
|
repo.checkCancel = nil
|
||||||
|
repo.checkReader = nil
|
||||||
|
repo.checkWriter = nil
|
||||||
|
repo.checkInUse = false
|
||||||
|
}
|
||||||
|
repo.LastCommitCache = nil
|
||||||
|
repo.tagCache = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,107 +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 (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
gitealog "code.gitea.io/gitea/modules/log"
|
|
||||||
"code.gitea.io/gitea/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"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
isGogit = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Repository represents a Git repository.
|
|
||||||
type Repository struct {
|
|
||||||
Path string
|
|
||||||
|
|
||||||
tagCache *ObjectCache
|
|
||||||
|
|
||||||
gogitRepo *gogit.Repository
|
|
||||||
gogitStorage *filesystem.Storage
|
|
||||||
gpgSettings *GPGSettings
|
|
||||||
|
|
||||||
Ctx context.Context
|
|
||||||
LastCommitCache *LastCommitCache
|
|
||||||
objectFormat ObjectFormat
|
|
||||||
}
|
|
||||||
|
|
||||||
// openRepositoryWithDefaultContext opens the repository at the given path with DefaultContext.
|
|
||||||
func openRepositoryWithDefaultContext(repoPath string) (*Repository, error) {
|
|
||||||
return OpenRepository(DefaultContext, repoPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenRepository opens the repository at the given path within the context.Context
|
|
||||||
func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) {
|
|
||||||
repoPath, err := filepath.Abs(repoPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if !isDir(repoPath) {
|
|
||||||
return nil, errors.New("no such file or directory")
|
|
||||||
}
|
|
||||||
|
|
||||||
fs := osfs.New(repoPath)
|
|
||||||
_, err = fs.Stat(".git")
|
|
||||||
if err == nil {
|
|
||||||
fs, err = fs.Chroot(".git")
|
|
||||||
if err != nil {
|
|
||||||
return nil, 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("/")
|
|
||||||
}
|
|
||||||
storage := filesystem.NewStorageWithOptions(fs, cache.NewObjectLRUDefault(), filesystem.Options{KeepDescriptors: true, LargeObjectThreshold: setting.Git.LargeObjectThreshold, AlternatesFS: altFs})
|
|
||||||
gogitRepo, err := gogit.Open(storage, fs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Repository{
|
|
||||||
Path: repoPath,
|
|
||||||
gogitRepo: gogitRepo,
|
|
||||||
gogitStorage: storage,
|
|
||||||
tagCache: newObjectCache(),
|
|
||||||
Ctx: ctx,
|
|
||||||
objectFormat: ParseGogitHash(plumbing.ZeroHash).Type(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close this repository, in particular close the underlying gogitStorage if this is not nil
|
|
||||||
func (repo *Repository) Close() error {
|
|
||||||
if repo == nil || repo.gogitStorage == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := repo.gogitStorage.Close(); err != nil {
|
|
||||||
gitealog.Error("Error closing storage: %v", err)
|
|
||||||
}
|
|
||||||
repo.gogitStorage = nil
|
|
||||||
repo.LastCommitCache = nil
|
|
||||||
repo.tagCache = nil
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GoGitRepo gets the go-git repo representation
|
|
||||||
func (repo *Repository) GoGitRepo() *gogit.Repository {
|
|
||||||
return repo.gogitRepo
|
|
||||||
}
|
|
|
@ -1,122 +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 (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
isGogit = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Repository represents a Git repository.
|
|
||||||
type Repository struct {
|
|
||||||
Path string
|
|
||||||
|
|
||||||
tagCache *ObjectCache
|
|
||||||
|
|
||||||
gpgSettings *GPGSettings
|
|
||||||
|
|
||||||
batchInUse bool
|
|
||||||
batchCancel context.CancelFunc
|
|
||||||
batchReader *bufio.Reader
|
|
||||||
batchWriter WriteCloserError
|
|
||||||
|
|
||||||
checkInUse bool
|
|
||||||
checkCancel context.CancelFunc
|
|
||||||
checkReader *bufio.Reader
|
|
||||||
checkWriter WriteCloserError
|
|
||||||
|
|
||||||
Ctx context.Context
|
|
||||||
LastCommitCache *LastCommitCache
|
|
||||||
|
|
||||||
objectFormat ObjectFormat
|
|
||||||
}
|
|
||||||
|
|
||||||
// openRepositoryWithDefaultContext opens the repository at the given path with DefaultContext.
|
|
||||||
func openRepositoryWithDefaultContext(repoPath string) (*Repository, error) {
|
|
||||||
return OpenRepository(DefaultContext, repoPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenRepository opens the repository at the given path with the provided context.
|
|
||||||
func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) {
|
|
||||||
repoPath, err := filepath.Abs(repoPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if !isDir(repoPath) {
|
|
||||||
return nil, errors.New("no such file or directory")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now because of some insanity with git cat-file not immediately failing if not run in a valid git directory we need to run git rev-parse first!
|
|
||||||
if err := EnsureValidGitRepository(ctx, repoPath); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
repo := &Repository{
|
|
||||||
Path: repoPath,
|
|
||||||
tagCache: newObjectCache(),
|
|
||||||
Ctx: ctx,
|
|
||||||
}
|
|
||||||
|
|
||||||
repo.batchWriter, repo.batchReader, repo.batchCancel = CatFileBatch(ctx, repoPath)
|
|
||||||
repo.checkWriter, repo.checkReader, repo.checkCancel = CatFileBatchCheck(ctx, repoPath)
|
|
||||||
|
|
||||||
return repo, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CatFileBatch obtains a CatFileBatch for this repository
|
|
||||||
func (repo *Repository) CatFileBatch(ctx context.Context) (WriteCloserError, *bufio.Reader, func()) {
|
|
||||||
if repo.batchCancel == nil || repo.batchInUse {
|
|
||||||
log.Debug("Opening temporary cat file batch for: %s", repo.Path)
|
|
||||||
return CatFileBatch(ctx, repo.Path)
|
|
||||||
}
|
|
||||||
repo.batchInUse = true
|
|
||||||
return repo.batchWriter, repo.batchReader, func() {
|
|
||||||
repo.batchInUse = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CatFileBatchCheck obtains a CatFileBatchCheck for this repository
|
|
||||||
func (repo *Repository) CatFileBatchCheck(ctx context.Context) (WriteCloserError, *bufio.Reader, func()) {
|
|
||||||
if repo.checkCancel == nil || repo.checkInUse {
|
|
||||||
log.Debug("Opening temporary cat file batch-check for: %s", repo.Path)
|
|
||||||
return CatFileBatchCheck(ctx, repo.Path)
|
|
||||||
}
|
|
||||||
repo.checkInUse = true
|
|
||||||
return repo.checkWriter, repo.checkReader, func() {
|
|
||||||
repo.checkInUse = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *Repository) Close() error {
|
|
||||||
if repo == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if repo.batchCancel != nil {
|
|
||||||
repo.batchCancel()
|
|
||||||
repo.batchReader = nil
|
|
||||||
repo.batchWriter = nil
|
|
||||||
repo.batchCancel = nil
|
|
||||||
repo.batchInUse = false
|
|
||||||
}
|
|
||||||
if repo.checkCancel != nil {
|
|
||||||
repo.checkCancel()
|
|
||||||
repo.checkCancel = nil
|
|
||||||
repo.checkReader = nil
|
|
||||||
repo.checkWriter = nil
|
|
||||||
repo.checkInUse = false
|
|
||||||
}
|
|
||||||
repo.LastCommitCache = nil
|
|
||||||
repo.tagCache = nil
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package git
|
|
||||||
|
|
||||||
// GetBlob finds the blob object in the repository.
|
|
||||||
func (repo *Repository) GetBlob(idStr string) (*Blob, error) {
|
|
||||||
id, err := NewIDFromString(idStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return repo.getBlob(id)
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build gogit
|
|
||||||
|
|
||||||
package git
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (repo *Repository) getBlob(id ObjectID) (*Blob, error) {
|
|
||||||
encodedObj, err := repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, plumbing.Hash(id.RawValue()))
|
|
||||||
if err != nil {
|
|
||||||
return nil, ErrNotExist{id.String(), ""}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Blob{
|
|
||||||
ID: id,
|
|
||||||
gogitEncodedObj: encodedObj,
|
|
||||||
}, nil
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build !gogit
|
|
||||||
|
|
||||||
package git
|
|
||||||
|
|
||||||
func (repo *Repository) getBlob(id ObjectID) (*Blob, error) {
|
|
||||||
if id.IsZero() {
|
|
||||||
return nil, ErrNotExist{id.String(), ""}
|
|
||||||
}
|
|
||||||
return &Blob{
|
|
||||||
ID: id,
|
|
||||||
repo: repo,
|
|
||||||
}, nil
|
|
||||||
}
|
|
|
@ -5,10 +5,15 @@
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BranchPrefix base dir of the branch information file store on git
|
// BranchPrefix base dir of the branch information file store on git
|
||||||
|
@ -157,3 +162,180 @@ func (repo *Repository) RenameBranch(from, to string) error {
|
||||||
_, _, err := NewCommand(repo.Ctx, "branch", "-m").AddDynamicArguments(from, to).RunStdString(&RunOpts{Dir: repo.Path})
|
_, _, err := NewCommand(repo.Ctx, "branch", "-m").AddDynamicArguments(from, to).RunStdString(&RunOpts{Dir: repo.Path})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsObjectExist returns true if given reference exists in the repository.
|
||||||
|
func (repo *Repository) IsObjectExist(name string) bool {
|
||||||
|
if name == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
wr, rd, cancel := repo.CatFileBatchCheck(repo.Ctx)
|
||||||
|
defer cancel()
|
||||||
|
_, err := wr.Write([]byte(name + "\n"))
|
||||||
|
if err != nil {
|
||||||
|
log.Debug("Error writing to CatFileBatchCheck %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
sha, _, _, err := ReadBatchLine(rd)
|
||||||
|
return err == nil && bytes.HasPrefix(sha, []byte(strings.TrimSpace(name)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsReferenceExist returns true if given reference exists in the repository.
|
||||||
|
func (repo *Repository) IsReferenceExist(name string) bool {
|
||||||
|
if name == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
wr, rd, cancel := repo.CatFileBatchCheck(repo.Ctx)
|
||||||
|
defer cancel()
|
||||||
|
_, err := wr.Write([]byte(name + "\n"))
|
||||||
|
if err != nil {
|
||||||
|
log.Debug("Error writing to CatFileBatchCheck %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, _, _, err = ReadBatchLine(rd)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsBranchExist returns true if given branch exists in current repository.
|
||||||
|
func (repo *Repository) IsBranchExist(name string) bool {
|
||||||
|
if repo == nil || name == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return repo.IsReferenceExist(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(skip, limit int) ([]string, int, error) {
|
||||||
|
return callShowRef(repo.Ctx, repo.Path, BranchPrefix, 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(refType ObjectType, skip, limit int, walkfn func(sha1, refname string) error) (int, error) {
|
||||||
|
var args TrustedCmdArgs
|
||||||
|
switch refType {
|
||||||
|
case ObjectTag:
|
||||||
|
args = TrustedCmdArgs{TagPrefix, "--sort=-taggerdate"}
|
||||||
|
case ObjectBranch:
|
||||||
|
args = TrustedCmdArgs{BranchPrefix, "--sort=-committerdate"}
|
||||||
|
}
|
||||||
|
|
||||||
|
return WalkShowRef(repo.Ctx, repo.Path, args, skip, limit, walkfn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// callShowRef return refs, if limit = 0 it will not limit
|
||||||
|
func callShowRef(ctx context.Context, repoPath, trimPrefix string, extraArgs TrustedCmdArgs, skip, limit int) (branchNames []string, countAll int, err error) {
|
||||||
|
countAll, err = WalkShowRef(ctx, repoPath, 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, repoPath string, extraArgs TrustedCmdArgs, skip, limit int, walkfn func(sha1, refname string) error) (countAll int, err error) {
|
||||||
|
stdoutReader, stdoutWriter := io.Pipe()
|
||||||
|
defer func() {
|
||||||
|
_ = stdoutReader.Close()
|
||||||
|
_ = stdoutWriter.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
stderrBuilder := &strings.Builder{}
|
||||||
|
args := TrustedCmdArgs{"for-each-ref", "--format=%(objectname) %(refname)"}
|
||||||
|
args = append(args, extraArgs...)
|
||||||
|
err := NewCommand(ctx, args...).Run(&RunOpts{
|
||||||
|
Dir: repoPath,
|
||||||
|
Stdout: stdoutWriter,
|
||||||
|
Stderr: stderrBuilder,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if stderrBuilder.Len() == 0 {
|
||||||
|
_ = stdoutWriter.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = stdoutWriter.CloseWithError(ConcatenateError(err, stderrBuilder.String()))
|
||||||
|
} else {
|
||||||
|
_ = stdoutWriter.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
bufReader := bufio.NewReader(stdoutReader)
|
||||||
|
for i < skip {
|
||||||
|
_, isPrefix, err := bufReader.ReadLine()
|
||||||
|
if err == io.EOF {
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return 0, 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 i, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return 0, 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 i, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return i, 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 i, err
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
// count all refs
|
||||||
|
for limit != 0 {
|
||||||
|
_, isPrefix, err := bufReader.ReadLine()
|
||||||
|
if err == io.EOF {
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if !isPrefix {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRefsBySha returns all references filtered with prefix that belong to a sha commit hash
|
||||||
|
func (repo *Repository) GetRefsBySha(sha, prefix string) ([]string, error) {
|
||||||
|
var revList []string
|
||||||
|
_, err := WalkShowRef(repo.Ctx, repo.Path, 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,147 +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 (
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/storer"
|
|
||||||
)
|
|
||||||
|
|
||||||
// IsObjectExist returns true if given reference exists in the repository.
|
|
||||||
func (repo *Repository) IsObjectExist(name string) bool {
|
|
||||||
if name == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := repo.gogitRepo.ResolveRevision(plumbing.Revision(name))
|
|
||||||
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsReferenceExist returns true if given reference exists in the repository.
|
|
||||||
func (repo *Repository) IsReferenceExist(name string) bool {
|
|
||||||
if name == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
reference, err := repo.gogitRepo.Reference(plumbing.ReferenceName(name), true)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return reference.Type() != plumbing.InvalidReference
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsBranchExist returns true if given branch exists in current repository.
|
|
||||||
func (repo *Repository) IsBranchExist(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 `-commiterdate` as the nogogit
|
|
||||||
// implementation. This requires full fetch, sort and then the
|
|
||||||
// skip/limit applies later as gogit returns in undefined order.
|
|
||||||
func (repo *Repository) GetBranchNames(skip, limit int) ([]string, int, error) {
|
|
||||||
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(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 nil
|
|
||||||
})
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRefsBySha returns all references filtered with prefix that belong to a sha commit hash
|
|
||||||
func (repo *Repository) GetRefsBySha(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 nil
|
|
||||||
})
|
|
||||||
return revList, err
|
|
||||||
}
|
|
|
@ -1,194 +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"
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// IsObjectExist returns true if given reference exists in the repository.
|
|
||||||
func (repo *Repository) IsObjectExist(name string) bool {
|
|
||||||
if name == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
wr, rd, cancel := repo.CatFileBatchCheck(repo.Ctx)
|
|
||||||
defer cancel()
|
|
||||||
_, err := wr.Write([]byte(name + "\n"))
|
|
||||||
if err != nil {
|
|
||||||
log.Debug("Error writing to CatFileBatchCheck %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
sha, _, _, err := ReadBatchLine(rd)
|
|
||||||
return err == nil && bytes.HasPrefix(sha, []byte(strings.TrimSpace(name)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsReferenceExist returns true if given reference exists in the repository.
|
|
||||||
func (repo *Repository) IsReferenceExist(name string) bool {
|
|
||||||
if name == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
wr, rd, cancel := repo.CatFileBatchCheck(repo.Ctx)
|
|
||||||
defer cancel()
|
|
||||||
_, err := wr.Write([]byte(name + "\n"))
|
|
||||||
if err != nil {
|
|
||||||
log.Debug("Error writing to CatFileBatchCheck %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
_, _, _, err = ReadBatchLine(rd)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsBranchExist returns true if given branch exists in current repository.
|
|
||||||
func (repo *Repository) IsBranchExist(name string) bool {
|
|
||||||
if repo == nil || name == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return repo.IsReferenceExist(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(skip, limit int) ([]string, int, error) {
|
|
||||||
return callShowRef(repo.Ctx, repo.Path, BranchPrefix, 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(refType ObjectType, skip, limit int, walkfn func(sha1, refname string) error) (int, error) {
|
|
||||||
var args TrustedCmdArgs
|
|
||||||
switch refType {
|
|
||||||
case ObjectTag:
|
|
||||||
args = TrustedCmdArgs{TagPrefix, "--sort=-taggerdate"}
|
|
||||||
case ObjectBranch:
|
|
||||||
args = TrustedCmdArgs{BranchPrefix, "--sort=-committerdate"}
|
|
||||||
}
|
|
||||||
|
|
||||||
return WalkShowRef(repo.Ctx, repo.Path, args, skip, limit, walkfn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// callShowRef return refs, if limit = 0 it will not limit
|
|
||||||
func callShowRef(ctx context.Context, repoPath, trimPrefix string, extraArgs TrustedCmdArgs, skip, limit int) (branchNames []string, countAll int, err error) {
|
|
||||||
countAll, err = WalkShowRef(ctx, repoPath, 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, repoPath string, extraArgs TrustedCmdArgs, skip, limit int, walkfn func(sha1, refname string) error) (countAll int, err error) {
|
|
||||||
stdoutReader, stdoutWriter := io.Pipe()
|
|
||||||
defer func() {
|
|
||||||
_ = stdoutReader.Close()
|
|
||||||
_ = stdoutWriter.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
stderrBuilder := &strings.Builder{}
|
|
||||||
args := TrustedCmdArgs{"for-each-ref", "--format=%(objectname) %(refname)"}
|
|
||||||
args = append(args, extraArgs...)
|
|
||||||
err := NewCommand(ctx, args...).Run(&RunOpts{
|
|
||||||
Dir: repoPath,
|
|
||||||
Stdout: stdoutWriter,
|
|
||||||
Stderr: stderrBuilder,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
if stderrBuilder.Len() == 0 {
|
|
||||||
_ = stdoutWriter.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_ = stdoutWriter.CloseWithError(ConcatenateError(err, stderrBuilder.String()))
|
|
||||||
} else {
|
|
||||||
_ = stdoutWriter.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
i := 0
|
|
||||||
bufReader := bufio.NewReader(stdoutReader)
|
|
||||||
for i < skip {
|
|
||||||
_, isPrefix, err := bufReader.ReadLine()
|
|
||||||
if err == io.EOF {
|
|
||||||
return i, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return 0, 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 i, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return 0, 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 i, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return i, 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 i, err
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
// count all refs
|
|
||||||
for limit != 0 {
|
|
||||||
_, isPrefix, err := bufReader.ReadLine()
|
|
||||||
if err == io.EOF {
|
|
||||||
return i, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if !isPrefix {
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return i, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRefsBySha returns all references filtered with prefix that belong to a sha commit hash
|
|
||||||
func (repo *Repository) GetRefsBySha(sha, prefix string) ([]string, error) {
|
|
||||||
var revList []string
|
|
||||||
_, err := WalkShowRef(repo.Ctx, repo.Path, nil, 0, 0, func(walkSha, refname string) error {
|
|
||||||
if walkSha == sha && strings.HasPrefix(refname, prefix) {
|
|
||||||
revList = append(revList, refname)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return revList, err
|
|
||||||
}
|
|
|
@ -5,12 +5,15 @@
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/cache"
|
"code.gitea.io/gitea/modules/cache"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -513,3 +516,149 @@ func (repo *Repository) AddLastCommitCache(cacheKey, fullName, sha string) error
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResolveReference resolves a name to a reference
|
||||||
|
func (repo *Repository) ResolveReference(name string) (string, error) {
|
||||||
|
stdout, _, err := NewCommand(repo.Ctx, "show-ref", "--hash").AddDynamicArguments(name).RunStdString(&RunOpts{Dir: repo.Path})
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "not a valid ref") {
|
||||||
|
return "", ErrNotExist{name, ""}
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
stdout = strings.TrimSpace(stdout)
|
||||||
|
if stdout == "" {
|
||||||
|
return "", ErrNotExist{name, ""}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stdout, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRefCommitID returns the last commit ID string of given reference (branch or tag).
|
||||||
|
func (repo *Repository) GetRefCommitID(name string) (string, error) {
|
||||||
|
wr, rd, cancel := repo.CatFileBatchCheck(repo.Ctx)
|
||||||
|
defer cancel()
|
||||||
|
_, err := wr.Write([]byte(name + "\n"))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
shaBs, _, _, err := ReadBatchLine(rd)
|
||||||
|
if IsErrNotExist(err) {
|
||||||
|
return "", ErrNotExist{name, ""}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(shaBs), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetReference sets the commit ID string of given reference (e.g. branch or tag).
|
||||||
|
func (repo *Repository) SetReference(name, commitID string) error {
|
||||||
|
_, _, err := NewCommand(repo.Ctx, "update-ref").AddDynamicArguments(name, commitID).RunStdString(&RunOpts{Dir: repo.Path})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveReference removes the given reference (e.g. branch or tag).
|
||||||
|
func (repo *Repository) RemoveReference(name string) error {
|
||||||
|
_, _, err := NewCommand(repo.Ctx, "update-ref", "--no-deref", "-d").AddDynamicArguments(name).RunStdString(&RunOpts{Dir: repo.Path})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsCommitExist returns true if given commit exists in current repository.
|
||||||
|
func (repo *Repository) IsCommitExist(name string) bool {
|
||||||
|
_, _, err := NewCommand(repo.Ctx, "cat-file", "-e").AddDynamicArguments(name).RunStdString(&RunOpts{Dir: repo.Path})
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *Repository) getCommit(id ObjectID) (*Commit, error) {
|
||||||
|
wr, rd, cancel := repo.CatFileBatch(repo.Ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, _ = wr.Write([]byte(id.String() + "\n"))
|
||||||
|
|
||||||
|
return repo.getCommitFromBatchReader(rd, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *Repository) getCommitFromBatchReader(rd *bufio.Reader, id ObjectID) (*Commit, error) {
|
||||||
|
_, typ, size, err := ReadBatchLine(rd)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, io.EOF) || IsErrNotExist(err) {
|
||||||
|
return nil, ErrNotExist{ID: id.String()}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch typ {
|
||||||
|
case "missing":
|
||||||
|
return nil, ErrNotExist{ID: id.String()}
|
||||||
|
case "tag":
|
||||||
|
// then we need to parse the tag
|
||||||
|
// and load the commit
|
||||||
|
data, err := io.ReadAll(io.LimitReader(rd, size))
|
||||||
|
if 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
|
||||||
|
}
|
||||||
|
|
||||||
|
commit, err := tag.Commit(repo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return commit, nil
|
||||||
|
case "commit":
|
||||||
|
commit, err := CommitFromReader(repo, id, io.LimitReader(rd, size))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = rd.Discard(1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return commit, nil
|
||||||
|
default:
|
||||||
|
log.Debug("Unknown typ: %s", typ)
|
||||||
|
if err := DiscardFull(rd, size+1); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, ErrNotExist{
|
||||||
|
ID: id.String(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertToGitID returns a GitHash object from a potential ID string
|
||||||
|
func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) {
|
||||||
|
objectFormat, err := repo.GetObjectFormat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(commitID) == objectFormat.FullLength() && objectFormat.IsValid(commitID) {
|
||||||
|
ID, err := NewIDFromString(commitID)
|
||||||
|
if err == nil {
|
||||||
|
return ID, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wr, rd, cancel := repo.CatFileBatchCheck(repo.Ctx)
|
||||||
|
defer cancel()
|
||||||
|
_, err = wr.Write([]byte(commitID + "\n"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sha, _, _, err := ReadBatchLine(rd)
|
||||||
|
if err != nil {
|
||||||
|
if IsErrNotExist(err) {
|
||||||
|
return nil, ErrNotExist{commitID, ""}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return MustIDFromString(string(sha)), nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,111 +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 (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/hash"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetRefCommitID returns the last commit ID string of given reference (branch or tag).
|
|
||||||
func (repo *Repository) GetRefCommitID(name string) (string, error) {
|
|
||||||
ref, err := repo.gogitRepo.Reference(plumbing.ReferenceName(name), true)
|
|
||||||
if err != nil {
|
|
||||||
if err == plumbing.ErrReferenceNotFound {
|
|
||||||
return "", ErrNotExist{
|
|
||||||
ID: name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ref.Hash().String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetReference sets the commit ID string of given reference (e.g. branch or tag).
|
|
||||||
func (repo *Repository) SetReference(name, commitID string) error {
|
|
||||||
return repo.gogitRepo.Storer.SetReference(plumbing.NewReferenceFromStrings(name, commitID))
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveReference removes the given reference (e.g. branch or tag).
|
|
||||||
func (repo *Repository) RemoveReference(name string) error {
|
|
||||||
return repo.gogitRepo.Storer.RemoveReference(plumbing.ReferenceName(name))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertToHash returns a Hash object from a potential ID string
|
|
||||||
func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) {
|
|
||||||
objectFormat, err := repo.GetObjectFormat()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(commitID) == hash.HexSize && objectFormat.IsValid(commitID) {
|
|
||||||
ID, err := NewIDFromString(commitID)
|
|
||||||
if err == nil {
|
|
||||||
return ID, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
actualCommitID, _, err := NewCommand(repo.Ctx, "rev-parse", "--verify").AddDynamicArguments(commitID).RunStdString(&RunOpts{Dir: repo.Path})
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsCommitExist returns true if given commit exists in current repository.
|
|
||||||
func (repo *Repository) IsCommitExist(name string) bool {
|
|
||||||
hash, err := repo.ConvertToGitID(name)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
_, err = repo.gogitRepo.CommitObject(plumbing.Hash(hash.RawValue()))
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *Repository) getCommit(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)
|
|
||||||
commit.repo = repo
|
|
||||||
|
|
||||||
tree, err := gogitCommit.Tree()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
commit.Tree.ID = ParseGogitHash(tree.Hash)
|
|
||||||
commit.Tree.gogitTree = tree
|
|
||||||
|
|
||||||
return commit, nil
|
|
||||||
}
|
|
|
@ -1,161 +0,0 @@
|
||||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build !gogit
|
|
||||||
|
|
||||||
package git
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ResolveReference resolves a name to a reference
|
|
||||||
func (repo *Repository) ResolveReference(name string) (string, error) {
|
|
||||||
stdout, _, err := NewCommand(repo.Ctx, "show-ref", "--hash").AddDynamicArguments(name).RunStdString(&RunOpts{Dir: repo.Path})
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(err.Error(), "not a valid ref") {
|
|
||||||
return "", ErrNotExist{name, ""}
|
|
||||||
}
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
stdout = strings.TrimSpace(stdout)
|
|
||||||
if stdout == "" {
|
|
||||||
return "", ErrNotExist{name, ""}
|
|
||||||
}
|
|
||||||
|
|
||||||
return stdout, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRefCommitID returns the last commit ID string of given reference (branch or tag).
|
|
||||||
func (repo *Repository) GetRefCommitID(name string) (string, error) {
|
|
||||||
wr, rd, cancel := repo.CatFileBatchCheck(repo.Ctx)
|
|
||||||
defer cancel()
|
|
||||||
_, err := wr.Write([]byte(name + "\n"))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
shaBs, _, _, err := ReadBatchLine(rd)
|
|
||||||
if IsErrNotExist(err) {
|
|
||||||
return "", ErrNotExist{name, ""}
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(shaBs), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetReference sets the commit ID string of given reference (e.g. branch or tag).
|
|
||||||
func (repo *Repository) SetReference(name, commitID string) error {
|
|
||||||
_, _, err := NewCommand(repo.Ctx, "update-ref").AddDynamicArguments(name, commitID).RunStdString(&RunOpts{Dir: repo.Path})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveReference removes the given reference (e.g. branch or tag).
|
|
||||||
func (repo *Repository) RemoveReference(name string) error {
|
|
||||||
_, _, err := NewCommand(repo.Ctx, "update-ref", "--no-deref", "-d").AddDynamicArguments(name).RunStdString(&RunOpts{Dir: repo.Path})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsCommitExist returns true if given commit exists in current repository.
|
|
||||||
func (repo *Repository) IsCommitExist(name string) bool {
|
|
||||||
_, _, err := NewCommand(repo.Ctx, "cat-file", "-e").AddDynamicArguments(name).RunStdString(&RunOpts{Dir: repo.Path})
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *Repository) getCommit(id ObjectID) (*Commit, error) {
|
|
||||||
wr, rd, cancel := repo.CatFileBatch(repo.Ctx)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
_, _ = wr.Write([]byte(id.String() + "\n"))
|
|
||||||
|
|
||||||
return repo.getCommitFromBatchReader(rd, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *Repository) getCommitFromBatchReader(rd *bufio.Reader, id ObjectID) (*Commit, error) {
|
|
||||||
_, typ, size, err := ReadBatchLine(rd)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, io.EOF) || IsErrNotExist(err) {
|
|
||||||
return nil, ErrNotExist{ID: id.String()}
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch typ {
|
|
||||||
case "missing":
|
|
||||||
return nil, ErrNotExist{ID: id.String()}
|
|
||||||
case "tag":
|
|
||||||
// then we need to parse the tag
|
|
||||||
// and load the commit
|
|
||||||
data, err := io.ReadAll(io.LimitReader(rd, size))
|
|
||||||
if 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
|
|
||||||
}
|
|
||||||
|
|
||||||
commit, err := tag.Commit(repo)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return commit, nil
|
|
||||||
case "commit":
|
|
||||||
commit, err := CommitFromReader(repo, id, io.LimitReader(rd, size))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = rd.Discard(1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return commit, nil
|
|
||||||
default:
|
|
||||||
log.Debug("Unknown typ: %s", typ)
|
|
||||||
if err := DiscardFull(rd, size+1); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return nil, ErrNotExist{
|
|
||||||
ID: id.String(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertToGitID returns a GitHash object from a potential ID string
|
|
||||||
func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) {
|
|
||||||
objectFormat, err := repo.GetObjectFormat()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(commitID) == objectFormat.FullLength() && objectFormat.IsValid(commitID) {
|
|
||||||
ID, err := NewIDFromString(commitID)
|
|
||||||
if err == nil {
|
|
||||||
return ID, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wr, rd, cancel := repo.CatFileBatchCheck(repo.Ctx)
|
|
||||||
defer cancel()
|
|
||||||
_, err = wr.Write([]byte(commitID + "\n"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
sha, _, _, err := ReadBatchLine(rd)
|
|
||||||
if err != nil {
|
|
||||||
if IsErrNotExist(err) {
|
|
||||||
return nil, ErrNotExist{commitID, ""}
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return MustIDFromString(string(sha)), nil
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
// Copyright 2019 The Gitea Authors.
|
|
||||||
// All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build gogit
|
|
||||||
|
|
||||||
package git
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
gitealog "code.gitea.io/gitea/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 (r *Repository) CommitNodeIndex() (cgobject.CommitNodeIndex, *os.File) {
|
|
||||||
indexPath := path.Join(r.Path, "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, r.gogitRepo.Storer), file
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !os.IsNotExist(err) {
|
|
||||||
gitealog.Warn("Unable to read commit-graph for %s: %v", r.Path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return cgobject.NewObjectCommitNodeIndex(r.gogitRepo.Storer), nil
|
|
||||||
}
|
|
|
@ -4,8 +4,17 @@
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"cmp"
|
||||||
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/analyze"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/optional"
|
||||||
|
|
||||||
|
"github.com/go-enry/go-enry/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -46,3 +55,194 @@ func mergeLanguageStats(stats map[string]int64) map[string]int64 {
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetLanguageStats calculates language stats for git repository at specified commit
|
||||||
|
func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, error) {
|
||||||
|
// We will feed the commit IDs in order into cat-file --batch, followed by blobs as necessary.
|
||||||
|
// so let's create a batch stdin and stdout
|
||||||
|
batchStdinWriter, batchReader, cancel := repo.CatFileBatch(repo.Ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
writeID := func(id string) error {
|
||||||
|
_, err := batchStdinWriter.Write([]byte(id + "\n"))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeID(commitID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
shaBytes, typ, size, err := ReadBatchLine(batchReader)
|
||||||
|
if typ != "commit" {
|
||||||
|
log.Debug("Unable to get commit for: %s. Err: %v", commitID, err)
|
||||||
|
return nil, ErrNotExist{commitID, ""}
|
||||||
|
}
|
||||||
|
|
||||||
|
sha, err := NewIDFromString(string(shaBytes))
|
||||||
|
if err != nil {
|
||||||
|
log.Debug("Unable to get commit for: %s. Err: %v", commitID, err)
|
||||||
|
return nil, ErrNotExist{commitID, ""}
|
||||||
|
}
|
||||||
|
|
||||||
|
commit, err := CommitFromReader(repo, sha, io.LimitReader(batchReader, size))
|
||||||
|
if err != nil {
|
||||||
|
log.Debug("Unable to get commit for: %s. Err: %v", commitID, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err = batchReader.Discard(1); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tree := commit.Tree
|
||||||
|
|
||||||
|
entries, err := tree.ListEntriesRecursiveWithSize()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
checker, err := repo.GitAttributeChecker(commitID, LinguistAttributes...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer checker.Close()
|
||||||
|
|
||||||
|
contentBuf := bytes.Buffer{}
|
||||||
|
var content []byte
|
||||||
|
|
||||||
|
// sizes contains the current calculated size of all files by language
|
||||||
|
sizes := make(map[string]int64)
|
||||||
|
// by default we will only count the sizes of programming languages or markup languages
|
||||||
|
// unless they are explicitly set using linguist-language
|
||||||
|
includedLanguage := map[string]bool{}
|
||||||
|
// or if there's only one language in the repository
|
||||||
|
firstExcludedLanguage := ""
|
||||||
|
firstExcludedLanguageSize := int64(0)
|
||||||
|
|
||||||
|
isTrue := func(v optional.Option[bool]) bool {
|
||||||
|
return v.ValueOrDefault(false)
|
||||||
|
}
|
||||||
|
isFalse := func(v optional.Option[bool]) bool {
|
||||||
|
return !v.ValueOrDefault(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range entries {
|
||||||
|
select {
|
||||||
|
case <-repo.Ctx.Done():
|
||||||
|
return sizes, repo.Ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
contentBuf.Reset()
|
||||||
|
content = contentBuf.Bytes()
|
||||||
|
|
||||||
|
if f.Size() == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
isVendored := optional.None[bool]()
|
||||||
|
isGenerated := optional.None[bool]()
|
||||||
|
isDocumentation := optional.None[bool]()
|
||||||
|
isDetectable := optional.None[bool]()
|
||||||
|
|
||||||
|
attrs, err := checker.CheckPath(f.Name())
|
||||||
|
if err == nil {
|
||||||
|
isVendored = attrs["linguist-vendored"].Bool()
|
||||||
|
isGenerated = attrs["linguist-generated"].Bool()
|
||||||
|
isDocumentation = attrs["linguist-documentation"].Bool()
|
||||||
|
isDetectable = attrs["linguist-detectable"].Bool()
|
||||||
|
if language := cmp.Or(
|
||||||
|
attrs["linguist-language"].String(),
|
||||||
|
attrs["gitlab-language"].Prefix(),
|
||||||
|
); language != "" {
|
||||||
|
// group languages, such as Pug -> HTML; SCSS -> CSS
|
||||||
|
group := enry.GetLanguageGroup(language)
|
||||||
|
if len(group) != 0 {
|
||||||
|
language = group
|
||||||
|
}
|
||||||
|
|
||||||
|
// this language will always be added to the size
|
||||||
|
sizes[language] += f.Size()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isFalse(isDetectable) || isTrue(isVendored) || isTrue(isDocumentation) ||
|
||||||
|
(!isFalse(isVendored) && analyze.IsVendor(f.Name())) ||
|
||||||
|
enry.IsDotFile(f.Name()) ||
|
||||||
|
enry.IsConfiguration(f.Name()) ||
|
||||||
|
(!isFalse(isDocumentation) && enry.IsDocumentation(f.Name())) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If content can not be read or file is too big just do detection by filename
|
||||||
|
|
||||||
|
if f.Size() <= bigFileSize {
|
||||||
|
if err := writeID(f.ID.String()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, _, size, err := ReadBatchLine(batchReader)
|
||||||
|
if err != nil {
|
||||||
|
log.Debug("Error reading blob: %s Err: %v", f.ID.String(), err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sizeToRead := size
|
||||||
|
discard := int64(1)
|
||||||
|
if size > fileSizeLimit {
|
||||||
|
sizeToRead = fileSizeLimit
|
||||||
|
discard = size - fileSizeLimit + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = contentBuf.ReadFrom(io.LimitReader(batchReader, sizeToRead))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
content = contentBuf.Bytes()
|
||||||
|
if err := DiscardFull(batchReader, discard); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isTrue(isGenerated) && enry.IsGenerated(f.Name(), content) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Why can't we split this and the IsGenerated tests to avoid reading the blob unless absolutely necessary?
|
||||||
|
// - eg. do the all the detection tests using filename first before reading content.
|
||||||
|
language := analyze.GetCodeLanguage(f.Name(), content)
|
||||||
|
if language == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// group languages, such as Pug -> HTML; SCSS -> CSS
|
||||||
|
group := enry.GetLanguageGroup(language)
|
||||||
|
if group != "" {
|
||||||
|
language = group
|
||||||
|
}
|
||||||
|
|
||||||
|
included, checked := includedLanguage[language]
|
||||||
|
langType := enry.GetLanguageType(language)
|
||||||
|
if !checked {
|
||||||
|
included = langType == enry.Programming || langType == enry.Markup
|
||||||
|
if !included && (isTrue(isDetectable) || (langType == enry.Prose && isFalse(isDocumentation))) {
|
||||||
|
included = true
|
||||||
|
}
|
||||||
|
includedLanguage[language] = included
|
||||||
|
}
|
||||||
|
if included {
|
||||||
|
sizes[language] += f.Size()
|
||||||
|
} else if len(sizes) == 0 && (firstExcludedLanguage == "" || firstExcludedLanguage == language) {
|
||||||
|
// Only consider Programming or Markup languages as fallback
|
||||||
|
if !(langType == enry.Programming || langType == enry.Markup) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
firstExcludedLanguage = language
|
||||||
|
firstExcludedLanguageSize += f.Size()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are no included languages add the first excluded language
|
||||||
|
if len(sizes) == 0 && firstExcludedLanguage != "" {
|
||||||
|
sizes[firstExcludedLanguage] = firstExcludedLanguageSize
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergeLanguageStats(sizes), nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,194 +0,0 @@
|
||||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
||||||
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build gogit
|
|
||||||
|
|
||||||
package git
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/analyze"
|
|
||||||
"code.gitea.io/gitea/modules/optional"
|
|
||||||
|
|
||||||
"github.com/go-enry/go-enry/v2"
|
|
||||||
"github.com/go-git/go-git/v5"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetLanguageStats calculates language stats for git repository at specified commit
|
|
||||||
func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, error) {
|
|
||||||
r, err := git.PlainOpen(repo.Path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
rev, err := r.ResolveRevision(plumbing.Revision(commitID))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
commit, err := r.CommitObject(*rev)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tree, err := commit.Tree()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
checker, deferable := repo.CheckAttributeReader(commitID)
|
|
||||||
defer deferable()
|
|
||||||
|
|
||||||
// sizes contains the current calculated size of all files by language
|
|
||||||
sizes := make(map[string]int64)
|
|
||||||
// by default we will only count the sizes of programming languages or markup languages
|
|
||||||
// unless they are explicitly set using linguist-language
|
|
||||||
includedLanguage := map[string]bool{}
|
|
||||||
// or if there's only one language in the repository
|
|
||||||
firstExcludedLanguage := ""
|
|
||||||
firstExcludedLanguageSize := int64(0)
|
|
||||||
|
|
||||||
isTrue := func(v optional.Option[bool]) bool {
|
|
||||||
return v.ValueOrDefault(false)
|
|
||||||
}
|
|
||||||
isFalse := func(v optional.Option[bool]) bool {
|
|
||||||
return !v.ValueOrDefault(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = tree.Files().ForEach(func(f *object.File) error {
|
|
||||||
if f.Size == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
isVendored := optional.None[bool]()
|
|
||||||
isGenerated := optional.None[bool]()
|
|
||||||
isDocumentation := optional.None[bool]()
|
|
||||||
isDetectable := optional.None[bool]()
|
|
||||||
|
|
||||||
if checker != nil {
|
|
||||||
attrs, err := checker.CheckPath(f.Name)
|
|
||||||
if err == nil {
|
|
||||||
isVendored = attributeToBool(attrs, "linguist-vendored")
|
|
||||||
isGenerated = attributeToBool(attrs, "linguist-generated")
|
|
||||||
isDocumentation = attributeToBool(attrs, "linguist-documentation")
|
|
||||||
isDetectable = attributeToBool(attrs, "linguist-detectable")
|
|
||||||
if language, has := attrs["linguist-language"]; has && language != "unspecified" && language != "" {
|
|
||||||
// group languages, such as Pug -> HTML; SCSS -> CSS
|
|
||||||
group := enry.GetLanguageGroup(language)
|
|
||||||
if len(group) != 0 {
|
|
||||||
language = group
|
|
||||||
}
|
|
||||||
|
|
||||||
// this language will always be added to the size
|
|
||||||
sizes[language] += f.Size
|
|
||||||
return nil
|
|
||||||
} else if language, has := attrs["gitlab-language"]; has && language != "unspecified" && language != "" {
|
|
||||||
// strip off a ? if present
|
|
||||||
if idx := strings.IndexByte(language, '?'); idx >= 0 {
|
|
||||||
language = language[:idx]
|
|
||||||
}
|
|
||||||
if len(language) != 0 {
|
|
||||||
// group languages, such as Pug -> HTML; SCSS -> CSS
|
|
||||||
group := enry.GetLanguageGroup(language)
|
|
||||||
if len(group) != 0 {
|
|
||||||
language = group
|
|
||||||
}
|
|
||||||
|
|
||||||
// this language will always be added to the size
|
|
||||||
sizes[language] += f.Size
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if isFalse(isDetectable) || isTrue(isVendored) || isTrue(isDocumentation) ||
|
|
||||||
(!isFalse(isVendored) && analyze.IsVendor(f.Name)) ||
|
|
||||||
enry.IsDotFile(f.Name) ||
|
|
||||||
enry.IsConfiguration(f.Name) ||
|
|
||||||
(!isFalse(isDocumentation) && enry.IsDocumentation(f.Name)) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// If content can not be read or file is too big just do detection by filename
|
|
||||||
var content []byte
|
|
||||||
if f.Size <= bigFileSize {
|
|
||||||
content, _ = readFile(f, fileSizeLimit)
|
|
||||||
}
|
|
||||||
if !isTrue(isGenerated) && enry.IsGenerated(f.Name, content) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Use .gitattributes file for linguist overrides
|
|
||||||
language := analyze.GetCodeLanguage(f.Name, content)
|
|
||||||
if language == enry.OtherLanguage || language == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// group languages, such as Pug -> HTML; SCSS -> CSS
|
|
||||||
group := enry.GetLanguageGroup(language)
|
|
||||||
if group != "" {
|
|
||||||
language = group
|
|
||||||
}
|
|
||||||
|
|
||||||
included, checked := includedLanguage[language]
|
|
||||||
langType := enry.GetLanguageType(language)
|
|
||||||
if !checked {
|
|
||||||
included = langType == enry.Programming || langType == enry.Markup
|
|
||||||
if !included && (isTrue(isDetectable) || (langType == enry.Prose && isFalse(isDocumentation))) {
|
|
||||||
included = true
|
|
||||||
}
|
|
||||||
includedLanguage[language] = included
|
|
||||||
}
|
|
||||||
if included {
|
|
||||||
sizes[language] += f.Size
|
|
||||||
} else if len(sizes) == 0 && (firstExcludedLanguage == "" || firstExcludedLanguage == language) {
|
|
||||||
// Only consider Programming or Markup languages as fallback
|
|
||||||
if !(langType == enry.Programming || langType == enry.Markup) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
firstExcludedLanguage = language
|
|
||||||
firstExcludedLanguageSize += f.Size
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there are no included languages add the first excluded language
|
|
||||||
if len(sizes) == 0 && firstExcludedLanguage != "" {
|
|
||||||
sizes[firstExcludedLanguage] = firstExcludedLanguageSize
|
|
||||||
}
|
|
||||||
|
|
||||||
return mergeLanguageStats(sizes), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readFile(f *object.File, limit int64) ([]byte, error) {
|
|
||||||
r, err := f.Reader()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer r.Close()
|
|
||||||
|
|
||||||
if limit <= 0 {
|
|
||||||
return io.ReadAll(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
size := f.Size
|
|
||||||
if limit > 0 && size > limit {
|
|
||||||
size = limit
|
|
||||||
}
|
|
||||||
buf := bytes.NewBuffer(nil)
|
|
||||||
buf.Grow(int(size))
|
|
||||||
_, err = io.Copy(buf, io.LimitReader(r, limit))
|
|
||||||
return buf.Bytes(), err
|
|
||||||
}
|
|
|
@ -1,210 +0,0 @@
|
||||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
||||||
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build !gogit
|
|
||||||
|
|
||||||
package git
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"cmp"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/analyze"
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
|
||||||
"code.gitea.io/gitea/modules/optional"
|
|
||||||
|
|
||||||
"github.com/go-enry/go-enry/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetLanguageStats calculates language stats for git repository at specified commit
|
|
||||||
func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, error) {
|
|
||||||
// We will feed the commit IDs in order into cat-file --batch, followed by blobs as necessary.
|
|
||||||
// so let's create a batch stdin and stdout
|
|
||||||
batchStdinWriter, batchReader, cancel := repo.CatFileBatch(repo.Ctx)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
writeID := func(id string) error {
|
|
||||||
_, err := batchStdinWriter.Write([]byte(id + "\n"))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := writeID(commitID); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
shaBytes, typ, size, err := ReadBatchLine(batchReader)
|
|
||||||
if typ != "commit" {
|
|
||||||
log.Debug("Unable to get commit for: %s. Err: %v", commitID, err)
|
|
||||||
return nil, ErrNotExist{commitID, ""}
|
|
||||||
}
|
|
||||||
|
|
||||||
sha, err := NewIDFromString(string(shaBytes))
|
|
||||||
if err != nil {
|
|
||||||
log.Debug("Unable to get commit for: %s. Err: %v", commitID, err)
|
|
||||||
return nil, ErrNotExist{commitID, ""}
|
|
||||||
}
|
|
||||||
|
|
||||||
commit, err := CommitFromReader(repo, sha, io.LimitReader(batchReader, size))
|
|
||||||
if err != nil {
|
|
||||||
log.Debug("Unable to get commit for: %s. Err: %v", commitID, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if _, err = batchReader.Discard(1); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tree := commit.Tree
|
|
||||||
|
|
||||||
entries, err := tree.ListEntriesRecursiveWithSize()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
checker, err := repo.GitAttributeChecker(commitID, LinguistAttributes...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer checker.Close()
|
|
||||||
|
|
||||||
contentBuf := bytes.Buffer{}
|
|
||||||
var content []byte
|
|
||||||
|
|
||||||
// sizes contains the current calculated size of all files by language
|
|
||||||
sizes := make(map[string]int64)
|
|
||||||
// by default we will only count the sizes of programming languages or markup languages
|
|
||||||
// unless they are explicitly set using linguist-language
|
|
||||||
includedLanguage := map[string]bool{}
|
|
||||||
// or if there's only one language in the repository
|
|
||||||
firstExcludedLanguage := ""
|
|
||||||
firstExcludedLanguageSize := int64(0)
|
|
||||||
|
|
||||||
isTrue := func(v optional.Option[bool]) bool {
|
|
||||||
return v.ValueOrDefault(false)
|
|
||||||
}
|
|
||||||
isFalse := func(v optional.Option[bool]) bool {
|
|
||||||
return !v.ValueOrDefault(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, f := range entries {
|
|
||||||
select {
|
|
||||||
case <-repo.Ctx.Done():
|
|
||||||
return sizes, repo.Ctx.Err()
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
contentBuf.Reset()
|
|
||||||
content = contentBuf.Bytes()
|
|
||||||
|
|
||||||
if f.Size() == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
isVendored := optional.None[bool]()
|
|
||||||
isGenerated := optional.None[bool]()
|
|
||||||
isDocumentation := optional.None[bool]()
|
|
||||||
isDetectable := optional.None[bool]()
|
|
||||||
|
|
||||||
attrs, err := checker.CheckPath(f.Name())
|
|
||||||
if err == nil {
|
|
||||||
isVendored = attrs["linguist-vendored"].Bool()
|
|
||||||
isGenerated = attrs["linguist-generated"].Bool()
|
|
||||||
isDocumentation = attrs["linguist-documentation"].Bool()
|
|
||||||
isDetectable = attrs["linguist-detectable"].Bool()
|
|
||||||
if language := cmp.Or(
|
|
||||||
attrs["linguist-language"].String(),
|
|
||||||
attrs["gitlab-language"].Prefix(),
|
|
||||||
); language != "" {
|
|
||||||
// group languages, such as Pug -> HTML; SCSS -> CSS
|
|
||||||
group := enry.GetLanguageGroup(language)
|
|
||||||
if len(group) != 0 {
|
|
||||||
language = group
|
|
||||||
}
|
|
||||||
|
|
||||||
// this language will always be added to the size
|
|
||||||
sizes[language] += f.Size()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if isFalse(isDetectable) || isTrue(isVendored) || isTrue(isDocumentation) ||
|
|
||||||
(!isFalse(isVendored) && analyze.IsVendor(f.Name())) ||
|
|
||||||
enry.IsDotFile(f.Name()) ||
|
|
||||||
enry.IsConfiguration(f.Name()) ||
|
|
||||||
(!isFalse(isDocumentation) && enry.IsDocumentation(f.Name())) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// If content can not be read or file is too big just do detection by filename
|
|
||||||
|
|
||||||
if f.Size() <= bigFileSize {
|
|
||||||
if err := writeID(f.ID.String()); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, _, size, err := ReadBatchLine(batchReader)
|
|
||||||
if err != nil {
|
|
||||||
log.Debug("Error reading blob: %s Err: %v", f.ID.String(), err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
sizeToRead := size
|
|
||||||
discard := int64(1)
|
|
||||||
if size > fileSizeLimit {
|
|
||||||
sizeToRead = fileSizeLimit
|
|
||||||
discard = size - fileSizeLimit + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = contentBuf.ReadFrom(io.LimitReader(batchReader, sizeToRead))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
content = contentBuf.Bytes()
|
|
||||||
if err := DiscardFull(batchReader, discard); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !isTrue(isGenerated) && enry.IsGenerated(f.Name(), content) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: Why can't we split this and the IsGenerated tests to avoid reading the blob unless absolutely necessary?
|
|
||||||
// - eg. do the all the detection tests using filename first before reading content.
|
|
||||||
language := analyze.GetCodeLanguage(f.Name(), content)
|
|
||||||
if language == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// group languages, such as Pug -> HTML; SCSS -> CSS
|
|
||||||
group := enry.GetLanguageGroup(language)
|
|
||||||
if group != "" {
|
|
||||||
language = group
|
|
||||||
}
|
|
||||||
|
|
||||||
included, checked := includedLanguage[language]
|
|
||||||
langType := enry.GetLanguageType(language)
|
|
||||||
if !checked {
|
|
||||||
included = langType == enry.Programming || langType == enry.Markup
|
|
||||||
if !included && (isTrue(isDetectable) || (langType == enry.Prose && isFalse(isDocumentation))) {
|
|
||||||
included = true
|
|
||||||
}
|
|
||||||
includedLanguage[language] = included
|
|
||||||
}
|
|
||||||
if included {
|
|
||||||
sizes[language] += f.Size()
|
|
||||||
} else if len(sizes) == 0 && (firstExcludedLanguage == "" || firstExcludedLanguage == language) {
|
|
||||||
// Only consider Programming or Markup languages as fallback
|
|
||||||
if !(langType == enry.Programming || langType == enry.Markup) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
firstExcludedLanguage = language
|
|
||||||
firstExcludedLanguageSize += f.Size()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there are no included languages add the first excluded language
|
|
||||||
if len(sizes) == 0 && firstExcludedLanguage != "" {
|
|
||||||
sizes[firstExcludedLanguage] = firstExcludedLanguageSize
|
|
||||||
}
|
|
||||||
|
|
||||||
return mergeLanguageStats(sizes), nil
|
|
||||||
}
|
|
|
@ -1,8 +1,6 @@
|
||||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
//go:build !gogit
|
|
||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
@ -4,8 +4,10 @@
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
@ -78,3 +80,78 @@ func (repo *Repository) ExpandRef(ref string) (string, error) {
|
||||||
}
|
}
|
||||||
return "", fmt.Errorf("could not expand reference '%s'", ref)
|
return "", fmt.Errorf("could not expand reference '%s'", ref)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetRefsFiltered returns all references of the repository that matches patterm exactly or starting with.
|
||||||
|
func (repo *Repository) GetRefsFiltered(pattern string) ([]*Reference, error) {
|
||||||
|
stdoutReader, stdoutWriter := io.Pipe()
|
||||||
|
defer func() {
|
||||||
|
_ = stdoutReader.Close()
|
||||||
|
_ = stdoutWriter.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
stderrBuilder := &strings.Builder{}
|
||||||
|
err := NewCommand(repo.Ctx, "for-each-ref").Run(&RunOpts{
|
||||||
|
Dir: repo.Path,
|
||||||
|
Stdout: stdoutWriter,
|
||||||
|
Stderr: stderrBuilder,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
_ = stdoutWriter.CloseWithError(ConcatenateError(err, stderrBuilder.String()))
|
||||||
|
} else {
|
||||||
|
_ = stdoutWriter.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
refs := make([]*Reference, 0)
|
||||||
|
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 nil, 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 nil, 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 nil, 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 refs, nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build gogit
|
|
||||||
|
|
||||||
package git
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5"
|
|
||||||
"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(pattern string) ([]*Reference, error) {
|
|
||||||
r, err := git.PlainOpen(repo.Path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
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(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,87 +0,0 @@
|
||||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build !gogit
|
|
||||||
|
|
||||||
package git
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetRefsFiltered returns all references of the repository that matches patterm exactly or starting with.
|
|
||||||
func (repo *Repository) GetRefsFiltered(pattern string) ([]*Reference, error) {
|
|
||||||
stdoutReader, stdoutWriter := io.Pipe()
|
|
||||||
defer func() {
|
|
||||||
_ = stdoutReader.Close()
|
|
||||||
_ = stdoutWriter.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
stderrBuilder := &strings.Builder{}
|
|
||||||
err := NewCommand(repo.Ctx, "for-each-ref").Run(&RunOpts{
|
|
||||||
Dir: repo.Path,
|
|
||||||
Stdout: stdoutWriter,
|
|
||||||
Stderr: stderrBuilder,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
_ = stdoutWriter.CloseWithError(ConcatenateError(err, stderrBuilder.String()))
|
|
||||||
} else {
|
|
||||||
_ = stdoutWriter.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
refs := make([]*Reference, 0)
|
|
||||||
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 nil, 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 nil, 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 nil, 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 refs, nil
|
|
||||||
}
|
|
|
@ -6,11 +6,13 @@ package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git/foreachref"
|
"code.gitea.io/gitea/modules/git/foreachref"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -236,3 +238,123 @@ func (repo *Repository) GetAnnotatedTag(sha string) (*Tag, error) {
|
||||||
}
|
}
|
||||||
return tag, nil
|
return tag, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsTagExist returns true if given tag exists in the repository.
|
||||||
|
func (repo *Repository) IsTagExist(name string) bool {
|
||||||
|
if repo == nil || name == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return repo.IsReferenceExist(TagPrefix + name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTags returns all tags of the repository.
|
||||||
|
// returning at most limit tags, or all if limit is 0.
|
||||||
|
func (repo *Repository) GetTags(skip, limit int) (tags []string, err error) {
|
||||||
|
tags, _, err = callShowRef(repo.Ctx, repo.Path, TagPrefix, TrustedCmdArgs{TagPrefix, "--sort=-taggerdate"}, skip, limit)
|
||||||
|
return tags, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTagType gets the type of the tag, either commit (simple) or tag (annotated)
|
||||||
|
func (repo *Repository) GetTagType(id ObjectID) (string, error) {
|
||||||
|
wr, rd, cancel := repo.CatFileBatchCheck(repo.Ctx)
|
||||||
|
defer cancel()
|
||||||
|
_, err := wr.Write([]byte(id.String() + "\n"))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
_, typ, _, err := ReadBatchLine(rd)
|
||||||
|
if IsErrNotExist(err) {
|
||||||
|
return "", ErrNotExist{ID: id.String()}
|
||||||
|
}
|
||||||
|
return typ, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *Repository) getTag(tagID ObjectID, name string) (*Tag, error) {
|
||||||
|
t, ok := repo.tagCache.Get(tagID.String())
|
||||||
|
if ok {
|
||||||
|
log.Debug("Hit cache: %s", tagID)
|
||||||
|
tagClone := *t.(*Tag)
|
||||||
|
tagClone.Name = name // This is necessary because lightweight tags may have same id
|
||||||
|
return &tagClone, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tp, err := repo.GetTagType(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(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(commitIDStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tag := &Tag{
|
||||||
|
Name: name,
|
||||||
|
ID: tagID,
|
||||||
|
Object: commitID,
|
||||||
|
Type: tp,
|
||||||
|
Tagger: commit.Committer,
|
||||||
|
Message: commit.Message(),
|
||||||
|
}
|
||||||
|
|
||||||
|
repo.tagCache.Set(tagID.String(), tag)
|
||||||
|
return tag, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// The tag is an annotated tag with a message.
|
||||||
|
wr, rd, cancel := repo.CatFileBatch(repo.Ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if _, err := wr.Write([]byte(tagID.String() + "\n")); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, typ, size, err := ReadBatchLine(rd)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, io.EOF) || IsErrNotExist(err) {
|
||||||
|
return nil, ErrNotExist{ID: tagID.String()}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
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
|
||||||
|
data, err := io.ReadAll(io.LimitReader(rd, size))
|
||||||
|
if 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,135 +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 (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// IsTagExist returns true if given tag exists in the repository.
|
|
||||||
func (repo *Repository) IsTagExist(name string) bool {
|
|
||||||
_, err := repo.gogitRepo.Reference(plumbing.ReferenceName(TagPrefix+name), true)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTags returns all tags of the repository.
|
|
||||||
// returning at most limit tags, or all if limit is 0.
|
|
||||||
func (repo *Repository) GetTags(skip, limit int) ([]string, error) {
|
|
||||||
var tagNames []string
|
|
||||||
|
|
||||||
tags, err := repo.gogitRepo.Tags()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = tags.ForEach(func(tag *plumbing.Reference) error {
|
|
||||||
tagNames = append(tagNames, strings.TrimPrefix(tag.Name().String(), TagPrefix))
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
// Reverse order
|
|
||||||
for i := 0; i < len(tagNames)/2; i++ {
|
|
||||||
j := len(tagNames) - i - 1
|
|
||||||
tagNames[i], tagNames[j] = tagNames[j], tagNames[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
// since we have to reverse order we can paginate only afterwards
|
|
||||||
if len(tagNames) < skip {
|
|
||||||
tagNames = []string{}
|
|
||||||
} else {
|
|
||||||
tagNames = tagNames[skip:]
|
|
||||||
}
|
|
||||||
if limit != 0 && len(tagNames) > limit {
|
|
||||||
tagNames = tagNames[:limit]
|
|
||||||
}
|
|
||||||
|
|
||||||
return tagNames, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTagType gets the type of the tag, either commit (simple) or tag (annotated)
|
|
||||||
func (repo *Repository) GetTagType(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(tagID ObjectID, name string) (*Tag, error) {
|
|
||||||
t, ok := repo.tagCache.Get(tagID.String())
|
|
||||||
if ok {
|
|
||||||
log.Debug("Hit cache: %s", tagID)
|
|
||||||
tagClone := *t.(*Tag)
|
|
||||||
tagClone.Name = name // This is necessary because lightweight tags may have same id
|
|
||||||
return &tagClone, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tp, err := repo.GetTagType(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(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(commitIDStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tag := &Tag{
|
|
||||||
Name: name,
|
|
||||||
ID: tagID,
|
|
||||||
Object: commitID,
|
|
||||||
Type: tp,
|
|
||||||
Tagger: commit.Committer,
|
|
||||||
Message: commit.Message(),
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
Message: 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 (
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// IsTagExist returns true if given tag exists in the repository.
|
|
||||||
func (repo *Repository) IsTagExist(name string) bool {
|
|
||||||
if repo == nil || name == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return repo.IsReferenceExist(TagPrefix + name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTags returns all tags of the repository.
|
|
||||||
// returning at most limit tags, or all if limit is 0.
|
|
||||||
func (repo *Repository) GetTags(skip, limit int) (tags []string, err error) {
|
|
||||||
tags, _, err = callShowRef(repo.Ctx, repo.Path, TagPrefix, TrustedCmdArgs{TagPrefix, "--sort=-taggerdate"}, skip, limit)
|
|
||||||
return tags, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTagType gets the type of the tag, either commit (simple) or tag (annotated)
|
|
||||||
func (repo *Repository) GetTagType(id ObjectID) (string, error) {
|
|
||||||
wr, rd, cancel := repo.CatFileBatchCheck(repo.Ctx)
|
|
||||||
defer cancel()
|
|
||||||
_, err := wr.Write([]byte(id.String() + "\n"))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
_, typ, _, err := ReadBatchLine(rd)
|
|
||||||
if IsErrNotExist(err) {
|
|
||||||
return "", ErrNotExist{ID: id.String()}
|
|
||||||
}
|
|
||||||
return typ, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *Repository) getTag(tagID ObjectID, name string) (*Tag, error) {
|
|
||||||
t, ok := repo.tagCache.Get(tagID.String())
|
|
||||||
if ok {
|
|
||||||
log.Debug("Hit cache: %s", tagID)
|
|
||||||
tagClone := *t.(*Tag)
|
|
||||||
tagClone.Name = name // This is necessary because lightweight tags may have same id
|
|
||||||
return &tagClone, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tp, err := repo.GetTagType(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(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(commitIDStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tag := &Tag{
|
|
||||||
Name: name,
|
|
||||||
ID: tagID,
|
|
||||||
Object: commitID,
|
|
||||||
Type: tp,
|
|
||||||
Tagger: commit.Committer,
|
|
||||||
Message: commit.Message(),
|
|
||||||
}
|
|
||||||
|
|
||||||
repo.tagCache.Set(tagID.String(), tag)
|
|
||||||
return tag, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// The tag is an annotated tag with a message.
|
|
||||||
wr, rd, cancel := repo.CatFileBatch(repo.Ctx)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if _, err := wr.Write([]byte(tagID.String() + "\n")); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, typ, size, err := ReadBatchLine(rd)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, io.EOF) || IsErrNotExist(err) {
|
|
||||||
return nil, ErrNotExist{ID: tagID.String()}
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
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
|
|
||||||
data, err := io.ReadAll(io.LimitReader(rd, size))
|
|
||||||
if 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
|
|
||||||
}
|
|
|
@ -6,6 +6,7 @@ package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -65,3 +66,88 @@ func (repo *Repository) CommitTree(author, committer *Signature, tree *Tree, opt
|
||||||
}
|
}
|
||||||
return NewIDFromString(strings.TrimSpace(stdout.String()))
|
return NewIDFromString(strings.TrimSpace(stdout.String()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (repo *Repository) getTree(id ObjectID) (*Tree, error) {
|
||||||
|
wr, rd, cancel := repo.CatFileBatch(repo.Ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, _ = wr.Write([]byte(id.String() + "\n"))
|
||||||
|
|
||||||
|
// ignore the SHA
|
||||||
|
_, typ, size, err := ReadBatchLine(rd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch typ {
|
||||||
|
case "tag":
|
||||||
|
resolvedID := id
|
||||||
|
data, err := io.ReadAll(io.LimitReader(rd, size))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tag, err := parseTagData(id.Type(), data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
commit, err := tag.Commit(repo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
commit.Tree.ResolvedID = resolvedID
|
||||||
|
return &commit.Tree, nil
|
||||||
|
case "commit":
|
||||||
|
commit, err := CommitFromReader(repo, id, io.LimitReader(rd, size))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := rd.Discard(1); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
commit.Tree.ResolvedID = commit.ID
|
||||||
|
return &commit.Tree, nil
|
||||||
|
case "tree":
|
||||||
|
tree := NewTree(repo, id)
|
||||||
|
tree.ResolvedID = id
|
||||||
|
objectFormat, err := repo.GetObjectFormat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tree.entries, err = catBatchParseTreeEntries(objectFormat, tree, rd, size)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tree.entriesParsed = true
|
||||||
|
return tree, nil
|
||||||
|
default:
|
||||||
|
if err := DiscardFull(rd, 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(idStr string) (*Tree, error) {
|
||||||
|
objectFormat, err := repo.GetObjectFormat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(idStr) != objectFormat.FullLength() {
|
||||||
|
res, err := repo.GetRefCommitID(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(id)
|
||||||
|
}
|
||||||
|
|
|
@ -1,53 +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"
|
|
||||||
|
|
||||||
func (repo *Repository) getTree(id ObjectID) (*Tree, error) {
|
|
||||||
gogitTree, err := repo.gogitRepo.TreeObject(plumbing.Hash(id.RawValue()))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tree := NewTree(repo, id)
|
|
||||||
tree.gogitTree = gogitTree
|
|
||||||
return tree, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTree find the tree object in the repository.
|
|
||||||
func (repo *Repository) GetTree(idStr string) (*Tree, error) {
|
|
||||||
objectFormat, err := repo.GetObjectFormat()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(idStr) != objectFormat.FullLength() {
|
|
||||||
res, _, err := NewCommand(repo.Ctx, "rev-parse", "--verify").AddDynamicArguments(idStr).RunStdString(&RunOpts{Dir: repo.Path})
|
|
||||||
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
|
|
||||||
}
|
|
||||||
resolvedID := id
|
|
||||||
commitObject, err := repo.gogitRepo.CommitObject(plumbing.Hash(id.RawValue()))
|
|
||||||
if err == nil {
|
|
||||||
id = ParseGogitHash(commitObject.TreeHash)
|
|
||||||
}
|
|
||||||
treeObject, err := repo.getTree(id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
treeObject.ResolvedID = resolvedID
|
|
||||||
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 (
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (repo *Repository) getTree(id ObjectID) (*Tree, error) {
|
|
||||||
wr, rd, cancel := repo.CatFileBatch(repo.Ctx)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
_, _ = wr.Write([]byte(id.String() + "\n"))
|
|
||||||
|
|
||||||
// ignore the SHA
|
|
||||||
_, typ, size, err := ReadBatchLine(rd)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch typ {
|
|
||||||
case "tag":
|
|
||||||
resolvedID := id
|
|
||||||
data, err := io.ReadAll(io.LimitReader(rd, size))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tag, err := parseTagData(id.Type(), data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
commit, err := tag.Commit(repo)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
commit.Tree.ResolvedID = resolvedID
|
|
||||||
return &commit.Tree, nil
|
|
||||||
case "commit":
|
|
||||||
commit, err := CommitFromReader(repo, id, io.LimitReader(rd, size))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if _, err := rd.Discard(1); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
commit.Tree.ResolvedID = commit.ID
|
|
||||||
return &commit.Tree, nil
|
|
||||||
case "tree":
|
|
||||||
tree := NewTree(repo, id)
|
|
||||||
tree.ResolvedID = id
|
|
||||||
objectFormat, err := repo.GetObjectFormat()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tree.entries, err = catBatchParseTreeEntries(objectFormat, tree, rd, size)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tree.entriesParsed = true
|
|
||||||
return tree, nil
|
|
||||||
default:
|
|
||||||
if err := DiscardFull(rd, 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(idStr string) (*Tree, error) {
|
|
||||||
objectFormat, err := repo.GetObjectFormat()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(idStr) != objectFormat.FullLength() {
|
|
||||||
res, err := repo.GetRefCommitID(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(id)
|
|
||||||
}
|
|
|
@ -5,13 +5,31 @@
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode decodes a byte array representing a signature to signature
|
||||||
|
func (s *Signature) Decode(b []byte) {
|
||||||
|
*s = *parseSignatureFromCommitLine(util.UnsafeBytesToString(b))
|
||||||
|
}
|
||||||
|
|
||||||
// Helper to get a signature from the commit line, which looks like:
|
// Helper to get a signature from the commit line, which looks like:
|
||||||
//
|
//
|
||||||
// full name <user@example.com> 1378823654 +0200
|
// full name <user@example.com> 1378823654 +0200
|
||||||
|
|
|
@ -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,30 +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"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode decodes a byte array representing a signature to signature
|
|
||||||
func (s *Signature) Decode(b []byte) {
|
|
||||||
*s = *parseSignatureFromCommitLine(util.UnsafeBytesToString(b))
|
|
||||||
}
|
|
|
@ -6,9 +6,26 @@ package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Tree represents a flat directory listing.
|
||||||
|
type Tree struct {
|
||||||
|
ID ObjectID
|
||||||
|
ResolvedID ObjectID
|
||||||
|
repo *Repository
|
||||||
|
|
||||||
|
// parent tree
|
||||||
|
ptree *Tree
|
||||||
|
|
||||||
|
entries Entries
|
||||||
|
entriesParsed bool
|
||||||
|
|
||||||
|
entriesRecursive Entries
|
||||||
|
entriesRecursiveParsed bool
|
||||||
|
}
|
||||||
|
|
||||||
// NewTree create a new tree according the repository and tree id
|
// NewTree create a new tree according the repository and tree id
|
||||||
func NewTree(repo *Repository, id ObjectID) *Tree {
|
func NewTree(repo *Repository, id ObjectID) *Tree {
|
||||||
return &Tree{
|
return &Tree{
|
||||||
|
@ -17,6 +34,100 @@ func NewTree(repo *Repository, id ObjectID) *Tree {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListEntries returns all entries of current tree.
|
||||||
|
func (t *Tree) ListEntries() (Entries, error) {
|
||||||
|
if t.entriesParsed {
|
||||||
|
return t.entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.repo != nil {
|
||||||
|
wr, rd, cancel := t.repo.CatFileBatch(t.repo.Ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, _ = wr.Write([]byte(t.ID.String() + "\n"))
|
||||||
|
_, typ, sz, err := ReadBatchLine(rd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if typ == "commit" {
|
||||||
|
treeID, err := ReadTreeID(rd, sz)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, _ = wr.Write([]byte(treeID + "\n"))
|
||||||
|
_, typ, sz, err = ReadBatchLine(rd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if typ == "tree" {
|
||||||
|
t.entries, err = catBatchParseTreeEntries(t.ID.Type(), t, rd, sz)
|
||||||
|
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, sz+1); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout, _, runErr := NewCommand(t.repo.Ctx, "ls-tree", "-l").AddDynamicArguments(t.ID.String()).RunStdBytes(&RunOpts{Dir: t.repo.Path})
|
||||||
|
if runErr != nil {
|
||||||
|
if strings.Contains(runErr.Error(), "fatal: Not a valid object name") || strings.Contains(runErr.Error(), "fatal: not a tree object") {
|
||||||
|
return nil, ErrNotExist{
|
||||||
|
ID: t.ID.String(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, runErr
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
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(extraArgs TrustedCmdArgs) (Entries, error) {
|
||||||
|
if t.entriesRecursiveParsed {
|
||||||
|
return t.entriesRecursive, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout, _, runErr := NewCommand(t.repo.Ctx, "ls-tree", "-t", "-r").
|
||||||
|
AddArguments(extraArgs...).
|
||||||
|
AddDynamicArguments(t.ID.String()).
|
||||||
|
RunStdBytes(&RunOpts{Dir: t.repo.Path})
|
||||||
|
if runErr != nil {
|
||||||
|
return nil, runErr
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
t.entriesRecursive, err = parseTreeEntries(stdout, t)
|
||||||
|
if err == nil {
|
||||||
|
t.entriesRecursiveParsed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.entriesRecursive, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListEntriesRecursiveFast returns all entries of current tree recursively including all subtrees, no size
|
||||||
|
func (t *Tree) ListEntriesRecursiveFast() (Entries, error) {
|
||||||
|
return t.listEntriesRecursive(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListEntriesRecursiveWithSize returns all entries of current tree recursively including all subtrees, with size
|
||||||
|
func (t *Tree) ListEntriesRecursiveWithSize() (Entries, error) {
|
||||||
|
return t.listEntriesRecursive(TrustedCmdArgs{"--long"})
|
||||||
|
}
|
||||||
|
|
||||||
// SubTree get a sub tree by the sub dir path
|
// SubTree get a sub tree by the sub dir path
|
||||||
func (t *Tree) SubTree(rpath string) (*Tree, error) {
|
func (t *Tree) SubTree(rpath string) (*Tree, error) {
|
||||||
if len(rpath) == 0 {
|
if len(rpath) == 0 {
|
||||||
|
|
|
@ -5,7 +5,48 @@
|
||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import "strings"
|
import (
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetTreeEntryByPath get the tree entries according the sub dir
|
||||||
|
func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) {
|
||||||
|
if len(relpath) == 0 {
|
||||||
|
return &TreeEntry{
|
||||||
|
ptree: t,
|
||||||
|
ID: t.ID,
|
||||||
|
name: "",
|
||||||
|
fullName: "",
|
||||||
|
entryMode: EntryModeTree,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: This should probably use git cat-file --batch to be a bit more efficient
|
||||||
|
relpath = path.Clean(relpath)
|
||||||
|
parts := strings.Split(relpath, "/")
|
||||||
|
var err error
|
||||||
|
tree := t
|
||||||
|
for i, name := range parts {
|
||||||
|
if i == len(parts)-1 {
|
||||||
|
entries, err := tree.ListEntries()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, v := range entries {
|
||||||
|
if v.Name() == name {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tree, err = tree.SubTree(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, ErrNotExist{"", relpath}
|
||||||
|
}
|
||||||
|
|
||||||
// GetBlobByPath get the blob object according the path
|
// GetBlobByPath get the blob object according the path
|
||||||
func (t *Tree) GetBlobByPath(relpath string) (*Blob, error) {
|
func (t *Tree) GetBlobByPath(relpath string) (*Blob, error) {
|
||||||
|
|
|
@ -1,65 +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 (
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"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"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetTreeEntryByPath get the tree entries according the sub dir
|
|
||||||
func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) {
|
|
||||||
if len(relpath) == 0 {
|
|
||||||
return &TreeEntry{
|
|
||||||
ID: t.ID,
|
|
||||||
// Type: ObjectTree,
|
|
||||||
gogitTreeEntry: &object.TreeEntry{
|
|
||||||
Name: "",
|
|
||||||
Mode: filemode.Dir,
|
|
||||||
Hash: plumbing.Hash(t.ID.RawValue()),
|
|
||||||
},
|
|
||||||
}, 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()
|
|
||||||
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(name)
|
|
||||||
if err != nil {
|
|
||||||
if err == plumbing.ErrObjectNotFound {
|
|
||||||
return nil, ErrNotExist{
|
|
||||||
RelPath: relpath,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, ErrNotExist{"", relpath}
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build !gogit
|
|
||||||
|
|
||||||
package git
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetTreeEntryByPath get the tree entries according the sub dir
|
|
||||||
func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) {
|
|
||||||
if len(relpath) == 0 {
|
|
||||||
return &TreeEntry{
|
|
||||||
ptree: t,
|
|
||||||
ID: t.ID,
|
|
||||||
name: "",
|
|
||||||
fullName: "",
|
|
||||||
entryMode: EntryModeTree,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: This should probably use git cat-file --batch to be a bit more efficient
|
|
||||||
relpath = path.Clean(relpath)
|
|
||||||
parts := strings.Split(relpath, "/")
|
|
||||||
var err error
|
|
||||||
tree := t
|
|
||||||
for i, name := range parts {
|
|
||||||
if i == len(parts)-1 {
|
|
||||||
entries, err := tree.ListEntries()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, v := range entries {
|
|
||||||
if v.Name() == name {
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tree, err = tree.SubTree(name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, ErrNotExist{"", relpath}
|
|
||||||
}
|
|
|
@ -8,8 +8,98 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TreeEntry the leaf in the git tree
|
||||||
|
type TreeEntry struct {
|
||||||
|
ID ObjectID
|
||||||
|
|
||||||
|
ptree *Tree
|
||||||
|
|
||||||
|
entryMode EntryMode
|
||||||
|
name string
|
||||||
|
|
||||||
|
size int64
|
||||||
|
sized bool
|
||||||
|
fullName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the name of the entry
|
||||||
|
func (te *TreeEntry) Name() string {
|
||||||
|
if te.fullName != "" {
|
||||||
|
return te.fullName
|
||||||
|
}
|
||||||
|
return te.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode returns the mode of the entry
|
||||||
|
func (te *TreeEntry) Mode() EntryMode {
|
||||||
|
return te.entryMode
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size returns the size of the entry
|
||||||
|
func (te *TreeEntry) Size() int64 {
|
||||||
|
if te.IsDir() {
|
||||||
|
return 0
|
||||||
|
} else if te.sized {
|
||||||
|
return te.size
|
||||||
|
}
|
||||||
|
|
||||||
|
wr, rd, cancel := te.ptree.repo.CatFileBatchCheck(te.ptree.repo.Ctx)
|
||||||
|
defer cancel()
|
||||||
|
_, err := wr.Write([]byte(te.ID.String() + "\n"))
|
||||||
|
if err != nil {
|
||||||
|
log.Debug("error whilst reading size for %s in %s. Error: %v", te.ID.String(), te.ptree.repo.Path, err)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
_, _, te.size, err = ReadBatchLine(rd)
|
||||||
|
if err != nil {
|
||||||
|
log.Debug("error whilst reading size for %s in %s. Error: %v", te.ID.String(), te.ptree.repo.Path, err)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
te.sized = true
|
||||||
|
return te.size
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSubModule if the entry is a sub module
|
||||||
|
func (te *TreeEntry) IsSubModule() bool {
|
||||||
|
return te.entryMode == EntryModeCommit
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDir if the entry is a sub dir
|
||||||
|
func (te *TreeEntry) IsDir() bool {
|
||||||
|
return te.entryMode == EntryModeTree
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsLink if the entry is a symlink
|
||||||
|
func (te *TreeEntry) IsLink() bool {
|
||||||
|
return te.entryMode == EntryModeSymlink
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRegular if the entry is a regular file
|
||||||
|
func (te *TreeEntry) IsRegular() bool {
|
||||||
|
return te.entryMode == EntryModeBlob
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsExecutable if the entry is an executable file (not necessarily binary)
|
||||||
|
func (te *TreeEntry) IsExecutable() bool {
|
||||||
|
return te.entryMode == EntryModeExec
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blob returns the blob object the entry
|
||||||
|
func (te *TreeEntry) Blob() *Blob {
|
||||||
|
return &Blob{
|
||||||
|
ID: te.ID,
|
||||||
|
name: te.Name(),
|
||||||
|
size: te.size,
|
||||||
|
gotSize: te.sized,
|
||||||
|
repo: te.ptree.repo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Type returns the type of the entry (commit, tree, blob)
|
// Type returns the type of the entry (commit, tree, blob)
|
||||||
func (te *TreeEntry) Type() string {
|
func (te *TreeEntry) Type() string {
|
||||||
switch te.Mode() {
|
switch te.Mode() {
|
||||||
|
|
|
@ -1,95 +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"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/filemode"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TreeEntry the leaf in the git tree
|
|
||||||
type TreeEntry struct {
|
|
||||||
ID ObjectID
|
|
||||||
|
|
||||||
gogitTreeEntry *object.TreeEntry
|
|
||||||
ptree *Tree
|
|
||||||
|
|
||||||
size int64
|
|
||||||
sized bool
|
|
||||||
fullName string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name returns the name of the entry
|
|
||||||
func (te *TreeEntry) Name() string {
|
|
||||||
if te.fullName != "" {
|
|
||||||
return te.fullName
|
|
||||||
}
|
|
||||||
return te.gogitTreeEntry.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mode returns the mode of the entry
|
|
||||||
func (te *TreeEntry) Mode() EntryMode {
|
|
||||||
return EntryMode(te.gogitTreeEntry.Mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Size returns the size of the entry
|
|
||||||
func (te *TreeEntry) Size() int64 {
|
|
||||||
if te.IsDir() {
|
|
||||||
return 0
|
|
||||||
} else if te.sized {
|
|
||||||
return te.size
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := te.ptree.gogitTree.TreeEntryFile(te.gogitTreeEntry)
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
te.sized = true
|
|
||||||
te.size = file.Size
|
|
||||||
return te.size
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsSubModule if the entry is a sub module
|
|
||||||
func (te *TreeEntry) IsSubModule() bool {
|
|
||||||
return te.gogitTreeEntry.Mode == filemode.Submodule
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsDir if the entry is a sub dir
|
|
||||||
func (te *TreeEntry) IsDir() bool {
|
|
||||||
return te.gogitTreeEntry.Mode == filemode.Dir
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsLink if the entry is a symlink
|
|
||||||
func (te *TreeEntry) IsLink() bool {
|
|
||||||
return te.gogitTreeEntry.Mode == filemode.Symlink
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsRegular if the entry is a regular file
|
|
||||||
func (te *TreeEntry) IsRegular() bool {
|
|
||||||
return te.gogitTreeEntry.Mode == filemode.Regular
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsExecutable if the entry is an executable file (not necessarily binary)
|
|
||||||
func (te *TreeEntry) IsExecutable() bool {
|
|
||||||
return te.gogitTreeEntry.Mode == filemode.Executable
|
|
||||||
}
|
|
||||||
|
|
||||||
// Blob returns the blob object the entry
|
|
||||||
func (te *TreeEntry) Blob() *Blob {
|
|
||||||
encodedObj, err := te.ptree.repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, te.gogitTreeEntry.Hash)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Blob{
|
|
||||||
ID: ParseGogitHash(te.gogitTreeEntry.Hash),
|
|
||||||
gogitEncodedObj: encodedObj,
|
|
||||||
name: te.Name(),
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,96 +0,0 @@
|
||||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build !gogit
|
|
||||||
|
|
||||||
package git
|
|
||||||
|
|
||||||
import "code.gitea.io/gitea/modules/log"
|
|
||||||
|
|
||||||
// TreeEntry the leaf in the git tree
|
|
||||||
type TreeEntry struct {
|
|
||||||
ID ObjectID
|
|
||||||
|
|
||||||
ptree *Tree
|
|
||||||
|
|
||||||
entryMode EntryMode
|
|
||||||
name string
|
|
||||||
|
|
||||||
size int64
|
|
||||||
sized bool
|
|
||||||
fullName string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name returns the name of the entry
|
|
||||||
func (te *TreeEntry) Name() string {
|
|
||||||
if te.fullName != "" {
|
|
||||||
return te.fullName
|
|
||||||
}
|
|
||||||
return te.name
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mode returns the mode of the entry
|
|
||||||
func (te *TreeEntry) Mode() EntryMode {
|
|
||||||
return te.entryMode
|
|
||||||
}
|
|
||||||
|
|
||||||
// Size returns the size of the entry
|
|
||||||
func (te *TreeEntry) Size() int64 {
|
|
||||||
if te.IsDir() {
|
|
||||||
return 0
|
|
||||||
} else if te.sized {
|
|
||||||
return te.size
|
|
||||||
}
|
|
||||||
|
|
||||||
wr, rd, cancel := te.ptree.repo.CatFileBatchCheck(te.ptree.repo.Ctx)
|
|
||||||
defer cancel()
|
|
||||||
_, err := wr.Write([]byte(te.ID.String() + "\n"))
|
|
||||||
if err != nil {
|
|
||||||
log.Debug("error whilst reading size for %s in %s. Error: %v", te.ID.String(), te.ptree.repo.Path, err)
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
_, _, te.size, err = ReadBatchLine(rd)
|
|
||||||
if err != nil {
|
|
||||||
log.Debug("error whilst reading size for %s in %s. Error: %v", te.ID.String(), te.ptree.repo.Path, err)
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
te.sized = true
|
|
||||||
return te.size
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsSubModule if the entry is a sub module
|
|
||||||
func (te *TreeEntry) IsSubModule() bool {
|
|
||||||
return te.entryMode == EntryModeCommit
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsDir if the entry is a sub dir
|
|
||||||
func (te *TreeEntry) IsDir() bool {
|
|
||||||
return te.entryMode == EntryModeTree
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsLink if the entry is a symlink
|
|
||||||
func (te *TreeEntry) IsLink() bool {
|
|
||||||
return te.entryMode == EntryModeSymlink
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsRegular if the entry is a regular file
|
|
||||||
func (te *TreeEntry) IsRegular() bool {
|
|
||||||
return te.entryMode == EntryModeBlob
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsExecutable if the entry is an executable file (not necessarily binary)
|
|
||||||
func (te *TreeEntry) IsExecutable() bool {
|
|
||||||
return te.entryMode == EntryModeExec
|
|
||||||
}
|
|
||||||
|
|
||||||
// Blob returns the blob object the entry
|
|
||||||
func (te *TreeEntry) Blob() *Blob {
|
|
||||||
return &Blob{
|
|
||||||
ID: te.ID,
|
|
||||||
name: te.Name(),
|
|
||||||
size: te.size,
|
|
||||||
gotSize: te.sized,
|
|
||||||
repo: te.ptree.repo,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,103 +0,0 @@
|
||||||
// Copyright 2017 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/go-git/go-git/v5/plumbing/object"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func getTestEntries() Entries {
|
|
||||||
return Entries{
|
|
||||||
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v1.0", Mode: filemode.Dir}},
|
|
||||||
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.0", Mode: filemode.Dir}},
|
|
||||||
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.1", Mode: filemode.Dir}},
|
|
||||||
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.12", Mode: filemode.Dir}},
|
|
||||||
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.2", Mode: filemode.Dir}},
|
|
||||||
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v12.0", Mode: filemode.Dir}},
|
|
||||||
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "abc", Mode: filemode.Regular}},
|
|
||||||
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "bcd", Mode: filemode.Regular}},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEntriesSort(t *testing.T) {
|
|
||||||
entries := getTestEntries()
|
|
||||||
entries.Sort()
|
|
||||||
assert.Equal(t, "v1.0", entries[0].Name())
|
|
||||||
assert.Equal(t, "v12.0", entries[1].Name())
|
|
||||||
assert.Equal(t, "v2.0", entries[2].Name())
|
|
||||||
assert.Equal(t, "v2.1", entries[3].Name())
|
|
||||||
assert.Equal(t, "v2.12", entries[4].Name())
|
|
||||||
assert.Equal(t, "v2.2", entries[5].Name())
|
|
||||||
assert.Equal(t, "abc", entries[6].Name())
|
|
||||||
assert.Equal(t, "bcd", entries[7].Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEntriesCustomSort(t *testing.T) {
|
|
||||||
entries := getTestEntries()
|
|
||||||
entries.CustomSort(func(s1, s2 string) bool {
|
|
||||||
return s1 > s2
|
|
||||||
})
|
|
||||||
assert.Equal(t, "v2.2", entries[0].Name())
|
|
||||||
assert.Equal(t, "v2.12", entries[1].Name())
|
|
||||||
assert.Equal(t, "v2.1", entries[2].Name())
|
|
||||||
assert.Equal(t, "v2.0", entries[3].Name())
|
|
||||||
assert.Equal(t, "v12.0", entries[4].Name())
|
|
||||||
assert.Equal(t, "v1.0", entries[5].Name())
|
|
||||||
assert.Equal(t, "bcd", entries[6].Name())
|
|
||||||
assert.Equal(t, "abc", entries[7].Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFollowLink(t *testing.T) {
|
|
||||||
r, err := openRepositoryWithDefaultContext("tests/repos/repo1_bare")
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer r.Close()
|
|
||||||
|
|
||||||
commit, err := r.GetCommit("37991dec2c8e592043f47155ce4808d4580f9123")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// get the symlink
|
|
||||||
lnk, err := commit.Tree.GetTreeEntryByPath("foo/bar/link_to_hello")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.True(t, lnk.IsLink())
|
|
||||||
|
|
||||||
// should be able to dereference to target
|
|
||||||
target, err := lnk.FollowLink()
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "hello", target.Name())
|
|
||||||
assert.False(t, target.IsLink())
|
|
||||||
assert.Equal(t, "b14df6442ea5a1b382985a6549b85d435376c351", target.ID.String())
|
|
||||||
|
|
||||||
// should error when called on normal file
|
|
||||||
target, err = commit.Tree.GetTreeEntryByPath("file1.txt")
|
|
||||||
require.NoError(t, err)
|
|
||||||
_, err = target.FollowLink()
|
|
||||||
assert.EqualError(t, err, "file1.txt: not a symlink")
|
|
||||||
|
|
||||||
// should error for broken links
|
|
||||||
target, err = commit.Tree.GetTreeEntryByPath("foo/broken_link")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.True(t, target.IsLink())
|
|
||||||
_, err = target.FollowLink()
|
|
||||||
assert.EqualError(t, err, "broken_link: broken link")
|
|
||||||
|
|
||||||
// should error for external links
|
|
||||||
target, err = commit.Tree.GetTreeEntryByPath("foo/outside_repo")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.True(t, target.IsLink())
|
|
||||||
_, err = target.FollowLink()
|
|
||||||
assert.EqualError(t, err, "outside_repo: points outside of repo")
|
|
||||||
|
|
||||||
// testing fix for short link bug
|
|
||||||
target, err = commit.Tree.GetTreeEntryByPath("foo/link_short")
|
|
||||||
require.NoError(t, err)
|
|
||||||
_, err = target.FollowLink()
|
|
||||||
assert.EqualError(t, err, "link_short: broken link")
|
|
||||||
}
|
|
|
@ -1,98 +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 (
|
|
||||||
"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 {
|
|
||||||
ID ObjectID
|
|
||||||
ResolvedID ObjectID
|
|
||||||
repo *Repository
|
|
||||||
|
|
||||||
gogitTree *object.Tree
|
|
||||||
|
|
||||||
// parent tree
|
|
||||||
ptree *Tree
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tree) loadTreeObject() error {
|
|
||||||
gogitTree, err := t.repo.gogitRepo.TreeObject(plumbing.Hash(t.ID.RawValue()))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
t.gogitTree = gogitTree
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListEntries returns all entries of current tree.
|
|
||||||
func (t *Tree) ListEntries() (Entries, error) {
|
|
||||||
if t.gogitTree == nil {
|
|
||||||
err := t.loadTreeObject()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
entries := make([]*TreeEntry, len(t.gogitTree.Entries))
|
|
||||||
for i, entry := range t.gogitTree.Entries {
|
|
||||||
entries[i] = &TreeEntry{
|
|
||||||
ID: ParseGogitHash(entry.Hash),
|
|
||||||
gogitTreeEntry: &t.gogitTree.Entries[i],
|
|
||||||
ptree: t,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return entries, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListEntriesRecursiveWithSize returns all entries of current tree recursively including all subtrees
|
|
||||||
func (t *Tree) ListEntriesRecursiveWithSize() (Entries, error) {
|
|
||||||
if t.gogitTree == nil {
|
|
||||||
err := t.loadTreeObject()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var entries []*TreeEntry
|
|
||||||
seen := map[plumbing.Hash]bool{}
|
|
||||||
walker := object.NewTreeWalker(t.gogitTree, true, seen)
|
|
||||||
for {
|
|
||||||
fullName, entry, err := walker.Next()
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if seen[entry.Hash] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
convertedEntry := &TreeEntry{
|
|
||||||
ID: ParseGogitHash(entry.Hash),
|
|
||||||
gogitTreeEntry: &entry,
|
|
||||||
ptree: t,
|
|
||||||
fullName: fullName,
|
|
||||||
}
|
|
||||||
entries = append(entries, convertedEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
return entries, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListEntriesRecursiveFast is the alias of ListEntriesRecursiveWithSize for the gogit version
|
|
||||||
func (t *Tree) ListEntriesRecursiveFast() (Entries, error) {
|
|
||||||
return t.ListEntriesRecursiveWithSize()
|
|
||||||
}
|
|
|
@ -1,121 +0,0 @@
|
||||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build !gogit
|
|
||||||
|
|
||||||
package git
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Tree represents a flat directory listing.
|
|
||||||
type Tree struct {
|
|
||||||
ID ObjectID
|
|
||||||
ResolvedID ObjectID
|
|
||||||
repo *Repository
|
|
||||||
|
|
||||||
// parent tree
|
|
||||||
ptree *Tree
|
|
||||||
|
|
||||||
entries Entries
|
|
||||||
entriesParsed bool
|
|
||||||
|
|
||||||
entriesRecursive Entries
|
|
||||||
entriesRecursiveParsed bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListEntries returns all entries of current tree.
|
|
||||||
func (t *Tree) ListEntries() (Entries, error) {
|
|
||||||
if t.entriesParsed {
|
|
||||||
return t.entries, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if t.repo != nil {
|
|
||||||
wr, rd, cancel := t.repo.CatFileBatch(t.repo.Ctx)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
_, _ = wr.Write([]byte(t.ID.String() + "\n"))
|
|
||||||
_, typ, sz, err := ReadBatchLine(rd)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if typ == "commit" {
|
|
||||||
treeID, err := ReadTreeID(rd, sz)
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, _ = wr.Write([]byte(treeID + "\n"))
|
|
||||||
_, typ, sz, err = ReadBatchLine(rd)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if typ == "tree" {
|
|
||||||
t.entries, err = catBatchParseTreeEntries(t.ID.Type(), t, rd, sz)
|
|
||||||
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, sz+1); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout, _, runErr := NewCommand(t.repo.Ctx, "ls-tree", "-l").AddDynamicArguments(t.ID.String()).RunStdBytes(&RunOpts{Dir: t.repo.Path})
|
|
||||||
if runErr != nil {
|
|
||||||
if strings.Contains(runErr.Error(), "fatal: Not a valid object name") || strings.Contains(runErr.Error(), "fatal: not a tree object") {
|
|
||||||
return nil, ErrNotExist{
|
|
||||||
ID: t.ID.String(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, runErr
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
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(extraArgs TrustedCmdArgs) (Entries, error) {
|
|
||||||
if t.entriesRecursiveParsed {
|
|
||||||
return t.entriesRecursive, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout, _, runErr := NewCommand(t.repo.Ctx, "ls-tree", "-t", "-r").
|
|
||||||
AddArguments(extraArgs...).
|
|
||||||
AddDynamicArguments(t.ID.String()).
|
|
||||||
RunStdBytes(&RunOpts{Dir: t.repo.Path})
|
|
||||||
if runErr != nil {
|
|
||||||
return nil, runErr
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
t.entriesRecursive, err = parseTreeEntries(stdout, t)
|
|
||||||
if err == nil {
|
|
||||||
t.entriesRecursiveParsed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return t.entriesRecursive, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListEntriesRecursiveFast returns all entries of current tree recursively including all subtrees, no size
|
|
||||||
func (t *Tree) ListEntriesRecursiveFast() (Entries, error) {
|
|
||||||
return t.listEntriesRecursive(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListEntriesRecursiveWithSize returns all entries of current tree recursively including all subtrees, with size
|
|
||||||
func (t *Tree) ListEntriesRecursiveWithSize() (Entries, error) {
|
|
||||||
return t.listEntriesRecursive(TrustedCmdArgs{"--long"})
|
|
||||||
}
|
|
|
@ -13,7 +13,7 @@ import (
|
||||||
// but not in production code.
|
// but not in production code.
|
||||||
|
|
||||||
func skipIfSHA256NotSupported(t *testing.T) {
|
func skipIfSHA256NotSupported(t *testing.T) {
|
||||||
if isGogit || CheckGitVersionAtLeast("2.42") != nil {
|
if CheckGitVersionAtLeast("2.42") != nil {
|
||||||
t.Skip("skipping because installed Git version doesn't support SHA256")
|
t.Skip("skipping because installed Git version doesn't support SHA256")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
//go:build !gogit
|
|
||||||
|
|
||||||
package gitrepo
|
package gitrepo
|
||||||
|
|
||||||
import (
|
import (
|
|
@ -1,40 +0,0 @@
|
||||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build gogit
|
|
||||||
|
|
||||||
package gitrepo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// WalkReferences walks all the references from the repository
|
|
||||||
// refname is empty, ObjectTag or ObjectBranch. All other values should be treated as equivalent to empty.
|
|
||||||
func WalkReferences(ctx context.Context, repo Repository, walkfn func(sha1, refname string) error) (int, error) {
|
|
||||||
gitRepo := repositoryFromContext(ctx, repo)
|
|
||||||
if gitRepo == nil {
|
|
||||||
var err error
|
|
||||||
gitRepo, err = OpenRepository(ctx, repo)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
defer gitRepo.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
i := 0
|
|
||||||
iter, err := gitRepo.GoGitRepo().References()
|
|
||||||
if err != nil {
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
defer iter.Close()
|
|
||||||
|
|
||||||
err = iter.ForEach(func(ref *plumbing.Reference) error {
|
|
||||||
err := walkfn(ref.Hash().String(), string(ref.Name()))
|
|
||||||
i++
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
return i, err
|
|
||||||
}
|
|
|
@ -1,8 +1,6 @@
|
||||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
//go:build !gogit
|
|
||||||
|
|
||||||
package lfs
|
package lfs
|
||||||
|
|
||||||
import (
|
import (
|
|
@ -1,62 +0,0 @@
|
||||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build gogit
|
|
||||||
|
|
||||||
package lfs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/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, errChan chan<- 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 > blobSizeCutoff {
|
|
||||||
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
|
|
||||||
})
|
|
||||||
}()
|
|
||||||
if err != nil {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
default:
|
|
||||||
errChan <- err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
close(pointerChan)
|
|
||||||
close(errChan)
|
|
||||||
}
|
|
1
release-notes/4941.md
Normal file
1
release-notes/4941.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Drop support to build Forgejo with the optional go-git Git backend. It only affects users who built Forgejo manually using `TAGS=gogits`, which no longer has any effect. Moving forward, we only support the default backend using the git binary. Please get in touch if you used the go-git backend and require any assistance moving away from it.
|
Loading…
Reference in a new issue