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
tokarchuk/v1.17
Filip Navara 6 years ago committed by Lunny Xiao
parent 19ec2606e9
commit 2af67f6044
  1. 4
      go.mod
  2. 2
      integrations/api_repo_git_blobs_test.go
  3. 15
      models/error.go
  4. 5
      models/pull.go
  5. 57
      models/repo_branch.go
  6. 3
      modules/context/repo.go
  7. 62
      modules/git/blob.go
  8. 39
      modules/git/blob_test.go
  9. 80
      modules/git/commit.go
  10. 418
      modules/git/commit_info.go
  11. 4
      modules/git/commit_info_test.go
  12. 15
      modules/git/error.go
  13. 25
      modules/git/parse.go
  14. 26
      modules/git/parse_test.go
  15. 25
      modules/git/repo.go
  16. 14
      modules/git/repo_blob.go
  17. 3
      modules/git/repo_blob_test.go
  18. 62
      modules/git/repo_branch.go
  19. 135
      modules/git/repo_commit.go
  20. 33
      modules/git/repo_tag.go
  21. 27
      modules/git/repo_tree.go
  22. 29
      modules/git/sha1.go
  23. 9
      modules/git/signature.go
  24. 77
      modules/git/tree.go
  25. 15
      modules/git/tree_blob.go
  26. 79
      modules/git/tree_entry.go
  27. 18
      modules/git/tree_entry_test.go
  28. 2
      modules/repofiles/blob_test.go
  29. 2
      modules/repofiles/content.go
  30. 2
      modules/repofiles/temp_repo.go
  31. 13
      modules/repofiles/tree.go
  32. 3
      modules/repofiles/tree_test.go
  33. 2
      modules/repofiles/update.go
  34. 2
      routers/api/v1/convert/convert.go
  35. 5
      routers/api/v1/repo/branch.go
  36. 1
      routers/repo/commit.go
  37. 11
      routers/repo/editor.go
  38. 5
      routers/repo/issue.go
  39. 4
      routers/repo/setting_protected_branch.go
  40. 11
      routers/repo/view.go
  41. 9
      routers/repo/wiki.go
  42. 3
      routers/repo/wiki_test.go
  43. 2
      templates/repo/diff/page.tmpl
  44. 14
      vendor/modules.txt

@ -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

@ -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

@ -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

@ -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

@ -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)
}

@ -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

@ -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.
// This can be very slow and memory consuming for huge content.
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 { gogitEncodedObj plumbing.EncodedObject
return nil, concatenateError(err, stderr.String()) name 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()
if err != nil {
return nil, fmt.Errorf("StdoutPipe: %v", err)
} }
if err = cmd.Start(); err != nil { // Size returns the uncompressed size of the blob
return nil, fmt.Errorf("Start: %v", err) func (b *Blob) Size() int64 {
return b.gogitEncodedObj.Size()
} }
return cmdReadCloser{stdout: stdout, cmd: cmd}, nil // Name returns name of the tree entry this blob object was created from (or empty string)
func (b *Blob) Name() string {
return b.name
} }
// 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

@ -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) {
for i := 0; i < b.N; i++ { repo, err := OpenRepository("../../.git")
r, err := testBlob.Data()
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }
ioutil.ReadAll(r) testBlob, err := repo.GetBlob("a8d4b49dd073a4a38a7e58385eeff7cc52568697")
} if err != nil {
b.Fatal(err)
} }
func Benchmark_Blob_DataPipeline(b *testing.B) {
stdout := new(bytes.Buffer)
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
stdout.Reset() r, err := testBlob.DataAsync()
if err := testBlob.DataPipeline(stdout, nil); err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }
defer r.Close()
ioutil.ReadAll(r)
} }
} }

@ -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") 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 _, err = fmt.Fprintf(&w, "\n\n%s", c.Message); err != nil {
return nil
}
return &CommitGPGSignature{
Signature: c.PGPSignature,
Payload: w.String(),
} }
sig.Signature = strings.Replace(string(data[signatureStart:signatureEnd+27]), "\n ", "\n", -1)
if tag {
sig.Payload = string(data[:signatureStart-1])
} else {
sig.Payload = string(data[:signatureStart-8]) + string(data[signatureEnd+27:])
} }
return sig, nil
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,
}
} }
// 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

