Improve listing performance by using go-git (#6478)

* Use go-git for tree reading and commit info lookup.

Signed-off-by: Filip Navara <navara@emclient.com>

* Use TreeEntry.IsRegular() instead of ObjectType that was removed.

Signed-off-by: Filip Navara <navara@emclient.com>

* Use the treePath to optimize commit info search.

Signed-off-by: Filip Navara <navara@emclient.com>

* Extract the latest commit at treePath along with the other commits.

Signed-off-by: Filip Navara <navara@emclient.com>

* Fix listing commit info for a directory that was created in one commit and never modified after.

Signed-off-by: Filip Navara <navara@emclient.com>

* Avoid nearly all external 'git' invocations when doing directory listing (.editorconfig code path is still hit).

Signed-off-by: Filip Navara <navara@emclient.com>

* Use go-git for reading blobs.

Signed-off-by: Filip Navara <navara@emclient.com>

* Make SHA1 type alias for plumbing.Hash in go-git.

Signed-off-by: Filip Navara <navara@emclient.com>

* Make Signature type alias for object.Signature in go-git.

Signed-off-by: Filip Navara <navara@emclient.com>

* Fix GetCommitsInfo for repository with only one commit.

Signed-off-by: Filip Navara <navara@emclient.com>

* Fix PGP signature verification.

Signed-off-by: Filip Navara <navara@emclient.com>

* Fix issues with walking commit graph across merges.

Signed-off-by: Filip Navara <navara@emclient.com>

* Fix typo in condition.

Signed-off-by: Filip Navara <navara@emclient.com>

* Speed up loading branch list by keeping the repository reference (and thus all the loaded packfile indexes).

Signed-off-by: Filip Navara <navara@emclient.com>

* Fix lising submodules.

Signed-off-by: Filip Navara <navara@emclient.com>

* Fix build

Signed-off-by: Filip Navara <navara@emclient.com>

* Add back commit cache because of name-rev

Signed-off-by: Filip Navara <navara@emclient.com>

* Fix tests

Signed-off-by: Filip Navara <navara@emclient.com>

* Fix code style

* Fix spelling

* Address PR feedback

Signed-off-by: Filip Navara <navara@emclient.com>

* Update vendor module list

Signed-off-by: Filip Navara <navara@emclient.com>

* Fix getting trees by commit id

Signed-off-by: Filip Navara <navara@emclient.com>

* Fix remaining unit test failures

* Fix GetTreeBySHA

* Avoid running `git name-rev` if not necessary

Signed-off-by: Filip Navara <navara@emclient.com>

* Move Branch code to git module

* Clean up GPG signature verification and fix it for tagged commits

* Address PR feedback (import formatting, copyright headers)

* Make blob lookup by SHA working

* Update tests to use public API

* Allow getting content from any type of object through the blob interface

* Change test to actually expect the object content that is in the GIT repository

* Change one more test to actually expect the object content that is in the GIT repository

* Add comments
This commit is contained in:
Filip Navara
2019-04-19 14:17:27 +02:00
committed by Lunny Xiao
parent 19ec2606e9
commit 2af67f6044
44 changed files with 759 additions and 783 deletions

4
go.mod
View File

@ -32,7 +32,7 @@ require (
github.com/dgrijalva/jwt-go v0.0.0-20161101193935-9ed569b5d1ac github.com/dgrijalva/jwt-go v0.0.0-20161101193935-9ed569b5d1ac
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712 // indirect github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712 // indirect
github.com/elazarl/go-bindata-assetfs v0.0.0-20151224045452-57eb5e1fc594 // indirect github.com/elazarl/go-bindata-assetfs v0.0.0-20151224045452-57eb5e1fc594 // indirect
github.com/emirpasic/gods v1.12.0 // indirect github.com/emirpasic/gods v1.12.0
github.com/etcd-io/bbolt v1.3.2 // indirect github.com/etcd-io/bbolt v1.3.2 // indirect
github.com/ethantkoenig/rupture v0.0.0-20180203182544-0a76f03a811a github.com/ethantkoenig/rupture v0.0.0-20180203182544-0a76f03a811a
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect
@ -127,7 +127,7 @@ require (
gopkg.in/ldap.v3 v3.0.2 gopkg.in/ldap.v3 v3.0.2
gopkg.in/macaron.v1 v1.3.2 gopkg.in/macaron.v1 v1.3.2
gopkg.in/redis.v2 v2.3.2 // indirect gopkg.in/redis.v2 v2.3.2 // indirect
gopkg.in/src-d/go-billy.v4 v4.3.0 // indirect gopkg.in/src-d/go-billy.v4 v4.3.0
gopkg.in/src-d/go-git.v4 v4.10.0 gopkg.in/src-d/go-git.v4 v4.10.0
gopkg.in/testfixtures.v2 v2.5.0 gopkg.in/testfixtures.v2 v2.5.0
mvdan.cc/xurls/v2 v2.0.0 mvdan.cc/xurls/v2 v2.0.0

View File

@ -38,7 +38,7 @@ func TestAPIReposGitBlobs(t *testing.T) {
var gitBlobResponse api.GitBlobResponse var gitBlobResponse api.GitBlobResponse
DecodeJSON(t, resp, &gitBlobResponse) DecodeJSON(t, resp, &gitBlobResponse)
assert.NotNil(t, gitBlobResponse) assert.NotNil(t, gitBlobResponse)
expectedContent := "Y29tbWl0IDY1ZjFiZjI3YmMzYmY3MGY2NDY1NzY1ODYzNWU2NjA5NGVkYmNiNGQKQXV0aG9yOiB1c2VyMSA8YWRkcmVzczFAZXhhbXBsZS5jb20+CkRhdGU6ICAgU3VuIE1hciAxOSAxNjo0Nzo1OSAyMDE3IC0wNDAwCgogICAgSW5pdGlhbCBjb21taXQKCmRpZmYgLS1naXQgYS9SRUFETUUubWQgYi9SRUFETUUubWQKbmV3IGZpbGUgbW9kZSAxMDA2NDQKaW5kZXggMDAwMDAwMC4uNGI0ODUxYQotLS0gL2Rldi9udWxsCisrKyBiL1JFQURNRS5tZApAQCAtMCwwICsxLDMgQEAKKyMgcmVwbzEKKworRGVzY3JpcHRpb24gZm9yIHJlcG8xClwgTm8gbmV3bGluZSBhdCBlbmQgb2YgZmlsZQo=" expectedContent := "dHJlZSAyYTJmMWQ0NjcwNzI4YTJlMTAwNDllMzQ1YmQ3YTI3NjQ2OGJlYWI2CmF1dGhvciB1c2VyMSA8YWRkcmVzczFAZXhhbXBsZS5jb20+IDE0ODk5NTY0NzkgLTA0MDAKY29tbWl0dGVyIEV0aGFuIEtvZW5pZyA8ZXRoYW50a29lbmlnQGdtYWlsLmNvbT4gMTQ4OTk1NjQ3OSAtMDQwMAoKSW5pdGlhbCBjb21taXQK"
assert.Equal(t, expectedContent, gitBlobResponse.Content) assert.Equal(t, expectedContent, gitBlobResponse.Content)
// Tests a private repo with no token so will fail // Tests a private repo with no token so will fail

View File

@ -874,21 +874,6 @@ func (err ErrUserDoesNotHaveAccessToRepo) Error() string {
// |______ / |__| (____ /___| /\___ >___| / // |______ / |__| (____ /___| /\___ >___| /
// \/ \/ \/ \/ \/ // \/ \/ \/ \/ \/
// ErrBranchNotExist represents a "BranchNotExist" kind of error.
type ErrBranchNotExist struct {
Name string
}
// IsErrBranchNotExist checks if an error is a ErrBranchNotExist.
func IsErrBranchNotExist(err error) bool {
_, ok := err.(ErrBranchNotExist)
return ok
}
func (err ErrBranchNotExist) Error() string {
return fmt.Sprintf("branch does not exist [name: %s]", err.Name)
}
// ErrBranchAlreadyExists represents an error that branch with such name already exists. // ErrBranchAlreadyExists represents an error that branch with such name already exists.
type ErrBranchAlreadyExists struct { type ErrBranchAlreadyExists struct {
BranchName string BranchName string

View File

@ -1,4 +1,5 @@
// Copyright 2015 The Gogs Authors. All rights reserved. // Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -165,8 +166,8 @@ func (pr *PullRequest) APIFormat() *api.PullRequest {
func (pr *PullRequest) apiFormat(e Engine) *api.PullRequest { func (pr *PullRequest) apiFormat(e Engine) *api.PullRequest {
var ( var (
baseBranch *Branch baseBranch *git.Branch
headBranch *Branch headBranch *git.Branch
baseCommit *git.Commit baseCommit *git.Commit
headCommit *git.Commit headCommit *git.Commit
err error err error

View File

@ -1,4 +1,5 @@
// Copyright 2016 The Gogs Authors. All rights reserved. // Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -86,53 +87,24 @@ func (repo *Repository) DeleteLocalBranch(branchName string) error {
return deleteLocalBranch(repo.LocalCopyPath(), repo.DefaultBranch, branchName) return deleteLocalBranch(repo.LocalCopyPath(), repo.DefaultBranch, branchName)
} }
// Branch holds the branch information
type Branch struct {
Path string
Name string
}
// GetBranchesByPath returns a branch by it's path
func GetBranchesByPath(path string) ([]*Branch, error) {
gitRepo, err := git.OpenRepository(path)
if err != nil {
return nil, err
}
brs, err := gitRepo.GetBranches()
if err != nil {
return nil, err
}
branches := make([]*Branch, len(brs))
for i := range brs {
branches[i] = &Branch{
Path: path,
Name: brs[i],
}
}
return branches, nil
}
// CanCreateBranch returns true if repository meets the requirements for creating new branches. // CanCreateBranch returns true if repository meets the requirements for creating new branches.
func (repo *Repository) CanCreateBranch() bool { func (repo *Repository) CanCreateBranch() bool {
return !repo.IsMirror return !repo.IsMirror
} }
// GetBranch returns a branch by it's name // GetBranch returns a branch by its name
func (repo *Repository) GetBranch(branch string) (*Branch, error) { func (repo *Repository) GetBranch(branch string) (*git.Branch, error) {
if !git.IsBranchExist(repo.RepoPath(), branch) { gitRepo, err := git.OpenRepository(repo.RepoPath())
return nil, ErrBranchNotExist{branch} if err != nil {
return nil, err
} }
return &Branch{
Path: repo.RepoPath(), return gitRepo.GetBranch(branch)
Name: branch,
}, nil
} }
// GetBranches returns all the branches of a repository // GetBranches returns all the branches of a repository
func (repo *Repository) GetBranches() ([]*Branch, error) { func (repo *Repository) GetBranches() ([]*git.Branch, error) {
return GetBranchesByPath(repo.RepoPath()) return git.GetBranchesByPath(repo.RepoPath())
} }
// CheckBranchName validates branch name with existing repository branches // CheckBranchName validates branch name with existing repository branches
@ -257,12 +229,3 @@ func (repo *Repository) CreateNewBranchFromCommit(doer *User, commit, branchName
return nil return nil
} }
// GetCommit returns all the commits of a branch
func (branch *Branch) GetCommit() (*git.Commit, error) {
gitRepo, err := git.OpenRepository(branch.Path)
if err != nil {
return nil, err
}
return gitRepo.GetBranchCommit(branch.Name)
}

View File

@ -157,10 +157,11 @@ func (r *Repository) GetEditorconfig() (*editorconfig.Editorconfig, error) {
if treeEntry.Blob().Size() >= setting.UI.MaxDisplayFileSize { if treeEntry.Blob().Size() >= setting.UI.MaxDisplayFileSize {
return nil, git.ErrNotExist{ID: "", RelPath: ".editorconfig"} return nil, git.ErrNotExist{ID: "", RelPath: ".editorconfig"}
} }
reader, err := treeEntry.Blob().Data() reader, err := treeEntry.Blob().DataAsync()
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer reader.Close()
data, err := ioutil.ReadAll(reader) data, err := ioutil.ReadAll(reader)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -1,76 +1,40 @@
// Copyright 2015 The Gogs Authors. All rights reserved. // Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package git package git
import ( import (
"bytes"
"encoding/base64" "encoding/base64"
"fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"os"
"os/exec" "gopkg.in/src-d/go-git.v4/plumbing"
) )
// Blob represents a Git object. // Blob represents a Git object.
type Blob struct { type Blob struct {
repo *Repository ID SHA1
*TreeEntry
}
// Data gets content of blob all at once and wrap it as io.Reader. gogitEncodedObj plumbing.EncodedObject
// This can be very slow and memory consuming for huge content. name string
func (b *Blob) Data() (io.Reader, error) {
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
// Preallocate memory to save ~50% memory usage on big files.
stdout.Grow(int(b.Size() + 2048))
if err := b.DataPipeline(stdout, stderr); err != nil {
return nil, concatenateError(err, stderr.String())
}
return stdout, nil
}
// DataPipeline gets content of blob and write the result or error to stdout or stderr
func (b *Blob) DataPipeline(stdout, stderr io.Writer) error {
return NewCommand("show", b.ID.String()).RunInDirPipeline(b.repo.Path, stdout, stderr)
}
type cmdReadCloser struct {
cmd *exec.Cmd
stdout io.Reader
}
func (c cmdReadCloser) Read(p []byte) (int, error) {
return c.stdout.Read(p)
}
func (c cmdReadCloser) Close() error {
io.Copy(ioutil.Discard, c.stdout)
return c.cmd.Wait()
} }
// DataAsync gets a ReadCloser for the contents of a blob without reading it all. // 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. // Calling the Close function on the result will discard all unread output.
func (b *Blob) DataAsync() (io.ReadCloser, error) { func (b *Blob) DataAsync() (io.ReadCloser, error) {
cmd := exec.Command("git", "show", b.ID.String()) return b.gogitEncodedObj.Reader()
cmd.Dir = b.repo.Path }
cmd.Stderr = os.Stderr
stdout, err := cmd.StdoutPipe() // Size returns the uncompressed size of the blob
if err != nil { func (b *Blob) Size() int64 {
return nil, fmt.Errorf("StdoutPipe: %v", err) return b.gogitEncodedObj.Size()
} }
if err = cmd.Start(); err != nil { // Name returns name of the tree entry this blob object was created from (or empty string)
return nil, fmt.Errorf("Start: %v", err) func (b *Blob) Name() string {
} return b.name
return cmdReadCloser{stdout: stdout, cmd: cmd}, nil
} }
// GetBlobContentBase64 Reads the content of the blob with a base64 encode and returns the encoded string // GetBlobContentBase64 Reads the content of the blob with a base64 encode and returns the encoded string

View File

@ -1,11 +1,11 @@
// Copyright 2015 The Gogs Authors. All rights reserved. // Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package git package git
import ( import (
"bytes"
"io/ioutil" "io/ioutil"
"testing" "testing"
@ -13,20 +13,6 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
var repoSelf = &Repository{
Path: "./",
}
var testBlob = &Blob{
repo: repoSelf,
TreeEntry: &TreeEntry{
ID: MustIDFromString("a8d4b49dd073a4a38a7e58385eeff7cc52568697"),
ptree: &Tree{
repo: repoSelf,
},
},
}
func TestBlob_Data(t *testing.T) { func TestBlob_Data(t *testing.T) {
output := `Copyright (c) 2016 The Gitea Authors output := `Copyright (c) 2016 The Gitea Authors
Copyright (c) 2015 The Gogs Authors Copyright (c) 2015 The Gogs Authors
@ -49,10 +35,15 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
` `
repo, err := OpenRepository("../../.git")
assert.NoError(t, err)
testBlob, err := repo.GetBlob("a8d4b49dd073a4a38a7e58385eeff7cc52568697")
assert.NoError(t, err)
r, err := testBlob.Data() r, err := testBlob.DataAsync()
assert.NoError(t, err) assert.NoError(t, err)
require.NotNil(t, r) require.NotNil(t, r)
defer r.Close()
data, err := ioutil.ReadAll(r) data, err := ioutil.ReadAll(r)
assert.NoError(t, err) assert.NoError(t, err)
@ -60,21 +51,21 @@ THE SOFTWARE.
} }
func Benchmark_Blob_Data(b *testing.B) { func Benchmark_Blob_Data(b *testing.B) {
repo, err := OpenRepository("../../.git")
if err != nil {
b.Fatal(err)
}
testBlob, err := repo.GetBlob("a8d4b49dd073a4a38a7e58385eeff7cc52568697")
if err != nil {
b.Fatal(err)
}
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
r, err := testBlob.Data() r, err := testBlob.DataAsync()
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }
defer r.Close()
ioutil.ReadAll(r) ioutil.ReadAll(r)
} }
} }
func Benchmark_Blob_DataPipeline(b *testing.B) {
stdout := new(bytes.Buffer)
for i := 0; i < b.N; i++ {
stdout.Reset()
if err := testBlob.DataPipeline(stdout, nil); err != nil {
b.Fatal(err)
}
}
}

View File

@ -14,6 +14,8 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"gopkg.in/src-d/go-git.v4/plumbing/object"
) )
// Commit represents a git commit. // Commit represents a git commit.
@ -36,20 +38,59 @@ type CommitGPGSignature struct {
Payload string //TODO check if can be reconstruct from the rest of commit information to not have duplicate data Payload string //TODO check if can be reconstruct from the rest of commit information to not have duplicate data
} }
// similar to https://github.com/git/git/blob/3bc53220cb2dcf709f7a027a3f526befd021d858/commit.c#L1128 func convertPGPSignature(c *object.Commit) *CommitGPGSignature {
func newGPGSignatureFromCommitline(data []byte, signatureStart int, tag bool) (*CommitGPGSignature, error) { if c.PGPSignature == "" {
sig := new(CommitGPGSignature) return nil
signatureEnd := bytes.LastIndex(data, []byte("-----END PGP SIGNATURE-----"))
if signatureEnd == -1 {
return nil, fmt.Errorf("end of commit signature not found")
} }
sig.Signature = strings.Replace(string(data[signatureStart:signatureEnd+27]), "\n ", "\n", -1)
if tag { var w strings.Builder
sig.Payload = string(data[:signatureStart-1]) var err error
} else {
sig.Payload = string(data[:signatureStart-8]) + string(data[signatureEnd+27:]) 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 _, err = fmt.Fprintf(&w, "\n\n%s", c.Message); err != nil {
return nil
}
return &CommitGPGSignature{
Signature: c.PGPSignature,
Payload: w.String(),
}
}
func convertCommit(c *object.Commit) *Commit {
return &Commit{
ID: c.Hash,
CommitMessage: c.Message,
Committer: &c.Committer,
Author: &c.Author,
Signature: convertPGPSignature(c),
parents: c.ParentHashes,
} }
return sig, nil
} }
// Message returns the commit message. Same as retrieving CommitMessage directly. // Message returns the commit message. Same as retrieving CommitMessage directly.
@ -281,11 +322,13 @@ func (c *Commit) GetSubModules() (*ObjectCache, error) {
} }
return nil, err return nil, err
} }
rd, err := entry.Blob().Data()
rd, err := entry.Blob().DataAsync()
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rd.Close()
scanner := bufio.NewScanner(rd) scanner := bufio.NewScanner(rd)
c.submoduleCache = newObjectCache() c.submoduleCache = newObjectCache()
var ismodule bool var ismodule bool
@ -326,6 +369,17 @@ func (c *Commit) GetSubModule(entryname string) (*SubModule, error) {
return nil, nil return nil, nil
} }
// GetBranchName gets the closes branch name (as returned by 'git name-rev')
func (c *Commit) GetBranchName() (string, error) {
data, err := NewCommand("name-rev", c.ID.String()).RunInDirBytes(c.repo.Path)
if err != nil {
return "", err
}
// name-rev commitID output will be "COMMIT_ID master" or "COMMIT_ID master~12"
return strings.Split(strings.Split(string(data), " ")[1], "~")[0], nil
}
// CommitFileStatus represents status of files in a commit. // CommitFileStatus represents status of files in a commit.
type CommitFileStatus struct { type CommitFileStatus struct {
Added []string Added []string

File diff suppressed because it is too large Load Diff

View File

@ -51,7 +51,7 @@ func testGetCommitsInfo(t *testing.T, repo1 *Repository) {
assert.NoError(t, err) assert.NoError(t, err)
entries, err := tree.ListEntries() entries, err := tree.ListEntries()
assert.NoError(t, err) assert.NoError(t, err)
commitsInfo, err := entries.GetCommitsInfo(commit, testCase.Path, nil) commitsInfo, _, err := entries.GetCommitsInfo(commit, testCase.Path, nil)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, commitsInfo, len(testCase.ExpectedIDs)) assert.Len(t, commitsInfo, len(testCase.ExpectedIDs))
for _, commitInfo := range commitsInfo { for _, commitInfo := range commitsInfo {
@ -107,7 +107,7 @@ func BenchmarkEntries_GetCommitsInfo(b *testing.B) {
b.ResetTimer() b.ResetTimer()
b.Run(benchmark.name, func(b *testing.B) { b.Run(benchmark.name, func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_, err := entries.GetCommitsInfo(commit, "", nil) _, _, err := entries.GetCommitsInfo(commit, "", nil)
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }

View File

@ -64,3 +64,18 @@ func IsErrUnsupportedVersion(err error) bool {
func (err ErrUnsupportedVersion) Error() string { func (err ErrUnsupportedVersion) Error() string {
return fmt.Sprintf("Operation requires higher version [required: %s]", err.Required) return fmt.Sprintf("Operation requires higher version [required: %s]", err.Required)
} }
// ErrBranchNotExist represents a "BranchNotExist" kind of error.
type ErrBranchNotExist struct {
Name string
}
// IsErrBranchNotExist checks if an error is a ErrBranchNotExist.
func IsErrBranchNotExist(err error) bool {
_, ok := err.(ErrBranchNotExist)
return ok
}
func (err ErrBranchNotExist) Error() string {
return fmt.Sprintf("branch does not exist [name: %s]", err.Name)
}

View File

@ -8,6 +8,10 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"strconv" "strconv"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/filemode"
"gopkg.in/src-d/go-git.v4/plumbing/object"
) )
// ParseTreeEntries parses the output of a `git ls-tree` command. // ParseTreeEntries parses the output of a `git ls-tree` command.
@ -20,30 +24,26 @@ func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) {
for pos := 0; pos < len(data); { for pos := 0; pos < len(data); {
// expect line to be of the form "<mode> <type> <sha>\t<filename>" // expect line to be of the form "<mode> <type> <sha>\t<filename>"
entry := new(TreeEntry) entry := new(TreeEntry)
entry.gogitTreeEntry = &object.TreeEntry{}
entry.ptree = ptree entry.ptree = ptree
if pos+6 > len(data) { if pos+6 > len(data) {
return nil, fmt.Errorf("Invalid ls-tree output: %s", string(data)) return nil, fmt.Errorf("Invalid ls-tree output: %s", string(data))
} }
switch string(data[pos : pos+6]) { switch string(data[pos : pos+6]) {
case "100644": case "100644":
entry.mode = EntryModeBlob entry.gogitTreeEntry.Mode = filemode.Regular
entry.Type = ObjectBlob
pos += 12 // skip over "100644 blob " pos += 12 // skip over "100644 blob "
case "100755": case "100755":
entry.mode = EntryModeExec entry.gogitTreeEntry.Mode = filemode.Executable
entry.Type = ObjectBlob
pos += 12 // skip over "100755 blob " pos += 12 // skip over "100755 blob "
case "120000": case "120000":
entry.mode = EntryModeSymlink entry.gogitTreeEntry.Mode = filemode.Symlink
entry.Type = ObjectBlob
pos += 12 // skip over "120000 blob " pos += 12 // skip over "120000 blob "
case "160000": case "160000":
entry.mode = EntryModeCommit entry.gogitTreeEntry.Mode = filemode.Submodule
entry.Type = ObjectCommit
pos += 14 // skip over "160000 object " pos += 14 // skip over "160000 object "
case "040000": case "040000":
entry.mode = EntryModeTree entry.gogitTreeEntry.Mode = filemode.Dir
entry.Type = ObjectTree
pos += 12 // skip over "040000 tree " pos += 12 // skip over "040000 tree "
default: default:
return nil, fmt.Errorf("unknown type: %v", string(data[pos:pos+6])) return nil, fmt.Errorf("unknown type: %v", string(data[pos:pos+6]))
@ -57,6 +57,7 @@ func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) {
return nil, fmt.Errorf("Invalid ls-tree output: %v", err) return nil, fmt.Errorf("Invalid ls-tree output: %v", err)
} }
entry.ID = id entry.ID = id
entry.gogitTreeEntry.Hash = plumbing.Hash(id)
pos += 41 // skip over sha and trailing space pos += 41 // skip over sha and trailing space
end := pos + bytes.IndexByte(data[pos:], '\n') end := pos + bytes.IndexByte(data[pos:], '\n')
@ -66,12 +67,12 @@ func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) {
// In case entry name is surrounded by double quotes(it happens only in git-shell). // In case entry name is surrounded by double quotes(it happens only in git-shell).
if data[pos] == '"' { if data[pos] == '"' {
entry.name, err = strconv.Unquote(string(data[pos:end])) entry.gogitTreeEntry.Name, err = strconv.Unquote(string(data[pos:end]))
if err != nil { if err != nil {
return nil, fmt.Errorf("Invalid ls-tree output: %v", err) return nil, fmt.Errorf("Invalid ls-tree output: %v", err)
} }
} else { } else {
entry.name = string(data[pos:end]) entry.gogitTreeEntry.Name = string(data[pos:end])
} }
pos = end + 1 pos = end + 1

View File

@ -8,6 +8,8 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"gopkg.in/src-d/go-git.v4/plumbing/filemode"
"gopkg.in/src-d/go-git.v4/plumbing/object"
) )
func TestParseTreeEntries(t *testing.T) { func TestParseTreeEntries(t *testing.T) {
@ -23,10 +25,12 @@ func TestParseTreeEntries(t *testing.T) {
Input: "100644 blob 61ab7345a1a3bbc590068ccae37b8515cfc5843c\texample/file2.txt\n", Input: "100644 blob 61ab7345a1a3bbc590068ccae37b8515cfc5843c\texample/file2.txt\n",
Expected: []*TreeEntry{ Expected: []*TreeEntry{
{ {
mode: EntryModeBlob, ID: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
Type: ObjectBlob, gogitTreeEntry: &object.TreeEntry{
ID: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"), Hash: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
name: "example/file2.txt", Name: "example/file2.txt",
Mode: filemode.Regular,
},
}, },
}, },
}, },
@ -35,16 +39,20 @@ func TestParseTreeEntries(t *testing.T) {
"040000 tree 1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8\texample\n", "040000 tree 1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8\texample\n",
Expected: []*TreeEntry{ Expected: []*TreeEntry{
{ {
ID: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"), ID: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
Type: ObjectBlob, gogitTreeEntry: &object.TreeEntry{
mode: EntryModeSymlink, Hash: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
name: "example/\n.txt", Name: "example/\n.txt",
Mode: filemode.Symlink,
},
}, },
{ {
ID: MustIDFromString("1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8"), ID: MustIDFromString("1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8"),
Type: ObjectTree, gogitTreeEntry: &object.TreeEntry{
mode: EntryModeTree, Hash: MustIDFromString("1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8"),
name: "example", Name: "example",
Mode: filemode.Dir,
},
}, },
}, },
}, },

View File

@ -16,14 +16,20 @@ import (
"time" "time"
"github.com/Unknwon/com" "github.com/Unknwon/com"
"gopkg.in/src-d/go-billy.v4/osfs"
gogit "gopkg.in/src-d/go-git.v4"
"gopkg.in/src-d/go-git.v4/plumbing/cache"
"gopkg.in/src-d/go-git.v4/storage/filesystem"
) )
// Repository represents a Git repository. // Repository represents a Git repository.
type Repository struct { type Repository struct {
Path string Path string
commitCache *ObjectCache tagCache *ObjectCache
tagCache *ObjectCache
gogitRepo *gogit.Repository
gogitStorage *filesystem.Storage
} }
const prettyLogFormat = `--pretty=format:%H` const prettyLogFormat = `--pretty=format:%H`
@ -77,10 +83,25 @@ func OpenRepository(repoPath string) (*Repository, error) {
return nil, errors.New("no such file or directory") 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
}
}
storage := filesystem.NewStorageWithOptions(fs, cache.NewObjectLRUDefault(), filesystem.Options{KeepDescriptors: true})
gogitRepo, err := gogit.Open(storage, fs)
if err != nil {
return nil, err
}
return &Repository{ return &Repository{
Path: repoPath, Path: repoPath,
commitCache: newObjectCache(), gogitRepo: gogitRepo,
tagCache: newObjectCache(), gogitStorage: storage,
tagCache: newObjectCache(),
}, nil }, nil
} }

View File

@ -4,19 +4,19 @@
package git package git
import (
"gopkg.in/src-d/go-git.v4/plumbing"
)
func (repo *Repository) getBlob(id SHA1) (*Blob, error) { func (repo *Repository) getBlob(id SHA1) (*Blob, error) {
if _, err := NewCommand("cat-file", "-p", id.String()).RunInDir(repo.Path); err != nil { encodedObj, err := repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, plumbing.Hash(id))
if err != nil {
return nil, ErrNotExist{id.String(), ""} return nil, ErrNotExist{id.String(), ""}
} }
return &Blob{ return &Blob{
repo: repo, ID: id,
TreeEntry: &TreeEntry{ gogitEncodedObj: encodedObj,
ID: id,
ptree: &Tree{
repo: repo,
},
},
}, nil }, nil
} }

View File

@ -30,8 +30,9 @@ func TestRepository_GetBlob_Found(t *testing.T) {
blob, err := r.GetBlob(testCase.OID) blob, err := r.GetBlob(testCase.OID)
assert.NoError(t, err) assert.NoError(t, err)
dataReader, err := blob.Data() dataReader, err := blob.DataAsync()
assert.NoError(t, err) assert.NoError(t, err)
defer dataReader.Close()
data, err := ioutil.ReadAll(dataReader) data, err := ioutil.ReadAll(dataReader)
assert.NoError(t, err) assert.NoError(t, err)

View File

@ -9,7 +9,6 @@ import (
"fmt" "fmt"
"strings" "strings"
"gopkg.in/src-d/go-git.v4"
"gopkg.in/src-d/go-git.v4/plumbing" "gopkg.in/src-d/go-git.v4/plumbing"
) )
@ -29,13 +28,19 @@ func IsBranchExist(repoPath, name string) bool {
// IsBranchExist returns true if given branch exists in current repository. // IsBranchExist returns true if given branch exists in current repository.
func (repo *Repository) IsBranchExist(name string) bool { func (repo *Repository) IsBranchExist(name string) bool {
return IsBranchExist(repo.Path, name) _, err := repo.gogitRepo.Reference(plumbing.ReferenceName(BranchPrefix+name), true)
if err != nil {
return false
}
return true
} }
// Branch represents a Git branch. // Branch represents a Git branch.
type Branch struct { type Branch struct {
Name string Name string
Path string Path string
gitRepo *Repository
} }
// GetHEADBranch returns corresponding branch of HEAD. // GetHEADBranch returns corresponding branch of HEAD.
@ -51,8 +56,9 @@ func (repo *Repository) GetHEADBranch() (*Branch, error) {
} }
return &Branch{ return &Branch{
Name: stdout[len(BranchPrefix):], Name: stdout[len(BranchPrefix):],
Path: stdout, Path: stdout,
gitRepo: repo,
}, nil }, nil
} }
@ -64,23 +70,56 @@ func (repo *Repository) SetDefaultBranch(name string) error {
// GetBranches returns all branches of the repository. // GetBranches returns all branches of the repository.
func (repo *Repository) GetBranches() ([]string, error) { func (repo *Repository) GetBranches() ([]string, error) {
r, err := git.PlainOpen(repo.Path) var branchNames []string
branches, err := repo.gogitRepo.Branches()
if err != nil { if err != nil {
return nil, err return nil, err
} }
branchIter, err := r.Branches() branches.ForEach(func(branch *plumbing.Reference) error {
branchNames = append(branchNames, strings.TrimPrefix(branch.Name().String(), BranchPrefix))
return nil
})
// TODO: Sort?
return branchNames, nil
}
// GetBranch returns a branch by it's name
func (repo *Repository) GetBranch(branch string) (*Branch, error) {
if !repo.IsBranchExist(branch) {
return nil, ErrBranchNotExist{branch}
}
return &Branch{
Path: repo.Path,
Name: branch,
gitRepo: repo,
}, nil
}
// GetBranchesByPath returns a branch by it's path
func GetBranchesByPath(path string) ([]*Branch, error) {
gitRepo, err := OpenRepository(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
branches := make([]string, 0)
if err = branchIter.ForEach(func(branch *plumbing.Reference) error { brs, err := gitRepo.GetBranches()
branches = append(branches, branch.Name().Short()) if err != nil {
return nil
}); err != nil {
return nil, err return nil, err
} }
branches := make([]*Branch, len(brs))
for i := range brs {
branches[i] = &Branch{
Path: path,
Name: brs[i],
gitRepo: gitRepo,
}
}
return branches, nil return branches, nil
} }
@ -132,3 +171,8 @@ func (repo *Repository) RemoveRemote(name string) error {
_, err := NewCommand("remote", "remove", name).RunInDir(repo.Path) _, err := NewCommand("remote", "remove", name).RunInDir(repo.Path)
return err return err
} }
// GetCommit returns the head commit of a branch
func (branch *Branch) GetCommit() (*Commit, error) {
return branch.gitRepo.GetBranchCommit(branch.Name)
}

View File

@ -1,4 +1,5 @@
// Copyright 2015 The Gogs Authors. All rights reserved. // Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -7,22 +8,23 @@ package git
import ( import (
"bytes" "bytes"
"container/list" "container/list"
"fmt"
"strconv" "strconv"
"strings" "strings"
"github.com/mcuadros/go-version" "github.com/mcuadros/go-version"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/object"
) )
// GetRefCommitID returns the last commit ID string of given reference (branch or tag). // GetRefCommitID returns the last commit ID string of given reference (branch or tag).
func (repo *Repository) GetRefCommitID(name string) (string, error) { func (repo *Repository) GetRefCommitID(name string) (string, error) {
stdout, err := NewCommand("show-ref", "--verify", name).RunInDir(repo.Path) ref, err := repo.gogitRepo.Reference(plumbing.ReferenceName(name), true)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "not a valid ref") {
return "", ErrNotExist{name, ""}
}
return "", err return "", err
} }
return strings.Split(stdout, " ")[0], nil
return ref.Hash().String(), nil
} }
// GetBranchCommitID returns last commit ID string of given branch. // GetBranchCommitID returns last commit ID string of given branch.
@ -42,114 +44,69 @@ func (repo *Repository) GetTagCommitID(name string) (string, error) {
return strings.TrimSpace(stdout), nil return strings.TrimSpace(stdout), nil
} }
// parseCommitData parses commit information from the (uncompressed) raw func convertPGPSignatureForTag(t *object.Tag) *CommitGPGSignature {
// data from the commit object. if t.PGPSignature == "" {
// \n\n separate headers from message return nil
func parseCommitData(data []byte) (*Commit, error) { }
commit := new(Commit)
commit.parents = make([]SHA1, 0, 1) var w strings.Builder
// we now have the contents of the commit object. Let's investigate... var err error
nextline := 0
l: if _, err = fmt.Fprintf(&w,
for { "object %s\ntype %s\ntag %s\ntagger ",
eol := bytes.IndexByte(data[nextline:], '\n') t.Target.String(), t.TargetType.Bytes(), t.Name); err != nil {
switch { return nil
case eol > 0: }
line := data[nextline : nextline+eol]
spacepos := bytes.IndexByte(line, ' ') if err = t.Tagger.Encode(&w); err != nil {
reftype := line[:spacepos] return nil
switch string(reftype) { }
case "tree", "object":
id, err := NewIDFromString(string(line[spacepos+1:])) if _, err = fmt.Fprintf(&w, "\n\n"); err != nil {
if err != nil { return nil
return nil, err }
}
commit.Tree.ID = id if _, err = fmt.Fprintf(&w, t.Message); err != nil {
case "parent": return nil
// A commit can have one or more parents }
oid, err := NewIDFromString(string(line[spacepos+1:]))
if err != nil { return &CommitGPGSignature{
return nil, err Signature: t.PGPSignature,
} Payload: strings.TrimSpace(w.String()) + "\n",
commit.parents = append(commit.parents, oid)
case "author", "tagger":
sig, err := newSignatureFromCommitline(line[spacepos+1:])
if err != nil {
return nil, err
}
commit.Author = sig
case "committer":
sig, err := newSignatureFromCommitline(line[spacepos+1:])
if err != nil {
return nil, err
}
commit.Committer = sig
case "gpgsig":
sig, err := newGPGSignatureFromCommitline(data, nextline+spacepos+1, false)
if err != nil {
return nil, err
}
commit.Signature = sig
}
nextline += eol + 1
case eol == 0:
cm := string(data[nextline+1:])
// Tag GPG signatures are stored below the commit message
sigindex := strings.Index(cm, "-----BEGIN PGP SIGNATURE-----")
if sigindex != -1 {
sig, err := newGPGSignatureFromCommitline(data, (nextline+1)+sigindex, true)
if err == nil && sig != nil {
// remove signature from commit message
if sigindex == 0 {
cm = ""
} else {
cm = cm[:sigindex-1]
}
commit.Signature = sig
}
}
commit.CommitMessage = cm
break l
default:
break l
}
} }
return commit, nil
} }
func (repo *Repository) getCommit(id SHA1) (*Commit, error) { func (repo *Repository) getCommit(id SHA1) (*Commit, error) {
c, ok := repo.commitCache.Get(id.String()) var tagObject *object.Tag
if ok {
log("Hit cache: %s", id)
return c.(*Commit), nil
}
data, err := NewCommand("cat-file", "-p", id.String()).RunInDirBytes(repo.Path) gogitCommit, err := repo.gogitRepo.CommitObject(plumbing.Hash(id))
if err != nil { if err == plumbing.ErrObjectNotFound {
if strings.Contains(err.Error(), "fatal: Not a valid object name") { tagObject, err = repo.gogitRepo.TagObject(plumbing.Hash(id))
return nil, ErrNotExist{id.String(), ""} if err == nil {
gogitCommit, err = repo.gogitRepo.CommitObject(tagObject.Target)
} }
return nil, err
} }
commit, err := parseCommitData(data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
commit := convertCommit(gogitCommit)
commit.repo = repo commit.repo = repo
commit.ID = id
data, err = NewCommand("name-rev", id.String()).RunInDirBytes(repo.Path) if tagObject != nil {
commit.CommitMessage = strings.TrimSpace(tagObject.Message)
commit.Author = &tagObject.Tagger
commit.Signature = convertPGPSignatureForTag(tagObject)
}
tree, err := gogitCommit.Tree()
if err != nil { if err != nil {
return nil, err return nil, err
} }
// name-rev commitID output will be "COMMIT_ID master" or "COMMIT_ID master~12" commit.Tree.ID = tree.Hash
commit.Branch = strings.Split(strings.Split(string(data), " ")[1], "~")[0] commit.Tree.gogitTree = tree
repo.commitCache.Set(id.String(), commit)
return commit, nil return commit, nil
} }

View File

@ -1,4 +1,5 @@
// Copyright 2015 The Gogs Authors. All rights reserved. // Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -8,6 +9,7 @@ import (
"strings" "strings"
"github.com/mcuadros/go-version" "github.com/mcuadros/go-version"
"gopkg.in/src-d/go-git.v4/plumbing"
) )
// TagPrefix tags prefix path on the repository // TagPrefix tags prefix path on the repository
@ -20,7 +22,11 @@ func IsTagExist(repoPath, name string) bool {
// IsTagExist returns true if given tag exists in the repository. // IsTagExist returns true if given tag exists in the repository.
func (repo *Repository) IsTagExist(name string) bool { func (repo *Repository) IsTagExist(name string) bool {
return IsTagExist(repo.Path, name) _, err := repo.gogitRepo.Reference(plumbing.ReferenceName(TagPrefix+name), true)
if err != nil {
return false
}
return true
} }
// CreateTag create one tag in the repository // CreateTag create one tag in the repository
@ -122,28 +128,25 @@ func (repo *Repository) GetTagInfos() ([]*Tag, error) {
// GetTags returns all tags of the repository. // GetTags returns all tags of the repository.
func (repo *Repository) GetTags() ([]string, error) { func (repo *Repository) GetTags() ([]string, error) {
cmd := NewCommand("tag", "-l") var tagNames []string
if version.Compare(gitVersion, "2.0.0", ">=") {
cmd.AddArguments("--sort=-v:refname")
}
stdout, err := cmd.RunInDir(repo.Path) tags, err := repo.gogitRepo.Tags()
if err != nil { if err != nil {
return nil, err return nil, err
} }
tags := strings.Split(stdout, "\n") tags.ForEach(func(tag *plumbing.Reference) error {
tags = tags[:len(tags)-1] tagNames = append(tagNames, strings.TrimPrefix(tag.Name().String(), TagPrefix))
return nil
})
if version.Compare(gitVersion, "2.0.0", "<") { version.Sort(tagNames)
version.Sort(tags)
// Reverse order // Reverse order
for i := 0; i < len(tags)/2; i++ { for i := 0; i < len(tagNames)/2; i++ {
j := len(tags) - i - 1 j := len(tagNames) - i - 1
tags[i], tags[j] = tags[j], tags[i] tagNames[i], tagNames[j] = tagNames[j], tagNames[i]
}
} }
return tags, nil return tagNames, nil
} }

View File

@ -1,19 +1,23 @@
// Copyright 2015 The Gogs Authors. All rights reserved. // Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package git package git
import (
"gopkg.in/src-d/go-git.v4/plumbing"
)
func (repo *Repository) getTree(id SHA1) (*Tree, error) { func (repo *Repository) getTree(id SHA1) (*Tree, error) {
treePath := filepathFromSHA1(repo.Path, id.String()) gogitTree, err := repo.gogitRepo.TreeObject(plumbing.Hash(id))
if isFile(treePath) { if err != nil {
_, err := NewCommand("ls-tree", id.String()).RunInDir(repo.Path) return nil, err
if err != nil {
return nil, ErrNotExist{id.String(), ""}
}
} }
return NewTree(repo, id), nil tree := NewTree(repo, id)
tree.gogitTree = gogitTree
return tree, nil
} }
// GetTree find the tree object in the repository. // GetTree find the tree object in the repository.
@ -31,5 +35,14 @@ func (repo *Repository) GetTree(idStr string) (*Tree, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return repo.getTree(id) commitObject, err := repo.gogitRepo.CommitObject(plumbing.Hash(id))
if err != nil {
return nil, err
}
treeObject, err := repo.getTree(SHA1(commitObject.TreeHash))
if err != nil {
return nil, err
}
treeObject.CommitID = id
return treeObject, nil
} }

View File

@ -1,44 +1,23 @@
// Copyright 2015 The Gogs Authors. All rights reserved. // Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package git package git
import ( import (
"bytes"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"strings" "strings"
"gopkg.in/src-d/go-git.v4/plumbing"
) )
// EmptySHA defines empty git SHA // EmptySHA defines empty git SHA
const EmptySHA = "0000000000000000000000000000000000000000" const EmptySHA = "0000000000000000000000000000000000000000"
// SHA1 a git commit name // SHA1 a git commit name
type SHA1 [20]byte type SHA1 = plumbing.Hash
// Equal returns true if s has the same SHA1 as caller.
// Support 40-length-string, []byte, SHA1.
func (id SHA1) Equal(s2 interface{}) bool {
switch v := s2.(type) {
case string:
if len(v) != 40 {
return false
}
return v == id.String()
case []byte:
return bytes.Equal(v, id[:])
case SHA1:
return v == id
default:
return false
}
}
// String returns string (hex) representation of the Oid.
func (id SHA1) String() string {
return hex.EncodeToString(id[:])
}
// MustID always creates a new SHA1 from a [20]byte array with no validation of input. // MustID always creates a new SHA1 from a [20]byte array with no validation of input.
func MustID(b []byte) SHA1 { func MustID(b []byte) SHA1 {

View File

@ -1,4 +1,5 @@
// Copyright 2015 The Gogs Authors. All rights reserved. // Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -8,14 +9,12 @@ import (
"bytes" "bytes"
"strconv" "strconv"
"time" "time"
"gopkg.in/src-d/go-git.v4/plumbing/object"
) )
// Signature represents the Author or Committer information. // Signature represents the Author or Committer information.
type Signature struct { type Signature = object.Signature
Email string
Name string
When time.Time
}
const ( const (
// GitTimeLayout is the (default) time layout used by git. // GitTimeLayout is the (default) time layout used by git.

View File

@ -1,29 +1,31 @@
// Copyright 2015 The Gogs Authors. All rights reserved. // Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package git package git
import ( import (
"io"
"strings" "strings"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/object"
) )
// Tree represents a flat directory listing. // Tree represents a flat directory listing.
type Tree struct { type Tree struct {
ID SHA1 ID SHA1
repo *Repository CommitID SHA1
repo *Repository
gogitTree *object.Tree
// parent tree // parent tree
ptree *Tree ptree *Tree
entries Entries
entriesParsed bool
entriesRecursive Entries
entriesRecursiveParsed bool
} }
// NewTree create a new tree according the repository and commit id // NewTree create a new tree according the repository and tree id
func NewTree(repo *Repository, id SHA1) *Tree { func NewTree(repo *Repository, id SHA1) *Tree {
return &Tree{ return &Tree{
ID: id, ID: id,
@ -60,39 +62,68 @@ func (t *Tree) SubTree(rpath string) (*Tree, error) {
return g, nil return g, nil
} }
func (t *Tree) loadTreeObject() error {
gogitTree, err := t.repo.gogitRepo.TreeObject(plumbing.Hash(t.ID))
if err != nil {
return err
}
t.gogitTree = gogitTree
return nil
}
// ListEntries returns all entries of current tree. // ListEntries returns all entries of current tree.
func (t *Tree) ListEntries() (Entries, error) { func (t *Tree) ListEntries() (Entries, error) {
if t.entriesParsed { if t.gogitTree == nil {
return t.entries, nil err := t.loadTreeObject()
if err != nil {
return nil, err
}
} }
stdout, err := NewCommand("ls-tree", t.ID.String()).RunInDirBytes(t.repo.Path) entries := make([]*TreeEntry, len(t.gogitTree.Entries))
if err != nil { for i, entry := range t.gogitTree.Entries {
return nil, err entries[i] = &TreeEntry{
ID: entry.Hash,
gogitTreeEntry: &t.gogitTree.Entries[i],
ptree: t,
}
} }
t.entries, err = parseTreeEntries(stdout, t) return entries, nil
if err == nil {
t.entriesParsed = true
}
return t.entries, err
} }
// ListEntriesRecursive returns all entries of current tree recursively including all subtrees // ListEntriesRecursive returns all entries of current tree recursively including all subtrees
func (t *Tree) ListEntriesRecursive() (Entries, error) { func (t *Tree) ListEntriesRecursive() (Entries, error) {
if t.entriesRecursiveParsed { if t.gogitTree == nil {
return t.entriesRecursive, nil err := t.loadTreeObject()
} if err != nil {
stdout, err := NewCommand("ls-tree", "-t", "-r", t.ID.String()).RunInDirBytes(t.repo.Path) return nil, err
if err != nil { }
return nil, err
} }
t.entriesRecursive, err = parseTreeEntries(stdout, t) var entries []*TreeEntry
if err == nil { seen := map[plumbing.Hash]bool{}
t.entriesRecursiveParsed = true walker := object.NewTreeWalker(t.gogitTree, true, seen)
for {
_, entry, err := walker.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if seen[entry.Hash] {
continue
}
convertedEntry := &TreeEntry{
ID: entry.Hash,
gogitTreeEntry: &entry,
ptree: t,
}
entries = append(entries, convertedEntry)
} }
return t.entriesRecursive, err return entries, nil
} }

View File

@ -1,4 +1,5 @@
// Copyright 2015 The Gogs Authors. All rights reserved. // Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -7,15 +8,23 @@ package git
import ( import (
"path" "path"
"strings" "strings"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/filemode"
"gopkg.in/src-d/go-git.v4/plumbing/object"
) )
// GetTreeEntryByPath get the tree entries according the sub dir // GetTreeEntryByPath get the tree entries according the sub dir
func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) { func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) {
if len(relpath) == 0 { if len(relpath) == 0 {
return &TreeEntry{ return &TreeEntry{
ID: t.ID, ID: t.ID,
Type: ObjectTree, //Type: ObjectTree,
mode: EntryModeTree, gogitTreeEntry: &object.TreeEntry{
Name: "",
Mode: filemode.Dir,
Hash: plumbing.Hash(t.ID),
},
}, nil }, nil
} }
@ -30,7 +39,7 @@ func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) {
return nil, err return nil, err
} }
for _, v := range entries { for _, v := range entries {
if v.name == name { if v.Name() == name {
return v, nil return v, nil
} }
} }

View File

@ -1,4 +1,5 @@
// Copyright 2015 The Gogs Authors. All rights reserved. // Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -7,8 +8,11 @@ package git
import ( import (
"io" "io"
"sort" "sort"
"strconv"
"strings" "strings"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/filemode"
"gopkg.in/src-d/go-git.v4/plumbing/object"
) )
// EntryMode the type of the object in the git tree // EntryMode the type of the object in the git tree
@ -18,28 +22,23 @@ type EntryMode int
// one of these. // one of these.
const ( const (
// EntryModeBlob // EntryModeBlob
EntryModeBlob EntryMode = 0x0100644 EntryModeBlob EntryMode = 0100644
// EntryModeExec // EntryModeExec
EntryModeExec EntryMode = 0x0100755 EntryModeExec EntryMode = 0100755
// EntryModeSymlink // EntryModeSymlink
EntryModeSymlink EntryMode = 0x0120000 EntryModeSymlink EntryMode = 0120000
// EntryModeCommit // EntryModeCommit
EntryModeCommit EntryMode = 0x0160000 EntryModeCommit EntryMode = 0160000
// EntryModeTree // EntryModeTree
EntryModeTree EntryMode = 0x0040000 EntryModeTree EntryMode = 0040000
) )
// TreeEntry the leaf in the git tree // TreeEntry the leaf in the git tree
type TreeEntry struct { type TreeEntry struct {
ID SHA1 ID SHA1
Type ObjectType
mode EntryMode gogitTreeEntry *object.TreeEntry
name string ptree *Tree
ptree *Tree
committed bool
size int64 size int64
sized bool sized bool
@ -47,12 +46,24 @@ type TreeEntry struct {
// Name returns the name of the entry // Name returns the name of the entry
func (te *TreeEntry) Name() string { func (te *TreeEntry) Name() string {
return te.name return te.gogitTreeEntry.Name
} }
// Mode returns the mode of the entry // Mode returns the mode of the entry
func (te *TreeEntry) Mode() EntryMode { func (te *TreeEntry) Mode() EntryMode {
return te.mode return EntryMode(te.gogitTreeEntry.Mode)
}
// Type returns the type of the entry (commit, tree, blob)
func (te *TreeEntry) Type() string {
switch te.Mode() {
case EntryModeCommit:
return "commit"
case EntryModeTree:
return "tree"
default:
return "blob"
}
} }
// Size returns the size of the entry // Size returns the size of the entry
@ -63,36 +74,47 @@ func (te *TreeEntry) Size() int64 {
return te.size return te.size
} }
stdout, err := NewCommand("cat-file", "-s", te.ID.String()).RunInDir(te.ptree.repo.Path) file, err := te.ptree.gogitTree.TreeEntryFile(te.gogitTreeEntry)
if err != nil { if err != nil {
return 0 return 0
} }
te.sized = true te.sized = true
te.size, _ = strconv.ParseInt(strings.TrimSpace(stdout), 10, 64) te.size = file.Size
return te.size return te.size
} }
// IsSubModule if the entry is a sub module // IsSubModule if the entry is a sub module
func (te *TreeEntry) IsSubModule() bool { func (te *TreeEntry) IsSubModule() bool {
return te.mode == EntryModeCommit return te.gogitTreeEntry.Mode == filemode.Submodule
} }
// IsDir if the entry is a sub dir // IsDir if the entry is a sub dir
func (te *TreeEntry) IsDir() bool { func (te *TreeEntry) IsDir() bool {
return te.mode == EntryModeTree return te.gogitTreeEntry.Mode == filemode.Dir
} }
// IsLink if the entry is a symlink // IsLink if the entry is a symlink
func (te *TreeEntry) IsLink() bool { func (te *TreeEntry) IsLink() bool {
return te.mode == EntryModeSymlink return te.gogitTreeEntry.Mode == filemode.Symlink
} }
// Blob retrun the blob object the entry // IsRegular if the entry is a regular file
func (te *TreeEntry) IsRegular() bool {
return te.gogitTreeEntry.Mode == filemode.Regular
}
// Blob returns the blob object the entry
func (te *TreeEntry) Blob() *Blob { 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{ return &Blob{
repo: te.ptree.repo, ID: te.gogitTreeEntry.Hash,
TreeEntry: te, gogitEncodedObj: encodedObj,
name: te.Name(),
} }
} }
@ -103,10 +125,11 @@ func (te *TreeEntry) FollowLink() (*TreeEntry, error) {
} }
// read the link // read the link
r, err := te.Blob().Data() r, err := te.Blob().DataAsync()
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer r.Close()
buf := make([]byte, te.Size()) buf := make([]byte, te.Size())
_, err = io.ReadFull(r, buf) _, err = io.ReadFull(r, buf)
if err != nil { if err != nil {
@ -140,18 +163,18 @@ func (te *TreeEntry) GetSubJumpablePathName() string {
if te.IsSubModule() || !te.IsDir() { if te.IsSubModule() || !te.IsDir() {
return "" return ""
} }
tree, err := te.ptree.SubTree(te.name) tree, err := te.ptree.SubTree(te.Name())
if err != nil { if err != nil {
return te.name return te.Name()
} }
entries, _ := tree.ListEntries() entries, _ := tree.ListEntries()
if len(entries) == 1 && entries[0].IsDir() { if len(entries) == 1 && entries[0].IsDir() {
name := entries[0].GetSubJumpablePathName() name := entries[0].GetSubJumpablePathName()
if name != "" { if name != "" {
return te.name + "/" + name return te.Name() + "/" + name
} }
} }
return te.name return te.Name()
} }
// Entries a list of entry // Entries a list of entry
@ -167,7 +190,7 @@ var sorter = []func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool{
return (t1.IsDir() || t1.IsSubModule()) && !t2.IsDir() && !t2.IsSubModule() return (t1.IsDir() || t1.IsSubModule()) && !t2.IsDir() && !t2.IsSubModule()
}, },
func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool { func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool {
return cmp(t1.name, t2.name) return cmp(t1.Name(), t2.Name())
}, },
} }

