* 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 commentstags/v1.9.0-rc1
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 |
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 |
// |______ / |__| (____ /___| /\___ >___| / | // |______ / |__| (____ /___| /\___ >___| / | ||||
// \/ \/ \/ \/ \/ | // \/ \/ \/ \/ \/ | ||||
// 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 |
// 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 |
// 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) | |||||
} |
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 |
// 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 |
// 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) | |||||
} | } | ||||
} | } |
"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 |
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 | |||||
} | } |
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) | ||||
} | } |
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) | |||||
} |
"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 |
"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, | |||||
}, | |||||
}, | }, | ||||
}, | }, | ||||
}, | }, |
"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 | ||||
} | } | ||||
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 | ||||
} | } | ||||
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) |
"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) | |||||
} |
// 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 | ||||
} | } | ||||
// 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 | |||||
} | } |
// 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 | |||||
} | } |
// 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 { |
// 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. |
// 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 | |||||
} | } |
// 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 | ||||
} | } | ||||
} | } |
// 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()) | |||||
}, | }, | ||||
} | } | ||||
"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}}, | |||||
} | } | ||||
} | } | ||||
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", |
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(), |
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 { |
} | } | ||||
} | } | ||||
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()) |
Page: 1, | Page: 1, | ||||
TotalCount: 1, | TotalCount: 1, | ||||
} | } | ||||
assert.EqualValues(t, tree, expectedTree) | |||||
assert.EqualValues(t, expectedTree, tree) | |||||
} | } |
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 { |
} | } | ||||
// 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), |
// 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) |
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) | ||||
} | } | ||||
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()) |
"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 |
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 | ||||
} | } |
} | } | ||||
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) |
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()) |
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) |
{{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"> |
# 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 |