Browse Source

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
tags/v1.9.0-rc1
Filip Navara 5 years ago
parent
commit
2af67f6044
44 changed files with 724 additions and 748 deletions
  1. 2
    2
      go.mod
  2. 1
    1
      integrations/api_repo_git_blobs_test.go
  3. 0
    15
      models/error.go
  4. 3
    2
      models/pull.go
  5. 10
    47
      models/repo_branch.go
  6. 2
    1
      modules/context/repo.go
  7. 15
    51
      modules/git/blob.go
  8. 18
    27
      modules/git/blob_test.go
  9. 67
    13
      modules/git/commit.go
  10. 183
    271
      modules/git/commit_info.go
  11. 2
    2
      modules/git/commit_info_test.go
  12. 15
    0
      modules/git/error.go
  13. 13
    12
      modules/git/parse.go
  14. 20
    12
      modules/git/parse_test.go
  15. 26
    5
      modules/git/repo.go
  16. 8
    8
      modules/git/repo_blob.go
  17. 2
    1
      modules/git/repo_blob_test.go
  18. 55
    11
      modules/git/repo_branch.go
  19. 52
    95
      modules/git/repo_commit.go
  20. 19
    16
      modules/git/repo_tag.go
  21. 21
    8
      modules/git/repo_tree.go
  22. 4
    25
      modules/git/sha1.go
  23. 4
    5
      modules/git/signature.go
  24. 61
    30
      modules/git/tree.go
  25. 13
    4
      modules/git/tree_blob.go
  26. 53
    30
      modules/git/tree_entry.go
  27. 10
    8
      modules/git/tree_entry_test.go
  28. 1
    1
      modules/repofiles/blob_test.go
  29. 1
    1
      modules/repofiles/content.go
  30. 1
    1
      modules/repofiles/temp_repo.go
  31. 7
    6
      modules/repofiles/tree.go
  32. 2
    1
      modules/repofiles/tree_test.go
  33. 1
    1
      modules/repofiles/update.go
  34. 1
    1
      routers/api/v1/convert/convert.go
  35. 3
    2
      routers/api/v1/repo/branch.go
  36. 1
    0
      routers/repo/commit.go
  37. 6
    5
      routers/repo/editor.go
  38. 2
    3
      routers/repo/issue.go
  39. 2
    2
      routers/repo/setting_protected_branch.go
  40. 2
    9
      routers/repo/view.go
  41. 5
    4
      routers/repo/wiki.go
  42. 2
    1
      routers/repo/wiki_test.go
  43. 1
    1
      templates/repo/diff/page.tmpl
  44. 7
    7
      vendor/modules.txt

+ 2
- 2
go.mod View File

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

+ 1
- 1
integrations/api_repo_git_blobs_test.go View File

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

+ 0
- 15
models/error.go View File

// |______ / |__| (____ /___| /\___ >___| / // |______ / |__| (____ /___| /\___ >___| /
// \/ \/ \/ \/ \/ // \/ \/ \/ \/ \/


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

+ 3
- 2
models/pull.go View File

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




func (pr *PullRequest) apiFormat(e Engine) *api.PullRequest { func (pr *PullRequest) apiFormat(e Engine) *api.PullRequest {
var ( var (
baseBranch *Branch
headBranch *Branch
baseBranch *git.Branch
headBranch *git.Branch
baseCommit *git.Commit baseCommit *git.Commit
headCommit *git.Commit headCommit *git.Commit
err error err error

+ 10
- 47
models/repo_branch.go View File

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


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
func (repo *Repository) GetBranch(branch string) (*Branch, error) {
if !git.IsBranchExist(repo.RepoPath(), branch) {
return nil, ErrBranchNotExist{branch}
// GetBranch returns a branch by its name
func (repo *Repository) GetBranch(branch string) (*git.Branch, error) {
gitRepo, err := git.OpenRepository(repo.RepoPath())
if err != nil {
return nil, err
} }
return &Branch{
Path: repo.RepoPath(),
Name: branch,
}, nil

return gitRepo.GetBranch(branch)
} }


// GetBranches returns all the branches of a repository // GetBranches returns all the branches of a repository
func (repo *Repository) GetBranches() ([]*Branch, error) {
return GetBranchesByPath(repo.RepoPath())
func (repo *Repository) GetBranches() ([]*git.Branch, error) {
return git.GetBranchesByPath(repo.RepoPath())
} }


// CheckBranchName validates branch name with existing repository branches // CheckBranchName validates branch name with existing repository branches


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

+ 2
- 1
modules/context/repo.go View File

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

+ 15
- 51
modules/git/blob.go View File

// 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
*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 {
return nil, concatenateError(err, stderr.String())
}
return stdout, nil
}
ID SHA1


// 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()
gogitEncodedObj plumbing.EncodedObject
name string
} }


// 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())
cmd.Dir = b.repo.Path
cmd.Stderr = os.Stderr

stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, fmt.Errorf("StdoutPipe: %v", err)
}
return b.gogitEncodedObj.Reader()
}