@ -5,325 +5,237 @@
package git package git
import ( import (
"bufio" "github.com/emirpasic/gods/trees/binaryheap"
"context" "gopkg.in/src-d/go-git.v4/plumbing"
"fmt" "gopkg.in/src-d/go-git.v4/plumbing/object"
"os/exec"
"path"
"runtime"
"strconv"
"strings"
"sync"
"time"
) )
const ( // GetCommitsInfo gets information of all commits that are corresponding to these entries
// parameters for searching for commit infos. If the untargeted search has func (tes Entries) GetCommitsInfo(commit *Commit, treePath string, cache LastCommitCache) ([][]interface{}, *Commit, error) {
// not found any entries in the past 5 commits, and 12 or fewer entries entryPaths := make([]string, len(tes)+1)
// remain, then we'll just let the targeted-searching threads finish off, // Get the commit for the treePath itself
// and stop the untargeted search to not interfere. entryPaths[0] = ""
deferToTargetedSearchColdStreak = 5 for i, entry := range tes {
deferToTargetedSearchNumRemainingEntries = 12 entryPaths[i+1] = entry.Name()
) }
// getCommitsInfoState shared state while getting commit info for entries
type getCommitsInfoState struct {
lock sync.Mutex
/* read-only fields, can be read without the mutex */
// entries and entryPaths are read-only after initialization, so they can
// safely be read without the mutex
entries []*TreeEntry
// set of filepaths to get info for
entryPaths map[string]struct{}
treePath string
headCommit *Commit
/* mutable fields, must hold mutex to read or write */ c, err := commit.repo.gogitRepo.CommitObject(plumbing.Hash(commit.ID))
// map from filepath to commit if err != nil {
commits map[string]*Commit return nil, nil, err
// set of filepaths that have been or are being searched for in a target search
targetedPaths map[string]struct{}
} }
func (state *getCommitsInfoState) numRemainingEntries() int { revs, err := getLastCommitForPaths(c, treePath, entryPaths)
state.lock.Lock() if err != nil {
defer state.lock.Unlock() return nil, nil, err
return len(state.entries) - len(state.commits)
} }
// getTargetEntryPath Returns the next path for a targeted-searching thread to commit.repo.gogitStorage.Close()
// search for, or returns the empty string if nothing left to search for
func (state *getCommitsInfoState) getTargetedEntryPath() string { commitsInfo := make([][]interface{}, len(tes))
var targetedEntryPath string for i, entry := range tes {
state.lock.Lock() if rev, ok := revs[entry.Name()]; ok {
defer state.lock.Unlock() entryCommit := convertCommit(rev)
for _, entry := range state.entries { if entry.IsSubModule() {
entryPath := path.Join(state.treePath, entry.Name()) subModuleURL := ""
if _, ok := state.commits[entryPath]; ok { if subModule, err := commit.GetSubModule(entry.Name()); err != nil {
continue return nil, nil, err
} else if _, ok = state.targetedPaths[entryPath]; ok { } else if subModule != nil {
continue subModuleURL = subModule.URL
} }
targetedEntryPath = entryPath subModuleFile := NewSubModuleFile(entryCommit, subModuleURL, entry.ID.String())
state.targetedPaths[entryPath] = struct{}{} commitsInfo[i] = []interface{}{entry, subModuleFile}
break } else {
commitsInfo[i] = []interface{}{entry, entryCommit}
}
} else {
commitsInfo[i] = []interface{}{entry, nil}
} }
return targetedEntryPath
} }
// repeatedly perform targeted searches for unpopulated entries // Retrieve the commit for the treePath itself (see above). We basically
func targetedSearch(state *getCommitsInfoState, done chan error, cache LastCommitCache) { // get it for free during the tree traversal and it's used for listing
for { // pages to display information about newest commit for a given path.
entryPath := state.getTargetedEntryPath() var treeCommit *Commit
if len(entryPath) == 0 { if rev, ok := revs[""]; ok {
done <- nil treeCommit = convertCommit(rev)
return
}
if cache != nil {
commit, err := cache.Get(state.headCommit.repo.Path, state.headCommit.ID.String(), entryPath)
if err == nil && commit != nil {
state.update(entryPath, commit)
continue
} }
return commitsInfo, treeCommit, nil
} }
command := NewCommand("rev-list", "-1", state.headCommit.ID.String(), "--", entryPath)
output, err := command.RunInDir(state.headCommit.repo.Path) type commitAndPaths struct {
if err != nil { commit *object.Commit
done <- err // Paths that are still on the branch represented by commit
return paths []string
// Set of hashes for the paths
hashes map[string]plumbing.Hash
} }
id, err := NewIDFromString(strings.TrimSpace(output))
func getCommitTree(c *object.Commit, treePath string) (*object.Tree, error) {
tree, err := c.Tree()
if err != nil { if err != nil {
done <- err return nil, err
return
} }
commit, err := state.headCommit.repo.getCommit(id)
// Optimize deep traversals by focusing only on the specific tree
if treePath != "" {
tree, err = tree.Tree(treePath)
if err != nil { if err != nil {
done <- err return nil, err
return
}
state.update(entryPath, commit)
if cache != nil {
cache.Put(state.headCommit.repo.Path, state.headCommit.ID.String(), entryPath, commit)
}
} }
} }
func initGetCommitInfoState(entries Entries, headCommit *Commit, treePath string) *getCommitsInfoState { return tree, nil
entryPaths := make(map[string]struct{}, len(entries))
for _, entry := range entries {
entryPaths[path.Join(treePath, entry.Name())] = struct{}{}
}
if treePath = path.Clean(treePath); treePath == "." {
treePath = ""
}
return &getCommitsInfoState{
entries: entries,
entryPaths: entryPaths,
commits: make(map[string]*Commit, len(entries)),
targetedPaths: make(map[string]struct{}, len(entries)),
treePath: treePath,
headCommit: headCommit,
}
} }
// GetCommitsInfo gets information of all commits that are corresponding to these entries func getFullPath(treePath, path string) string {
func (tes Entries) GetCommitsInfo(commit *Commit, treePath string, cache LastCommitCache) ([][]interface{}, error) { if treePath != "" {
state := initGetCommitInfoState(tes, commit, treePath) if path != "" {
if err := getCommitsInfo(state, cache); err != nil { return treePath + "/" + path
return nil, err
} }
if len(state.commits) < len(state.entryPaths) { return treePath
return nil, fmt.Errorf("could not find commits for all entries") }
return path
} }
commitsInfo := make([][]interface{}, len(tes)) func getFileHashes(c *object.Commit, treePath string, paths []string) (map[string]plumbing.Hash, error) {
for i, entry := range tes { tree, err := getCommitTree(c, treePath)
commit, ok := state.commits[path.Join(treePath, entry.Name())] if err == object.ErrDirectoryNotFound {
if !ok { // The whole tree didn't exist, so return empty map
return nil, fmt.Errorf("could not find commit for %s", entry.Name()) return make(map[string]plumbing.Hash), nil
} }
switch entry.Type { if err != nil {
case ObjectCommit:
subModuleURL := ""
if subModule, err := state.headCommit.GetSubModule(entry.Name()); err != nil {
return nil, err return nil, err
} else if subModule != nil {
subModuleURL = subModule.URL
}
subModuleFile := NewSubModuleFile(commit, subModuleURL, entry.ID.String())
commitsInfo[i] = []interface{}{entry, subModuleFile}
default:
commitsInfo[i] = []interface{}{entry, commit}
}
}
return commitsInfo, nil
} }
func (state *getCommitsInfoState) cleanEntryPath(rawEntryPath string) (string, error) { hashes := make(map[string]plumbing.Hash)
if rawEntryPath[0] == '"' { for _, path := range paths {
var err error if path != "" {
rawEntryPath, err = strconv.Unquote(rawEntryPath) entry, err := tree.FindEntry(path)
if err != nil { if err == nil {
return rawEntryPath, err hashes[path] = entry.Hash
} }
} else {
hashes[path] = tree.Hash
} }
var entryNameStartIndex int
if len(state.treePath) > 0 {
entryNameStartIndex = len(state.treePath) + 1
} }
if index := strings.IndexByte(rawEntryPath[entryNameStartIndex:], '/'); index >= 0 { return hashes, nil
return rawEntryPath[:entryNameStartIndex+index], nil
}
return rawEntryPath, nil
} }
// update report that the given path was last modified by the given commit. func getLastCommitForPaths(c *object.Commit, treePath string, paths []string) (map[string]*object.Commit, error) {
// Returns whether state.commits was updated // We do a tree traversal with nodes sorted by commit time
func (state *getCommitsInfoState) update(entryPath string, commit *Commit) bool { seen := make(map[plumbing.Hash]bool)
if _, ok := state.entryPaths[entryPath]; !ok { heap := binaryheap.NewWith(func(a, b interface{}) int {
return false if a.(*commitAndPaths).commit.Committer.When.Before(b.(*commitAndPaths).commit.Committer.When) {
return 1
} }
return -1
})
var updated bool result := make(map[string]*object.Commit)
state.lock.Lock() initialHashes, err := getFileHashes(c, treePath, paths)
defer state.lock.Unlock() if err != nil {
if _, ok := state.commits[entryPath]; !ok { return nil, err
state.commits[entryPath] = commit
updated = true
}
return updated
} }
const getCommitsInfoPretty = "--pretty=format:%H %ct %s" // Start search from the root commit and with full set of paths
heap.Push(&commitAndPaths{c, paths, initialHashes})
func getCommitsInfo(state *getCommitsInfoState, cache LastCommitCache) error { for {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) cIn, ok := heap.Pop()
defer cancel() if !ok {
break
}
current := cIn.(*commitAndPaths)
currentID := current.commit.ID()
args := []string{"log", state.headCommit.ID.String(), getCommitsInfoPretty, "--name-status", "-c"} if seen[currentID] {
if len(state.treePath) > 0 { continue
args = append(args, "--", state.treePath)
} }
cmd := exec.CommandContext(ctx, "git", args...) seen[currentID] = true
cmd.Dir = state.headCommit.repo.Path
readCloser, err := cmd.StdoutPipe() // Load the parent commits for the one we are currently examining
numParents := current.commit.NumParents()
var parents []*object.Commit
for i := 0; i < numParents; i++ {
parent, err := current.commit.Parent(i)
if err != nil { if err != nil {
return err break
} }
parents = append(parents, parent)
if err := cmd.Start(); err != nil {
return err
} }
// it's okay to ignore the error returned by cmd.Wait(); we expect the
// subprocess to sometimes have a non-zero exit status, since we may
// prematurely close stdout, resulting in a broken pipe.
defer cmd.Wait()
numThreads := runtime.NumCPU() // Examine the current commit and set of interesting paths
done := make(chan error, numThreads) numOfParentsWithPath := make([]int, len(current.paths))
for i := 0; i < numThreads; i++ { pathChanged := make([]bool, len(current.paths))
go targetedSearch(state, done, cache) 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
} }
scanner := bufio.NewScanner(readCloser) for i, path := range current.paths {
err = state.processGitLogOutput(scanner) if parentHashes[j][path] != plumbing.ZeroHash {
numOfParentsWithPath[i]++
// it is important that we close stdout here; if we do not close if parentHashes[j][path] != current.hashes[path] {
// stdout, the subprocess will keep running, and the deffered call pathChanged[i] = true
// cmd.Wait() may block for a long time.
if closeErr := readCloser.Close(); closeErr != nil && err == nil {
err = closeErr
} }
for i := 0; i < numThreads; i++ {
doneErr := <-done
if doneErr != nil && err == nil {
err = doneErr
} }
} }
return err
} }
func (state *getCommitsInfoState) processGitLogOutput(scanner *bufio.Scanner) error { var remainingPaths []string
// keep a local cache of seen paths to avoid acquiring a lock for paths for i, path := range current.paths {
// we've already seen switch numOfParentsWithPath[i] {
seenPaths := make(map[string]struct{}, len(state.entryPaths)) case 0:
// number of consecutive commits without any finds // The path didn't exist in any parent, so it must have been created by
coldStreak := 0 // this commit. The results could already contain some newer change from
var commit *Commit // different path, so don't override that.
var err error if result[path] == nil {
for scanner.Scan() { result[path] = current.commit
line := scanner.Text()
if len(line) == 0 { // in-between commits
numRemainingEntries := state.numRemainingEntries()
if numRemainingEntries == 0 {
break
}
if coldStreak >= deferToTargetedSearchColdStreak &&
numRemainingEntries <= deferToTargetedSearchNumRemainingEntries {
// stop this untargeted search, and let the targeted-search threads
// finish the work
break
} }
continue case 1:
// The file is present on exactly one parent, so check if it was changed
// and save the revision if it did.
if pathChanged[i] {
if result[path] == nil {
result[path] = current.commit
} }
if line[0] >= 'A' && line[0] <= 'X' { // a file was changed by the current commit } else {
// look for the last tab, since for copies (C) and renames (R) two remainingPaths = append(remainingPaths, path)
// filenames are printed: src, then dest
tabIndex := strings.LastIndexByte(line, '\t')
if tabIndex < 1 {
return fmt.Errorf("misformatted line: %s", line)
} }
entryPath, err := state.cleanEntryPath(line[tabIndex+1:]) default:
if err != nil { // The file is present on more than one of the parent paths, so this is
return err // a merge. We have to examine all the parent trees to find out where
} // the change occurred. pathChanged[i] would tell us that the file was
if _, ok := seenPaths[entryPath]; !ok { // changed during the merge, but it wouldn't tell us the relevant commit
if state.update(entryPath, commit) { // that introduced it.
coldStreak = 0 remainingPaths = append(remainingPaths, path)
} }
seenPaths[entryPath] = struct{}{}
} }
if len(remainingPaths) > 0 {
// Add the parent nodes along with remaining paths to the heap for further
// processing.
for j, parent := range parents {
if seen[parent.ID()] {
continue continue
} }
// a new commit // Combine remainingPath with paths available on the parent branch
commit, err = parseCommitInfo(line) // and make union of them
if err != nil { var remainingPathsForParent []string
return err for _, path := range remainingPaths {
} if parentHashes[j][path] != plumbing.ZeroHash {
coldStreak++ remainingPathsForParent = append(remainingPathsForParent, path)
} }
return scanner.Err()
} }
// parseCommitInfo parse a commit from a line of `git log` output. Expects the heap.Push(&commitAndPaths{parent, remainingPathsForParent, parentHashes[j]})
// line to be formatted according to getCommitsInfoPretty.
func parseCommitInfo(line string) (*Commit, error) {
if len(line) < 43 {
return nil, fmt.Errorf("invalid git output: %s", line)
} }
ref, err := NewIDFromString(line[:40])
if err != nil {
return nil, err
} }
spaceIndex := strings.IndexByte(line[41:], ' ')
if spaceIndex < 0 {
return nil, fmt.Errorf("invalid git output: %s", line)
} }
unixSeconds, err := strconv.Atoi(line[41 : 41+spaceIndex])
if err != nil { return result, nil
return nil, err
}
message := line[spaceIndex+42:]
return &Commit{
ID: ref,
CommitMessage: message,
Committer: &Signature{
When: time.Unix(int64(unixSeconds), 0),
},
}, nil
} }