View File

@ -8,18 +8,20 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"gopkg.in/src-d/go-git.v4/plumbing/filemode"
"gopkg.in/src-d/go-git.v4/plumbing/object"
) )
func getTestEntries() Entries { func getTestEntries() Entries {
return Entries{ return Entries{
&TreeEntry{name: "v1.0", mode: EntryModeTree}, &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v1.0", Mode: filemode.Dir}},
&TreeEntry{name: "v2.0", mode: EntryModeTree}, &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.0", Mode: filemode.Dir}},
&TreeEntry{name: "v2.1", mode: EntryModeTree}, &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.1", Mode: filemode.Dir}},
&TreeEntry{name: "v2.12", mode: EntryModeTree}, &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.12", Mode: filemode.Dir}},
&TreeEntry{name: "v2.2", mode: EntryModeTree}, &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.2", Mode: filemode.Dir}},
&TreeEntry{name: "v12.0", mode: EntryModeTree}, &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v12.0", Mode: filemode.Dir}},
&TreeEntry{name: "abc", mode: EntryModeBlob}, &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "abc", Mode: filemode.Regular}},
&TreeEntry{name: "bcd", mode: EntryModeBlob}, &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "bcd", Mode: filemode.Regular}},
} }
} }

View File

@ -27,7 +27,7 @@ func TestGetBlobBySHA(t *testing.T) {
gbr, err := GetBlobBySHA(ctx.Repo.Repository, ctx.Params(":sha")) gbr, err := GetBlobBySHA(ctx.Repo.Repository, ctx.Params(":sha"))
expectedGBR := &api.GitBlobResponse{ expectedGBR := &api.GitBlobResponse{
Content: "Y29tbWl0IDY1ZjFiZjI3YmMzYmY3MGY2NDY1NzY1ODYzNWU2NjA5NGVkYmNiNGQKQXV0aG9yOiB1c2VyMSA8YWRkcmVzczFAZXhhbXBsZS5jb20+CkRhdGU6ICAgU3VuIE1hciAxOSAxNjo0Nzo1OSAyMDE3IC0wNDAwCgogICAgSW5pdGlhbCBjb21taXQKCmRpZmYgLS1naXQgYS9SRUFETUUubWQgYi9SRUFETUUubWQKbmV3IGZpbGUgbW9kZSAxMDA2NDQKaW5kZXggMDAwMDAwMC4uNGI0ODUxYQotLS0gL2Rldi9udWxsCisrKyBiL1JFQURNRS5tZApAQCAtMCwwICsxLDMgQEAKKyMgcmVwbzEKKworRGVzY3JpcHRpb24gZm9yIHJlcG8xClwgTm8gbmV3bGluZSBhdCBlbmQgb2YgZmlsZQo=", Content: "dHJlZSAyYTJmMWQ0NjcwNzI4YTJlMTAwNDllMzQ1YmQ3YTI3NjQ2OGJlYWI2CmF1dGhvciB1c2VyMSA8YWRkcmVzczFAZXhhbXBsZS5jb20+IDE0ODk5NTY0NzkgLTA0MDAKY29tbWl0dGVyIEV0aGFuIEtvZW5pZyA8ZXRoYW50a29lbmlnQGdtYWlsLmNvbT4gMTQ4OTk1NjQ3OSAtMDQwMAoKSW5pdGlhbCBjb21taXQK",
Encoding: "base64", Encoding: "base64",
URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/65f1bf27bc3bf70f64657658635e66094edbcb4d", URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/65f1bf27bc3bf70f64657658635e66094edbcb4d",
SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d", SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d",

View File

@ -61,7 +61,7 @@ func GetFileContents(repo *models.Repository, treePath, ref string) (*api.FileCo
HTMLURL: htmlURL.String(), HTMLURL: htmlURL.String(),
GitURL: gitURL.String(), GitURL: gitURL.String(),
DownloadURL: downloadURL.String(), DownloadURL: downloadURL.String(),
Type: string(entry.Type), Type: entry.Type(),
Links: &api.FileLinksResponse{ Links: &api.FileLinksResponse{
Self: selfURL.String(), Self: selfURL.String(),
GitURL: gitURL.String(), GitURL: gitURL.String(),

View File

@ -58,7 +58,7 @@ func (t *TemporaryUploadRepository) Clone(branch string) error {
fmt.Sprintf("Clone (git clone -s --bare): %s", t.basePath), fmt.Sprintf("Clone (git clone -s --bare): %s", t.basePath),
"git", "clone", "-s", "--bare", "-b", branch, t.repo.RepoPath(), t.basePath); err != nil { "git", "clone", "-s", "--bare", "-b", branch, t.repo.RepoPath(), t.basePath); err != nil {
if matched, _ := regexp.MatchString(".*Remote branch .* not found in upstream origin.*", stderr); matched { if matched, _ := regexp.MatchString(".*Remote branch .* not found in upstream origin.*", stderr); matched {
return models.ErrBranchNotExist{ return git.ErrBranchNotExist{
Name: branch, Name: branch,
} }
} else if matched, _ := regexp.MatchString(".* repository .* does not exist.*", stderr); matched { } else if matched, _ := regexp.MatchString(".* repository .* does not exist.*", stderr); matched {

Some files were not shown because too many files have changed in this diff Show More