if err = cmd.Start(); err != nil {
return nil, fmt.Errorf("Start: %v", err)
}
// Size returns the uncompressed size of the blob
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

+ 18
- 27
modules/git/blob_test.go View File

// 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"


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


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

+ 67
- 13
modules/git/commit.go View File

"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.
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 newGPGSignatureFromCommitline(data []byte, signatureStart int, tag bool) (*CommitGPGSignature, error) {
sig := new(CommitGPGSignature)
signatureEnd := bytes.LastIndex(data, []byte("-----END PGP SIGNATURE-----"))
if signatureEnd == -1 {
return nil, fmt.Errorf("end of commit signature not found")
func convertPGPSignature(c *object.Commit) *CommitGPGSignature {
if c.PGPSignature == "" {
return nil
}

var w strings.Builder
var err error

if _, err = fmt.Fprintf(&w, "tree %s\n", c.TreeHash.String()); err != nil {
return nil
}

for _, parent := range c.ParentHashes {
if _, err = fmt.Fprintf(&w, "parent %s\n", parent.String()); err != nil {
return nil
}
}

if _, err = fmt.Fprint(&w, "author "); err != nil {
return nil
} }
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:])

if err = c.Author.Encode(&w); err != nil {
return nil
}

if _, err = fmt.Fprint(&w, "\ncommitter "); err != nil {
return nil
}

if err = c.Committer.Encode(&w); err != nil {
return nil
}

if _, err = fmt.Fprintf(&w, "\n\n%s", c.Message); err != nil {
return nil
}

return &CommitGPGSignature{
Signature: c.PGPSignature,
Payload: w.String(),
}
}

func convertCommit(c *object.Commit) *Commit {
return &Commit{
ID: c.Hash,
CommitMessage: c.Message,
Committer: &c.Committer,
Author: &c.Author,
Signature: convertPGPSignature(c),
parents: c.ParentHashes,
} }
return sig, nil
} }


// Message returns the commit message. Same as retrieving CommitMessage directly. // Message returns the commit message. Same as retrieving CommitMessage directly.
} }
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
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

+ 183
- 271
modules/git/commit_info.go View File

package git package git


import ( import (
"bufio"
"context"
"fmt"
"os/exec"
"path"
"runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/emirpasic/gods/trees/binaryheap"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/object"
) )


const (
// parameters for searching for commit infos. If the untargeted search has
// not found any entries in the past 5 commits, and 12 or fewer entries
// remain, then we'll just let the targeted-searching threads finish off,
// and stop the untargeted search to not interfere.
deferToTargetedSearchColdStreak = 5
deferToTargetedSearchNumRemainingEntries = 12
)
// GetCommitsInfo gets information of all commits that are corresponding to these entries
func (tes Entries) GetCommitsInfo(commit *Commit, treePath string, cache LastCommitCache) ([][]interface{}, *Commit, error) {
entryPaths := make([]string, len(tes)+1)
// Get the commit for the treePath itself
entryPaths[0] = ""
for i, entry := range tes {
entryPaths[i+1] = entry.Name()
}


// 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
c, err := commit.repo.gogitRepo.CommitObject(plumbing.Hash(commit.ID))
if err != nil {
return nil, nil, err
}


/* mutable fields, must hold mutex to read or write */
// map from filepath to commit
commits map[string]*Commit
// set of filepaths that have been or are being searched for in a target search
targetedPaths map[string]struct{}
}
revs, err := getLastCommitForPaths(c, treePath, entryPaths)
if err != nil {
return nil, nil, err
}