@ -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)
} }

@ -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)
}

@ -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

@ -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,
Type: ObjectBlob,
ID: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"), ID: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
name: "example/file2.txt", gogitTreeEntry: &object.TreeEntry{
Hash: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
Name: "example/file2.txt",
Mode: filemode.Regular,
},
}, },
}, },
}, },
@ -36,15 +40,19 @@ func TestParseTreeEntries(t *testing.T) {
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,
},
}, },
}, },
}, },

@ -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,9 +83,24 @@ 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,
gogitStorage: storage,
tagCache: newObjectCache(), tagCache: newObjectCache(),
}, nil }, nil
} }

@ -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,
TreeEntry: &TreeEntry{
ID: id, ID: id,
ptree: &Tree{ gogitEncodedObj: encodedObj,
repo: repo,
},
},
}, nil }, nil
} }

@ -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)

@ -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.
@ -53,6 +58,7 @@ 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)
}

@ -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)
// we now have the contents of the commit object. Let's investigate...
nextline := 0
l:
for {
eol := bytes.IndexByte(data[nextline:], '\n')
switch {
case eol > 0:
line := data[nextline : nextline+eol]
spacepos := bytes.IndexByte(line, ' ')
reftype := line[:spacepos]
switch string(reftype) {
case "tree", "object":
id, err := NewIDFromString(string(line[spacepos+1:]))
if err != nil {
return nil, err
}
commit.Tree.ID = id
case "parent":
// A commit can have one or more parents
oid, err := NewIDFromString(string(line[spacepos+1:]))
if err != nil {
return nil, err
}
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 var w strings.Builder
sigindex := strings.Index(cm, "-----BEGIN PGP SIGNATURE-----") var err error
if sigindex != -1 {
sig, err := newGPGSignatureFromCommitline(data, (nextline+1)+sigindex, true) if _, err = fmt.Fprintf(&w,
if err == nil && sig != nil { "object %s\ntype %s\ntag %s\ntagger ",
// remove signature from commit message t.Target.String(), t.TargetType.Bytes(), t.Name); err != nil {
if sigindex == 0 { return nil
cm = ""
} else {
cm = cm[:sigindex-1]
} }
commit.Signature = sig
if err = t.Tagger.Encode(&w); err != nil {
return nil
} }
if _, err = fmt.Fprintf(&w, "\n\n"); err != nil {
return nil
} }
commit.CommitMessage = cm if _, err = fmt.Fprintf(&w, t.Message); err != nil {
break l return nil
default:
break l
} }
return &CommitGPGSignature{
Signature: t.PGPSignature,
Payload: strings.TrimSpace(w.String()) + "\n",
} }
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
} }