func (state *getCommitsInfoState) numRemainingEntries() int {
state.lock.Lock()
defer state.lock.Unlock()
return len(state.entries) - len(state.commits)
}
commit.repo.gogitStorage.Close()


// getTargetEntryPath Returns the next path for a targeted-searching thread to
// search for, or returns the empty string if nothing left to search for
func (state *getCommitsInfoState) getTargetedEntryPath() string {
var targetedEntryPath string
state.lock.Lock()
defer state.lock.Unlock()
for _, entry := range state.entries {
entryPath := path.Join(state.treePath, entry.Name())
if _, ok := state.commits[entryPath]; ok {
continue
} else if _, ok = state.targetedPaths[entryPath]; ok {
continue
commitsInfo := make([][]interface{}, len(tes))
for i, entry := range tes {
if rev, ok := revs[entry.Name()]; ok {
entryCommit := convertCommit(rev)
if entry.IsSubModule() {
subModuleURL := ""
if subModule, err := commit.GetSubModule(entry.Name()); err != nil {
return nil, nil, err
} else if subModule != nil {
subModuleURL = subModule.URL
}
subModuleFile := NewSubModuleFile(entryCommit, subModuleURL, entry.ID.String())
commitsInfo[i] = []interface{}{entry, subModuleFile}
} else {
commitsInfo[i] = []interface{}{entry, entryCommit}
}
} else {
commitsInfo[i] = []interface{}{entry, nil}
} }
targetedEntryPath = entryPath
state.targetedPaths[entryPath] = struct{}{}
break
} }
return targetedEntryPath
}


// repeatedly perform targeted searches for unpopulated entries
func targetedSearch(state *getCommitsInfoState, done chan error, cache LastCommitCache) {
for {
entryPath := state.getTargetedEntryPath()
if len(entryPath) == 0 {
done <- nil
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
}
}
command := NewCommand("rev-list", "-1", state.headCommit.ID.String(), "--", entryPath)
output, err := command.RunInDir(state.headCommit.repo.Path)
if err != nil {
done <- err
return
}
id, err := NewIDFromString(strings.TrimSpace(output))
if err != nil {
done <- err
return
}
commit, err := state.headCommit.repo.getCommit(id)
if err != nil {
done <- err
return
}
state.update(entryPath, commit)
if cache != nil {
cache.Put(state.headCommit.repo.Path, state.headCommit.ID.String(), entryPath, commit)
}
// Retrieve the commit for the treePath itself (see above). We basically
// get it for free during the tree traversal and it's used for listing
// pages to display information about newest commit for a given path.
var treeCommit *Commit
if rev, ok := revs[""]; ok {
treeCommit = convertCommit(rev)
} }
return commitsInfo, treeCommit, nil
} }


func initGetCommitInfoState(entries Entries, headCommit *Commit, treePath string) *getCommitsInfoState {
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,
}
type commitAndPaths struct {
commit *object.Commit
// Paths that are still on the branch represented by commit
paths []string
// Set of hashes for the paths
hashes map[string]plumbing.Hash
} }


// GetCommitsInfo gets information of all commits that are corresponding to these entries
func (tes Entries) GetCommitsInfo(commit *Commit, treePath string, cache LastCommitCache) ([][]interface{}, error) {
state := initGetCommitInfoState(tes, commit, treePath)
if err := getCommitsInfo(state, cache); err != nil {
func getCommitTree(c *object.Commit, treePath string) (*object.Tree, error) {
tree, err := c.Tree()
if err != nil {
return nil, err return nil, err
} }
if len(state.commits) < len(state.entryPaths) {
return nil, fmt.Errorf("could not find commits for all entries")
}

commitsInfo := make([][]interface{}, len(tes))
for i, entry := range tes {
commit, ok := state.commits[path.Join(treePath, entry.Name())]
if !ok {
return nil, fmt.Errorf("could not find commit for %s", entry.Name())
}
switch entry.Type {
case ObjectCommit:
subModuleURL := ""
if subModule, err := state.headCommit.GetSubModule(entry.Name()); err != nil {
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) {
if rawEntryPath[0] == '"' {
var err error
rawEntryPath, err = strconv.Unquote(rawEntryPath)
// Optimize deep traversals by focusing only on the specific tree
if treePath != "" {
tree, err = tree.Tree(treePath)
if err != nil { if err != nil {
return rawEntryPath, err
return nil, err
} }
} }
var entryNameStartIndex int
if len(state.treePath) > 0 {
entryNameStartIndex = len(state.treePath) + 1
}


if index := strings.IndexByte(rawEntryPath[entryNameStartIndex:], '/'); index >= 0 {
return rawEntryPath[:entryNameStartIndex+index], nil
}
return rawEntryPath, nil
return tree, nil
} }


// update report that the given path was last modified by the given commit.
// Returns whether state.commits was updated
func (state *getCommitsInfoState) update(entryPath string, commit *Commit) bool {
if _, ok := state.entryPaths[entryPath]; !ok {
return false
}

var updated bool
state.lock.Lock()
defer state.lock.Unlock()
if _, ok := state.commits[entryPath]; !ok {
state.commits[entryPath] = commit
updated = true
func getFullPath(treePath, path string) string {
if treePath != "" {
if path != "" {
return treePath + "/" + path
}
return treePath
} }
return updated
return path
} }


const getCommitsInfoPretty = "--pretty=format:%H %ct %s"

func getCommitsInfo(state *getCommitsInfoState, cache LastCommitCache) error {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()

args := []string{"log", state.headCommit.ID.String(), getCommitsInfoPretty, "--name-status", "-c"}
if len(state.treePath) > 0 {
args = append(args, "--", state.treePath)
func getFileHashes(c *object.Commit, treePath string, paths []string) (map[string]plumbing.Hash, error) {
tree, err := getCommitTree(c, treePath)
if err == object.ErrDirectoryNotFound {
// The whole tree didn't exist, so return empty map
return make(map[string]plumbing.Hash), nil
} }
cmd := exec.CommandContext(ctx, "git", args...)
cmd.Dir = state.headCommit.repo.Path

readCloser, err := cmd.StdoutPipe()
if err != nil { if err != nil {
return err
return nil, err
} }


if err := cmd.Start(); err != nil {
return err
hashes := make(map[string]plumbing.Hash)
for _, path := range paths {
if path != "" {
entry, err := tree.FindEntry(path)
if err == nil {
hashes[path] = entry.Hash
}
} else {
hashes[path] = tree.Hash
}
} }
// 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()
done := make(chan error, numThreads)
for i := 0; i < numThreads; i++ {
go targetedSearch(state, done, cache)
}
return hashes, nil
}


scanner := bufio.NewScanner(readCloser)
err = state.processGitLogOutput(scanner)
func getLastCommitForPaths(c *object.Commit, treePath string, paths []string) (map[string]*object.Commit, error) {
// We do a tree traversal with nodes sorted by commit time
seen := make(map[plumbing.Hash]bool)
heap := binaryheap.NewWith(func(a, b interface{}) int {
if a.(*commitAndPaths).commit.Committer.When.Before(b.(*commitAndPaths).commit.Committer.When) {
return 1
}
return -1
})


// it is important that we close stdout here; if we do not close
// stdout, the subprocess will keep running, and the deffered call
// cmd.Wait() may block for a long time.
if closeErr := readCloser.Close(); closeErr != nil && err == nil {
err = closeErr
result := make(map[string]*object.Commit)
initialHashes, err := getFileHashes(c, treePath, paths)
if err != nil {
return nil, err
} }


for i := 0; i < numThreads; i++ {
doneErr := <-done
if doneErr != nil && err == nil {
err = doneErr
// Start search from the root commit and with full set of paths
heap.Push(&commitAndPaths{c, paths, initialHashes})

for {
cIn, ok := heap.Pop()
if !ok {
break
} }
}
return err
}
current := cIn.(*commitAndPaths)
currentID := current.commit.ID()


func (state *getCommitsInfoState) processGitLogOutput(scanner *bufio.Scanner) error {
// keep a local cache of seen paths to avoid acquiring a lock for paths
// we've already seen
seenPaths := make(map[string]struct{}, len(state.entryPaths))
// number of consecutive commits without any finds
coldStreak := 0
var commit *Commit
var err error
for scanner.Scan() {
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
}
if seen[currentID] {
continue continue
} }
if line[0] >= 'A' && line[0] <= 'X' { // a file was changed by the current commit
// look for the last tab, since for copies (C) and renames (R) two
// filenames are printed: src, then dest
tabIndex := strings.LastIndexByte(line, '\t')
if tabIndex < 1 {
return fmt.Errorf("misformatted line: %s", line)
seen[currentID] = true

// 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 {
break
} }
entryPath, err := state.cleanEntryPath(line[tabIndex+1:])
parents = append(parents, parent)
}

// Examine the current commit and set of interesting paths
numOfParentsWithPath := make([]int, len(current.paths))
pathChanged := make([]bool, len(current.paths))
parentHashes := make([]map[string]plumbing.Hash, len(parents))
for j, parent := range parents {
parentHashes[j], err = getFileHashes(parent, treePath, current.paths)
if err != nil { if err != nil {
return err
break
} }
if _, ok := seenPaths[entryPath]; !ok {
if state.update(entryPath, commit) {
coldStreak = 0

for i, path := range current.paths {
if parentHashes[j][path] != plumbing.ZeroHash {
numOfParentsWithPath[i]++
if parentHashes[j][path] != current.hashes[path] {
pathChanged[i] = true
}
} }
seenPaths[entryPath] = struct{}{}
} }
continue
} }


// a new commit
commit, err = parseCommitInfo(line)
if err != nil {
return err
var remainingPaths []string
for i, path := range current.paths {
switch numOfParentsWithPath[i] {
case 0:
// The path didn't exist in any parent, so it must have been created by
// this commit. The results could already contain some newer change from
// different path, so don't override that.
if result[path] == nil {
result[path] = current.commit
}
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
}
} else {
remainingPaths = append(remainingPaths, path)
}
default:
// The file is present on more than one of the parent paths, so this is
// 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
// changed during the merge, but it wouldn't tell us the relevant commit
// that introduced it.
remainingPaths = append(remainingPaths, path)
}
} }
coldStreak++
}
return scanner.Err()
}