@ -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
} }

@ -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) {
_, err := NewCommand("ls-tree", id.String()).RunInDir(repo.Path)
if err != nil { if err != nil {
return nil, ErrNotExist{id.String(), ""} return nil, err
}
} }
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
} }

@ -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 {

@ -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.

@ -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
CommitID SHA1
repo *Repository 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
} }
// ListEntries returns all entries of current tree. func (t *Tree) loadTreeObject() error {
func (t *Tree) ListEntries() (Entries, error) { gogitTree, err := t.repo.gogitRepo.TreeObject(plumbing.Hash(t.ID))
if t.entriesParsed { if err != nil {
return t.entries, nil return err
} }
stdout, err := NewCommand("ls-tree", t.ID.String()).RunInDirBytes(t.repo.Path) 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 { if err != nil {
return nil, err return nil, err
} }
}
t.entries, err = parseTreeEntries(stdout, t) entries := make([]*TreeEntry, len(t.gogitTree.Entries))
if err == nil { for i, entry := range t.gogitTree.Entries {
t.entriesParsed = true entries[i] = &TreeEntry{
ID: entry.Hash,
gogitTreeEntry: &t.gogitTree.Entries[i],
ptree: t,
}
} }
return t.entries, err return entries, nil
} }
// 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 {
return nil, err
}
}
var entries []*TreeEntry
seen := map[plumbing.Hash]bool{}
walker := object.NewTreeWalker(t.gogitTree, true, seen)
for {
_, entry, err := walker.Next()
if err == io.EOF {
break
} }
stdout, err := NewCommand("ls-tree", "-t", "-r", t.ID.String()).RunInDirBytes(t.repo.Path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if seen[entry.Hash] {
continue
}
t.entriesRecursive, err = parseTreeEntries(stdout, t) convertedEntry := &TreeEntry{
if err == nil { ID: entry.Hash,
t.entriesRecursiveParsed = true gogitTreeEntry: &entry,
ptree: t,
}
entries = append(entries, convertedEntry)
} }
return t.entriesRecursive, err return entries, nil
} }