// parseCommitInfo parse a commit from a line of `git log` output. Expects the
// 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 nil, err
if len(remainingPaths) > 0 {
// Add the parent nodes along with remaining paths to the heap for further
// processing.
for j, parent := range parents {
if seen[parent.ID()] {
continue
}

// Combine remainingPath with paths available on the parent branch
// and make union of them
var remainingPathsForParent []string
for _, path := range remainingPaths {
if parentHashes[j][path] != plumbing.ZeroHash {
remainingPathsForParent = append(remainingPathsForParent, path)
}
}

heap.Push(&commitAndPaths{parent, remainingPathsForParent, parentHashes[j]})
}
}
} }
message := line[spaceIndex+42:]
return &Commit{
ID: ref,
CommitMessage: message,
Committer: &Signature{
When: time.Unix(int64(unixSeconds), 0),
},
}, nil

return result, nil
} }

+ 2
- 2
modules/git/commit_info_test.go View File

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

+ 15
- 0
modules/git/error.go View File

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

+ 13
- 12
modules/git/parse.go View File

"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.
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.Type = ObjectBlob
entry.gogitTreeEntry.Mode = filemode.Regular
pos += 12 // skip over "100644 blob " pos += 12 // skip over "100644 blob "
case "100755": case "100755":
entry.mode = EntryModeExec
entry.Type = ObjectBlob
entry.gogitTreeEntry.Mode = filemode.Executable
pos += 12 // skip over "100755 blob " pos += 12 // skip over "100755 blob "
case "120000": case "120000":
entry.mode = EntryModeSymlink
entry.Type = ObjectBlob
entry.gogitTreeEntry.Mode = filemode.Symlink
pos += 12 // skip over "120000 blob " pos += 12 // skip over "120000 blob "
case "160000": case "160000":
entry.mode = EntryModeCommit
entry.Type = ObjectCommit
entry.gogitTreeEntry.Mode = filemode.Submodule
pos += 14 // skip over "160000 object " pos += 14 // skip over "160000 object "
case "040000": case "040000":
entry.mode = EntryModeTree
entry.Type = ObjectTree
entry.gogitTreeEntry.Mode = filemode.Dir
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]))
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')


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

+ 20
- 12
modules/git/parse_test.go View File

"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) {
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"),
name: "example/file2.txt",
ID: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
gogitTreeEntry: &object.TreeEntry{
Hash: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
Name: "example/file2.txt",
Mode: filemode.Regular,
},
}, },
}, },
}, },
"040000 tree 1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8\texample\n", "040000 tree 1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8\texample\n",
Expected: []*TreeEntry{ Expected: []*TreeEntry{
{ {
ID: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
Type: ObjectBlob,
mode: EntryModeSymlink,
name: "example/\n.txt",
ID: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
gogitTreeEntry: &object.TreeEntry{
Hash: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
Name: "example/\n.txt",
Mode: filemode.Symlink,
},
}, },
{ {
ID: MustIDFromString("1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8"),
Type: ObjectTree,
mode: EntryModeTree,
name: "example",
ID: MustIDFromString("1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8"),
gogitTreeEntry: &object.TreeEntry{
Hash: MustIDFromString("1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8"),
Name: "example",
Mode: filemode.Dir,
},
}, },
}, },
}, },

+ 26
- 5
modules/git/repo.go View File

"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`
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,
commitCache: newObjectCache(),
tagCache: newObjectCache(),
Path: repoPath,
gogitRepo: gogitRepo,
gogitStorage: storage,
tagCache: newObjectCache(),
}, nil }, nil
} }



+ 8
- 8
modules/git/repo_blob.go View File



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



+ 2
- 1
modules/git/repo_blob_test.go View File

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)

+ 55
- 11
modules/git/repo_branch.go View File

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




// 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.
} }


return &Branch{ return &Branch{
Name: stdout[len(BranchPrefix):],
Path: stdout,
Name: stdout[len(BranchPrefix):],
Path: stdout,
gitRepo: repo,
}, nil }, nil
} }




// 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 {
branches = append(branches, branch.Name().Short())
return nil
}); err != nil {

brs, err := gitRepo.GetBranches()
if 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
} }


_, 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)
}

+ 52
- 95
modules/git/repo_commit.go View File

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


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.
return strings.TrimSpace(stdout), nil return strings.TrimSpace(stdout), nil
} }


// parseCommitData parses commit information from the (uncompressed) raw
// data from the commit object.
// \n\n separate headers from message
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
sigindex := strings.Index(cm, "-----BEGIN PGP SIGNATURE-----")
if sigindex != -1 {
sig, err := newGPGSignatureFromCommitline(data, (nextline+1)+sigindex, true)
if err == nil && sig != nil {
// remove signature from commit message
if sigindex == 0 {
cm = ""
} else {
cm = cm[:sigindex-1]
}
commit.Signature = sig
}
}
func convertPGPSignatureForTag(t *object.Tag) *CommitGPGSignature {
if t.PGPSignature == "" {
return nil
}


commit.CommitMessage = cm
break l
default:
break l
}
var w strings.Builder
var err error

if _, err = fmt.Fprintf(&w,
"object %s\ntype %s\ntag %s\ntagger ",
t.Target.String(), t.TargetType.Bytes(), t.Name); err != nil {
return nil
}

if err = t.Tagger.Encode(&w); err != nil {
return nil
}

if _, err = fmt.Fprintf(&w, "\n\n"); err != nil {
return nil
}

if _, err = fmt.Fprintf(&w, t.Message); err != nil {
return nil
}

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())
if ok {
log("Hit cache: %s", id)
return c.(*Commit), nil
}
var tagObject *object.Tag