@ -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,6 +8,10 @@ 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
@ -14,8 +19,12 @@ 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
} }
} }

@ -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,41 +22,48 @@ 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
name string
gogitTreeEntry *object.TreeEntry
ptree *Tree ptree *Tree
committed bool
size int64 size int64
sized bool sized bool
} }
// 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())
}, },
} }

@ -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}},
} }
} }

@ -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",

@ -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(),

@ -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 {

@ -23,7 +23,7 @@ func GetTreeBySHA(repo *models.Repository, sha string, page, perPage int, recurs
} }
} }
tree := new(api.GitTreeResponse) tree := new(api.GitTreeResponse)
tree.SHA = gitTree.ID.String() tree.SHA = gitTree.CommitID.String()
tree.URL = repo.APIURL() + "/git/trees/" + tree.SHA tree.URL = repo.APIURL() + "/git/trees/" + tree.SHA
var entries git.Entries var entries git.Entries
if recursive { if recursive {
@ -74,11 +74,12 @@ func GetTreeBySHA(repo *models.Repository, sha string, page, perPage int, recurs
tree.Entries = make([]api.GitEntry, rangeEnd-rangeStart) tree.Entries = make([]api.GitEntry, rangeEnd-rangeStart)
for e := rangeStart; e < rangeEnd; e++ { for e := rangeStart; e < rangeEnd; e++ {
i := e - rangeStart i := e - rangeStart
tree.Entries[i].Path = entries[e].Name()
tree.Entries[i].Mode = fmt.Sprintf("%06x", entries[e].Mode()) tree.Entries[e].Path = entries[e].Name()
tree.Entries[i].Type = string(entries[e].Type) tree.Entries[e].Mode = fmt.Sprintf("%06o", entries[e].Mode())
tree.Entries[i].Size = entries[e].Size() tree.Entries[e].Type = entries[e].Type()
tree.Entries[i].SHA = entries[e].ID.String() tree.Entries[e].Size = entries[e].Size()
tree.Entries[e].SHA = entries[e].ID.String()
if entries[e].IsDir() { if entries[e].IsDir() {
copy(treeURL[copyPos:], entries[e].ID.String()) copy(treeURL[copyPos:], entries[e].ID.String())

@ -46,5 +46,6 @@ func TestGetTreeBySHA(t *testing.T) {
Page: 1, Page: 1,
TotalCount: 1, TotalCount: 1,
} }
assert.EqualValues(t, tree, expectedTree)
assert.EqualValues(t, expectedTree, tree)
} }

@ -62,7 +62,7 @@ func CreateOrUpdateRepoFile(repo *models.Repository, doer *models.User, opts *Up
BranchName: opts.NewBranch, BranchName: opts.NewBranch,
} }
} }
if err != nil && !models.IsErrBranchNotExist(err) { if err != nil && !git.IsErrBranchNotExist(err) {
return nil, err return nil, err
} }
} else { } else {

@ -27,7 +27,7 @@ func ToEmail(email *models.EmailAddress) *api.Email {
} }
// ToBranch convert a commit and branch to an api.Branch // ToBranch convert a commit and branch to an api.Branch
func ToBranch(repo *models.Repository, b *models.Branch, c *git.Commit) *api.Branch { func ToBranch(repo *models.Repository, b *git.Branch, c *git.Commit) *api.Branch {
return &api.Branch{ return &api.Branch{
Name: b.Name, Name: b.Name,
Commit: ToCommit(repo, c), Commit: ToCommit(repo, c),

@ -1,12 +1,13 @@
// 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.
package repo package repo
import ( import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/routers/api/v1/convert" "code.gitea.io/gitea/routers/api/v1/convert"
api "code.gitea.io/sdk/gitea" api "code.gitea.io/sdk/gitea"
@ -47,7 +48,7 @@ func GetBranch(ctx *context.APIContext) {
} }
branch, err := ctx.Repo.Repository.GetBranch(ctx.Repo.BranchName) branch, err := ctx.Repo.Repository.GetBranch(ctx.Repo.BranchName)
if err != nil { if err != nil {
if models.IsErrBranchNotExist(err) { if git.IsErrBranchNotExist(err) {
ctx.NotFound(err) ctx.NotFound(err)
} else { } else {
ctx.Error(500, "GetBranch", err) ctx.Error(500, "GetBranch", err)

@ -243,6 +243,7 @@ func Diff(ctx *context.Context) {
ctx.Data["BeforeSourcePath"] = setting.AppSubURL + "/" + path.Join(userName, repoName, "src", "commit", parents[0]) ctx.Data["BeforeSourcePath"] = setting.AppSubURL + "/" + path.Join(userName, repoName, "src", "commit", parents[0])
} }
ctx.Data["RawPath"] = setting.AppSubURL + "/" + path.Join(userName, repoName, "raw", "commit", commitID) ctx.Data["RawPath"] = setting.AppSubURL + "/" + path.Join(userName, repoName, "raw", "commit", commitID)
ctx.Data["BranchName"], err = commit.GetBranchName()
ctx.HTML(200, tplDiff) ctx.HTML(200, tplDiff)
} }

@ -95,11 +95,12 @@ func editFile(ctx *context.Context, isNewFile bool) {
return return
} }
dataRc, err := blob.Data() dataRc, err := blob.DataAsync()
if err != nil { if err != nil {
ctx.NotFound("blob.Data", err) ctx.NotFound("blob.Data", err)
return return
} }
defer dataRc.Close()
ctx.Data["FileSize"] = blob.Size() ctx.Data["FileSize"] = blob.Size()
ctx.Data["FileName"] = blob.Name() ctx.Data["FileName"] = blob.Name()
@ -251,9 +252,9 @@ func editFilePost(ctx *context.Context, form auth.EditRepoFileForm, isNewFile bo
} else if models.IsErrRepoFileAlreadyExists(err) { } else if models.IsErrRepoFileAlreadyExists(err) {
ctx.Data["Err_TreePath"] = true ctx.Data["Err_TreePath"] = true
ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplEditFile, &form) ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplEditFile, &form)
} else if models.IsErrBranchNotExist(err) { } else if git.IsErrBranchNotExist(err) {
// For when a user adds/updates a file to a branch that no longer exists // For when a user adds/updates a file to a branch that no longer exists
if branchErr, ok := err.(models.ErrBranchNotExist); ok { if branchErr, ok := err.(git.ErrBranchNotExist); ok {
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplEditFile, &form) ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplEditFile, &form)
} else { } else {
ctx.Error(500, err.Error()) ctx.Error(500, err.Error())
@ -417,9 +418,9 @@ func DeleteFilePost(ctx *context.Context, form auth.DeleteRepoFileForm) {
} else { } else {
ctx.ServerError("DeleteRepoFile", err) ctx.ServerError("DeleteRepoFile", err)
} }
} else if models.IsErrBranchNotExist(err) { } else if git.IsErrBranchNotExist(err) {
// For when a user deletes a file to a branch that no longer exists // For when a user deletes a file to a branch that no longer exists
if branchErr, ok := err.(models.ErrBranchNotExist); ok { if branchErr, ok := err.(git.ErrBranchNotExist); ok {
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplEditFile, &form) ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplEditFile, &form)
} else { } else {
ctx.Error(500, err.Error()) ctx.Error(500, err.Error())

@ -9,7 +9,6 @@ import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"strconv" "strconv"
@ -363,7 +362,6 @@ func RetrieveRepoMetas(ctx *context.Context, repo *models.Repository) []*models.
} }
func getFileContentFromDefaultBranch(ctx *context.Context, filename string) (string, bool) { func getFileContentFromDefaultBranch(ctx *context.Context, filename string) (string, bool) {
var r io.Reader
var bytes []byte var bytes []byte
if ctx.Repo.Commit == nil { if ctx.Repo.Commit == nil {
@ -381,10 +379,11 @@ func getFileContentFromDefaultBranch(ctx *context.Context, filename string) (str
if entry.Blob().Size() >= setting.UI.MaxDisplayFileSize { if entry.Blob().Size() >= setting.UI.MaxDisplayFileSize {
return "", false return "", false
} }
r, err = entry.Blob().Data() r, err := entry.Blob().DataAsync()
if err != nil { if err != nil {
return "", false return "", false
} }
defer r.Close()
bytes, err = ioutil.ReadAll(r) bytes, err = ioutil.ReadAll(r)
if err != nil { if err != nil {
return "", false return "", false

@ -103,7 +103,7 @@ func SettingsProtectedBranch(c *context.Context) {
protectBranch, err := models.GetProtectedBranchBy(c.Repo.Repository.ID, branch) protectBranch, err := models.GetProtectedBranchBy(c.Repo.Repository.ID, branch)
if err != nil { if err != nil {
if !models.IsErrBranchNotExist(err) { if !git.IsErrBranchNotExist(err) {
c.ServerError("GetProtectBranchOfRepoByName", err) c.ServerError("GetProtectBranchOfRepoByName", err)
return return
} }
@ -152,7 +152,7 @@ func SettingsProtectedBranchPost(ctx *context.Context, f auth.ProtectBranchForm)
protectBranch, err := models.GetProtectedBranchBy(ctx.Repo.Repository.ID, branch) protectBranch, err := models.GetProtectedBranchBy(ctx.Repo.Repository.ID, branch)
if err != nil { if err != nil {
if !models.IsErrBranchNotExist(err) { if !git.IsErrBranchNotExist(err) {
ctx.ServerError("GetProtectBranchOfRepoByName", err) ctx.ServerError("GetProtectBranchOfRepoByName", err)
return return
} }

@ -49,7 +49,8 @@ func renderDirectory(ctx *context.Context, treeLink string) {
} }
entries.CustomSort(base.NaturalSortLess) entries.CustomSort(base.NaturalSortLess)
ctx.Data["Files"], err = entries.GetCommitsInfo(ctx.Repo.Commit, ctx.Repo.TreePath, nil) var latestCommit *git.Commit
ctx.Data["Files"], latestCommit, err = entries.GetCommitsInfo(ctx.Repo.Commit, ctx.Repo.TreePath, nil)
if err != nil { if err != nil {
ctx.ServerError("GetCommitsInfo", err) ctx.ServerError("GetCommitsInfo", err)
return return
@ -178,14 +179,6 @@ func renderDirectory(ctx *context.Context, treeLink string) {
// Show latest commit info of repository in table header, // Show latest commit info of repository in table header,
// or of directory if not in root directory. // or of directory if not in root directory.
latestCommit := ctx.Repo.Commit
if len(ctx.Repo.TreePath) > 0 {
latestCommit, err = ctx.Repo.Commit.GetCommitByPath(ctx.Repo.TreePath)
if err != nil {
ctx.ServerError("GetCommitByPath", err)
return
}
}
ctx.Data["LatestCommit"] = latestCommit ctx.Data["LatestCommit"] = latestCommit
ctx.Data["LatestCommitVerification"] = models.ParseCommitWithSignature(latestCommit) ctx.Data["LatestCommitVerification"] = models.ParseCommitWithSignature(latestCommit)
ctx.Data["LatestCommitUser"] = models.ValidateCommitWithEmail(latestCommit) ctx.Data["LatestCommitUser"] = models.ValidateCommitWithEmail(latestCommit)

@ -57,7 +57,7 @@ func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error)
return nil, err return nil, err
} }
for _, entry := range entries { for _, entry := range entries {
if entry.Type == git.ObjectBlob && entry.Name() == target { if entry.IsRegular() && entry.Name() == target {
return entry, nil return entry, nil
} }
} }
@ -81,11 +81,12 @@ func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, err
// wikiContentsByEntry returns the contents of the wiki page referenced by the // wikiContentsByEntry returns the contents of the wiki page referenced by the
// given tree entry. Writes to ctx if an error occurs. // given tree entry. Writes to ctx if an error occurs.
func wikiContentsByEntry(ctx *context.Context, entry *git.TreeEntry) []byte { func wikiContentsByEntry(ctx *context.Context, entry *git.TreeEntry) []byte {
reader, err := entry.Blob().Data() reader, err := entry.Blob().DataAsync()
if err != nil { if err != nil {
ctx.ServerError("Blob.Data", err) ctx.ServerError("Blob.Data", err)
return nil return nil
} }
defer reader.Close()
content, err := ioutil.ReadAll(reader) content, err := ioutil.ReadAll(reader)
if err != nil { if err != nil {
ctx.ServerError("ReadAll", err) ctx.ServerError("ReadAll", err)
@ -125,7 +126,7 @@ func renderWikiPage(ctx *context.Context, isViewPage bool) (*git.Repository, *gi
} }
pages := make([]PageMeta, 0, len(entries)) pages := make([]PageMeta, 0, len(entries))
for _, entry := range entries { for _, entry := range entries {
if entry.Type != git.ObjectBlob { if !entry.IsRegular() {
continue continue
} }
wikiName, err := models.WikiFilenameToName(entry.Name()) wikiName, err := models.WikiFilenameToName(entry.Name())
@ -259,7 +260,7 @@ func WikiPages(ctx *context.Context) {
} }
pages := make([]PageMeta, 0, len(entries)) pages := make([]PageMeta, 0, len(entries))
for _, entry := range entries { for _, entry := range entries {
if entry.Type != git.ObjectBlob { if !entry.IsRegular() {
continue continue
} }
c, err := wikiRepo.GetCommitByPath(entry.Name()) c, err := wikiRepo.GetCommitByPath(entry.Name())

@ -40,8 +40,9 @@ func wikiContent(t *testing.T, repo *models.Repository, wikiName string) string
if !assert.NotNil(t, entry) { if !assert.NotNil(t, entry) {
return "" return ""
} }
reader, err := entry.Blob().Data() reader, err := entry.Blob().DataAsync()
assert.NoError(t, err) assert.NoError(t, err)
defer reader.Close()
bytes, err := ioutil.ReadAll(reader) bytes, err := ioutil.ReadAll(reader)
assert.NoError(t, err) assert.NoError(t, err)
return string(bytes) return string(bytes)

@ -13,7 +13,7 @@
{{if IsMultilineCommitMessage .Commit.Message}} {{if IsMultilineCommitMessage .Commit.Message}}
<pre class="commit-body">{{RenderCommitBody .Commit.Message $.RepoLink $.Repository.ComposeMetas}}</pre> <pre class="commit-body">{{RenderCommitBody .Commit.Message $.RepoLink $.Repository.ComposeMetas}}</pre>
{{end}} {{end}}
<span class="text grey"><i class="octicon octicon-git-branch"></i>{{.Commit.Branch}}</span> <span class="text grey"><i class="octicon octicon-git-branch"></i>{{.BranchName}}</span>
</div> </div>
<div class="ui attached info segment {{if .Commit.Signature}} isSigned {{if .Verification.Verified }} isVerified {{end}}{{end}}"> <div class="ui attached info segment {{if .Commit.Signature}} isSigned {{if .Verification.Verified }} isVerified {{end}}{{end}}">
<div class="ui stackable grid"> <div class="ui stackable grid">

14
vendor/modules.txt vendored

@ -410,8 +410,8 @@ gopkg.in/macaron.v1
# gopkg.in/redis.v2 v2.3.2 # gopkg.in/redis.v2 v2.3.2
gopkg.in/redis.v2 gopkg.in/redis.v2
# gopkg.in/src-d/go-billy.v4 v4.3.0 # gopkg.in/src-d/go-billy.v4 v4.3.0
gopkg.in/src-d/go-billy.v4
gopkg.in/src-d/go-billy.v4/osfs gopkg.in/src-d/go-billy.v4/osfs
gopkg.in/src-d/go-billy.v4
gopkg.in/src-d/go-billy.v4/util gopkg.in/src-d/go-billy.v4/util
gopkg.in/src-d/go-billy.v4/helper/chroot gopkg.in/src-d/go-billy.v4/helper/chroot
gopkg.in/src-d/go-billy.v4/helper/polyfill gopkg.in/src-d/go-billy.v4/helper/polyfill
@ -419,13 +419,14 @@ gopkg.in/src-d/go-billy.v4/helper/polyfill
gopkg.in/src-d/go-git.v4 gopkg.in/src-d/go-git.v4
gopkg.in/src-d/go-git.v4/config gopkg.in/src-d/go-git.v4/config
gopkg.in/src-d/go-git.v4/plumbing gopkg.in/src-d/go-git.v4/plumbing
gopkg.in/src-d/go-git.v4/internal/revision
gopkg.in/src-d/go-git.v4/plumbing/cache gopkg.in/src-d/go-git.v4/plumbing/cache
gopkg.in/src-d/go-git.v4/plumbing/filemode gopkg.in/src-d/go-git.v4/plumbing/filemode
gopkg.in/src-d/go-git.v4/plumbing/object
gopkg.in/src-d/go-git.v4/storage/filesystem
gopkg.in/src-d/go-git.v4/internal/revision
gopkg.in/src-d/go-git.v4/plumbing/format/gitignore gopkg.in/src-d/go-git.v4/plumbing/format/gitignore
gopkg.in/src-d/go-git.v4/plumbing/format/index gopkg.in/src-d/go-git.v4/plumbing/format/index
gopkg.in/src-d/go-git.v4/plumbing/format/packfile gopkg.in/src-d/go-git.v4/plumbing/format/packfile
gopkg.in/src-d/go-git.v4/plumbing/object
gopkg.in/src-d/go-git.v4/plumbing/protocol/packp gopkg.in/src-d/go-git.v4/plumbing/protocol/packp
gopkg.in/src-d/go-git.v4/plumbing/protocol/packp/capability gopkg.in/src-d/go-git.v4/plumbing/protocol/packp/capability
gopkg.in/src-d/go-git.v4/plumbing/protocol/packp/sideband gopkg.in/src-d/go-git.v4/plumbing/protocol/packp/sideband
@ -434,7 +435,6 @@ gopkg.in/src-d/go-git.v4/plumbing/storer
gopkg.in/src-d/go-git.v4/plumbing/transport gopkg.in/src-d/go-git.v4/plumbing/transport
gopkg.in/src-d/go-git.v4/plumbing/transport/client gopkg.in/src-d/go-git.v4/plumbing/transport/client
gopkg.in/src-d/go-git.v4/storage gopkg.in/src-d/go-git.v4/storage
gopkg.in/src-d/go-git.v4/storage/filesystem
gopkg.in/src-d/go-git.v4/storage/memory gopkg.in/src-d/go-git.v4/storage/memory
gopkg.in/src-d/go-git.v4/utils/diff gopkg.in/src-d/go-git.v4/utils/diff
gopkg.in/src-d/go-git.v4/utils/ioutil gopkg.in/src-d/go-git.v4/utils/ioutil
@ -444,16 +444,16 @@ gopkg.in/src-d/go-git.v4/utils/merkletrie/index
gopkg.in/src-d/go-git.v4/utils/merkletrie/noder gopkg.in/src-d/go-git.v4/utils/merkletrie/noder
gopkg.in/src-d/go-git.v4/internal/url gopkg.in/src-d/go-git.v4/internal/url
gopkg.in/src-d/go-git.v4/plumbing/format/config gopkg.in/src-d/go-git.v4/plumbing/format/config
gopkg.in/src-d/go-git.v4/plumbing/format/diff
gopkg.in/src-d/go-git.v4/utils/binary gopkg.in/src-d/go-git.v4/utils/binary
gopkg.in/src-d/go-git.v4/plumbing/format/idxfile gopkg.in/src-d/go-git.v4/plumbing/format/idxfile
gopkg.in/src-d/go-git.v4/plumbing/format/diff gopkg.in/src-d/go-git.v4/plumbing/format/objfile
gopkg.in/src-d/go-git.v4/storage/filesystem/dotgit
gopkg.in/src-d/go-git.v4/plumbing/format/pktline gopkg.in/src-d/go-git.v4/plumbing/format/pktline
gopkg.in/src-d/go-git.v4/plumbing/transport/file gopkg.in/src-d/go-git.v4/plumbing/transport/file
gopkg.in/src-d/go-git.v4/plumbing/transport/git gopkg.in/src-d/go-git.v4/plumbing/transport/git
gopkg.in/src-d/go-git.v4/plumbing/transport/http gopkg.in/src-d/go-git.v4/plumbing/transport/http
gopkg.in/src-d/go-git.v4/plumbing/transport/ssh gopkg.in/src-d/go-git.v4/plumbing/transport/ssh
gopkg.in/src-d/go-git.v4/plumbing/format/objfile
gopkg.in/src-d/go-git.v4/storage/filesystem/dotgit
gopkg.in/src-d/go-git.v4/utils/merkletrie/internal/frame gopkg.in/src-d/go-git.v4/utils/merkletrie/internal/frame
gopkg.in/src-d/go-git.v4/plumbing/transport/internal/common gopkg.in/src-d/go-git.v4/plumbing/transport/internal/common
gopkg.in/src-d/go-git.v4/plumbing/transport/server gopkg.in/src-d/go-git.v4/plumbing/transport/server

Loading…
Cancel
Save