data, err := NewCommand("cat-file", "-p", id.String()).RunInDirBytes(repo.Path)
if err != nil {
if strings.Contains(err.Error(), "fatal: Not a valid object name") {
return nil, ErrNotExist{id.String(), ""}
gogitCommit, err := repo.gogitRepo.CommitObject(plumbing.Hash(id))
if err == plumbing.ErrObjectNotFound {
tagObject, err = repo.gogitRepo.TagObject(plumbing.Hash(id))
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.Branch = strings.Split(strings.Split(string(data), " ")[1], "~")[0]
commit.Tree.ID = tree.Hash
commit.Tree.gogitTree = tree


repo.commitCache.Set(id.String(), commit)
return commit, nil return commit, nil
} }



+ 19
- 16
modules/git/repo_tag.go View File

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


"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


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


// 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")
if version.Compare(gitVersion, "2.0.0", ">=") {
cmd.AddArguments("--sort=-v:refname")
}
var tagNames []string


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 = tags[:len(tags)-1]
tags.ForEach(func(tag *plumbing.Reference) error {
tagNames = append(tagNames, strings.TrimPrefix(tag.Name().String(), TagPrefix))
return nil
})


if version.Compare(gitVersion, "2.0.0", "<") {
version.Sort(tags)
version.Sort(tagNames)


// Reverse order
for i := 0; i < len(tags)/2; i++ {
j := len(tags) - i - 1
tags[i], tags[j] = tags[j], tags[i]
}
// Reverse order
for i := 0; i < len(tagNames)/2; i++ {
j := len(tagNames) - i - 1
tagNames[i], tagNames[j] = tagNames[j], tagNames[i]
} }


return tags, nil
return tagNames, nil
} }

+ 21
- 8
modules/git/repo_tree.go View File

// 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())
if isFile(treePath) {
_, err := NewCommand("ls-tree", id.String()).RunInDir(repo.Path)
if err != nil {
return nil, ErrNotExist{id.String(), ""}
}
gogitTree, err := repo.gogitRepo.TreeObject(plumbing.Hash(id))
if err != nil {
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.
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
} }

+ 4
- 25
modules/git/sha1.go View File

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

// 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[:])
}
type SHA1 = plumbing.Hash


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

+ 4
- 5
modules/git/signature.go View File

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


"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 {
Email string
Name string
When time.Time
}
type Signature = object.Signature


const ( const (
// GitTimeLayout is the (default) time layout used by git. // GitTimeLayout is the (default) time layout used by git.

+ 61
- 30
modules/git/tree.go View File

// 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
repo *Repository
ID SHA1
CommitID SHA1
repo *Repository

gogitTree *object.Tree


// parent tree // parent tree
ptree *Tree ptree *Tree

entries Entries
entriesParsed bool

entriesRecursive Entries
entriesRecursiveParsed bool
} }


// NewTree create a new tree according the repository and commit id
// NewTree create a new tree according the repository and tree id
func NewTree(repo *Repository, id SHA1) *Tree { func NewTree(repo *Repository, id SHA1) *Tree {
return &Tree{ return &Tree{
ID: id, ID: id,
return g, nil return g, nil
} }


// ListEntries returns all entries of current tree.
func (t *Tree) ListEntries() (Entries, error) {
if t.entriesParsed {
return t.entries, nil
func (t *Tree) loadTreeObject() error {
gogitTree, err := t.repo.gogitRepo.TreeObject(plumbing.Hash(t.ID))
if err != nil {
return err
} }


stdout, err := NewCommand("ls-tree", t.ID.String()).RunInDirBytes(t.repo.Path)
if err != nil {
return nil, err
t.gogitTree = gogitTree
return nil
}

// ListEntries returns all entries of current tree.
func (t *Tree) ListEntries() (Entries, error) {
if t.gogitTree == nil {
err := t.loadTreeObject()
if err != nil {
return nil, err
}
} }


t.entries, err = parseTreeEntries(stdout, t)
if err == nil {
t.entriesParsed = true
entries := make([]*TreeEntry, len(t.gogitTree.Entries))
for i, entry := range t.gogitTree.Entries {
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 {
return t.entriesRecursive, nil
}
stdout, err := NewCommand("ls-tree", "-t", "-r", t.ID.String()).RunInDirBytes(t.repo.Path)
if err != nil {
return nil, err
if t.gogitTree == nil {
err := t.loadTreeObject()
if err != nil {
return nil, err
}
} }


t.entriesRecursive, err = parseTreeEntries(stdout, t)
if err == nil {
t.entriesRecursiveParsed = true
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
}
if err != nil {
return nil, err
}
if seen[entry.Hash] {
continue
}

convertedEntry := &TreeEntry{
ID: entry.Hash,
gogitTreeEntry: &entry,
ptree: t,
}
entries = append(entries, convertedEntry)
} }


return t.entriesRecursive, err
return entries, nil
} }

+ 13
- 4
modules/git/tree_blob.go View File

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


import ( import (
"path" "path"
"strings" "strings"

"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/filemode"
"gopkg.in/src-d/go-git.v4/plumbing/object"
) )


// GetTreeEntryByPath get the tree entries according the sub dir // GetTreeEntryByPath get the tree entries according the sub dir
func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) { func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) {
if len(relpath) == 0 { if len(relpath) == 0 {
return &TreeEntry{ return &TreeEntry{
ID: t.ID,
Type: ObjectTree,
mode: EntryModeTree,
ID: t.ID,
//Type: ObjectTree,
gogitTreeEntry: &object.TreeEntry{
Name: "",
Mode: filemode.Dir,
Hash: plumbing.Hash(t.ID),
},
}, nil }, nil
} }


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

+ 53
- 30
modules/git/tree_entry.go View File

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


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
// 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
Type ObjectType

mode EntryMode
name string

ptree *Tree
ID SHA1


committed bool
gogitTreeEntry *object.TreeEntry
ptree *Tree


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
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,
TreeEntry: te,
ID: te.gogitTreeEntry.Hash,
gogitEncodedObj: encodedObj,
name: te.Name(),
} }
} }


} }


// 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 {
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
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())
}, },
} }



+ 10
- 8
modules/git/tree_entry_test.go View File

"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{name: "v2.0", mode: EntryModeTree},
&TreeEntry{name: "v2.1", mode: EntryModeTree},
&TreeEntry{name: "v2.12", mode: EntryModeTree},
&TreeEntry{name: "v2.2", mode: EntryModeTree},
&TreeEntry{name: "v12.0", mode: EntryModeTree},
&TreeEntry{name: "abc", mode: EntryModeBlob},
&TreeEntry{name: "bcd", mode: EntryModeBlob},
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v1.0", Mode: filemode.Dir}},
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.0", Mode: filemode.Dir}},
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.1", Mode: filemode.Dir}},
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.12", Mode: filemode.Dir}},
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.2", Mode: filemode.Dir}},
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v12.0", Mode: filemode.Dir}},
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "abc", Mode: filemode.Regular}},
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "bcd", Mode: filemode.Regular}},
} }
} }



+ 1
- 1
modules/repofiles/blob_test.go View File



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

+ 1
- 1
modules/repofiles/content.go View File

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

+ 1
- 1
modules/repofiles/temp_repo.go View File

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 {

+ 7
- 6
modules/repofiles/tree.go View File

} }
} }
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 {
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[i].Type = string(entries[e].Type)
tree.Entries[i].Size = entries[e].Size()
tree.Entries[i].SHA = entries[e].ID.String()

tree.Entries[e].Path = entries[e].Name()
tree.Entries[e].Mode = fmt.Sprintf("%06o", entries[e].Mode())
tree.Entries[e].Type = entries[e].Type()
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())

+ 2
- 1
modules/repofiles/tree_test.go View File

Page: 1, Page: 1,
TotalCount: 1, TotalCount: 1,
} }
assert.EqualValues(t, tree, expectedTree)

assert.EqualValues(t, expectedTree, tree)
} }

+ 1
- 1
modules/repofiles/update.go View File

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 {

+ 1
- 1
routers/api/v1/convert/convert.go View File

} }


// 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),

+ 3
- 2
routers/api/v1/repo/branch.go View File

// 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"
} }
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)

+ 1
- 0
routers/repo/commit.go View File

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



+ 6
- 5
routers/repo/editor.go View File

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

+ 2
- 3
routers/repo/issue.go View File

"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"strconv" "strconv"
} }


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

+ 2
- 2
routers/repo/setting_protected_branch.go View File



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


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

+ 2
- 9
routers/repo/view.go View File

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


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

+ 5
- 4
routers/repo/wiki.go View File

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
} }
} }
// 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)
} }
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())
} }
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())

+ 2
- 1
routers/repo/wiki_test.go View File

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)

+ 1
- 1
templates/repo/diff/page.tmpl View File

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

+ 7
- 7
vendor/modules.txt View File

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