aboutsummaryrefslogtreecommitdiffstats
path: root/modules/git
diff options
context:
space:
mode:
authorzeripath <art27@cantab.net>2020-12-17 14:00:47 +0000
committerGitHub <noreply@github.com>2020-12-17 22:00:47 +0800
commit511f6138d4b5b7a464a8fa3d7f8fc52bec3789a4 (patch)
tree126d29951a505dfe499357131b31b0bde57a7896 /modules/git
parent0851a895819e0a5a1a79dcbd596d4c93d4d47a76 (diff)
downloadgitea-511f6138d4b5b7a464a8fa3d7f8fc52bec3789a4.tar.gz
gitea-511f6138d4b5b7a464a8fa3d7f8fc52bec3789a4.zip
Use native git variants by default with go-git variants as build tag (#13673)
* Move last commit cache back into modules/git Signed-off-by: Andrew Thornton <art27@cantab.net> * Remove go-git from the interface for last commit cache Signed-off-by: Andrew Thornton <art27@cantab.net> * move cacheref to last_commit_cache Signed-off-by: Andrew Thornton <art27@cantab.net> * Remove go-git from routers/private/hook Signed-off-by: Andrew Thornton <art27@cantab.net> * Move FindLFSFiles to pipeline Signed-off-by: Andrew Thornton <art27@cantab.net> * Make no-go-git variants Signed-off-by: Andrew Thornton <art27@cantab.net> * Submodule RefID Signed-off-by: Andrew Thornton <art27@cantab.net> * fix issue with GetCommitsInfo Signed-off-by: Andrew Thornton <art27@cantab.net> * fix GetLastCommitForPaths Signed-off-by: Andrew Thornton <art27@cantab.net> * Improve efficiency Signed-off-by: Andrew Thornton <art27@cantab.net> * More efficiency Signed-off-by: Andrew Thornton <art27@cantab.net> * even faster Signed-off-by: Andrew Thornton <art27@cantab.net> * Reduce duplication * As per @lunny Signed-off-by: Andrew Thornton <art27@cantab.net> * attempt to fix drone Signed-off-by: Andrew Thornton <art27@cantab.net> * fix test-tags Signed-off-by: Andrew Thornton <art27@cantab.net> * default to use no-go-git variants and add gogit build tag Signed-off-by: Andrew Thornton <art27@cantab.net> * placate lint Signed-off-by: Andrew Thornton <art27@cantab.net> * as per @6543 Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: techknowlogick <techknowlogick@gitea.io>
Diffstat (limited to 'modules/git')
-rw-r--r--modules/git/batch_reader_nogogit.go243
-rw-r--r--modules/git/blob.go21
-rw-r--r--modules/git/blob_gogit.go33
-rw-r--r--modules/git/blob_nogogit.go77
-rw-r--r--modules/git/cache.go13
-rw-r--r--modules/git/command.go2
-rw-r--r--modules/git/commit.go59
-rw-r--r--modules/git/commit_convert_gogit.go70
-rw-r--r--modules/git/commit_info.go287
-rw-r--r--modules/git/commit_info_gogit.go291
-rw-r--r--modules/git/commit_info_nogogit.go370
-rw-r--r--modules/git/commit_info_test.go16
-rw-r--r--modules/git/commit_reader.go46
-rw-r--r--modules/git/last_commit_cache.go29
-rw-r--r--modules/git/last_commit_cache_gogit.go113
-rw-r--r--modules/git/last_commit_cache_nogogit.go103
-rw-r--r--modules/git/notes.go65
-rw-r--r--modules/git/notes_gogit.go72
-rw-r--r--modules/git/notes_nogogit.go59
-rw-r--r--modules/git/parse_gogit.go (renamed from modules/git/parse.go)2
-rw-r--r--modules/git/parse_gogit_test.go (renamed from modules/git/parse_test.go)2
-rw-r--r--modules/git/parse_nogogit.go78
-rw-r--r--modules/git/pipeline/lfs.go159
-rw-r--r--modules/git/pipeline/lfs_nogogit.go266
-rw-r--r--modules/git/repo.go64
-rw-r--r--modules/git/repo_base_gogit.go76
-rw-r--r--modules/git/repo_base_nogogit.go40
-rw-r--r--modules/git/repo_blob.go18
-rw-r--r--modules/git/repo_blob_gogit.go23
-rw-r--r--modules/git/repo_blob_nogogit.go17
-rw-r--r--modules/git/repo_branch.go33
-rw-r--r--modules/git/repo_branch_gogit.go45
-rw-r--r--modules/git/repo_branch_nogogit.go82
-rw-r--r--modules/git/repo_commit.go98
-rw-r--r--modules/git/repo_commit_gogit.go110
-rw-r--r--modules/git/repo_commit_nogogit.go109
-rw-r--r--modules/git/repo_commitgraph_gogit.go (renamed from modules/git/repo_commitgraph.go)2
-rw-r--r--modules/git/repo_language_stats.go106
-rw-r--r--modules/git/repo_language_stats_gogit.go113
-rw-r--r--modules/git/repo_language_stats_nogogit.go109
-rw-r--r--modules/git/repo_object.go5
-rw-r--r--modules/git/repo_ref.go45
-rw-r--r--modules/git/repo_ref_gogit.go52
-rw-r--r--modules/git/repo_ref_nogogit.go84
-rw-r--r--modules/git/repo_tag.go31
-rw-r--r--modules/git/repo_tag_gogit.go43
-rw-r--r--modules/git/repo_tag_nogogit.go18
-rw-r--r--modules/git/repo_tree.go41
-rw-r--r--modules/git/repo_tree_gogit.go47
-rw-r--r--modules/git/repo_tree_nogogit.go98
-rw-r--r--modules/git/sha1.go5
-rw-r--r--modules/git/sha1_gogit.go20
-rw-r--r--modules/git/sha1_nogogit.go62
-rw-r--r--modules/git/signature.go46
-rw-r--r--modules/git/signature_gogit.go54
-rw-r--r--modules/git/signature_nogogit.go95
-rw-r--r--modules/git/tag.go31
-rw-r--r--modules/git/tree.go83
-rw-r--r--modules/git/tree_blob.go58
-rw-r--r--modules/git/tree_blob_gogit.go66
-rw-r--r--modules/git/tree_blob_nogogit.go49
-rw-r--r--modules/git/tree_entry.go104
-rw-r--r--modules/git/tree_entry_gogit.go96
-rw-r--r--modules/git/tree_entry_mode.go36
-rw-r--r--modules/git/tree_entry_nogogit.go91
-rw-r--r--modules/git/tree_entry_test.go2
-rw-r--r--modules/git/tree_gogit.go94
-rw-r--r--modules/git/tree_nogogit.go69
-rw-r--r--modules/git/utils.go32
69 files changed, 3870 insertions, 1208 deletions
diff --git a/modules/git/batch_reader_nogogit.go b/modules/git/batch_reader_nogogit.go
new file mode 100644
index 0000000000..6a236e5002
--- /dev/null
+++ b/modules/git/batch_reader_nogogit.go
@@ -0,0 +1,243 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// +build !gogit
+
+package git
+
+import (
+ "bufio"
+ "bytes"
+ "math"
+ "strconv"
+)
+
+// ReadBatchLine reads the header line from cat-file --batch
+// We expect:
+// <sha> SP <type> SP <size> LF
+func ReadBatchLine(rd *bufio.Reader) (sha []byte, typ string, size int64, err error) {
+ sha, err = rd.ReadBytes(' ')
+ if err != nil {
+ return
+ }
+ sha = sha[:len(sha)-1]
+
+ typ, err = rd.ReadString(' ')
+ if err != nil {
+ return
+ }
+ typ = typ[:len(typ)-1]
+
+ var sizeStr string
+ sizeStr, err = rd.ReadString('\n')
+ if err != nil {
+ return
+ }
+
+ size, err = strconv.ParseInt(sizeStr[:len(sizeStr)-1], 10, 64)
+ return
+}
+
+// ReadTagObjectID reads a tag object ID hash from a cat-file --batch stream, throwing away the rest of the stream.
+func ReadTagObjectID(rd *bufio.Reader, size int64) (string, error) {
+ id := ""
+ var n int64
+headerLoop:
+ for {
+ line, err := rd.ReadBytes('\n')
+ if err != nil {
+ return "", err
+ }
+ n += int64(len(line))
+ idx := bytes.Index(line, []byte{' '})
+ if idx < 0 {
+ continue
+ }
+
+ if string(line[:idx]) == "object" {
+ id = string(line[idx+1 : len(line)-1])
+ break headerLoop
+ }
+ }
+
+ // Discard the rest of the tag
+ discard := size - n
+ for discard > math.MaxInt32 {
+ _, err := rd.Discard(math.MaxInt32)
+ if err != nil {
+ return id, err
+ }
+ discard -= math.MaxInt32
+ }
+ _, err := rd.Discard(int(discard))
+ return id, err
+}
+
+// ReadTreeID reads a tree ID from a cat-file --batch stream, throwing away the rest of the stream.
+func ReadTreeID(rd *bufio.Reader, size int64) (string, error) {
+ id := ""
+ var n int64
+headerLoop:
+ for {
+ line, err := rd.ReadBytes('\n')
+ if err != nil {
+ return "", err
+ }
+ n += int64(len(line))
+ idx := bytes.Index(line, []byte{' '})
+ if idx < 0 {
+ continue
+ }
+
+ if string(line[:idx]) == "tree" {
+ id = string(line[idx+1 : len(line)-1])
+ break headerLoop
+ }
+ }
+
+ // Discard the rest of the commit
+ discard := size - n
+ for discard > math.MaxInt32 {
+ _, err := rd.Discard(math.MaxInt32)
+ if err != nil {
+ return id, err
+ }
+ discard -= math.MaxInt32
+ }
+ _, err := rd.Discard(int(discard))
+ return id, err
+}
+
+// git tree files are a list:
+// <mode-in-ascii> SP <fname> NUL <20-byte SHA>
+//
+// Unfortunately this 20-byte notation is somewhat in conflict to all other git tools
+// Therefore we need some method to convert these 20-byte SHAs to a 40-byte SHA
+
+// constant hextable to help quickly convert between 20byte and 40byte hashes
+const hextable = "0123456789abcdef"
+
+// to40ByteSHA converts a 20-byte SHA in a 40-byte slice into a 40-byte sha in place
+// without allocations. This is at least 100x quicker that hex.EncodeToString
+// NB This requires that sha is a 40-byte slice
+func to40ByteSHA(sha []byte) []byte {
+ for i := 19; i >= 0; i-- {
+ v := sha[i]
+ vhi, vlo := v>>4, v&0x0f
+ shi, slo := hextable[vhi], hextable[vlo]
+ sha[i*2], sha[i*2+1] = shi, slo
+ }
+ return sha
+}
+
+// ParseTreeLineSkipMode reads an entry from a tree in a cat-file --batch stream
+// This simply skips the mode - saving a substantial amount of time and carefully avoids allocations - except where fnameBuf is too small.
+// It is recommended therefore to pass in an fnameBuf large enough to avoid almost all allocations
+//
+// Each line is composed of:
+// <mode-in-ascii-dropping-initial-zeros> SP <fname> NUL <20-byte SHA>
+//
+// We don't attempt to convert the 20-byte SHA to 40-byte SHA to save a lot of time
+func ParseTreeLineSkipMode(rd *bufio.Reader, fnameBuf, shaBuf []byte) (fname, sha []byte, n int, err error) {
+ var readBytes []byte
+ // Skip the Mode
+ readBytes, err = rd.ReadSlice(' ') // NB: DOES NOT ALLOCATE SIMPLY RETURNS SLICE WITHIN READER BUFFER
+ if err != nil {
+ return
+ }
+ n += len(readBytes)
+
+ // Deal with the fname
+ readBytes, err = rd.ReadSlice('\x00')
+ copy(fnameBuf, readBytes)
+ if len(fnameBuf) > len(readBytes) {
+ fnameBuf = fnameBuf[:len(readBytes)] // cut the buf the correct size
+ } else {
+ fnameBuf = append(fnameBuf, readBytes[len(fnameBuf):]...) // extend the buf and copy in the missing bits
+ }
+ for err == bufio.ErrBufferFull { // Then we need to read more
+ readBytes, err = rd.ReadSlice('\x00')
+ fnameBuf = append(fnameBuf, readBytes...) // there is little point attempting to avoid allocations here so just extend
+ }
+ n += len(fnameBuf)
+ if err != nil {
+ return
+ }
+ fnameBuf = fnameBuf[:len(fnameBuf)-1] // Drop the terminal NUL
+ fname = fnameBuf // set the returnable fname to the slice
+
+ // Now deal with the 20-byte SHA
+ idx := 0
+ for idx < 20 {
+ read := 0
+ read, err = rd.Read(shaBuf[idx:20])
+ n += read
+ if err != nil {
+ return
+ }
+ idx += read
+ }
+ sha = shaBuf
+ return
+}
+
+// ParseTreeLine reads an entry from a tree in a cat-file --batch stream
+// This carefully avoids allocations - except where fnameBuf is too small.
+// It is recommended therefore to pass in an fnameBuf large enough to avoid almost all allocations
+//
+// Each line is composed of:
+// <mode-in-ascii-dropping-initial-zeros> SP <fname> NUL <20-byte SHA>
+//
+// We don't attempt to convert the 20-byte SHA to 40-byte SHA to save a lot of time
+func ParseTreeLine(rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) {
+ var readBytes []byte
+
+ // Read the Mode
+ readBytes, err = rd.ReadSlice(' ')
+ if err != nil {
+ return
+ }
+ n += len(readBytes)
+ copy(modeBuf, readBytes)
+ if len(modeBuf) > len(readBytes) {
+ modeBuf = modeBuf[:len(readBytes)]
+ } else {
+ modeBuf = append(modeBuf, readBytes[len(modeBuf):]...)
+
+ }
+ mode = modeBuf[:len(modeBuf)-1] // Drop the SP
+
+ // Deal with the fname
+ readBytes, err = rd.ReadSlice('\x00')
+ copy(fnameBuf, readBytes)
+ if len(fnameBuf) > len(readBytes) {
+ fnameBuf = fnameBuf[:len(readBytes)]
+ } else {
+ fnameBuf = append(fnameBuf, readBytes[len(fnameBuf):]...)
+ }
+ for err == bufio.ErrBufferFull {
+ readBytes, err = rd.ReadSlice('\x00')
+ fnameBuf = append(fnameBuf, readBytes...)
+ }
+ n += len(fnameBuf)
+ if err != nil {
+ return
+ }
+ fnameBuf = fnameBuf[:len(fnameBuf)-1]
+ fname = fnameBuf
+
+ // Deal with the 20-byte SHA
+ idx := 0
+ for idx < 20 {
+ read := 0
+ read, err = rd.Read(shaBuf[idx:20])
+ n += read
+ if err != nil {
+ return
+ }
+ idx += read
+ }
+ sha = shaBuf
+ return
+}
diff --git a/modules/git/blob.go b/modules/git/blob.go
index 98545f2f90..674a6a9592 100644
--- a/modules/git/blob.go
+++ b/modules/git/blob.go
@@ -10,28 +10,9 @@ import (
"encoding/base64"
"io"
"io/ioutil"
-
- "github.com/go-git/go-git/v5/plumbing"
)
-// Blob represents a Git object.
-type Blob struct {
- ID SHA1
-
- gogitEncodedObj plumbing.EncodedObject
- name string
-}
-
-// DataAsync gets a ReadCloser for the contents of a blob without reading it all.
-// Calling the Close function on the result will discard all unread output.
-func (b *Blob) DataAsync() (io.ReadCloser, error) {
- return b.gogitEncodedObj.Reader()
-}
-
-// Size returns the uncompressed size of the blob
-func (b *Blob) Size() int64 {
- return b.gogitEncodedObj.Size()
-}
+// This file contains common functions between the gogit and !gogit variants for git Blobs
// Name returns name of the tree entry this blob object was created from (or empty string)
func (b *Blob) Name() string {
diff --git a/modules/git/blob_gogit.go b/modules/git/blob_gogit.go
new file mode 100644
index 0000000000..7a82eb5c37
--- /dev/null
+++ b/modules/git/blob_gogit.go
@@ -0,0 +1,33 @@
+// 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
+// license that can be found in the LICENSE file.
+
+// +build gogit
+
+package git
+
+import (
+ "io"
+
+ "github.com/go-git/go-git/v5/plumbing"
+)
+
+// Blob represents a Git object.
+type Blob struct {
+ ID SHA1
+
+ gogitEncodedObj plumbing.EncodedObject
+ name string
+}
+
+// DataAsync gets a ReadCloser for the contents of a blob without reading it all.
+// Calling the Close function on the result will discard all unread output.
+func (b *Blob) DataAsync() (io.ReadCloser, error) {
+ return b.gogitEncodedObj.Reader()
+}
+
+// Size returns the uncompressed size of the blob
+func (b *Blob) Size() int64 {
+ return b.gogitEncodedObj.Size()
+}
diff --git a/modules/git/blob_nogogit.go b/modules/git/blob_nogogit.go
new file mode 100644
index 0000000000..401b172860
--- /dev/null
+++ b/modules/git/blob_nogogit.go
@@ -0,0 +1,77 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// +build !gogit
+
+package git
+
+import (
+ "bufio"
+ "io"
+ "strconv"
+ "strings"
+)
+
+// Blob represents a Git object.
+type Blob struct {
+ ID SHA1
+
+ gotSize bool
+ size int64
+ repoPath string
+ name string
+}
+
+// DataAsync gets a ReadCloser for the contents of a blob without reading it all.
+// Calling the Close function on the result will discard all unread output.
+func (b *Blob) DataAsync() (io.ReadCloser, error) {
+ stdoutReader, stdoutWriter := io.Pipe()
+ var err error
+
+ go func() {
+ stderr := &strings.Builder{}
+ err = NewCommand("cat-file", "--batch").RunInDirFullPipeline(b.repoPath, stdoutWriter, stderr, strings.NewReader(b.ID.String()+"\n"))
+ if err != nil {
+ err = ConcatenateError(err, stderr.String())
+ _ = stdoutWriter.CloseWithError(err)
+ } else {
+ _ = stdoutWriter.Close()
+ }
+ }()
+
+ bufReader := bufio.NewReader(stdoutReader)
+ _, _, size, err := ReadBatchLine(bufReader)
+ if err != nil {
+ stdoutReader.Close()
+ return nil, err
+ }
+
+ return &LimitedReaderCloser{
+ R: bufReader,
+ C: stdoutReader,
+ N: int64(size),
+ }, err
+}
+
+// Size returns the uncompressed size of the blob
+func (b *Blob) Size() int64 {
+ if b.gotSize {
+ return b.size
+ }
+
+ size, err := NewCommand("cat-file", "-s", b.ID.String()).RunInDir(b.repoPath)
+ if err != nil {
+ log("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repoPath, err)
+ return 0
+ }
+
+ b.size, err = strconv.ParseInt(size[:len(size)-1], 10, 64)
+ if err != nil {
+ log("error whilst parsing size %s for %s in %s. Error: %v", size, b.ID.String(), b.repoPath, err)
+ return 0
+ }
+ b.gotSize = true
+
+ return b.size
+}
diff --git a/modules/git/cache.go b/modules/git/cache.go
deleted file mode 100644
index a1f0f8a57b..0000000000
--- a/modules/git/cache.go
+++ /dev/null
@@ -1,13 +0,0 @@
-// Copyright 2019 The Gitea Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
-
-package git
-
-import "github.com/go-git/go-git/v5/plumbing/object"
-
-// LastCommitCache cache
-type LastCommitCache interface {
- Get(ref, entryPath string) (*object.Commit, error)
- Put(ref, entryPath, commitID string) error
-}
diff --git a/modules/git/command.go b/modules/git/command.go
index c9d1732416..fe25895462 100644
--- a/modules/git/command.go
+++ b/modules/git/command.go
@@ -189,7 +189,7 @@ func (c *Command) RunInDirTimeoutEnv(env []string, timeout time.Duration, dir st
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
if err := c.RunInDirTimeoutEnvPipeline(env, timeout, dir, stdout, stderr); err != nil {
- return nil, concatenateError(err, stderr.String())
+ return nil, ConcatenateError(err, stderr.String())
}
if stdout.Len() > 0 {
diff --git a/modules/git/commit.go b/modules/git/commit.go
index 6425345ea8..ce82c2f582 100644
--- a/modules/git/commit.go
+++ b/modules/git/commit.go
@@ -19,8 +19,6 @@ import (
"net/http"
"strconv"
"strings"
-
- "github.com/go-git/go-git/v5/plumbing/object"
)
// Commit represents a git commit.
@@ -43,61 +41,6 @@ type CommitGPGSignature struct {
Payload string //TODO check if can be reconstruct from the rest of commit information to not have duplicate data
}
-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
- }
-
- 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,
- }
-}
-
// Message returns the commit message. Same as retrieving CommitMessage directly.
func (c *Commit) Message() string {
return c.CommitMessage
@@ -576,7 +519,7 @@ func GetCommitFileStatus(repoPath, commitID string) (*CommitFileStatus, error) {
err := NewCommand("show", "--name-status", "--pretty=format:''", commitID).RunInDirPipeline(repoPath, w, stderr)
w.Close() // Close writer to exit parsing goroutine
if err != nil {
- return nil, concatenateError(err, stderr.String())
+ return nil, ConcatenateError(err, stderr.String())
}
<-done
diff --git a/modules/git/commit_convert_gogit.go b/modules/git/commit_convert_gogit.go
new file mode 100644
index 0000000000..be2b948b36
--- /dev/null
+++ b/modules/git/commit_convert_gogit.go
@@ -0,0 +1,70 @@
+// Copyright 2015 The Gogs Authors. All rights reserved.
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// +build gogit
+
+package git
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/go-git/go-git/v5/plumbing/object"
+)
+
+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
+ }
+
+ 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,
+ }
+}
diff --git a/modules/git/commit_info.go b/modules/git/commit_info.go
index e03ea00fc6..83e23545de 100644
--- a/modules/git/commit_info.go
+++ b/modules/git/commit_info.go
@@ -4,286 +4,9 @@
package git
-import (
- "path"
-
- "github.com/emirpasic/gods/trees/binaryheap"
- "github.com/go-git/go-git/v5/plumbing"
- "github.com/go-git/go-git/v5/plumbing/object"
- cgobject "github.com/go-git/go-git/v5/plumbing/object/commitgraph"
-)
-
-// GetCommitsInfo gets information of all commits that are corresponding to these entries
-func (tes Entries) GetCommitsInfo(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()
- }
-
- commitNodeIndex, commitGraphFile := commit.repo.CommitNodeIndex()
- if commitGraphFile != nil {
- defer commitGraphFile.Close()
- }
-
- c, err := commitNodeIndex.Get(commit.ID)
- if err != nil {
- return nil, nil, err
- }
-
- var revs map[string]*object.Commit
- if cache != nil {
- var unHitPaths []string
- revs, unHitPaths, err = getLastCommitForPathsByCache(commit.ID.String(), treePath, entryPaths, cache)
- if err != nil {
- return nil, nil, err
- }
- if len(unHitPaths) > 0 {
- revs2, err := GetLastCommitForPaths(c, treePath, unHitPaths)
- if err != nil {
- return nil, nil, err
- }
-
- for k, v := range revs2 {
- if err := cache.Put(commit.ID.String(), path.Join(treePath, k), v.ID().String()); err != nil {
- return nil, nil, err
- }
- revs[k] = v
- }
- }
- } else {
- revs, err = GetLastCommitForPaths(c, treePath, entryPaths)
- }
- if err != nil {
- return nil, nil, err
- }
-
- commit.repo.gogitStorage.Close()
-
- commitsInfo := make([][]interface{}, len(tes))
- for i, entry := range tes {
- if rev, ok := revs[entry.Name()]; ok {
- entryCommit := convertCommit(rev)
- if entry.IsSubModule() {
- subModuleURL := ""
- var fullPath string
- if len(treePath) > 0 {
- fullPath = treePath + "/" + entry.Name()
- } else {
- fullPath = entry.Name()
- }
- if subModule, err := commit.GetSubModule(fullPath); err != nil {
- return nil, nil, err
- } else if subModule != nil {
- subModuleURL = subModule.URL
- }
- subModuleFile := NewSubModuleFile(entryCommit, subModuleURL, entry.ID.String())
- commitsInfo[i] = []interface{}{entry, subModuleFile}
- } else {
- commitsInfo[i] = []interface{}{entry, entryCommit}
- }
- } else {
- commitsInfo[i] = []interface{}{entry, nil}
- }
- }
-
- // 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 treePath == "" {
- treeCommit = commit
- } else if rev, ok := revs[""]; ok {
- treeCommit = convertCommit(rev)
- treeCommit.repo = commit.repo
- }
- return commitsInfo, treeCommit, nil
-}
-
-type commitAndPaths struct {
- commit cgobject.CommitNode
- // Paths that are still on the branch represented by commit
- paths []string
- // Set of hashes for the paths
- hashes map[string]plumbing.Hash
-}
-
-func getCommitTree(c cgobject.CommitNode, treePath string) (*object.Tree, error) {
- tree, err := c.Tree()
- if err != nil {
- return nil, err
- }
-
- // Optimize deep traversals by focusing only on the specific tree
- if treePath != "" {
- tree, err = tree.Tree(treePath)
- if err != nil {
- return nil, err
- }
- }
-
- return tree, nil
-}
-
-func getFileHashes(c cgobject.CommitNode, treePath string, paths []string) (map[string]plumbing.Hash, error) {
- tree, err := getCommitTree(c, treePath)
- if err == object.ErrDirectoryNotFound {
- // The whole tree didn't exist, so return empty map
- return make(map[string]plumbing.Hash), nil
- }
- if err != nil {
- return nil, err
- }
-
- hashes := make(map[string]plumbing.Hash)
- for _, path := range paths {
- if path != "" {
- entry, err := tree.FindEntry(path)
- if err == nil {
- hashes[path] = entry.Hash
- }
- } else {
- hashes[path] = tree.Hash
- }
- }
-
- return hashes, nil
-}
-
-func getLastCommitForPathsByCache(commitID, treePath string, paths []string, cache LastCommitCache) (map[string]*object.Commit, []string, error) {
- var unHitEntryPaths []string
- var results = make(map[string]*object.Commit)
- for _, p := range paths {
- lastCommit, err := cache.Get(commitID, path.Join(treePath, p))
- if err != nil {
- return nil, nil, err
- }
- if lastCommit != nil {
- results[p] = lastCommit
- continue
- }
-
- unHitEntryPaths = append(unHitEntryPaths, p)
- }
-
- return results, unHitEntryPaths, nil
-}
-
-// GetLastCommitForPaths returns last commit information
-func GetLastCommitForPaths(c cgobject.CommitNode, treePath string, paths []string) (map[string]*object.Commit, error) {
- // We do a tree traversal with nodes sorted by commit time
- heap := binaryheap.NewWith(func(a, b interface{}) int {
- if a.(*commitAndPaths).commit.CommitTime().Before(b.(*commitAndPaths).commit.CommitTime()) {
- return 1
- }
- return -1
- })
-
- resultNodes := make(map[string]cgobject.CommitNode)
- initialHashes, err := getFileHashes(c, treePath, paths)
- if err != nil {
- return nil, err
- }
-
- // Start search from the root commit and with full set of paths
- heap.Push(&commitAndPaths{c, paths, initialHashes})
-
- for {
- cIn, ok := heap.Pop()
- if !ok {
- break
- }
- current := cIn.(*commitAndPaths)
-
- // Load the parent commits for the one we are currently examining
- numParents := current.commit.NumParents()
- var parents []cgobject.CommitNode
- for i := 0; i < numParents; i++ {
- parent, err := current.commit.ParentNode(i)
- if err != nil {
- break
- }
- parents = append(parents, parent)
- }
-
- // Examine the current commit and set of interesting paths
- pathUnchanged := make([]bool, len(current.paths))
- parentHashes := make([]map[string]plumbing.Hash, len(parents))
- for j, parent := range parents {
- parentHashes[j], err = getFileHashes(parent, treePath, current.paths)
- if err != nil {
- break
- }
-
- for i, path := range current.paths {
- if parentHashes[j][path] == current.hashes[path] {
- pathUnchanged[i] = true
- }
- }
- }
-
- var remainingPaths []string
- for i, path := range current.paths {
- // The results could already contain some newer change for the same path,
- // so don't override that and bail out on the file early.
- if resultNodes[path] == nil {
- if pathUnchanged[i] {
- // The path existed with the same hash in at least one parent so it could
- // not have been changed in this commit directly.
- remainingPaths = append(remainingPaths, path)
- } else {
- // There are few possible cases how can we get here:
- // - The path didn't exist in any parent, so it must have been created by
- // this commit.
- // - The path did exist in the parent commit, but the hash of the file has
- // changed.
- // - We are looking at a merge commit and the hash of the file doesn't
- // match any of the hashes being merged. This is more common for directories,
- // but it can also happen if a file is changed through conflict resolution.
- resultNodes[path] = current.commit
- }
- }
- }
-
- if len(remainingPaths) > 0 {
- // Add the parent nodes along with remaining paths to the heap for further
- // processing.
- for j, parent := range parents {
- // Combine remainingPath with paths available on the parent branch
- // and make union of them
- remainingPathsForParent := make([]string, 0, len(remainingPaths))
- newRemainingPaths := make([]string, 0, len(remainingPaths))
- for _, path := range remainingPaths {
- if parentHashes[j][path] == current.hashes[path] {
- remainingPathsForParent = append(remainingPathsForParent, path)
- } else {
- newRemainingPaths = append(newRemainingPaths, path)
- }
- }
-
- if remainingPathsForParent != nil {
- heap.Push(&commitAndPaths{parent, remainingPathsForParent, parentHashes[j]})
- }
-
- if len(newRemainingPaths) == 0 {
- break
- } else {
- remainingPaths = newRemainingPaths
- }
- }
- }
- }
-
- // Post-processing
- result := make(map[string]*object.Commit)
- for path, commitNode := range resultNodes {
- var err error
- result[path], err = commitNode.Commit()
- if err != nil {
- return nil, err
- }
- }
-
- return result, nil
+// CommitInfo describes the first commit with the provided entry
+type CommitInfo struct {
+ Entry *TreeEntry
+ Commit *Commit
+ SubModuleFile *SubModuleFile
}
diff --git a/modules/git/commit_info_gogit.go b/modules/git/commit_info_gogit.go
new file mode 100644
index 0000000000..6d95e22d0c
--- /dev/null
+++ b/modules/git/commit_info_gogit.go
@@ -0,0 +1,291 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// +build gogit
+
+package git
+
+import (
+ "path"
+
+ "github.com/emirpasic/gods/trees/binaryheap"
+ "github.com/go-git/go-git/v5/plumbing"
+ "github.com/go-git/go-git/v5/plumbing/object"
+ cgobject "github.com/go-git/go-git/v5/plumbing/object/commitgraph"
+)
+
+// GetCommitsInfo gets information of all commits that are corresponding to these entries
+func (tes Entries) GetCommitsInfo(commit *Commit, treePath string, cache *LastCommitCache) ([]CommitInfo, *Commit, error) {
+ entryPaths := make([]string, len(tes)+1)
+ // Get the commit for the treePath itself
+ entryPaths[0] = ""
+ for i, entry := range tes {
+ entryPaths[i+1] = entry.Name()
+ }
+
+ commitNodeIndex, commitGraphFile := commit.repo.CommitNodeIndex()
+ if commitGraphFile != nil {
+ defer commitGraphFile.Close()
+ }
+
+ c, err := commitNodeIndex.Get(commit.ID)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ var revs map[string]*object.Commit
+ if cache != nil {
+ var unHitPaths []string
+ revs, unHitPaths, err = getLastCommitForPathsByCache(commit.ID.String(), treePath, entryPaths, cache)
+ if err != nil {
+ return nil, nil, err
+ }
+ if len(unHitPaths) > 0 {
+ revs2, err := GetLastCommitForPaths(c, treePath, unHitPaths)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ for k, v := range revs2 {
+ if err := cache.Put(commit.ID.String(), path.Join(treePath, k), v.ID().String()); err != nil {
+ return nil, nil, err
+ }
+ revs[k] = v
+ }
+ }
+ } else {
+ revs, err = GetLastCommitForPaths(c, treePath, entryPaths)
+ }
+ if err != nil {
+ return nil, nil, err
+ }
+
+ commit.repo.gogitStorage.Close()
+
+ commitsInfo := make([]CommitInfo, len(tes))
+ for i, entry := range tes {
+ commitsInfo[i] = CommitInfo{
+ Entry: entry,
+ }
+ if rev, ok := revs[entry.Name()]; ok {
+ entryCommit := convertCommit(rev)
+ commitsInfo[i].Commit = entryCommit
+ if entry.IsSubModule() {
+ subModuleURL := ""
+ var fullPath string
+ if len(treePath) > 0 {
+ fullPath = treePath + "/" + entry.Name()
+ } else {
+ fullPath = entry.Name()
+ }
+ if subModule, err := commit.GetSubModule(fullPath); err != nil {
+ return nil, nil, err
+ } else if subModule != nil {
+ subModuleURL = subModule.URL
+ }
+ subModuleFile := NewSubModuleFile(entryCommit, subModuleURL, entry.ID.String())
+ commitsInfo[i].SubModuleFile = subModuleFile
+ }
+ }
+ }
+
+ // Retrieve the commit for the treePath itself (see above). We basically
+ // get it for free during the tree traversal and it's used for listing
+ // pages to display information about newest commit for a given path.
+ var treeCommit *Commit
+ if treePath == "" {
+ treeCommit = commit
+ } else if rev, ok := revs[""]; ok {
+ treeCommit = convertCommit(rev)
+ treeCommit.repo = commit.repo
+ }
+ return commitsInfo, treeCommit, nil
+}
+
+type commitAndPaths struct {
+ commit cgobject.CommitNode
+ // Paths that are still on the branch represented by commit
+ paths []string
+ // Set of hashes for the paths
+ hashes map[string]plumbing.Hash
+}
+
+func getCommitTree(c cgobject.CommitNode, treePath string) (*object.Tree, error) {
+ tree, err := c.Tree()
+ if err != nil {
+ return nil, err
+ }
+
+ // Optimize deep traversals by focusing only on the specific tree
+ if treePath != "" {
+ tree, err = tree.Tree(treePath)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return tree, nil
+}
+
+func getFileHashes(c cgobject.CommitNode, treePath string, paths []string) (map[string]plumbing.Hash, error) {
+ tree, err := getCommitTree(c, treePath)
+ if err == object.ErrDirectoryNotFound {
+ // The whole tree didn't exist, so return empty map
+ return make(map[string]plumbing.Hash), nil
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ hashes := make(map[string]plumbing.Hash)
+ for _, path := range paths {
+ if path != "" {
+ entry, err := tree.FindEntry(path)
+ if err == nil {
+ hashes[path] = entry.Hash
+ }
+ } else {
+ hashes[path] = tree.Hash
+ }
+ }
+
+ return hashes, nil
+}
+
+func getLastCommitForPathsByCache(commitID, treePath string, paths []string, cache *LastCommitCache) (map[string]*object.Commit, []string, error) {
+ var unHitEntryPaths []string
+ var results = make(map[string]*object.Commit)
+ for _, p := range paths {
+ lastCommit, err := cache.Get(commitID, path.Join(treePath, p))
+ if err != nil {
+ return nil, nil, err
+ }
+ if lastCommit != nil {
+ results[p] = lastCommit.(*object.Commit)
+ continue
+ }
+
+ unHitEntryPaths = append(unHitEntryPaths, p)
+ }
+
+ return results, unHitEntryPaths, nil
+}
+
+// GetLastCommitForPaths returns last commit information
+func GetLastCommitForPaths(c cgobject.CommitNode, treePath string, paths []string) (map[string]*object.Commit, error) {
+ // We do a tree traversal with nodes sorted by commit time
+ heap := binaryheap.NewWith(func(a, b interface{}) int {
+ if a.(*commitAndPaths).commit.CommitTime().Before(b.(*commitAndPaths).commit.CommitTime()) {
+ return 1
+ }
+ return -1
+ })
+
+ resultNodes := make(map[string]cgobject.CommitNode)
+ initialHashes, err := getFileHashes(c, treePath, paths)
+ if err != nil {
+ return nil, err
+ }
+
+ // Start search from the root commit and with full set of paths
+ heap.Push(&commitAndPaths{c, paths, initialHashes})
+
+ for {
+ cIn, ok := heap.Pop()
+ if !ok {
+ break
+ }
+ current := cIn.(*commitAndPaths)
+
+ // Load the parent commits for the one we are currently examining
+ numParents := current.commit.NumParents()
+ var parents []cgobject.CommitNode
+ for i := 0; i < numParents; i++ {
+ parent, err := current.commit.ParentNode(i)
+ if err != nil {
+ break
+ }
+ parents = append(parents, parent)
+ }
+
+ // Examine the current commit and set of interesting paths
+ pathUnchanged := make([]bool, len(current.paths))
+ parentHashes := make([]map[string]plumbing.Hash, len(parents))
+ for j, parent := range parents {
+ parentHashes[j], err = getFileHashes(parent, treePath, current.paths)
+ if err != nil {
+ break
+ }
+
+ for i, path := range current.paths {
+ if parentHashes[j][path] == current.hashes[path] {
+ pathUnchanged[i] = true
+ }
+ }
+ }
+
+ var remainingPaths []string
+ for i, path := range current.paths {
+ // The results could already contain some newer change for the same path,
+ // so don't override that and bail out on the file early.
+ if resultNodes[path] == nil {
+ if pathUnchanged[i] {
+ // The path existed with the same hash in at least one parent so it could
+ // not have been changed in this commit directly.
+ remainingPaths = append(remainingPaths, path)
+ } else {
+ // There are few possible cases how can we get here:
+ // - The path didn't exist in any parent, so it must have been created by
+ // this commit.
+ // - The path did exist in the parent commit, but the hash of the file has
+ // changed.
+ // - We are looking at a merge commit and the hash of the file doesn't
+ // match any of the hashes being merged. This is more common for directories,
+ // but it can also happen if a file is changed through conflict resolution.
+ resultNodes[path] = current.commit
+ }
+ }
+ }
+
+ if len(remainingPaths) > 0 {
+ // Add the parent nodes along with remaining paths to the heap for further
+ // processing.
+ for j, parent := range parents {
+ // Combine remainingPath with paths available on the parent branch
+ // and make union of them
+ remainingPathsForParent := make([]string, 0, len(remainingPaths))
+ newRemainingPaths := make([]string, 0, len(remainingPaths))
+ for _, path := range remainingPaths {
+ if parentHashes[j][path] == current.hashes[path] {
+ remainingPathsForParent = append(remainingPathsForParent, path)
+ } else {
+ newRemainingPaths = append(newRemainingPaths, path)
+ }
+ }
+
+ if remainingPathsForParent != nil {
+ heap.Push(&commitAndPaths{parent, remainingPathsForParent, parentHashes[j]})
+ }
+
+ if len(newRemainingPaths) == 0 {
+ break
+ } else {
+ remainingPaths = newRemainingPaths
+ }
+ }
+ }
+ }
+
+ // Post-processing
+ result := make(map[string]*object.Commit)
+ for path, commitNode := range resultNodes {
+ var err error
+ result[path], err = commitNode.Commit()
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return result, nil
+}
diff --git a/modules/git/commit_info_nogogit.go b/modules/git/commit_info_nogogit.go
new file mode 100644
index 0000000000..ac0c7cff5d
--- /dev/null
+++ b/modules/git/commit_info_nogogit.go
@@ -0,0 +1,370 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// +build !gogit
+
+package git
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "io"
+ "math"
+ "path"
+ "sort"
+ "strings"
+)
+
+// GetCommitsInfo gets information of all commits that are corresponding to these entries
+func (tes Entries) GetCommitsInfo(commit *Commit, treePath string, cache *LastCommitCache) ([]CommitInfo, *Commit, error) {
+ entryPaths := make([]string, len(tes)+1)
+ // Get the commit for the treePath itself
+ entryPaths[0] = ""
+ for i, entry := range tes {
+ entryPaths[i+1] = entry.Name()
+ }
+
+ var err error
+
+ var revs map[string]*Commit
+ if cache != nil {
+ var unHitPaths []string
+ revs, unHitPaths, err = getLastCommitForPathsByCache(commit.ID.String(), treePath, entryPaths, cache)
+ if err != nil {
+ return nil, nil, err
+ }
+ if len(unHitPaths) > 0 {
+ sort.Strings(unHitPaths)
+ commits, err := GetLastCommitForPaths(commit, treePath, unHitPaths)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ for i, found := range commits {
+ if err := cache.Put(commit.ID.String(), path.Join(treePath, unHitPaths[i]), found.ID.String()); err != nil {
+ return nil, nil, err
+ }
+ revs[unHitPaths[i]] = found
+ }
+ }
+ } else {
+ sort.Strings(entryPaths)
+ revs = map[string]*Commit{}
+ var foundCommits []*Commit
+ foundCommits, err = GetLastCommitForPaths(commit, treePath, entryPaths)
+ for i, found := range foundCommits {
+ revs[entryPaths[i]] = found
+ }
+ }
+ if err != nil {
+ return nil, nil, err
+ }
+
+ commitsInfo := make([]CommitInfo, len(tes))
+ for i, entry := range tes {
+ commitsInfo[i] = CommitInfo{
+ Entry: entry,
+ }
+ if entryCommit, ok := revs[entry.Name()]; ok {
+ commitsInfo[i].Commit = entryCommit
+ if entry.IsSubModule() {
+ subModuleURL := ""
+ var fullPath string
+ if len(treePath) > 0 {
+ fullPath = treePath + "/" + entry.Name()
+ } else {
+ fullPath = entry.Name()
+ }
+ if subModule, err := commit.GetSubModule(fullPath); err != nil {
+ return nil, nil, err
+ } else if subModule != nil {
+ subModuleURL = subModule.URL
+ }
+ subModuleFile := NewSubModuleFile(entryCommit, subModuleURL, entry.ID.String())
+ commitsInfo[i].SubModuleFile = subModuleFile
+ }
+ }
+ }
+
+ // Retrieve the commit for the treePath itself (see above). We basically
+ // get it for free during the tree traversal and it's used for listing
+ // pages to display information about newest commit for a given path.
+ var treeCommit *Commit
+ var ok bool
+ if treePath == "" {
+ treeCommit = commit
+ } else if treeCommit, ok = revs[""]; ok {
+ treeCommit.repo = commit.repo
+ }
+ return commitsInfo, treeCommit, nil
+}
+
+func getLastCommitForPathsByCache(commitID, treePath string, paths []string, cache *LastCommitCache) (map[string]*Commit, []string, error) {
+ var unHitEntryPaths []string
+ var results = make(map[string]*Commit)
+ for _, p := range paths {
+ lastCommit, err := cache.Get(commitID, path.Join(treePath, p))
+ if err != nil {
+ return nil, nil, err
+ }
+ if lastCommit != nil {
+ results[p] = lastCommit.(*Commit)
+ continue
+ }
+
+ unHitEntryPaths = append(unHitEntryPaths, p)
+ }
+
+ return results, unHitEntryPaths, nil
+}
+
+// GetLastCommitForPaths returns last commit information
+func GetLastCommitForPaths(commit *Commit, treePath string, paths []string) ([]*Commit, error) {
+ // We read backwards from the commit to obtain all of the commits
+
+ // We'll do this by using rev-list to provide us with parent commits in order
+ revListReader, revListWriter := io.Pipe()
+ defer func() {
+ _ = revListWriter.Close()
+ _ = revListReader.Close()
+ }()
+
+ go func() {
+ stderr := strings.Builder{}
+ err := NewCommand("rev-list", "--format=%T", commit.ID.String()).RunInDirPipeline(commit.repo.Path, revListWriter, &stderr)
+ if err != nil {
+ _ = revListWriter.CloseWithError(ConcatenateError(err, (&stderr).String()))
+ } else {
+ _ = revListWriter.Close()
+ }
+ }()
+
+ // We feed the commits in order into cat-file --batch, followed by their trees and sub trees as necessary.
+ // so let's create a batch stdin and stdout
+ batchStdinReader, batchStdinWriter := io.Pipe()
+ batchStdoutReader, batchStdoutWriter := io.Pipe()
+ defer func() {
+ _ = batchStdinReader.Close()
+ _ = batchStdinWriter.Close()
+ _ = batchStdoutReader.Close()
+ _ = batchStdoutWriter.Close()
+ }()
+
+ go func() {
+ stderr := strings.Builder{}
+ err := NewCommand("cat-file", "--batch").RunInDirFullPipeline(commit.repo.Path, batchStdoutWriter, &stderr, batchStdinReader)
+ if err != nil {
+ _ = revListWriter.CloseWithError(ConcatenateError(err, (&stderr).String()))
+ } else {
+ _ = revListWriter.Close()
+ }
+ }()
+
+ // For simplicities sake we'll us a buffered reader
+ batchReader := bufio.NewReader(batchStdoutReader)
+
+ mapsize := 4096
+ if len(paths) > mapsize {
+ mapsize = len(paths)
+ }
+
+ path2idx := make(map[string]int, mapsize)
+ for i, path := range paths {
+ path2idx[path] = i
+ }
+
+ fnameBuf := make([]byte, 4096)
+ modeBuf := make([]byte, 40)
+
+ allShaBuf := make([]byte, (len(paths)+1)*20)
+ shaBuf := make([]byte, 20)
+ tmpTreeID := make([]byte, 40)
+
+ // commits is the returnable commits matching the paths provided
+ commits := make([]string, len(paths))
+ // ids are the blob/tree ids for the paths
+ ids := make([][]byte, len(paths))
+
+ // We'll use a scanner for the revList because it's simpler than a bufio.Reader
+ scan := bufio.NewScanner(revListReader)
+revListLoop:
+ for scan.Scan() {
+ // Get the next parent commit ID
+ commitID := scan.Text()
+ if !scan.Scan() {
+ break revListLoop
+ }
+ commitID = commitID[7:]
+ rootTreeID := scan.Text()
+
+ // push the tree to the cat-file --batch process
+ _, err := batchStdinWriter.Write([]byte(rootTreeID + "\n"))
+ if err != nil {
+ return nil, err
+ }
+
+ currentPath := ""
+
+ // OK if the target tree path is "" and the "" is in the paths just set this now
+ if treePath == "" && paths[0] == "" {
+ // If this is the first time we see this set the id appropriate for this paths to this tree and set the last commit to curCommit
+ if len(ids[0]) == 0 {
+ ids[0] = []byte(rootTreeID)
+ commits[0] = string(commitID)
+ } else if bytes.Equal(ids[0], []byte(rootTreeID)) {
+ commits[0] = string(commitID)
+ }
+ }
+
+ treeReadingLoop:
+ for {
+ _, _, size, err := ReadBatchLine(batchReader)
+ if err != nil {
+ return nil, err
+ }
+
+ // Handle trees
+
+ // n is counter for file position in the tree file
+ var n int64
+
+ // Two options: currentPath is the targetTreepath
+ if treePath == currentPath {
+ // We are in the right directory
+ // Parse each tree line in turn. (don't care about mode here.)
+ for n < size {
+ fname, sha, count, err := ParseTreeLineSkipMode(batchReader, fnameBuf, shaBuf)
+ shaBuf = sha
+ if err != nil {
+ return nil, err
+ }
+ n += int64(count)
+ idx, ok := path2idx[string(fname)]
+ if ok {
+ // Now if this is the first time round set the initial Blob(ish) SHA ID and the commit
+ if len(ids[idx]) == 0 {
+ copy(allShaBuf[20*(idx+1):20*(idx+2)], shaBuf)
+ ids[idx] = allShaBuf[20*(idx+1) : 20*(idx+2)]
+ commits[idx] = string(commitID)
+ } else if bytes.Equal(ids[idx], shaBuf) {
+ commits[idx] = string(commitID)
+ }
+ }
+ // FIXME: is there any order to the way strings are emitted from cat-file?
+ // if there is - then we could skip once we've passed all of our data
+ }
+ break treeReadingLoop
+ }
+
+ var treeID []byte
+
+ // We're in the wrong directory
+ // Find target directory in this directory
+ idx := len(currentPath)
+ if idx > 0 {
+ idx++
+ }
+ target := strings.SplitN(treePath[idx:], "/", 2)[0]
+
+ for n < size {
+ // Read each tree entry in turn
+ mode, fname, sha, count, err := ParseTreeLine(batchReader, modeBuf, fnameBuf, shaBuf)
+ if err != nil {
+ return nil, err
+ }
+ n += int64(count)
+
+ // if we have found the target directory
+ if bytes.Equal(fname, []byte(target)) && bytes.Equal(mode, []byte("40000")) {
+ copy(tmpTreeID, sha)
+ treeID = tmpTreeID
+ break
+ }
+ }
+
+ if n < size {
+ // Discard any remaining entries in the current tree
+ discard := size - n
+ for discard > math.MaxInt32 {
+ _, err := batchReader.Discard(math.MaxInt32)
+ if err != nil {
+ return nil, err
+ }
+ discard -= math.MaxInt32
+ }
+ _, err := batchReader.Discard(int(discard))
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ // if we haven't found a treeID for the target directory our search is over
+ if len(treeID) == 0 {
+ break treeReadingLoop
+ }
+
+ // add the target to the current path
+ if idx > 0 {
+ currentPath += "/"
+ }
+ currentPath += target
+
+ // if we've now found the current path check its sha id and commit status
+ if treePath == currentPath && paths[0] == "" {
+ if len(ids[0]) == 0 {
+ copy(allShaBuf[0:20], treeID)
+ ids[0] = allShaBuf[0:20]
+ commits[0] = string(commitID)
+ } else if bytes.Equal(ids[0], treeID) {
+ commits[0] = string(commitID)
+ }
+ }
+ treeID = to40ByteSHA(treeID)
+ _, err = batchStdinWriter.Write(treeID)
+ if err != nil {
+ return nil, err
+ }
+ _, err = batchStdinWriter.Write([]byte("\n"))
+ if err != nil {
+ return nil, err
+ }
+ }
+ }
+
+ commitsMap := make(map[string]*Commit, len(commits))
+ commitsMap[commit.ID.String()] = commit
+
+ commitCommits := make([]*Commit, len(commits))
+ for i, commitID := range commits {
+ c, ok := commitsMap[commitID]
+ if ok {
+ commitCommits[i] = c
+ continue
+ }
+
+ if len(commitID) == 0 {
+ continue
+ }
+
+ _, err := batchStdinWriter.Write([]byte(commitID + "\n"))
+ if err != nil {
+ return nil, err
+ }
+ _, typ, size, err := ReadBatchLine(batchReader)
+ if err != nil {
+ return nil, err
+ }
+ if typ != "commit" {
+ return nil, fmt.Errorf("unexpected type: %s for commit id: %s", typ, commitID)
+ }
+ c, err = CommitFromReader(commit.repo, MustIDFromString(string(commitID)), io.LimitReader(batchReader, int64(size)))
+ if err != nil {
+ return nil, err
+ }
+ commitCommits[i] = c
+ }
+
+ return commitCommits, scan.Err()
+}
diff --git a/modules/git/commit_info_test.go b/modules/git/commit_info_test.go
index 8bdf1a769b..3966419bc1 100644
--- a/modules/git/commit_info_test.go
+++ b/modules/git/commit_info_test.go
@@ -58,17 +58,27 @@ func testGetCommitsInfo(t *testing.T, repo1 *Repository) {
for _, testCase := range testCases {
commit, err := repo1.GetCommit(testCase.CommitID)
assert.NoError(t, err)
+ assert.NotNil(t, commit)
+ assert.NotNil(t, commit.Tree)
+ assert.NotNil(t, commit.Tree.repo)
+
tree, err := commit.Tree.SubTree(testCase.Path)
+ assert.NotNil(t, tree, "tree is nil for testCase CommitID %s in Path %s", testCase.CommitID, testCase.Path)
+ assert.NotNil(t, tree.repo, "repo is nil for testCase CommitID %s in Path %s", testCase.CommitID, testCase.Path)
+
assert.NoError(t, err)
entries, err := tree.ListEntries()
assert.NoError(t, err)
commitsInfo, treeCommit, err := entries.GetCommitsInfo(commit, testCase.Path, nil)
- assert.Equal(t, testCase.ExpectedTreeCommit, treeCommit.ID.String())
assert.NoError(t, err)
+ if err != nil {
+ t.FailNow()
+ }
+ assert.Equal(t, testCase.ExpectedTreeCommit, treeCommit.ID.String())
assert.Len(t, commitsInfo, len(testCase.ExpectedIDs))
for _, commitInfo := range commitsInfo {
- entry := commitInfo[0].(*TreeEntry)
- commit := commitInfo[1].(*Commit)
+ entry := commitInfo.Entry
+ commit := commitInfo.Commit
expectedID, ok := testCase.ExpectedIDs[entry.Name()]
if !assert.True(t, ok) {
continue
diff --git a/modules/git/commit_reader.go b/modules/git/commit_reader.go
index fdcb6dca84..4eb861040e 100644
--- a/modules/git/commit_reader.go
+++ b/modules/git/commit_reader.go
@@ -9,13 +9,13 @@ import (
"bytes"
"io"
"strings"
-
- "github.com/go-git/go-git/v5/plumbing"
)
// CommitFromReader will generate a Commit from a provided reader
-// We will need this to interpret commits from cat-file
-func CommitFromReader(gitRepo *Repository, sha plumbing.Hash, reader io.Reader) (*Commit, error) {
+// We need this to interpret commits from cat-file or cat-file --batch
+//
+// If used as part of a cat-file --batch stream you need to limit the reader to the correct size
+func CommitFromReader(gitRepo *Repository, sha SHA1, reader io.Reader) (*Commit, error) {
commit := &Commit{
ID: sha,
}
@@ -26,26 +26,20 @@ func CommitFromReader(gitRepo *Repository, sha plumbing.Hash, reader io.Reader)
message := false
pgpsig := false
- scanner := bufio.NewScanner(reader)
- // Split by '\n' but include the '\n'
- scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
- if atEOF && len(data) == 0 {
- return 0, nil, nil
- }
- if i := bytes.IndexByte(data, '\n'); i >= 0 {
- // We have a full newline-terminated line.
- return i + 1, data[0 : i+1], nil
- }
- // If we're at EOF, we have a final, non-terminated line. Return it.
- if atEOF {
- return len(data), data, nil
- }
- // Request more data.
- return 0, nil, nil
- })
+ bufReader, ok := reader.(*bufio.Reader)
+ if !ok {
+ bufReader = bufio.NewReader(reader)
+ }
- for scanner.Scan() {
- line := scanner.Bytes()
+readLoop:
+ for {
+ line, err := bufReader.ReadBytes('\n')
+ if err != nil {
+ if err == io.EOF {
+ break readLoop
+ }
+ return nil, err
+ }
if pgpsig {
if len(line) > 0 && line[0] == ' ' {
_, _ = signatureSB.Write(line[1:])
@@ -72,10 +66,10 @@ func CommitFromReader(gitRepo *Repository, sha plumbing.Hash, reader io.Reader)
switch string(split[0]) {
case "tree":
- commit.Tree = *NewTree(gitRepo, plumbing.NewHash(string(data)))
+ commit.Tree = *NewTree(gitRepo, MustIDFromString(string(data)))
_, _ = payloadSB.Write(line)
case "parent":
- commit.Parents = append(commit.Parents, plumbing.NewHash(string(data)))
+ commit.Parents = append(commit.Parents, MustIDFromString(string(data)))
_, _ = payloadSB.Write(line)
case "author":
commit.Author = &Signature{}
@@ -104,5 +98,5 @@ func CommitFromReader(gitRepo *Repository, sha plumbing.Hash, reader io.Reader)
commit.Signature = nil
}
- return commit, scanner.Err()
+ return commit, nil
}
diff --git a/modules/git/last_commit_cache.go b/modules/git/last_commit_cache.go
new file mode 100644
index 0000000000..7cca601226
--- /dev/null
+++ b/modules/git/last_commit_cache.go
@@ -0,0 +1,29 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package git
+
+import (
+ "crypto/sha256"
+ "fmt"
+)
+
+// Cache represents a caching interface
+type Cache interface {
+ // Put puts value into cache with key and expire time.
+ Put(key string, val interface{}, timeout int64) error
+ // Get gets cached value by given key.
+ Get(key string) interface{}
+}
+
+func (c *LastCommitCache) getCacheKey(repoPath, ref, entryPath string) string {
+ hashBytes := sha256.Sum256([]byte(fmt.Sprintf("%s:%s:%s", repoPath, ref, entryPath)))
+ return fmt.Sprintf("last_commit:%x", hashBytes)
+}
+
+// Put put the last commit id with commit and entry path
+func (c *LastCommitCache) Put(ref, entryPath, commitID string) error {
+ log("LastCommitCache save: [%s:%s:%s]", ref, entryPath, commitID)
+ return c.cache.Put(c.getCacheKey(c.repoPath, ref, entryPath), commitID, c.ttl)
+}
diff --git a/modules/git/last_commit_cache_gogit.go b/modules/git/last_commit_cache_gogit.go
new file mode 100644
index 0000000000..76c97a4cc0
--- /dev/null
+++ b/modules/git/last_commit_cache_gogit.go
@@ -0,0 +1,113 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// +build gogit
+
+package git
+
+import (
+ "path"
+
+ "github.com/go-git/go-git/v5/plumbing/object"
+ cgobject "github.com/go-git/go-git/v5/plumbing/object/commitgraph"
+)
+
+// LastCommitCache represents a cache to store last commit
+type LastCommitCache struct {
+ repoPath string
+ ttl int64
+ repo *Repository
+ commitCache map[string]*object.Commit
+ cache Cache
+}
+
+// NewLastCommitCache creates a new last commit cache for repo
+func NewLastCommitCache(repoPath string, gitRepo *Repository, ttl int64, cache Cache) *LastCommitCache {
+ if cache == nil {
+ return nil
+ }
+ return &LastCommitCache{
+ repoPath: repoPath,
+ repo: gitRepo,
+ commitCache: make(map[string]*object.Commit),
+ ttl: ttl,
+ cache: cache,
+ }
+}
+
+// Get get the last commit information by commit id and entry path
+func (c *LastCommitCache) Get(ref, entryPath string) (interface{}, error) {
+ v := c.cache.Get(c.getCacheKey(c.repoPath, ref, entryPath))
+ if vs, ok := v.(string); ok {
+ log("LastCommitCache hit level 1: [%s:%s:%s]", ref, entryPath, vs)
+ if commit, ok := c.commitCache[vs]; ok {
+ log("LastCommitCache hit level 2: [%s:%s:%s]", ref, entryPath, vs)
+ return commit, nil
+ }
+ id, err := c.repo.ConvertToSHA1(vs)
+ if err != nil {
+ return nil, err
+ }
+ commit, err := c.repo.GoGitRepo().CommitObject(id)
+ if err != nil {
+ return nil, err
+ }
+ c.commitCache[vs] = commit
+ return commit, nil
+ }
+ return nil, nil
+}
+
+// CacheCommit will cache the commit from the gitRepository
+func (c *LastCommitCache) CacheCommit(commit *Commit) error {
+
+ commitNodeIndex, _ := commit.repo.CommitNodeIndex()
+
+ index, err := commitNodeIndex.Get(commit.ID)
+ if err != nil {
+ return err
+ }
+
+ return c.recursiveCache(index, &commit.Tree, "", 1)
+}
+
+func (c *LastCommitCache) recursiveCache(index cgobject.CommitNode, tree *Tree, treePath string, level int) error {
+ if level == 0 {
+ return nil
+ }
+
+ entries, err := tree.ListEntries()
+ if err != nil {
+ return err
+ }
+
+ entryPaths := make([]string, len(entries))
+ entryMap := make(map[string]*TreeEntry)
+ for i, entry := range entries {
+ entryPaths[i] = entry.Name()
+ entryMap[entry.Name()] = entry
+ }
+
+ commits, err := GetLastCommitForPaths(index, treePath, entryPaths)
+ if err != nil {
+ return err
+ }
+
+ for entry, cm := range commits {
+ if err := c.Put(index.ID().String(), path.Join(treePath, entry), cm.ID().String()); err != nil {
+ return err
+ }
+ if entryMap[entry].IsDir() {
+ subTree, err := tree.SubTree(entry)
+ if err != nil {
+ return err
+ }
+ if err := c.recursiveCache(index, subTree, entry, level-1); err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
diff --git a/modules/git/last_commit_cache_nogogit.go b/modules/git/last_commit_cache_nogogit.go
new file mode 100644
index 0000000000..b9c50b5cfb
--- /dev/null
+++ b/modules/git/last_commit_cache_nogogit.go
@@ -0,0 +1,103 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// +build !gogit
+
+package git
+
+import (
+ "path"
+)
+
+// LastCommitCache represents a cache to store last commit
+type LastCommitCache struct {
+ repoPath string
+ ttl int64
+ repo *Repository
+ commitCache map[string]*Commit
+ cache Cache
+}
+
+// NewLastCommitCache creates a new last commit cache for repo
+func NewLastCommitCache(repoPath string, gitRepo *Repository, ttl int64, cache Cache) *LastCommitCache {
+ if cache == nil {
+ return nil
+ }
+ return &LastCommitCache{
+ repoPath: repoPath,
+ repo: gitRepo,
+ commitCache: make(map[string]*Commit),
+ ttl: ttl,
+ cache: cache,
+ }
+}
+
+// Get get the last commit information by commit id and entry path
+func (c *LastCommitCache) Get(ref, entryPath string) (interface{}, error) {
+ v := c.cache.Get(c.getCacheKey(c.repoPath, ref, entryPath))
+ if vs, ok := v.(string); ok {
+ log("LastCommitCache hit level 1: [%s:%s:%s]", ref, entryPath, vs)
+ if commit, ok := c.commitCache[vs]; ok {
+ log("LastCommitCache hit level 2: [%s:%s:%s]", ref, entryPath, vs)
+ return commit, nil
+ }
+ id, err := c.repo.ConvertToSHA1(vs)
+ if err != nil {
+ return nil, err
+ }
+ commit, err := c.repo.getCommit(id)
+ if err != nil {
+ return nil, err
+ }
+ c.commitCache[vs] = commit
+ return commit, nil
+ }
+ return nil, nil
+}
+
+// CacheCommit will cache the commit from the gitRepository
+func (c *LastCommitCache) CacheCommit(commit *Commit) error {
+ return c.recursiveCache(commit, &commit.Tree, "", 1)
+}
+
+func (c *LastCommitCache) recursiveCache(commit *Commit, tree *Tree, treePath string, level int) error {
+ if level == 0 {
+ return nil
+ }
+
+ entries, err := tree.ListEntries()
+ if err != nil {
+ return err
+ }
+
+ entryPaths := make([]string, len(entries))
+ entryMap := make(map[string]*TreeEntry)
+ for i, entry := range entries {
+ entryPaths[i] = entry.Name()
+ entryMap[entry.Name()] = entry
+ }
+
+ commits, err := GetLastCommitForPaths(commit, treePath, entryPaths)
+ if err != nil {
+ return err
+ }
+
+ for i, entryCommit := range commits {
+ entry := entryPaths[i]
+ if err := c.Put(commit.ID.String(), path.Join(treePath, entryPaths[i]), entryCommit.ID.String()); err != nil {
+ return err
+ }
+ if entryMap[entry].IsDir() {
+ subTree, err := tree.SubTree(entry)
+ if err != nil {
+ return err
+ }
+ if err := c.recursiveCache(commit, subTree, entry, level-1); err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
diff --git a/modules/git/notes.go b/modules/git/notes.go
index ba19fa4893..a8dd66df0b 100644
--- a/modules/git/notes.go
+++ b/modules/git/notes.go
@@ -4,12 +4,6 @@
package git
-import (
- "io/ioutil"
-
- "github.com/go-git/go-git/v5/plumbing/object"
-)
-
// NotesRef is the git ref where Gitea will look for git-notes data.
// The value ("refs/notes/commits") is the default ref used by git-notes.
const NotesRef = "refs/notes/commits"
@@ -19,62 +13,3 @@ type Note struct {
Message []byte
Commit *Commit
}
-
-// GetNote retrieves the git-notes data for a given commit.
-func GetNote(repo *Repository, commitID string, note *Note) error {
- notes, err := repo.GetCommit(NotesRef)
- if err != nil {
- return err
- }
-
- remainingCommitID := commitID
- path := ""
- currentTree := notes.Tree.gogitTree
- var file *object.File
- for len(remainingCommitID) > 2 {
- file, err = currentTree.File(remainingCommitID)
- if err == nil {
- path += remainingCommitID
- break
- }
- if err == object.ErrFileNotFound {
- currentTree, err = currentTree.Tree(remainingCommitID[0:2])
- path += remainingCommitID[0:2] + "/"
- remainingCommitID = remainingCommitID[2:]
- }
- if err != nil {
- return err
- }
- }
-
- blob := file.Blob
- dataRc, err := blob.Reader()
- if err != nil {
- return err
- }
-
- defer dataRc.Close()
- d, err := ioutil.ReadAll(dataRc)
- if err != nil {
- return err
- }
- note.Message = d
-
- commitNodeIndex, commitGraphFile := repo.CommitNodeIndex()
- if commitGraphFile != nil {
- defer commitGraphFile.Close()
- }
-
- commitNode, err := commitNodeIndex.Get(notes.ID)
- if err != nil {
- return err
- }
-
- lastCommits, err := GetLastCommitForPaths(commitNode, "", []string{path})
- if err != nil {
- return err
- }
- note.Commit = convertCommit(lastCommits[path])
-
- return nil
-}
diff --git a/modules/git/notes_gogit.go b/modules/git/notes_gogit.go
new file mode 100644
index 0000000000..173d29cee6
--- /dev/null
+++ b/modules/git/notes_gogit.go
@@ -0,0 +1,72 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// +build gogit
+
+package git
+
+import (
+ "io/ioutil"
+
+ "github.com/go-git/go-git/v5/plumbing/object"
+)
+
+// GetNote retrieves the git-notes data for a given commit.
+func GetNote(repo *Repository, commitID string, note *Note) error {
+ notes, err := repo.GetCommit(NotesRef)
+ if err != nil {
+ return err
+ }
+
+ remainingCommitID := commitID
+ path := ""
+ currentTree := notes.Tree.gogitTree
+ var file *object.File
+ for len(remainingCommitID) > 2 {
+ file, err = currentTree.File(remainingCommitID)
+ if err == nil {
+ path += remainingCommitID
+ break
+ }
+ if err == object.ErrFileNotFound {
+ currentTree, err = currentTree.Tree(remainingCommitID[0:2])
+ path += remainingCommitID[0:2] + "/"
+ remainingCommitID = remainingCommitID[2:]
+ }
+ if err != nil {
+ return err
+ }
+ }
+
+ blob := file.Blob
+ dataRc, err := blob.Reader()
+ if err != nil {
+ return err
+ }
+
+ defer dataRc.Close()
+ d, err := ioutil.ReadAll(dataRc)
+ if err != nil {
+ return err
+ }
+ note.Message = d
+
+ commitNodeIndex, commitGraphFile := repo.CommitNodeIndex()
+ if commitGraphFile != nil {
+ defer commitGraphFile.Close()
+ }
+
+ commitNode, err := commitNodeIndex.Get(notes.ID)
+ if err != nil {
+ return err
+ }
+
+ lastCommits, err := GetLastCommitForPaths(commitNode, "", []string{path})
+ if err != nil {
+ return err
+ }
+ note.Commit = convertCommit(lastCommits[path])
+
+ return nil
+}
diff --git a/modules/git/notes_nogogit.go b/modules/git/notes_nogogit.go
new file mode 100644
index 0000000000..613efd2e0e
--- /dev/null
+++ b/modules/git/notes_nogogit.go
@@ -0,0 +1,59 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// +build !gogit
+
+package git
+
+import (
+ "io/ioutil"
+)
+
+// GetNote retrieves the git-notes data for a given commit.
+func GetNote(repo *Repository, commitID string, note *Note) error {
+ notes, err := repo.GetCommit(NotesRef)
+ if err != nil {
+ return err
+ }
+
+ path := ""
+
+ tree := &notes.Tree
+
+ var entry *TreeEntry
+ for len(commitID) > 2 {
+ entry, err = tree.GetTreeEntryByPath(commitID)
+ if err == nil {
+ path += commitID
+ break
+ }
+ if IsErrNotExist(err) {
+ tree, err = tree.SubTree(commitID[0:2])
+ path += commitID[0:2] + "/"
+ commitID = commitID[2:]
+ }
+ if err != nil {
+ return err
+ }
+ }
+
+ dataRc, err := entry.Blob().DataAsync()
+ if err != nil {
+ return err
+ }
+ defer dataRc.Close()
+ d, err := ioutil.ReadAll(dataRc)
+ if err != nil {
+ return err
+ }
+ note.Message = d
+
+ lastCommits, err := GetLastCommitForPaths(notes, "", []string{path})
+ if err != nil {
+ return err
+ }
+ note.Commit = lastCommits[0]
+
+ return nil
+}
diff --git a/modules/git/parse.go b/modules/git/parse_gogit.go
index 89b4488600..434fb4160f 100644
--- a/modules/git/parse.go
+++ b/modules/git/parse_gogit.go
@@ -2,6 +2,8 @@
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
+// +build gogit
+
package git
import (
diff --git a/modules/git/parse_test.go b/modules/git/parse_gogit_test.go
index 8e0be828b3..cf38c29932 100644
--- a/modules/git/parse_test.go
+++ b/modules/git/parse_gogit_test.go
@@ -2,6 +2,8 @@
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
+// +build gogit
+
package git
import (
diff --git a/modules/git/parse_nogogit.go b/modules/git/parse_nogogit.go
new file mode 100644
index 0000000000..26dd700af7
--- /dev/null
+++ b/modules/git/parse_nogogit.go
@@ -0,0 +1,78 @@
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// +build !gogit
+
+package git
+
+import (
+ "bytes"
+ "fmt"
+ "strconv"
+)
+
+// ParseTreeEntries parses the output of a `git ls-tree` command.
+func ParseTreeEntries(data []byte) ([]*TreeEntry, error) {
+ return parseTreeEntries(data, nil)
+}
+
+func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) {
+ entries := make([]*TreeEntry, 0, 10)
+ for pos := 0; pos < len(data); {
+ // expect line to be of the form "<mode> <type> <sha>\t<filename>"
+ entry := new(TreeEntry)
+ entry.ptree = ptree
+ if pos+6 > len(data) {
+ return nil, fmt.Errorf("Invalid ls-tree output: %s", string(data))
+ }
+ switch string(data[pos : pos+6]) {
+ case "100644":
+ entry.entryMode = EntryModeBlob
+ pos += 12 // skip over "100644 blob "
+ case "100755":
+ entry.entryMode = EntryModeExec
+ pos += 12 // skip over "100755 blob "
+ case "120000":
+ entry.entryMode = EntryModeSymlink
+ pos += 12 // skip over "120000 blob "
+ case "160000":
+ entry.entryMode = EntryModeCommit
+ pos += 14 // skip over "160000 object "
+ case "040000":
+ entry.entryMode = EntryModeTree
+ pos += 12 // skip over "040000 tree "
+ default:
+ return nil, fmt.Errorf("unknown type: %v", string(data[pos:pos+6]))
+ }
+
+ if pos+40 > len(data) {
+ return nil, fmt.Errorf("Invalid ls-tree output: %s", string(data))
+ }
+ id, err := NewIDFromString(string(data[pos : pos+40]))
+ if err != nil {
+ return nil, fmt.Errorf("Invalid ls-tree output: %v", err)
+ }
+ entry.ID = id
+ pos += 41 // skip over sha and trailing space
+
+ end := pos + bytes.IndexByte(data[pos:], '\n')
+ if end < pos {
+ return nil, fmt.Errorf("Invalid ls-tree output: %s", string(data))
+ }
+
+ // In case entry name is surrounded by double quotes(it happens only in git-shell).
+ if data[pos] == '"' {
+ entry.name, err = strconv.Unquote(string(data[pos:end]))
+ if err != nil {
+ return nil, fmt.Errorf("Invalid ls-tree output: %v", err)
+ }
+ } else {
+ entry.name = string(data[pos:end])
+ }
+
+ pos = end + 1
+ entries = append(entries, entry)
+ }
+ return entries, nil
+}
diff --git a/modules/git/pipeline/lfs.go b/modules/git/pipeline/lfs.go
new file mode 100644
index 0000000000..d47b7d91ea
--- /dev/null
+++ b/modules/git/pipeline/lfs.go
@@ -0,0 +1,159 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// +build gogit
+
+package pipeline
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "sort"
+ "strings"
+ "sync"
+ "time"
+
+ "code.gitea.io/gitea/modules/git"
+ gogit "github.com/go-git/go-git/v5"
+ "github.com/go-git/go-git/v5/plumbing/object"
+)
+
+// LFSResult represents commits found using a provided pointer file hash
+type LFSResult struct {
+ Name string
+ SHA string
+ Summary string
+ When time.Time
+ ParentHashes []git.SHA1
+ BranchName string
+ FullCommitName string
+}
+
+type lfsResultSlice []*LFSResult
+
+func (a lfsResultSlice) Len() int { return len(a) }
+func (a lfsResultSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+func (a lfsResultSlice) Less(i, j int) bool { return a[j].When.After(a[i].When) }
+
+// FindLFSFile finds commits that contain a provided pointer file hash
+func FindLFSFile(repo *git.Repository, hash git.SHA1) ([]*LFSResult, error) {
+ resultsMap := map[string]*LFSResult{}
+ results := make([]*LFSResult, 0)
+
+ basePath := repo.Path
+ gogitRepo := repo.GoGitRepo()
+
+ commitsIter, err := gogitRepo.Log(&gogit.LogOptions{
+ Order: gogit.LogOrderCommitterTime,
+ All: true,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("Failed to get GoGit CommitsIter. Error: %w", err)
+ }
+
+ err = commitsIter.ForEach(func(gitCommit *object.Commit) error {
+ tree, err := gitCommit.Tree()
+ if err != nil {
+ return err
+ }
+ treeWalker := object.NewTreeWalker(tree, true, nil)
+ defer treeWalker.Close()
+ for {
+ name, entry, err := treeWalker.Next()
+ if err == io.EOF {
+ break
+ }
+ if entry.Hash == hash {
+ result := LFSResult{
+ Name: name,
+ SHA: gitCommit.Hash.String(),
+ Summary: strings.Split(strings.TrimSpace(gitCommit.Message), "\n")[0],
+ When: gitCommit.Author.When,
+ ParentHashes: gitCommit.ParentHashes,
+ }
+ resultsMap[gitCommit.Hash.String()+":"+name] = &result
+ }
+ }
+ return nil
+ })
+ if err != nil && err != io.EOF {
+ return nil, fmt.Errorf("Failure in CommitIter.ForEach: %w", err)
+ }
+
+ for _, result := range resultsMap {
+ hasParent := false
+ for _, parentHash := range result.ParentHashes {
+ if _, hasParent = resultsMap[parentHash.String()+":"+result.Name]; hasParent {
+ break
+ }
+ }
+ if !hasParent {
+ results = append(results, result)
+ }
+ }
+
+ sort.Sort(lfsResultSlice(results))
+
+ // Should really use a go-git function here but name-rev is not completed and recapitulating it is not simple
+ shasToNameReader, shasToNameWriter := io.Pipe()
+ nameRevStdinReader, nameRevStdinWriter := io.Pipe()
+ errChan := make(chan error, 1)
+ wg := sync.WaitGroup{}
+ wg.Add(3)
+
+ go func() {
+ defer wg.Done()
+ scanner := bufio.NewScanner(nameRevStdinReader)
+ i := 0
+ for scanner.Scan() {
+ line := scanner.Text()
+ if len(line) == 0 {
+ continue
+ }
+ result := results[i]
+ result.FullCommitName = line
+ result.BranchName = strings.Split(line, "~")[0]
+ i++
+ }
+ }()
+ go NameRevStdin(shasToNameReader, nameRevStdinWriter, &wg, basePath)
+ go func() {
+ defer wg.Done()
+ defer shasToNameWriter.Close()
+ for _, result := range results {
+ i := 0
+ if i < len(result.SHA) {
+ n, err := shasToNameWriter.Write([]byte(result.SHA)[i:])
+ if err != nil {
+ errChan <- err
+ break
+ }
+ i += n
+ }
+ n := 0
+ for n < 1 {
+ n, err = shasToNameWriter.Write([]byte{'\n'})
+ if err != nil {
+ errChan <- err
+ break
+ }
+
+ }
+
+ }
+ }()
+
+ wg.Wait()
+
+ select {
+ case err, has := <-errChan:
+ if has {
+ return nil, fmt.Errorf("Unable to obtain name for LFS files. Error: %w", err)
+ }
+ default:
+ }
+
+ return results, nil
+}
diff --git a/modules/git/pipeline/lfs_nogogit.go b/modules/git/pipeline/lfs_nogogit.go
new file mode 100644
index 0000000000..30d33e27e0
--- /dev/null
+++ b/modules/git/pipeline/lfs_nogogit.go
@@ -0,0 +1,266 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// +build !gogit
+
+package pipeline
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "io"
+ "sort"
+ "strings"
+ "sync"
+ "time"
+
+ "code.gitea.io/gitea/modules/git"
+)
+
+// LFSResult represents commits found using a provided pointer file hash
+type LFSResult struct {
+ Name string
+ SHA string
+ Summary string
+ When time.Time
+ ParentHashes []git.SHA1
+ BranchName string
+ FullCommitName string
+}
+
+type lfsResultSlice []*LFSResult
+
+func (a lfsResultSlice) Len() int { return len(a) }
+func (a lfsResultSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+func (a lfsResultSlice) Less(i, j int) bool { return a[j].When.After(a[i].When) }
+
+// FindLFSFile finds commits that contain a provided pointer file hash
+func FindLFSFile(repo *git.Repository, hash git.SHA1) ([]*LFSResult, error) {
+ resultsMap := map[string]*LFSResult{}
+ results := make([]*LFSResult, 0)
+
+ basePath := repo.Path
+
+ hashStr := hash.String()
+
+ // Use rev-list to provide us with all commits in order
+ revListReader, revListWriter := io.Pipe()
+ defer func() {
+ _ = revListWriter.Close()
+ _ = revListReader.Close()
+ }()
+
+ go func() {
+ stderr := strings.Builder{}
+ err := git.NewCommand("rev-list", "--all").RunInDirPipeline(repo.Path, revListWriter, &stderr)
+ if err != nil {
+ _ = revListWriter.CloseWithError(git.ConcatenateError(err, (&stderr).String()))
+ } else {
+ _ = revListWriter.Close()
+ }
+ }()
+
+ // Next feed the commits in order into cat-file --batch, followed by their trees and sub trees as necessary.
+ // so let's create a batch stdin and stdout
+ batchStdinReader, batchStdinWriter := io.Pipe()
+ batchStdoutReader, batchStdoutWriter := io.Pipe()
+ defer func() {
+ _ = batchStdinReader.Close()
+ _ = batchStdinWriter.Close()
+ _ = batchStdoutReader.Close()
+ _ = batchStdoutWriter.Close()
+ }()
+
+ go func() {
+ stderr := strings.Builder{}
+ err := git.NewCommand("cat-file", "--batch").RunInDirFullPipeline(repo.Path, batchStdoutWriter, &stderr, batchStdinReader)
+ if err != nil {
+ _ = revListWriter.CloseWithError(git.ConcatenateError(err, (&stderr).String()))
+ } else {
+ _ = revListWriter.Close()
+ }
+ }()
+
+ // For simplicities sake we'll us a buffered reader to read from the cat-file --batch
+ batchReader := bufio.NewReader(batchStdoutReader)
+
+ // We'll use a scanner for the revList because it's simpler than a bufio.Reader
+ scan := bufio.NewScanner(revListReader)
+ trees := [][]byte{}
+ paths := []string{}
+
+ fnameBuf := make([]byte, 4096)
+ modeBuf := make([]byte, 40)
+ workingShaBuf := make([]byte, 40)
+
+ for scan.Scan() {
+ // Get the next commit ID
+ commitID := scan.Bytes()
+
+ // push the commit to the cat-file --batch process
+ _, err := batchStdinWriter.Write(commitID)
+ if err != nil {
+ return nil, err
+ }
+ _, err = batchStdinWriter.Write([]byte{'\n'})
+ if err != nil {
+ return nil, err
+ }
+
+ var curCommit *git.Commit
+ curPath := ""
+
+ commitReadingLoop:
+ for {
+ _, typ, size, err := git.ReadBatchLine(batchReader)
+ if err != nil {
+ return nil, err
+ }
+
+ switch typ {
+ case "tag":
+ // This shouldn't happen but if it does well just get the commit and try again
+ id, err := git.ReadTagObjectID(batchReader, size)
+ if err != nil {
+ return nil, err
+ }
+ _, err = batchStdinWriter.Write([]byte(id + "\n"))
+ if err != nil {
+ return nil, err
+ }
+ continue
+ case "commit":
+ // Read in the commit to get its tree and in case this is one of the last used commits
+ curCommit, err = git.CommitFromReader(repo, git.MustIDFromString(string(commitID)), io.LimitReader(batchReader, int64(size)))
+ if err != nil {
+ return nil, err
+ }
+
+ _, err := batchStdinWriter.Write([]byte(curCommit.Tree.ID.String() + "\n"))
+ if err != nil {
+ return nil, err
+ }
+ curPath = ""
+ case "tree":
+ var n int64
+ for n < size {
+ mode, fname, sha, count, err := git.ParseTreeLine(batchReader, modeBuf, fnameBuf, workingShaBuf)
+ if err != nil {
+ return nil, err
+ }
+ n += int64(count)
+ if bytes.Equal(sha, []byte(hashStr)) {
+ result := LFSResult{
+ Name: curPath + string(fname),
+ SHA: curCommit.ID.String(),
+ Summary: strings.Split(strings.TrimSpace(curCommit.CommitMessage), "\n")[0],
+ When: curCommit.Author.When,
+ ParentHashes: curCommit.Parents,
+ }
+ resultsMap[curCommit.ID.String()+":"+curPath+string(fname)] = &result
+ } else if string(mode) == git.EntryModeTree.String() {
+ trees = append(trees, sha)
+ paths = append(paths, curPath+string(fname)+"/")
+ }
+ }
+ if len(trees) > 0 {
+ _, err := batchStdinWriter.Write(trees[len(trees)-1])
+ if err != nil {
+ return nil, err
+ }
+ _, err = batchStdinWriter.Write([]byte("\n"))
+ if err != nil {
+ return nil, err
+ }
+ curPath = paths[len(paths)-1]
+ trees = trees[:len(trees)-1]
+ paths = paths[:len(paths)-1]
+ } else {
+ break commitReadingLoop
+ }
+ }
+ }
+ }
+
+ if err := scan.Err(); err != nil {
+ return nil, err
+ }
+
+ for _, result := range resultsMap {
+ hasParent := false
+ for _, parentHash := range result.ParentHashes {
+ if _, hasParent = resultsMap[parentHash.String()+":"+result.Name]; hasParent {
+ break
+ }
+ }
+ if !hasParent {
+ results = append(results, result)
+ }
+ }
+
+ sort.Sort(lfsResultSlice(results))
+
+ // Should really use a go-git function here but name-rev is not completed and recapitulating it is not simple
+ shasToNameReader, shasToNameWriter := io.Pipe()
+ nameRevStdinReader, nameRevStdinWriter := io.Pipe()
+ errChan := make(chan error, 1)
+ wg := sync.WaitGroup{}
+ wg.Add(3)
+
+ go func() {
+ defer wg.Done()
+ scanner := bufio.NewScanner(nameRevStdinReader)
+ i := 0
+ for scanner.Scan() {
+ line := scanner.Text()
+ if len(line) == 0 {
+ continue
+ }
+ result := results[i]
+ result.FullCommitName = line
+ result.BranchName = strings.Split(line, "~")[0]
+ i++
+ }
+ }()
+ go NameRevStdin(shasToNameReader, nameRevStdinWriter, &wg, basePath)
+ go func() {
+ defer wg.Done()
+ defer shasToNameWriter.Close()
+ for _, result := range results {
+ i := 0
+ if i < len(result.SHA) {
+ n, err := shasToNameWriter.Write([]byte(result.SHA)[i:])
+ if err != nil {
+ errChan <- err
+ break
+ }
+ i += n
+ }
+ var err error
+ n := 0
+ for n < 1 {
+ n, err = shasToNameWriter.Write([]byte{'\n'})
+ if err != nil {
+ errChan <- err
+ break
+ }
+
+ }
+
+ }
+ }()
+
+ wg.Wait()
+
+ select {
+ case err, has := <-errChan:
+ if has {
+ return nil, fmt.Errorf("Unable to obtain name for LFS files. Error: %w", err)
+ }
+ default:
+ }
+
+ return results, nil
+}
diff --git a/modules/git/repo.go b/modules/git/repo.go
index 9b1da87a32..e824dcc3f2 100644
--- a/modules/git/repo.go
+++ b/modules/git/repo.go
@@ -9,34 +9,16 @@ import (
"bytes"
"container/list"
"context"
- "errors"
"fmt"
"os"
"path"
- "path/filepath"
"strconv"
"strings"
"time"
- gitealog "code.gitea.io/gitea/modules/log"
- "github.com/go-git/go-billy/v5/osfs"
- gogit "github.com/go-git/go-git/v5"
- "github.com/go-git/go-git/v5/plumbing/cache"
- "github.com/go-git/go-git/v5/storage/filesystem"
"github.com/unknwon/com"
)
-// Repository represents a Git repository.
-type Repository struct {
- Path string
-
- tagCache *ObjectCache
-
- gogitRepo *gogit.Repository
- gogitStorage *filesystem.Storage
- gpgSettings *GPGSettings
-}
-
// GPGSettings represents the default GPG settings for this repository
type GPGSettings struct {
Sign bool
@@ -93,52 +75,6 @@ func InitRepository(repoPath string, bare bool) error {
return err
}
-// OpenRepository opens the repository at the given path.
-func OpenRepository(repoPath string) (*Repository, error) {
- repoPath, err := filepath.Abs(repoPath)
- if err != nil {
- return nil, err
- } else if !isDir(repoPath) {
- return nil, errors.New("no such file or directory")
- }
-
- fs := osfs.New(repoPath)
- _, err = fs.Stat(".git")
- if err == nil {
- fs, err = fs.Chroot(".git")
- if err != nil {
- return nil, err
- }
- }
- storage := filesystem.NewStorageWithOptions(fs, cache.NewObjectLRUDefault(), filesystem.Options{KeepDescriptors: true})
- gogitRepo, err := gogit.Open(storage, fs)
- if err != nil {
- return nil, err
- }
-
- return &Repository{
- Path: repoPath,
- gogitRepo: gogitRepo,
- gogitStorage: storage,
- tagCache: newObjectCache(),
- }, nil
-}
-
-// Close this repository, in particular close the underlying gogitStorage if this is not nil
-func (repo *Repository) Close() {
- if repo == nil || repo.gogitStorage == nil {
- return
- }
- if err := repo.gogitStorage.Close(); err != nil {
- gitealog.Error("Error closing storage: %v", err)
- }
-}
-
-// GoGitRepo gets the go-git repo representation
-func (repo *Repository) GoGitRepo() *gogit.Repository {
- return repo.gogitRepo
-}
-
// IsEmpty Check if repository is empty.
func (repo *Repository) IsEmpty() (bool, error) {
var errbuf strings.Builder
diff --git a/modules/git/repo_base_gogit.go b/modules/git/repo_base_gogit.go
new file mode 100644
index 0000000000..19a3f84571
--- /dev/null
+++ b/modules/git/repo_base_gogit.go
@@ -0,0 +1,76 @@
+// Copyright 2015 The Gogs Authors. All rights reserved.
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// +build gogit
+
+package git
+
+import (
+ "errors"
+ "path/filepath"
+
+ gitealog "code.gitea.io/gitea/modules/log"
+ "github.com/go-git/go-billy/v5/osfs"
+ gogit "github.com/go-git/go-git/v5"
+ "github.com/go-git/go-git/v5/plumbing/cache"
+ "github.com/go-git/go-git/v5/storage/filesystem"
+)
+
+// Repository represents a Git repository.
+type Repository struct {
+ Path string
+
+ tagCache *ObjectCache
+
+ gogitRepo *gogit.Repository
+ gogitStorage *filesystem.Storage
+ gpgSettings *GPGSettings
+}
+
+// OpenRepository opens the repository at the given path.
+func OpenRepository(repoPath string) (*Repository, error) {
+ repoPath, err := filepath.Abs(repoPath)
+ if err != nil {
+ return nil, err
+ } else if !isDir(repoPath) {
+ return nil, errors.New("no such file or directory")
+ }
+
+ fs := osfs.New(repoPath)
+ _, err = fs.Stat(".git")
+ if err == nil {
+ fs, err = fs.Chroot(".git")
+ if err != nil {
+ return nil, err
+ }
+ }
+ storage := filesystem.NewStorageWithOptions(fs, cache.NewObjectLRUDefault(), filesystem.Options{KeepDescriptors: true})
+ gogitRepo, err := gogit.Open(storage, fs)
+ if err != nil {
+ return nil, err
+ }
+
+ return &Repository{
+ Path: repoPath,
+ gogitRepo: gogitRepo,
+ gogitStorage: storage,
+ tagCache: newObjectCache(),
+ }, nil
+}
+
+// Close this repository, in particular close the underlying gogitStorage if this is not nil
+func (repo *Repository) Close() {
+ if repo == nil || repo.gogitStorage == nil {
+ return
+ }
+ if err := repo.gogitStorage.Close(); err != nil {
+ gitealog.Error("Error closing storage: %v", err)
+ }
+}
+
+// GoGitRepo gets the go-git repo representation
+func (repo *Repository) GoGitRepo() *gogit.Repository {
+ return repo.gogitRepo
+}
diff --git a/modules/git/repo_base_nogogit.go b/modules/git/repo_base_nogogit.go
new file mode 100644
index 0000000000..e05219a4e7
--- /dev/null
+++ b/modules/git/repo_base_nogogit.go
@@ -0,0 +1,40 @@
+// Copyright 2015 The Gogs Authors. All rights reserved.
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// +build !gogit
+
+package git
+
+import (
+ "errors"
+ "path/filepath"
+)
+
+// Repository represents a Git repository.
+type Repository struct {
+ Path string
+
+ tagCache *ObjectCache
+
+ gpgSettings *GPGSettings
+}
+
+// OpenRepository opens the repository at the given path.
+func OpenRepository(repoPath string) (*Repository, error) {
+ repoPath, err := filepath.Abs(repoPath)
+ if err != nil {
+ return nil, err
+ } else if !isDir(repoPath) {
+ return nil, errors.New("no such file or directory")
+ }
+ return &Repository{
+ Path: repoPath,
+ tagCache: newObjectCache(),
+ }, nil
+}
+
+// Close this repository, in particular close the underlying gogitStorage if this is not nil
+func (repo *Repository) Close() {
+}
diff --git a/modules/git/repo_blob.go b/modules/git/repo_blob.go
index ce0ad6b50f..5397f24cb6 100644
--- a/modules/git/repo_blob.go
+++ b/modules/git/repo_blob.go
@@ -1,25 +1,9 @@
-// Copyright 2018 The Gitea Authors. All rights reserved.
+// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package git
-import (
- "github.com/go-git/go-git/v5/plumbing"
-)
-
-func (repo *Repository) getBlob(id SHA1) (*Blob, error) {
- encodedObj, err := repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, id)
- if err != nil {
- return nil, ErrNotExist{id.String(), ""}
- }
-
- return &Blob{
- ID: id,
- gogitEncodedObj: encodedObj,
- }, nil
-}
-
// GetBlob finds the blob object in the repository.
func (repo *Repository) GetBlob(idStr string) (*Blob, error) {
id, err := NewIDFromString(idStr)
diff --git a/modules/git/repo_blob_gogit.go b/modules/git/repo_blob_gogit.go
new file mode 100644
index 0000000000..485c233ff8
--- /dev/null
+++ b/modules/git/repo_blob_gogit.go
@@ -0,0 +1,23 @@
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// +build gogit
+
+package git
+
+import (
+ "github.com/go-git/go-git/v5/plumbing"
+)
+
+func (repo *Repository) getBlob(id SHA1) (*Blob, error) {
+ encodedObj, err := repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, id)
+ if err != nil {
+ return nil, ErrNotExist{id.String(), ""}
+ }
+
+ return &Blob{
+ ID: id,
+ gogitEncodedObj: encodedObj,
+ }, nil
+}
diff --git a/modules/git/repo_blob_nogogit.go b/modules/git/repo_blob_nogogit.go
new file mode 100644
index 0000000000..9959420df4
--- /dev/null
+++ b/modules/git/repo_blob_nogogit.go
@@ -0,0 +1,17 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// +build !gogit
+
+package git
+
+func (repo *Repository) getBlob(id SHA1) (*Blob, error) {
+ if id.IsZero() {
+ return nil, ErrNotExist{id.String(), ""}
+ }
+ return &Blob{
+ ID: id,
+ repoPath: repo.Path,
+ }, nil
+}
diff --git a/modules/git/repo_branch.go b/modules/git/repo_branch.go
index cd30c191ea..25438530f5 100644
--- a/modules/git/repo_branch.go
+++ b/modules/git/repo_branch.go
@@ -8,8 +8,6 @@ package git
import (
"fmt"
"strings"
-
- "github.com/go-git/go-git/v5/plumbing"
)
// BranchPrefix base dir of the branch information file store on git
@@ -26,18 +24,6 @@ func IsBranchExist(repoPath, name string) bool {
return IsReferenceExist(repoPath, BranchPrefix+name)
}
-// IsBranchExist returns true if given branch exists in current repository.
-func (repo *Repository) IsBranchExist(name string) bool {
- if name == "" {
- return false
- }
- reference, err := repo.gogitRepo.Reference(plumbing.ReferenceName(BranchPrefix+name), true)
- if err != nil {
- return false
- }
- return reference.Type() != plumbing.InvalidReference
-}
-
// Branch represents a Git branch.
type Branch struct {
Name string
@@ -79,25 +65,6 @@ func (repo *Repository) GetDefaultBranch() (string, error) {
return NewCommand("symbolic-ref", "HEAD").RunInDir(repo.Path)
}
-// GetBranches returns all branches of the repository.
-func (repo *Repository) GetBranches() ([]string, error) {
- var branchNames []string
-
- branches, err := repo.gogitRepo.Branches()
- if err != nil {
- return nil, err
- }
-
- _ = 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) {
diff --git a/modules/git/repo_branch_gogit.go b/modules/git/repo_branch_gogit.go
new file mode 100644
index 0000000000..65cb77a8b5
--- /dev/null
+++ b/modules/git/repo_branch_gogit.go
@@ -0,0 +1,45 @@
+// Copyright 2015 The Gogs Authors. All rights reserved.
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// +build gogit
+
+package git
+
+import (
+ "strings"
+
+ "github.com/go-git/go-git/v5/plumbing"
+)
+
+// IsBranchExist returns true if given branch exists in current repository.
+func (repo *Repository) IsBranchExist(name string) bool {
+ if name == "" {
+ return false
+ }
+ reference, err := repo.gogitRepo.Reference(plumbing.ReferenceName(BranchPrefix+name), true)
+ if err != nil {
+ return false
+ }
+ return reference.Type() != plumbing.InvalidReference
+}
+
+// GetBranches returns all branches of the repository.
+func (repo *Repository) GetBranches() ([]string, error) {
+ var branchNames []string
+
+ branches, err := repo.gogitRepo.Branches()
+ if err != nil {
+ return nil, err
+ }
+
+ _ = branches.ForEach(func(branch *plumbing.Reference) error {
+ branchNames = append(branchNames, strings.TrimPrefix(branch.Name().String(), BranchPrefix))
+ return nil
+ })
+
+ // TODO: Sort?
+
+ return branchNames, nil
+}
diff --git a/modules/git/repo_branch_nogogit.go b/modules/git/repo_branch_nogogit.go
new file mode 100644
index 0000000000..5ec46d725e
--- /dev/null
+++ b/modules/git/repo_branch_nogogit.go
@@ -0,0 +1,82 @@
+// Copyright 2015 The Gogs Authors. All rights reserved.
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// +build !gogit
+
+package git
+
+import (
+ "bufio"
+ "io"
+ "strings"
+)
+
+// IsBranchExist returns true if given branch exists in current repository.
+func (repo *Repository) IsBranchExist(name string) bool {
+ if name == "" {
+ return false
+ }
+ return IsReferenceExist(repo.Path, BranchPrefix+name)
+}
+
+// GetBranches returns all branches of the repository.
+func (repo *Repository) GetBranches() ([]string, error) {
+ return callShowRef(repo.Path, BranchPrefix, "--heads")
+}
+
+func callShowRef(repoPath, prefix, arg string) ([]string, error) {
+ var branchNames []string
+
+ stdoutReader, stdoutWriter := io.Pipe()
+ defer func() {
+ _ = stdoutReader.Close()
+ _ = stdoutWriter.Close()
+ }()
+
+ go func() {
+ stderrBuilder := &strings.Builder{}
+ err := NewCommand("show-ref", arg).RunInDirPipeline(repoPath, stdoutWriter, stderrBuilder)
+ if err != nil {
+ if stderrBuilder.Len() == 0 {
+ _ = stdoutWriter.Close()
+ return
+ }
+ _ = stdoutWriter.CloseWithError(ConcatenateError(err, stderrBuilder.String()))
+ } else {
+ _ = stdoutWriter.Close()
+ }
+ }()
+
+ bufReader := bufio.NewReader(stdoutReader)
+ for {
+ // The output of show-ref is simply a list:
+ // <sha> SP <ref> LF
+ _, err := bufReader.ReadSlice(' ')
+ for err == bufio.ErrBufferFull {
+ // This shouldn't happen but we'll tolerate it for the sake of peace
+ _, err = bufReader.ReadSlice(' ')
+ }
+ if err == io.EOF {
+ return branchNames, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ branchName, err := bufReader.ReadString('\n')
+ if err == io.EOF {
+ // This shouldn't happen... but we'll tolerate it for the sake of peace
+ return branchNames, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+ branchName = strings.TrimPrefix(branchName, prefix)
+ if len(branchName) > 0 {
+ branchName = branchName[:len(branchName)-1]
+ }
+ branchNames = append(branchNames, branchName)
+ }
+}
diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go
index ee3b05447b..c31f416628 100644
--- a/modules/git/repo_commit.go
+++ b/modules/git/repo_commit.go
@@ -8,36 +8,10 @@ package git
import (
"bytes"
"container/list"
- "fmt"
"strconv"
"strings"
-
- "github.com/go-git/go-git/v5/plumbing"
- "github.com/go-git/go-git/v5/plumbing/object"
)
-// GetRefCommitID returns the last commit ID string of given reference (branch or tag).
-func (repo *Repository) GetRefCommitID(name string) (string, error) {
- ref, err := repo.gogitRepo.Reference(plumbing.ReferenceName(name), true)
- if err != nil {
- if err == plumbing.ErrReferenceNotFound {
- return "", ErrNotExist{
- ID: name,
- }
- }
- return "", err
- }
-
- return ref.Hash().String(), nil
-}
-
-// IsCommitExist returns true if given commit exists in current repository.
-func (repo *Repository) IsCommitExist(name string) bool {
- hash := plumbing.NewHash(name)
- _, err := repo.gogitRepo.CommitObject(hash)
- return err == nil
-}
-
// GetBranchCommitID returns last commit ID string of given branch.
func (repo *Repository) GetBranchCommitID(name string) (string, error) {
return repo.GetRefCommitID(BranchPrefix + name)
@@ -55,78 +29,6 @@ func (repo *Repository) GetTagCommitID(name string) (string, error) {
return strings.TrimSpace(stdout), nil
}
-func convertPGPSignatureForTag(t *object.Tag) *CommitGPGSignature {
- if t.PGPSignature == "" {
- return nil
- }
-
- 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",
- }
-}
-
-func (repo *Repository) getCommit(id SHA1) (*Commit, error) {
- var tagObject *object.Tag
-
- gogitCommit, err := repo.gogitRepo.CommitObject(id)
- if err == plumbing.ErrObjectNotFound {
- tagObject, err = repo.gogitRepo.TagObject(id)
- if err == plumbing.ErrObjectNotFound {
- return nil, ErrNotExist{
- ID: id.String(),
- }
- }
- if err == nil {
- gogitCommit, err = repo.gogitRepo.CommitObject(tagObject.Target)
- }
- // if we get a plumbing.ErrObjectNotFound here then the repository is broken and it should be 500
- }
- if err != nil {
- return nil, err
- }
-
- commit := convertCommit(gogitCommit)
- commit.repo = repo
-
- if tagObject != nil {
- commit.CommitMessage = strings.TrimSpace(tagObject.Message)
- commit.Author = &tagObject.Tagger
- commit.Signature = convertPGPSignatureForTag(tagObject)
- }
-
- tree, err := gogitCommit.Tree()
- if err != nil {
- return nil, err
- }
-
- commit.Tree.ID = tree.Hash
- commit.Tree.gogitTree = tree
-
- return commit, nil
-}
-
// ConvertToSHA1 returns a Hash object from a potential ID string
func (repo *Repository) ConvertToSHA1(commitID string) (SHA1, error) {
if len(commitID) != 40 {
diff --git a/modules/git/repo_commit_gogit.go b/modules/git/repo_commit_gogit.go
new file mode 100644
index 0000000000..48b0cfe19d
--- /dev/null
+++ b/modules/git/repo_commit_gogit.go
@@ -0,0 +1,110 @@
+// 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
+// license that can be found in the LICENSE file.
+
+// +build gogit
+
+package git
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/go-git/go-git/v5/plumbing"
+ "github.com/go-git/go-git/v5/plumbing/object"
+)
+
+// GetRefCommitID returns the last commit ID string of given reference (branch or tag).
+func (repo *Repository) GetRefCommitID(name string) (string, error) {
+ ref, err := repo.gogitRepo.Reference(plumbing.ReferenceName(name), true)
+ if err != nil {
+ if err == plumbing.ErrReferenceNotFound {
+ return "", ErrNotExist{
+ ID: name,
+ }
+ }
+ return "", err
+ }
+
+ return ref.Hash().String(), nil
+}
+
+// IsCommitExist returns true if given commit exists in current repository.
+func (repo *Repository) IsCommitExist(name string) bool {
+ hash := plumbing.NewHash(name)
+ _, err := repo.gogitRepo.CommitObject(hash)
+ return err == nil
+}
+
+func convertPGPSignatureForTag(t *object.Tag) *CommitGPGSignature {
+ if t.PGPSignature == "" {
+ return nil
+ }
+
+ 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",
+ }
+}
+
+func (repo *Repository) getCommit(id SHA1) (*Commit, error) {
+ var tagObject *object.Tag
+
+ gogitCommit, err := repo.gogitRepo.CommitObject(id)
+ if err == plumbing.ErrObjectNotFound {
+ tagObject, err = repo.gogitRepo.TagObject(id)
+ if err == plumbing.ErrObjectNotFound {
+ return nil, ErrNotExist{
+ ID: id.String(),
+ }
+ }
+ if err == nil {
+ gogitCommit, err = repo.gogitRepo.CommitObject(tagObject.Target)
+ }
+ // if we get a plumbing.ErrObjectNotFound here then the repository is broken and it should be 500
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ commit := convertCommit(gogitCommit)
+ commit.repo = repo
+
+ if tagObject != nil {
+ commit.CommitMessage = strings.TrimSpace(tagObject.Message)
+ commit.Author = &tagObject.Tagger
+ commit.Signature = convertPGPSignatureForTag(tagObject)
+ }
+
+ tree, err := gogitCommit.Tree()
+ if err != nil {
+ return nil, err
+ }
+
+ commit.Tree.ID = tree.Hash
+ commit.Tree.gogitTree = tree
+
+ return commit, nil
+}
diff --git a/modules/git/repo_commit_nogogit.go b/modules/git/repo_commit_nogogit.go
new file mode 100644
index 0000000000..a43fe4b334
--- /dev/null
+++ b/modules/git/repo_commit_nogogit.go
@@ -0,0 +1,109 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// +build !gogit
+
+package git
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "strings"
+)
+
+// ResolveReference resolves a name to a reference
+func (repo *Repository) ResolveReference(name string) (string, error) {
+ stdout, err := NewCommand("show-ref", "--hash", name).RunInDir(repo.Path)
+ if err != nil {
+ if strings.Contains(err.Error(), "not a valid ref") {
+ return "", ErrNotExist{name, ""}
+ }
+ return "", err
+ }
+ stdout = strings.TrimSpace(stdout)
+ if stdout == "" {
+ return "", ErrNotExist{name, ""}
+ }
+
+ return stdout, nil
+}
+
+// GetRefCommitID returns the last commit ID string of given reference (branch or tag).
+func (repo *Repository) GetRefCommitID(name string) (string, error) {
+ stdout, err := NewCommand("show-ref", "--verify", "--hash", name).RunInDir(repo.Path)
+ if err != nil {
+ if strings.Contains(err.Error(), "not a valid ref") {
+ return "", ErrNotExist{name, ""}
+ }
+ return "", err
+ }
+
+ return strings.TrimSpace(stdout), nil
+}
+
+// IsCommitExist returns true if given commit exists in current repository.
+func (repo *Repository) IsCommitExist(name string) bool {
+ _, err := NewCommand("cat-file", "-e", name).RunInDir(repo.Path)
+ return err == nil
+}
+
+func (repo *Repository) getCommit(id SHA1) (*Commit, error) {
+ stdoutReader, stdoutWriter := io.Pipe()
+ defer func() {
+ _ = stdoutReader.Close()
+ _ = stdoutWriter.Close()
+ }()
+
+ go func() {
+ stderr := strings.Builder{}
+ err := NewCommand("cat-file", "--batch").RunInDirFullPipeline(repo.Path, stdoutWriter, &stderr, strings.NewReader(id.String()+"\n"))
+ if err != nil {
+ _ = stdoutWriter.CloseWithError(ConcatenateError(err, (&stderr).String()))
+ } else {
+ _ = stdoutWriter.Close()
+ }
+ }()
+
+ bufReader := bufio.NewReader(stdoutReader)
+ _, typ, size, err := ReadBatchLine(bufReader)
+ if err != nil {
+ return nil, err
+ }
+
+ switch typ {
+ case "tag":
+ // then we need to parse the tag
+ // and load the commit
+ data, err := ioutil.ReadAll(io.LimitReader(bufReader, size))
+ if err != nil {
+ return nil, err
+ }
+ tag, err := parseTagData(data)
+ if err != nil {
+ return nil, err
+ }
+ tag.repo = repo
+
+ commit, err := tag.Commit()
+ if err != nil {
+ return nil, err
+ }
+
+ commit.CommitMessage = strings.TrimSpace(tag.Message)
+ commit.Author = tag.Tagger
+ commit.Signature = tag.Signature
+
+ return commit, nil
+ case "commit":
+ return CommitFromReader(repo, id, io.LimitReader(bufReader, size))
+ default:
+ _ = stdoutReader.CloseWithError(fmt.Errorf("unknown typ: %s", typ))
+ log("Unknown typ: %s", typ)
+ return nil, ErrNotExist{
+ ID: id.String(),
+ }
+ }
+}
diff --git a/modules/git/repo_commitgraph.go b/modules/git/repo_commitgraph_gogit.go
index 00111f5503..6773109451 100644
--- a/modules/git/repo_commitgraph.go
+++ b/modules/git/repo_commitgraph_gogit.go
@@ -3,6 +3,8 @@
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
+// +build gogit
+
package git
import (
diff --git a/modules/git/repo_language_stats.go b/modules/git/repo_language_stats.go
index b721b996e4..ac23caa0fc 100644
--- a/modules/git/repo_language_stats.go
+++ b/modules/git/repo_language_stats.go
@@ -4,111 +4,5 @@
package git
-import (
- "bytes"
- "io"
- "io/ioutil"
-
- "code.gitea.io/gitea/modules/analyze"
-
- "github.com/go-enry/go-enry/v2"
- "github.com/go-git/go-git/v5"
- "github.com/go-git/go-git/v5/plumbing"
- "github.com/go-git/go-git/v5/plumbing/object"
-)
-
const fileSizeLimit int64 = 16 * 1024 // 16 KiB
const bigFileSize int64 = 1024 * 1024 // 1 MiB
-
-// GetLanguageStats calculates language stats for git repository at specified commit
-func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, error) {
- r, err := git.PlainOpen(repo.Path)
- if err != nil {
- return nil, err
- }
-
- rev, err := r.ResolveRevision(plumbing.Revision(commitID))
- if err != nil {
- return nil, err
- }
-
- commit, err := r.CommitObject(*rev)
- if err != nil {
- return nil, err
- }
-
- tree, err := commit.Tree()
- if err != nil {
- return nil, err
- }
-
- sizes := make(map[string]int64)
- err = tree.Files().ForEach(func(f *object.File) error {
- if f.Size == 0 || enry.IsVendor(f.Name) || enry.IsDotFile(f.Name) ||
- enry.IsDocumentation(f.Name) || enry.IsConfiguration(f.Name) {
- return nil
- }
-
- // If content can not be read or file is too big just do detection by filename
- var content []byte
- if f.Size <= bigFileSize {
- content, _ = readFile(f, fileSizeLimit)
- }
- if enry.IsGenerated(f.Name, content) {
- return nil
- }
-
- // TODO: Use .gitattributes file for linguist overrides
-
- language := analyze.GetCodeLanguage(f.Name, content)
- if language == enry.OtherLanguage || language == "" {
- return nil
- }
-
- // group languages, such as Pug -> HTML; SCSS -> CSS
- group := enry.GetLanguageGroup(language)
- if group != "" {
- language = group
- }
-
- sizes[language] += f.Size
-
- return nil
- })
- if err != nil {
- return nil, err
- }
-
- // filter special languages unless they are the only language
- if len(sizes) > 1 {
- for language := range sizes {
- langtype := enry.GetLanguageType(language)
- if langtype != enry.Programming && langtype != enry.Markup {
- delete(sizes, language)
- }
- }
- }
-
- return sizes, nil
-}
-
-func readFile(f *object.File, limit int64) ([]byte, error) {
- r, err := f.Reader()
- if err != nil {
- return nil, err
- }
- defer r.Close()
-
- if limit <= 0 {
- return ioutil.ReadAll(r)
- }
-
- size := f.Size
- if limit > 0 && size > limit {
- size = limit
- }
- buf := bytes.NewBuffer(nil)
- buf.Grow(int(size))
- _, err = io.Copy(buf, io.LimitReader(r, limit))
- return buf.Bytes(), err
-}
diff --git a/modules/git/repo_language_stats_gogit.go b/modules/git/repo_language_stats_gogit.go
new file mode 100644
index 0000000000..b5a235921c
--- /dev/null
+++ b/modules/git/repo_language_stats_gogit.go
@@ -0,0 +1,113 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// +build gogit
+
+package git
+
+import (
+ "bytes"
+ "io"
+ "io/ioutil"
+
+ "code.gitea.io/gitea/modules/analyze"
+
+ "github.com/go-enry/go-enry/v2"
+ "github.com/go-git/go-git/v5"
+ "github.com/go-git/go-git/v5/plumbing"
+ "github.com/go-git/go-git/v5/plumbing/object"
+)
+
+// GetLanguageStats calculates language stats for git repository at specified commit
+func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, error) {
+ r, err := git.PlainOpen(repo.Path)
+ if err != nil {
+ return nil, err
+ }
+
+ rev, err := r.ResolveRevision(plumbing.Revision(commitID))
+ if err != nil {
+ return nil, err
+ }
+
+ commit, err := r.CommitObject(*rev)
+ if err != nil {
+ return nil, err
+ }
+
+ tree, err := commit.Tree()
+ if err != nil {
+ return nil, err
+ }
+
+ sizes := make(map[string]int64)
+ err = tree.Files().ForEach(func(f *object.File) error {
+ if f.Size == 0 || enry.IsVendor(f.Name) || enry.IsDotFile(f.Name) ||
+ enry.IsDocumentation(f.Name) || enry.IsConfiguration(f.Name) {
+ return nil
+ }
+
+ // If content can not be read or file is too big just do detection by filename
+ var content []byte
+ if f.Size <= bigFileSize {
+ content, _ = readFile(f, fileSizeLimit)
+ }
+ if enry.IsGenerated(f.Name, content) {
+ return nil
+ }
+
+ // TODO: Use .gitattributes file for linguist overrides
+
+ language := analyze.GetCodeLanguage(f.Name, content)
+ if language == enry.OtherLanguage || language == "" {
+ return nil
+ }
+
+ // group languages, such as Pug -> HTML; SCSS -> CSS
+ group := enry.GetLanguageGroup(language)
+ if group != "" {
+ language = group
+ }
+
+ sizes[language] += f.Size
+
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ // filter special languages unless they are the only language
+ if len(sizes) > 1 {
+ for language := range sizes {
+ langtype := enry.GetLanguageType(language)
+ if langtype != enry.Programming && langtype != enry.Markup {
+ delete(sizes, language)
+ }
+ }
+ }
+
+ return sizes, nil
+}
+
+func readFile(f *object.File, limit int64) ([]byte, error) {
+ r, err := f.Reader()
+ if err != nil {
+ return nil, err
+ }
+ defer r.Close()
+
+ if limit <= 0 {
+ return ioutil.ReadAll(r)
+ }
+
+ size := f.Size
+ if limit > 0 && size > limit {
+ size = limit
+ }
+ buf := bytes.NewBuffer(nil)
+ buf.Grow(int(size))
+ _, err = io.Copy(buf, io.LimitReader(r, limit))
+ return buf.Bytes(), err
+}
diff --git a/modules/git/repo_language_stats_nogogit.go b/modules/git/repo_language_stats_nogogit.go
new file mode 100644
index 0000000000..5607e4591a
--- /dev/null
+++ b/modules/git/repo_language_stats_nogogit.go
@@ -0,0 +1,109 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// +build !gogit
+
+package git
+
+import (
+ "bytes"
+ "io"
+ "io/ioutil"
+
+ "code.gitea.io/gitea/modules/analyze"
+
+ "github.com/go-enry/go-enry/v2"
+)
+
+// GetLanguageStats calculates language stats for git repository at specified commit
+func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, error) {
+ // FIXME: We can be more efficient here...
+ //
+ // We're expecting that we will be reading a lot of blobs and the trees
+ // Thus we should use a shared `cat-file --batch` to get all of this data
+ // And keep the buffers around with resets as necessary.
+ //
+ // It's more complicated so...
+ commit, err := repo.GetCommit(commitID)
+ if err != nil {
+ log("Unable to get commit for: %s", commitID)
+ return nil, err
+ }
+
+ tree := commit.Tree
+
+ entries, err := tree.ListEntriesRecursive()
+ if err != nil {
+ return nil, err
+ }
+
+ sizes := make(map[string]int64)
+ for _, f := range entries {
+ if f.Size() == 0 || enry.IsVendor(f.Name()) || enry.IsDotFile(f.Name()) ||
+ enry.IsDocumentation(f.Name()) || enry.IsConfiguration(f.Name()) {
+ continue
+ }
+
+ // If content can not be read or file is too big just do detection by filename
+ var content []byte
+ if f.Size() <= bigFileSize {
+ content, _ = readFile(f, fileSizeLimit)
+ }
+ if enry.IsGenerated(f.Name(), content) {
+ continue
+ }
+
+ // TODO: Use .gitattributes file for linguist overrides
+ // FIXME: Why can't we split this and the IsGenerated tests to avoid reading the blob unless absolutely necessary?
+ // - eg. do the all the detection tests using filename first before reading content.
+ language := analyze.GetCodeLanguage(f.Name(), content)
+ if language == enry.OtherLanguage || language == "" {
+ continue
+ }
+
+ // group languages, such as Pug -> HTML; SCSS -> CSS
+ group := enry.GetLanguageGroup(language)
+ if group != "" {
+ language = group
+ }
+
+ sizes[language] += f.Size()
+
+ continue
+ }
+
+ // filter special languages unless they are the only language
+ if len(sizes) > 1 {
+ for language := range sizes {
+ langtype := enry.GetLanguageType(language)
+ if langtype != enry.Programming && langtype != enry.Markup {
+ delete(sizes, language)
+ }
+ }
+ }
+
+ return sizes, nil
+}
+
+func readFile(entry *TreeEntry, limit int64) ([]byte, error) {
+ // FIXME: We can probably be a little more efficient here... see above
+ r, err := entry.Blob().DataAsync()
+ if err != nil {
+ return nil, err
+ }
+ defer r.Close()
+
+ if limit <= 0 {
+ return ioutil.ReadAll(r)
+ }
+
+ size := entry.Size()
+ if limit > 0 && size > limit {
+ size = limit
+ }
+ buf := bytes.NewBuffer(nil)
+ buf.Grow(int(size))
+ _, err = io.Copy(buf, io.LimitReader(r, limit))
+ return buf.Bytes(), err
+}
diff --git a/modules/git/repo_object.go b/modules/git/repo_object.go
index d4d638a743..f054c34902 100644
--- a/modules/git/repo_object.go
+++ b/modules/git/repo_object.go
@@ -27,6 +27,11 @@ const (
ObjectBranch ObjectType = "branch"
)
+// Bytes returns the byte array for the Object Type
+func (o ObjectType) Bytes() []byte {
+ return []byte(o)
+}
+
// HashObject takes a reader and returns SHA1 hash for that reader
func (repo *Repository) HashObject(reader io.Reader) (SHA1, error) {
idStr, err := repo.hashObject(reader)
diff --git a/modules/git/repo_ref.go b/modules/git/repo_ref.go
index be2a38c5f0..397434e12f 100644
--- a/modules/git/repo_ref.go
+++ b/modules/git/repo_ref.go
@@ -4,52 +4,7 @@
package git
-import (
- "strings"
-
- "github.com/go-git/go-git/v5"
- "github.com/go-git/go-git/v5/plumbing"
-)
-
// GetRefs returns all references of the repository.
func (repo *Repository) GetRefs() ([]*Reference, error) {
return repo.GetRefsFiltered("")
}
-
-// GetRefsFiltered returns all references of the repository that matches patterm exactly or starting with.
-func (repo *Repository) GetRefsFiltered(pattern string) ([]*Reference, error) {
- r, err := git.PlainOpen(repo.Path)
- if err != nil {
- return nil, err
- }
-
- refsIter, err := r.References()
- if err != nil {
- return nil, err
- }
- refs := make([]*Reference, 0)
- if err = refsIter.ForEach(func(ref *plumbing.Reference) error {
- if ref.Name() != plumbing.HEAD && !ref.Name().IsRemote() &&
- (pattern == "" || strings.HasPrefix(ref.Name().String(), pattern)) {
- refType := string(ObjectCommit)
- if ref.Name().IsTag() {
- // tags can be of type `commit` (lightweight) or `tag` (annotated)
- if tagType, _ := repo.GetTagType(ref.Hash()); err == nil {
- refType = tagType
- }
- }
- r := &Reference{
- Name: ref.Name().String(),
- Object: ref.Hash(),
- Type: refType,
- repo: repo,
- }
- refs = append(refs, r)
- }
- return nil
- }); err != nil {
- return nil, err
- }
-
- return refs, nil
-}
diff --git a/modules/git/repo_ref_gogit.go b/modules/git/repo_ref_gogit.go
new file mode 100644
index 0000000000..2e83e6c462
--- /dev/null
+++ b/modules/git/repo_ref_gogit.go
@@ -0,0 +1,52 @@
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// +build gogit
+
+package git
+
+import (
+ "strings"
+
+ "github.com/go-git/go-git/v5"
+ "github.com/go-git/go-git/v5/plumbing"
+)
+
+// GetRefsFiltered returns all references of the repository that matches patterm exactly or starting with.
+func (repo *Repository) GetRefsFiltered(pattern string) ([]*Reference, error) {
+ r, err := git.PlainOpen(repo.Path)
+ if err != nil {
+ return nil, err
+ }
+
+ refsIter, err := r.References()
+ if err != nil {
+ return nil, err
+ }
+ refs := make([]*Reference, 0)
+ if err = refsIter.ForEach(func(ref *plumbing.Reference) error {
+ if ref.Name() != plumbing.HEAD && !ref.Name().IsRemote() &&
+ (pattern == "" || strings.HasPrefix(ref.Name().String(), pattern)) {
+ refType := string(ObjectCommit)
+ if ref.Name().IsTag() {
+ // tags can be of type `commit` (lightweight) or `tag` (annotated)
+ if tagType, _ := repo.GetTagType(ref.Hash()); err == nil {
+ refType = tagType
+ }
+ }
+ r := &Reference{
+ Name: ref.Name().String(),
+ Object: ref.Hash(),
+ Type: refType,
+ repo: repo,
+ }
+ refs = append(refs, r)
+ }
+ return nil
+ }); err != nil {
+ return nil, err
+ }
+
+ return refs, nil
+}
diff --git a/modules/git/repo_ref_nogogit.go b/modules/git/repo_ref_nogogit.go
new file mode 100644
index 0000000000..540961592b
--- /dev/null
+++ b/modules/git/repo_ref_nogogit.go
@@ -0,0 +1,84 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// +build !gogit
+
+package git
+
+import (
+ "bufio"
+ "io"
+ "strings"
+)
+
+// GetRefsFiltered returns all references of the repository that matches patterm exactly or starting with.
+func (repo *Repository) GetRefsFiltered(pattern string) ([]*Reference, error) {
+ stdoutReader, stdoutWriter := io.Pipe()
+ defer func() {
+ _ = stdoutReader.Close()
+ _ = stdoutWriter.Close()
+ }()
+
+ go func() {
+ stderrBuilder := &strings.Builder{}
+ err := NewCommand("for-each-ref").RunInDirPipeline(repo.Path, stdoutWriter, stderrBuilder)
+ if err != nil {
+ _ = stdoutWriter.CloseWithError(ConcatenateError(err, stderrBuilder.String()))
+ } else {
+ _ = stdoutWriter.Close()
+ }
+ }()
+
+ refs := make([]*Reference, 0)
+ bufReader := bufio.NewReader(stdoutReader)
+ for {
+ // The output of for-each-ref is simply a list:
+ // <sha> SP <type> TAB <ref> LF
+ sha, err := bufReader.ReadString(' ')
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return nil, err
+ }
+ sha = sha[:len(sha)-1]
+
+ typ, err := bufReader.ReadString('\t')
+ if err == io.EOF {
+ // This should not happen, but we'll tolerate it
+ break
+ }
+ if err != nil {
+ return nil, err
+ }
+ typ = typ[:len(typ)-1]
+
+ refName, err := bufReader.ReadString('\n')
+ if err == io.EOF {
+ // This should not happen, but we'll tolerate it
+ break
+ }
+ if err != nil {
+ return nil, err
+ }
+ refName = refName[:len(refName)-1]
+
+ // refName cannot be HEAD but can be remotes or stash
+ if strings.HasPrefix(refName, "/refs/remotes/") || refName == "/refs/stash" {
+ continue
+ }
+
+ if pattern == "" || strings.HasPrefix(refName, pattern) {
+ r := &Reference{
+ Name: refName,
+ Object: MustIDFromString(sha),
+ Type: typ,
+ repo: repo,
+ }
+ refs = append(refs, r)
+ }
+ }
+
+ return refs, nil
+}
diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go
index 376a699502..3e8f80fe82 100644
--- a/modules/git/repo_tag.go
+++ b/modules/git/repo_tag.go
@@ -8,8 +8,6 @@ package git
import (
"fmt"
"strings"
-
- "github.com/go-git/go-git/v5/plumbing"
)
// TagPrefix tags prefix path on the repository
@@ -20,12 +18,6 @@ func IsTagExist(repoPath, name string) bool {
return IsReferenceExist(repoPath, TagPrefix+name)
}
-// IsTagExist returns true if given tag exists in the repository.
-func (repo *Repository) IsTagExist(name string) bool {
- _, err := repo.gogitRepo.Reference(plumbing.ReferenceName(TagPrefix+name), true)
- return err == nil
-}
-
// CreateTag create one tag in the repository
func (repo *Repository) CreateTag(name, revision string) error {
_, err := NewCommand("tag", "--", name, revision).RunInDir(repo.Path)
@@ -224,29 +216,6 @@ func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, error) {
return tags, nil
}
-// GetTags returns all tags of the repository.
-func (repo *Repository) GetTags() ([]string, error) {
- var tagNames []string
-
- tags, err := repo.gogitRepo.Tags()
- if err != nil {
- return nil, err
- }
-
- _ = tags.ForEach(func(tag *plumbing.Reference) error {
- tagNames = append(tagNames, strings.TrimPrefix(tag.Name().String(), TagPrefix))
- return nil
- })
-
- // Reverse order
- for i := 0; i < len(tagNames)/2; i++ {
- j := len(tagNames) - i - 1
- tagNames[i], tagNames[j] = tagNames[j], tagNames[i]
- }
-
- return tagNames, nil
-}
-
// GetTagType gets the type of the tag, either commit (simple) or tag (annotated)
func (repo *Repository) GetTagType(id SHA1) (string, error) {
// Get tag type
diff --git a/modules/git/repo_tag_gogit.go b/modules/git/repo_tag_gogit.go
new file mode 100644
index 0000000000..3ac097c9a8
--- /dev/null
+++ b/modules/git/repo_tag_gogit.go
@@ -0,0 +1,43 @@
+// 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
+// license that can be found in the LICENSE file.
+
+// +build gogit
+
+package git
+
+import (
+ "strings"
+
+ "github.com/go-git/go-git/v5/plumbing"
+)
+
+// IsTagExist returns true if given tag exists in the repository.
+func (repo *Repository) IsTagExist(name string) bool {
+ _, err := repo.gogitRepo.Reference(plumbing.ReferenceName(TagPrefix+name), true)
+ return err == nil
+}
+
+// GetTags returns all tags of the repository.
+func (repo *Repository) GetTags() ([]string, error) {
+ var tagNames []string
+
+ tags, err := repo.gogitRepo.Tags()
+ if err != nil {
+ return nil, err
+ }
+
+ _ = tags.ForEach(func(tag *plumbing.Reference) error {
+ tagNames = append(tagNames, strings.TrimPrefix(tag.Name().String(), TagPrefix))
+ return nil
+ })
+
+ // Reverse order
+ for i := 0; i < len(tagNames)/2; i++ {
+ j := len(tagNames) - i - 1
+ tagNames[i], tagNames[j] = tagNames[j], tagNames[i]
+ }
+
+ return tagNames, nil
+}
diff --git a/modules/git/repo_tag_nogogit.go b/modules/git/repo_tag_nogogit.go
new file mode 100644
index 0000000000..83cbc58e34
--- /dev/null
+++ b/modules/git/repo_tag_nogogit.go
@@ -0,0 +1,18 @@
+// 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
+// license that can be found in the LICENSE file.
+
+// +build !gogit
+
+package git
+
+// IsTagExist returns true if given tag exists in the repository.
+func (repo *Repository) IsTagExist(name string) bool {
+ return IsReferenceExist(repo.Path, TagPrefix+name)
+}
+
+// GetTags returns all tags of the repository.
+func (repo *Repository) GetTags() ([]string, error) {
+ return callShowRef(repo.Path, TagPrefix, "--tags")
+}
diff --git a/modules/git/repo_tree.go b/modules/git/repo_tree.go
index 0b08a10d55..2053b6a1de 100644
--- a/modules/git/repo_tree.go
+++ b/modules/git/repo_tree.go
@@ -13,45 +13,6 @@ import (
"time"
)
-func (repo *Repository) getTree(id SHA1) (*Tree, error) {
- gogitTree, err := repo.gogitRepo.TreeObject(id)
- if err != nil {
- return nil, err
- }
-
- tree := NewTree(repo, id)
- tree.gogitTree = gogitTree
- return tree, nil
-}
-
-// GetTree find the tree object in the repository.
-func (repo *Repository) GetTree(idStr string) (*Tree, error) {
- if len(idStr) != 40 {
- res, err := NewCommand("rev-parse", "--verify", idStr).RunInDir(repo.Path)
- if err != nil {
- return nil, err
- }
- if len(res) > 0 {
- idStr = res[:len(res)-1]
- }
- }
- id, err := NewIDFromString(idStr)
- if err != nil {
- return nil, err
- }
- resolvedID := id
- commitObject, err := repo.gogitRepo.CommitObject(id)
- if err == nil {
- id = SHA1(commitObject.TreeHash)
- }
- treeObject, err := repo.getTree(id)
- if err != nil {
- return nil, err
- }
- treeObject.ResolvedID = resolvedID
- return treeObject, nil
-}
-
// CommitTreeOpts represents the possible options to CommitTree
type CommitTreeOpts struct {
Parents []string
@@ -102,7 +63,7 @@ func (repo *Repository) CommitTree(author *Signature, committer *Signature, tree
err = cmd.RunInDirTimeoutEnvFullPipeline(env, -1, repo.Path, stdout, stderr, messageBytes)
if err != nil {
- return SHA1{}, concatenateError(err, stderr.String())
+ return SHA1{}, ConcatenateError(err, stderr.String())
}
return NewIDFromString(strings.TrimSpace(stdout.String()))
}
diff --git a/modules/git/repo_tree_gogit.go b/modules/git/repo_tree_gogit.go
new file mode 100644
index 0000000000..d878f5e7a7
--- /dev/null
+++ b/modules/git/repo_tree_gogit.go
@@ -0,0 +1,47 @@
+// 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
+// license that can be found in the LICENSE file.
+
+// +build gogit
+
+package git
+
+func (repo *Repository) getTree(id SHA1) (*Tree, error) {
+ gogitTree, err := repo.gogitRepo.TreeObject(id)
+ if err != nil {
+ return nil, err
+ }
+
+ tree := NewTree(repo, id)
+ tree.gogitTree = gogitTree
+ return tree, nil
+}
+
+// GetTree find the tree object in the repository.
+func (repo *Repository) GetTree(idStr string) (*Tree, error) {
+ if len(idStr) != 40 {
+ res, err := NewCommand("rev-parse", "--verify", idStr).RunInDir(repo.Path)
+ if err != nil {
+ return nil, err
+ }
+ if len(res) > 0 {
+ idStr = res[:len(res)-1]
+ }
+ }
+ id, err := NewIDFromString(idStr)
+ if err != nil {
+ return nil, err
+ }
+ resolvedID := id
+ commitObject, err := repo.gogitRepo.CommitObject(id)
+ if err == nil {
+ id = SHA1(commitObject.TreeHash)
+ }
+ treeObject, err := repo.getTree(id)
+ if err != nil {
+ return nil, err
+ }
+ treeObject.ResolvedID = resolvedID
+ return treeObject, nil
+}
diff --git a/modules/git/repo_tree_nogogit.go b/modules/git/repo_tree_nogogit.go
new file mode 100644
index 0000000000..416205d8a0
--- /dev/null
+++ b/modules/git/repo_tree_nogogit.go
@@ -0,0 +1,98 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// +build !gogit
+
+package git
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "strings"
+)
+
+func (repo *Repository) getTree(id SHA1) (*Tree, error) {
+ stdoutReader, stdoutWriter := io.Pipe()
+ defer func() {
+ _ = stdoutReader.Close()
+ _ = stdoutWriter.Close()
+ }()
+
+ go func() {
+ stderr := &strings.Builder{}
+ err := NewCommand("cat-file", "--batch").RunInDirFullPipeline(repo.Path, stdoutWriter, stderr, strings.NewReader(id.String()+"\n"))
+ if err != nil {
+ _ = stdoutWriter.CloseWithError(ConcatenateError(err, stderr.String()))
+ } else {
+ _ = stdoutWriter.Close()
+ }
+ }()
+
+ bufReader := bufio.NewReader(stdoutReader)
+ // ignore the SHA
+ _, typ, _, err := ReadBatchLine(bufReader)
+ if err != nil {
+ return nil, err
+ }
+
+ switch typ {
+ case "tag":
+ resolvedID := id
+ data, err := ioutil.ReadAll(bufReader)
+ if err != nil {
+ return nil, err
+ }
+ tag, err := parseTagData(data)
+ if err != nil {
+ return nil, err
+ }
+ commit, err := tag.Commit()
+ if err != nil {
+ return nil, err
+ }
+ commit.Tree.ResolvedID = resolvedID
+ log("tag.commit.Tree: %s %v", commit.Tree.ID.String(), commit.Tree.repo)
+ return &commit.Tree, nil
+ case "commit":
+ commit, err := CommitFromReader(repo, id, bufReader)
+ if err != nil {
+ _ = stdoutReader.CloseWithError(err)
+ return nil, err
+ }
+ commit.Tree.ResolvedID = commit.ID
+ log("commit.Tree: %s %v", commit.Tree.ID.String(), commit.Tree.repo)
+ return &commit.Tree, nil
+ case "tree":
+ stdoutReader.Close()
+ tree := NewTree(repo, id)
+ tree.ResolvedID = id
+ return tree, nil
+ default:
+ _ = stdoutReader.CloseWithError(fmt.Errorf("unknown typ: %s", typ))
+ return nil, ErrNotExist{
+ ID: id.String(),
+ }
+ }
+}
+
+// GetTree find the tree object in the repository.
+func (repo *Repository) GetTree(idStr string) (*Tree, error) {
+ if len(idStr) != 40 {
+ res, err := NewCommand("rev-parse", "--verify", idStr).RunInDir(repo.Path)
+ if err != nil {
+ return nil, err
+ }
+ if len(res) > 0 {
+ idStr = res[:len(res)-1]
+ }
+ }
+ id, err := NewIDFromString(idStr)
+ if err != nil {
+ return nil, err
+ }
+
+ return repo.getTree(id)
+}
diff --git a/modules/git/sha1.go b/modules/git/sha1.go
index 06c8ad14b5..2da74733df 100644
--- a/modules/git/sha1.go
+++ b/modules/git/sha1.go
@@ -10,8 +10,6 @@ import (
"fmt"
"regexp"
"strings"
-
- "github.com/go-git/go-git/v5/plumbing"
)
// EmptySHA defines empty git SHA
@@ -23,9 +21,6 @@ const EmptyTreeSHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
// SHAPattern can be used to determine if a string is an valid sha
var SHAPattern = regexp.MustCompile(`^[0-9a-f]{4,40}$`)
-// SHA1 a git commit name
-type SHA1 = plumbing.Hash
-
// MustID always creates a new SHA1 from a [20]byte array with no validation of input.
func MustID(b []byte) SHA1 {
var id SHA1
diff --git a/modules/git/sha1_gogit.go b/modules/git/sha1_gogit.go
new file mode 100644
index 0000000000..5953af58bf
--- /dev/null
+++ b/modules/git/sha1_gogit.go
@@ -0,0 +1,20 @@
+// 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
+// license that can be found in the LICENSE file.
+
+// +build gogit
+
+package git
+
+import (
+ "github.com/go-git/go-git/v5/plumbing"
+)
+
+// SHA1 a git commit name
+type SHA1 = plumbing.Hash
+
+// ComputeBlobHash compute the hash for a given blob content
+func ComputeBlobHash(content []byte) SHA1 {
+ return plumbing.ComputeHash(plumbing.BlobObject, content)
+}
diff --git a/modules/git/sha1_nogogit.go b/modules/git/sha1_nogogit.go
new file mode 100644
index 0000000000..09b5baacd5
--- /dev/null
+++ b/modules/git/sha1_nogogit.go
@@ -0,0 +1,62 @@
+// 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
+// license that can be found in the LICENSE file.
+
+// +build !gogit
+
+package git
+
+import (
+ "crypto/sha1"
+ "encoding/hex"
+ "hash"
+ "strconv"
+)
+
+// SHA1 a git commit name
+type SHA1 [20]byte
+
+// String returns a string representation of the SHA
+func (s SHA1) String() string {
+ return hex.EncodeToString(s[:])
+}
+
+// IsZero returns whether this SHA1 is all zeroes
+func (s SHA1) IsZero() bool {
+ var empty SHA1
+ return s == empty
+}
+
+// ComputeBlobHash compute the hash for a given blob content
+func ComputeBlobHash(content []byte) SHA1 {
+ return ComputeHash(ObjectBlob, content)
+}
+
+// ComputeHash compute the hash for a given ObjectType and content
+func ComputeHash(t ObjectType, content []byte) SHA1 {
+ h := NewHasher(t, int64(len(content)))
+ _, _ = h.Write(content)
+ return h.Sum()
+}
+
+// Hasher is a struct that will generate a SHA1
+type Hasher struct {
+ hash.Hash
+}
+
+// NewHasher takes an object type and size and creates a hasher to generate a SHA
+func NewHasher(t ObjectType, size int64) Hasher {
+ h := Hasher{sha1.New()}
+ _, _ = h.Write(t.Bytes())
+ _, _ = h.Write([]byte(" "))
+ _, _ = h.Write([]byte(strconv.FormatInt(size, 10)))
+ _, _ = h.Write([]byte{0})
+ return h
+}
+
+// Sum generates a SHA1 for the provided hash
+func (h Hasher) Sum() (sha1 SHA1) {
+ copy(sha1[:], h.Hash.Sum(nil))
+ return
+}
diff --git a/modules/git/signature.go b/modules/git/signature.go
index 4cb56b29f4..b59db8f490 100644
--- a/modules/git/signature.go
+++ b/modules/git/signature.go
@@ -5,53 +5,7 @@
package git
-import (
- "bytes"
- "strconv"
- "time"
-
- "github.com/go-git/go-git/v5/plumbing/object"
-)
-
-// Signature represents the Author or Committer information.
-type Signature = object.Signature
-
const (
// GitTimeLayout is the (default) time layout used by git.
GitTimeLayout = "Mon Jan _2 15:04:05 2006 -0700"
)
-
-// Helper to get a signature from the commit line, which looks like these:
-// author Patrick Gundlach <gundlach@speedata.de> 1378823654 +0200
-// author Patrick Gundlach <gundlach@speedata.de> Thu, 07 Apr 2005 22:13:13 +0200
-// but without the "author " at the beginning (this method should)
-// be used for author and committer.
-//
-// FIXME: include timezone for timestamp!
-func newSignatureFromCommitline(line []byte) (_ *Signature, err error) {
- sig := new(Signature)
- emailStart := bytes.IndexByte(line, '<')
- sig.Name = string(line[:emailStart-1])
- emailEnd := bytes.IndexByte(line, '>')
- sig.Email = string(line[emailStart+1 : emailEnd])
-
- // Check date format.
- if len(line) > emailEnd+2 {
- firstChar := line[emailEnd+2]
- if firstChar >= 48 && firstChar <= 57 {
- timestop := bytes.IndexByte(line[emailEnd+2:], ' ')
- timestring := string(line[emailEnd+2 : emailEnd+2+timestop])
- seconds, _ := strconv.ParseInt(timestring, 10, 64)
- sig.When = time.Unix(seconds, 0)
- } else {
- sig.When, err = time.Parse(GitTimeLayout, string(line[emailEnd+2:]))
- if err != nil {
- return nil, err
- }
- }
- } else {
- // Fall back to unix 0 time
- sig.When = time.Unix(0, 0)
- }
- return sig, nil
-}
diff --git a/modules/git/signature_gogit.go b/modules/git/signature_gogit.go
new file mode 100644
index 0000000000..804c0074d3
--- /dev/null
+++ b/modules/git/signature_gogit.go
@@ -0,0 +1,54 @@
+// 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
+// license that can be found in the LICENSE file.
+
+// +build gogit
+
+package git
+
+import (
+ "bytes"
+ "strconv"
+ "time"
+
+ "github.com/go-git/go-git/v5/plumbing/object"
+)
+
+// Signature represents the Author or Committer information.
+type Signature = object.Signature
+
+// Helper to get a signature from the commit line, which looks like these:
+// author Patrick Gundlach <gundlach@speedata.de> 1378823654 +0200
+// author Patrick Gundlach <gundlach@speedata.de> Thu, 07 Apr 2005 22:13:13 +0200
+// but without the "author " at the beginning (this method should)
+// be used for author and committer.
+//
+// FIXME: include timezone for timestamp!
+func newSignatureFromCommitline(line []byte) (_ *Signature, err error) {
+ sig := new(Signature)
+ emailStart := bytes.IndexByte(line, '<')
+ sig.Name = string(line[:emailStart-1])
+ emailEnd := bytes.IndexByte(line, '>')
+ sig.Email = string(line[emailStart+1 : emailEnd])
+
+ // Check date format.
+ if len(line) > emailEnd+2 {
+ firstChar := line[emailEnd+2]
+ if firstChar >= 48 && firstChar <= 57 {
+ timestop := bytes.IndexByte(line[emailEnd+2:], ' ')
+ timestring := string(line[emailEnd+2 : emailEnd+2+timestop])
+ seconds, _ := strconv.ParseInt(timestring, 10, 64)
+ sig.When = time.Unix(seconds, 0)
+ } else {
+ sig.When, err = time.Parse(GitTimeLayout, string(line[emailEnd+2:]))
+ if err != nil {
+ return nil, err
+ }
+ }
+ } else {
+ // Fall back to unix 0 time
+ sig.When = time.Unix(0, 0)
+ }
+ return sig, nil
+}
diff --git a/modules/git/signature_nogogit.go b/modules/git/signature_nogogit.go
new file mode 100644
index 0000000000..753d87b605
--- /dev/null
+++ b/modules/git/signature_nogogit.go
@@ -0,0 +1,95 @@
+// 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
+// license that can be found in the LICENSE file.
+
+// +build !gogit
+
+package git
+
+import (
+ "bytes"
+ "fmt"
+ "strconv"
+ "time"
+)
+
+// Signature represents the Author or Committer information.
+type Signature struct {
+ // Name represents a person name. It is an arbitrary string.
+ Name string
+ // Email is an email, but it cannot be assumed to be well-formed.
+ Email string
+ // When is the timestamp of the signature.
+ When time.Time
+}
+
+func (s *Signature) String() string {
+ return fmt.Sprintf("%s <%s>", s.Name, s.Email)
+}
+
+// Decode decodes a byte array representing a signature to signature
+func (s *Signature) Decode(b []byte) {
+ sig, _ := newSignatureFromCommitline(b)
+ s.Email = sig.Email
+ s.Name = sig.Name
+ s.When = sig.When
+}
+
+// Helper to get a signature from the commit line, which looks like these:
+// author Patrick Gundlach <gundlach@speedata.de> 1378823654 +0200
+// author Patrick Gundlach <gundlach@speedata.de> Thu, 07 Apr 2005 22:13:13 +0200
+// but without the "author " at the beginning (this method should)
+// be used for author and committer.
+func newSignatureFromCommitline(line []byte) (sig *Signature, err error) {
+ sig = new(Signature)
+ emailStart := bytes.LastIndexByte(line, '<')
+ emailEnd := bytes.LastIndexByte(line, '>')
+ if emailStart == -1 || emailEnd == -1 || emailEnd < emailStart {
+ return
+ }
+
+ sig.Name = string(line[:emailStart-1])
+ sig.Email = string(line[emailStart+1 : emailEnd])
+
+ hasTime := emailEnd+2 < len(line)
+ if !hasTime {
+ return
+ }
+
+ // Check date format.
+ firstChar := line[emailEnd+2]
+ if firstChar >= 48 && firstChar <= 57 {
+ idx := bytes.IndexByte(line[emailEnd+2:], ' ')
+ if idx < 0 {
+ return
+ }
+
+ timestring := string(line[emailEnd+2 : emailEnd+2+idx])
+ seconds, _ := strconv.ParseInt(timestring, 10, 64)
+ sig.When = time.Unix(seconds, 0)
+
+ idx += emailEnd + 3
+ if idx >= len(line) || idx+5 > len(line) {
+ return
+ }
+
+ timezone := string(line[idx : idx+5])
+ tzhours, err1 := strconv.ParseInt(timezone[0:3], 10, 64)
+ tzmins, err2 := strconv.ParseInt(timezone[3:], 10, 64)
+ if err1 != nil || err2 != nil {
+ return
+ }
+ if tzhours < 0 {
+ tzmins *= -1
+ }
+ tz := time.FixedZone("", int(tzhours*60*60+tzmins*60))
+ sig.When = sig.When.In(tz)
+ } else {
+ sig.When, err = time.Parse(GitTimeLayout, string(line[emailEnd+2:]))
+ if err != nil {
+ return
+ }
+ }
+ return
+}
diff --git a/modules/git/tag.go b/modules/git/tag.go
index c97f574fa6..d58a9a202d 100644
--- a/modules/git/tag.go
+++ b/modules/git/tag.go
@@ -10,15 +10,19 @@ import (
"strings"
)
+const beginpgp = "\n-----BEGIN PGP SIGNATURE-----\n"
+const endpgp = "\n-----END PGP SIGNATURE-----"
+
// Tag represents a Git tag.
type Tag struct {
- Name string
- ID SHA1
- repo *Repository
- Object SHA1 // The id of this commit object
- Type string
- Tagger *Signature
- Message string
+ Name string
+ ID SHA1
+ repo *Repository
+ Object SHA1 // The id of this commit object
+ Type string
+ Tagger *Signature
+ Message string
+ Signature *CommitGPGSignature
}
// Commit return the commit of the tag reference
@@ -60,12 +64,23 @@ l:
}
nextline += eol + 1
case eol == 0:
- tag.Message = strings.TrimRight(string(data[nextline+1:]), "\n")
+ tag.Message = string(data[nextline+1 : len(data)-1])
break l
default:
break l
}
}
+ idx := strings.LastIndex(tag.Message, beginpgp)
+ if idx > 0 {
+ endSigIdx := strings.Index(tag.Message[idx:], endpgp)
+ if endSigIdx > 0 {
+ tag.Signature = &CommitGPGSignature{
+ Signature: tag.Message[idx+1 : idx+endSigIdx+len(endpgp)],
+ Payload: string(data[:bytes.LastIndex(data, []byte(beginpgp))+1]),
+ }
+ tag.Message = tag.Message[:idx+1]
+ }
+ }
return tag, nil
}
diff --git a/modules/git/tree.go b/modules/git/tree.go
index 258b11aaac..059f0a8287 100644
--- a/modules/git/tree.go
+++ b/modules/git/tree.go
@@ -6,25 +6,9 @@
package git
import (
- "io"
"strings"
-
- "github.com/go-git/go-git/v5/plumbing"
- "github.com/go-git/go-git/v5/plumbing/object"
)
-// Tree represents a flat directory listing.
-type Tree struct {
- ID SHA1
- ResolvedID SHA1
- repo *Repository
-
- gogitTree *object.Tree
-
- // parent tree
- ptree *Tree
-}
-
// NewTree create a new tree according the repository and tree id
func NewTree(repo *Repository, id SHA1) *Tree {
return &Tree{
@@ -61,70 +45,3 @@ func (t *Tree) SubTree(rpath string) (*Tree, error) {
}
return g, nil
}
-
-func (t *Tree) loadTreeObject() error {
- gogitTree, err := t.repo.gogitRepo.TreeObject(t.ID)
- if err != nil {
- return err
- }
-
- t.gogitTree = gogitTree
- return nil
-}
-
-// ListEntries returns all entries of current tree.
-func (t *Tree) ListEntries() (Entries, error) {
- if t.gogitTree == nil {
- err := t.loadTreeObject()
- if err != nil {
- return nil, err
- }
- }
-
- entries := make([]*TreeEntry, len(t.gogitTree.Entries))
- for i, entry := range t.gogitTree.Entries {
- entries[i] = &TreeEntry{
- ID: entry.Hash,
- gogitTreeEntry: &t.gogitTree.Entries[i],
- ptree: t,
- }
- }
-
- return entries, nil
-}
-
-// ListEntriesRecursive returns all entries of current tree recursively including all subtrees
-func (t *Tree) ListEntriesRecursive() (Entries, error) {
- if t.gogitTree == nil {
- err := t.loadTreeObject()
- if err != nil {
- return nil, err
- }
- }
-
- var entries []*TreeEntry
- seen := map[plumbing.Hash]bool{}
- walker := object.NewTreeWalker(t.gogitTree, true, seen)
- for {
- fullName, entry, err := walker.Next()
- if err == io.EOF {
- break
- }
- if err != nil {
- return nil, err
- }
- if seen[entry.Hash] {
- continue
- }
-
- convertedEntry := &TreeEntry{
- ID: entry.Hash,
- gogitTreeEntry: &entry,
- ptree: t,
- fullName: fullName,
- }
- entries = append(entries, convertedEntry)
- }
-
- return entries, nil
-}
diff --git a/modules/git/tree_blob.go b/modules/git/tree_blob.go
index f9fc6db497..19edcf4c6c 100644
--- a/modules/git/tree_blob.go
+++ b/modules/git/tree_blob.go
@@ -5,64 +5,6 @@
package git
-import (
- "path"
- "strings"
-
- "github.com/go-git/go-git/v5/plumbing"
- "github.com/go-git/go-git/v5/plumbing/filemode"
- "github.com/go-git/go-git/v5/plumbing/object"
-)
-
-// GetTreeEntryByPath get the tree entries according the sub dir
-func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) {
- if len(relpath) == 0 {
- return &TreeEntry{
- ID: t.ID,
- //Type: ObjectTree,
- gogitTreeEntry: &object.TreeEntry{
- Name: "",
- Mode: filemode.Dir,
- Hash: t.ID,
- },
- }, nil
- }
-
- relpath = path.Clean(relpath)
- parts := strings.Split(relpath, "/")
- var err error
- tree := t
- for i, name := range parts {
- if i == len(parts)-1 {
- entries, err := tree.ListEntries()
- if err != nil {
- if err == plumbing.ErrObjectNotFound {
- return nil, ErrNotExist{
- RelPath: relpath,
- }
- }
- return nil, err
- }
- for _, v := range entries {
- if v.Name() == name {
- return v, nil
- }
- }
- } else {
- tree, err = tree.SubTree(name)
- if err != nil {
- if err == plumbing.ErrObjectNotFound {
- return nil, ErrNotExist{
- RelPath: relpath,
- }
- }
- return nil, err
- }
- }
- }
- return nil, ErrNotExist{"", relpath}
-}
-
// GetBlobByPath get the blob object according the path
func (t *Tree) GetBlobByPath(relpath string) (*Blob, error) {
entry, err := t.GetTreeEntryByPath(relpath)
diff --git a/modules/git/tree_blob_gogit.go b/modules/git/tree_blob_gogit.go
new file mode 100644
index 0000000000..93ebc8a367
--- /dev/null
+++ b/modules/git/tree_blob_gogit.go
@@ -0,0 +1,66 @@
+// 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
+// license that can be found in the LICENSE file.
+
+// +build gogit
+
+package git
+
+import (
+ "path"
+ "strings"
+
+ "github.com/go-git/go-git/v5/plumbing"
+ "github.com/go-git/go-git/v5/plumbing/filemode"
+ "github.com/go-git/go-git/v5/plumbing/object"
+)
+
+// GetTreeEntryByPath get the tree entries according the sub dir
+func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) {
+ if len(relpath) == 0 {
+ return &TreeEntry{
+ ID: t.ID,
+ //Type: ObjectTree,
+ gogitTreeEntry: &object.TreeEntry{
+ Name: "",
+ Mode: filemode.Dir,
+ Hash: t.ID,
+ },
+ }, nil
+ }
+
+ relpath = path.Clean(relpath)
+ parts := strings.Split(relpath, "/")
+ var err error
+ tree := t
+ for i, name := range parts {
+ if i == len(parts)-1 {
+ entries, err := tree.ListEntries()
+ if err != nil {
+ if err == plumbing.ErrObjectNotFound {
+ return nil, ErrNotExist{
+ RelPath: relpath,
+ }
+ }
+ return nil, err
+ }
+ for _, v := range entries {
+ if v.Name() == name {
+ return v, nil
+ }
+ }
+ } else {
+ tree, err = tree.SubTree(name)
+ if err != nil {
+ if err == plumbing.ErrObjectNotFound {
+ return nil, ErrNotExist{
+ RelPath: relpath,
+ }
+ }
+ return nil, err
+ }
+ }
+ }
+ return nil, ErrNotExist{"", relpath}
+}
diff --git a/modules/git/tree_blob_nogogit.go b/modules/git/tree_blob_nogogit.go
new file mode 100644
index 0000000000..6da0ccfe8e
--- /dev/null
+++ b/modules/git/tree_blob_nogogit.go
@@ -0,0 +1,49 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// +build !gogit
+
+package git
+
+import (
+ "path"
+ "strings"
+)
+
+// GetTreeEntryByPath get the tree entries according the sub dir
+func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) {
+ if len(relpath) == 0 {
+ return &TreeEntry{
+ ID: t.ID,
+ name: "",
+ fullName: "",
+ entryMode: EntryModeTree,
+ }, nil
+ }
+
+ // FIXME: This should probably use git cat-file --batch to be a bit more efficient
+ relpath = path.Clean(relpath)
+ parts := strings.Split(relpath, "/")
+ var err error
+ tree := t
+ for i, name := range parts {
+ if i == len(parts)-1 {
+ entries, err := tree.ListEntries()
+ if err != nil {
+ return nil, err
+ }
+ for _, v := range entries {
+ if v.Name() == name {
+ return v, nil
+ }
+ }
+ } else {
+ tree, err = tree.SubTree(name)
+ if err != nil {
+ return nil, err
+ }
+ }
+ }
+ return nil, ErrNotExist{"", relpath}
+}
diff --git a/modules/git/tree_entry.go b/modules/git/tree_entry.go
index d981412095..498767a63e 100644
--- a/modules/git/tree_entry.go
+++ b/modules/git/tree_entry.go
@@ -9,55 +9,8 @@ import (
"io"
"sort"
"strings"
-
- "github.com/go-git/go-git/v5/plumbing"
- "github.com/go-git/go-git/v5/plumbing/filemode"
- "github.com/go-git/go-git/v5/plumbing/object"
)
-// EntryMode the type of the object in the git tree
-type EntryMode int
-
-// There are only a few file modes in Git. They look like unix file modes, but they can only be
-// one of these.
-const (
- // EntryModeBlob
- EntryModeBlob EntryMode = 0100644
- // EntryModeExec
- EntryModeExec EntryMode = 0100755
- // EntryModeSymlink
- EntryModeSymlink EntryMode = 0120000
- // EntryModeCommit
- EntryModeCommit EntryMode = 0160000
- // EntryModeTree
- EntryModeTree EntryMode = 0040000
-)
-
-// TreeEntry the leaf in the git tree
-type TreeEntry struct {
- ID SHA1
-
- gogitTreeEntry *object.TreeEntry
- ptree *Tree
-
- size int64
- sized bool
- fullName string
-}
-
-// Name returns the name of the entry
-func (te *TreeEntry) Name() string {
- if te.fullName != "" {
- return te.fullName
- }
- return te.gogitTreeEntry.Name
-}
-
-// Mode returns the mode of the entry
-func (te *TreeEntry) Mode() EntryMode {
- return EntryMode(te.gogitTreeEntry.Mode)
-}
-
// Type returns the type of the entry (commit, tree, blob)
func (te *TreeEntry) Type() string {
switch te.Mode() {
@@ -70,63 +23,6 @@ func (te *TreeEntry) Type() string {
}
}
-// Size returns the size of the entry
-func (te *TreeEntry) Size() int64 {
- if te.IsDir() {
- return 0
- } else if te.sized {
- return te.size
- }
-
- file, err := te.ptree.gogitTree.TreeEntryFile(te.gogitTreeEntry)
- if err != nil {
- return 0
- }
-
- te.sized = true
- te.size = file.Size
- return te.size
-}
-
-// IsSubModule if the entry is a sub module
-func (te *TreeEntry) IsSubModule() bool {
- return te.gogitTreeEntry.Mode == filemode.Submodule
-}
-
-// IsDir if the entry is a sub dir
-func (te *TreeEntry) IsDir() bool {
- return te.gogitTreeEntry.Mode == filemode.Dir
-}
-
-// IsLink if the entry is a symlink
-func (te *TreeEntry) IsLink() bool {
- return te.gogitTreeEntry.Mode == filemode.Symlink
-}
-
-// IsRegular if the entry is a regular file
-func (te *TreeEntry) IsRegular() bool {
- return te.gogitTreeEntry.Mode == filemode.Regular
-}
-
-// IsExecutable if the entry is an executable file (not necessarily binary)
-func (te *TreeEntry) IsExecutable() bool {
- return te.gogitTreeEntry.Mode == filemode.Executable
-}
-
-// Blob returns the blob object the entry
-func (te *TreeEntry) Blob() *Blob {
- encodedObj, err := te.ptree.repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, te.gogitTreeEntry.Hash)
- if err != nil {
- return nil
- }
-
- return &Blob{
- ID: te.gogitTreeEntry.Hash,
- gogitEncodedObj: encodedObj,
- name: te.Name(),
- }
-}
-
// FollowLink returns the entry pointed to by a symlink
func (te *TreeEntry) FollowLink() (*TreeEntry, error) {
if !te.IsLink() {
diff --git a/modules/git/tree_entry_gogit.go b/modules/git/tree_entry_gogit.go
new file mode 100644
index 0000000000..219251a77e
--- /dev/null
+++ b/modules/git/tree_entry_gogit.go
@@ -0,0 +1,96 @@
+// 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
+// license that can be found in the LICENSE file.
+
+// +build gogit
+
+package git
+
+import (
+ "github.com/go-git/go-git/v5/plumbing"
+ "github.com/go-git/go-git/v5/plumbing/filemode"
+ "github.com/go-git/go-git/v5/plumbing/object"
+)
+
+// TreeEntry the leaf in the git tree
+type TreeEntry struct {
+ ID SHA1
+
+ gogitTreeEntry *object.TreeEntry
+ ptree *Tree
+
+ size int64
+ sized bool
+ fullName string
+}
+
+// Name returns the name of the entry
+func (te *TreeEntry) Name() string {
+ if te.fullName != "" {
+ return te.fullName
+ }
+ return te.gogitTreeEntry.Name
+}
+
+// Mode returns the mode of the entry
+func (te *TreeEntry) Mode() EntryMode {
+ return EntryMode(te.gogitTreeEntry.Mode)
+}
+
+// Size returns the size of the entry
+func (te *TreeEntry) Size() int64 {
+ if te.IsDir() {
+ return 0
+ } else if te.sized {
+ return te.size
+ }
+
+ file, err := te.ptree.gogitTree.TreeEntryFile(te.gogitTreeEntry)
+ if err != nil {
+ return 0
+ }
+
+ te.sized = true
+ te.size = file.Size
+ return te.size
+}
+
+// IsSubModule if the entry is a sub module
+func (te *TreeEntry) IsSubModule() bool {
+ return te.gogitTreeEntry.Mode == filemode.Submodule
+}
+
+// IsDir if the entry is a sub dir
+func (te *TreeEntry) IsDir() bool {
+ return te.gogitTreeEntry.Mode == filemode.Dir
+}
+
+// IsLink if the entry is a symlink
+func (te *TreeEntry) IsLink() bool {
+ return te.gogitTreeEntry.Mode == filemode.Symlink
+}
+
+// IsRegular if the entry is a regular file
+func (te *TreeEntry) IsRegular() bool {
+ return te.gogitTreeEntry.Mode == filemode.Regular
+}
+
+// IsExecutable if the entry is an executable file (not necessarily binary)
+func (te *TreeEntry) IsExecutable() bool {
+ return te.gogitTreeEntry.Mode == filemode.Executable
+}
+
+// Blob returns the blob object the entry
+func (te *TreeEntry) Blob() *Blob {
+ encodedObj, err := te.ptree.repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, te.gogitTreeEntry.Hash)
+ if err != nil {
+ return nil
+ }
+
+ return &Blob{
+ ID: te.gogitTreeEntry.Hash,
+ gogitEncodedObj: encodedObj,
+ name: te.Name(),
+ }
+}
diff --git a/modules/git/tree_entry_mode.go b/modules/git/tree_entry_mode.go
new file mode 100644
index 0000000000..b029c6fc47
--- /dev/null
+++ b/modules/git/tree_entry_mode.go
@@ -0,0 +1,36 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package git
+
+import "strconv"
+
+// EntryMode the type of the object in the git tree
+type EntryMode int
+
+// There are only a few file modes in Git. They look like unix file modes, but they can only be
+// one of these.
+const (
+ // EntryModeBlob
+ EntryModeBlob EntryMode = 0100644
+ // EntryModeExec
+ EntryModeExec EntryMode = 0100755
+ // EntryModeSymlink
+ EntryModeSymlink EntryMode = 0120000
+ // EntryModeCommit
+ EntryModeCommit EntryMode = 0160000
+ // EntryModeTree
+ EntryModeTree EntryMode = 0040000
+)
+
+// String converts an EntryMode to a string
+func (e EntryMode) String() string {
+ return strconv.FormatInt(int64(e), 8)
+}
+
+// ToEntryMode converts a string to an EntryMode
+func ToEntryMode(value string) EntryMode {
+ v, _ := strconv.ParseInt(value, 8, 32)
+ return EntryMode(v)
+}
diff --git a/modules/git/tree_entry_nogogit.go b/modules/git/tree_entry_nogogit.go
new file mode 100644
index 0000000000..f18daee778
--- /dev/null
+++ b/modules/git/tree_entry_nogogit.go
@@ -0,0 +1,91 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// +build !gogit
+
+package git
+
+import (
+ "strconv"
+ "strings"
+)
+
+// TreeEntry the leaf in the git tree
+type TreeEntry struct {
+ ID SHA1
+
+ ptree *Tree
+
+ entryMode EntryMode
+ name string
+
+ size int64
+ sized bool
+ fullName string
+}
+
+// Name returns the name of the entry
+func (te *TreeEntry) Name() string {
+ if te.fullName != "" {
+ return te.fullName
+ }
+ return te.name
+}
+
+// Mode returns the mode of the entry
+func (te *TreeEntry) Mode() EntryMode {
+ return te.entryMode
+}
+
+// Size returns the size of the entry
+func (te *TreeEntry) Size() int64 {
+ if te.IsDir() {
+ return 0
+ } else if te.sized {
+ return te.size
+ }
+
+ stdout, err := NewCommand("cat-file", "-s", te.ID.String()).RunInDir(te.ptree.repo.Path)
+ if err != nil {
+ return 0
+ }
+
+ te.sized = true
+ te.size, _ = strconv.ParseInt(strings.TrimSpace(stdout), 10, 64)
+ return te.size
+}
+
+// IsSubModule if the entry is a sub module
+func (te *TreeEntry) IsSubModule() bool {
+ return te.entryMode == EntryModeCommit
+}
+
+// IsDir if the entry is a sub dir
+func (te *TreeEntry) IsDir() bool {
+ return te.entryMode == EntryModeTree
+}
+
+// IsLink if the entry is a symlink
+func (te *TreeEntry) IsLink() bool {
+ return te.entryMode == EntryModeSymlink
+}
+
+// IsRegular if the entry is a regular file
+func (te *TreeEntry) IsRegular() bool {
+ return te.entryMode == EntryModeBlob
+}
+
+// IsExecutable if the entry is an executable file (not necessarily binary)
+func (te *TreeEntry) IsExecutable() bool {
+ return te.entryMode == EntryModeExec
+}
+
+// Blob returns the blob object the entry
+func (te *TreeEntry) Blob() *Blob {
+ return &Blob{
+ ID: te.ID,
+ repoPath: te.ptree.repo.Path,
+ name: te.Name(),
+ }
+}
diff --git a/modules/git/tree_entry_test.go b/modules/git/tree_entry_test.go
index 4878fce0b8..16cfbc4fc3 100644
--- a/modules/git/tree_entry_test.go
+++ b/modules/git/tree_entry_test.go
@@ -2,6 +2,8 @@
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
+// +build gogit
+
package git
import (
diff --git a/modules/git/tree_gogit.go b/modules/git/tree_gogit.go
new file mode 100644
index 0000000000..79132c5548
--- /dev/null
+++ b/modules/git/tree_gogit.go
@@ -0,0 +1,94 @@
+// 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
+// license that can be found in the LICENSE file.
+
+// +build gogit
+
+package git
+
+import (
+ "io"
+
+ "github.com/go-git/go-git/v5/plumbing"
+ "github.com/go-git/go-git/v5/plumbing/object"
+)
+
+// Tree represents a flat directory listing.
+type Tree struct {
+ ID SHA1
+ ResolvedID SHA1
+ repo *Repository
+
+ gogitTree *object.Tree
+
+ // parent tree
+ ptree *Tree
+}
+
+func (t *Tree) loadTreeObject() error {
+ gogitTree, err := t.repo.gogitRepo.TreeObject(t.ID)
+ if err != nil {
+ return err
+ }
+
+ t.gogitTree = gogitTree
+ return nil
+}
+
+// ListEntries returns all entries of current tree.
+func (t *Tree) ListEntries() (Entries, error) {
+ if t.gogitTree == nil {
+ err := t.loadTreeObject()
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ entries := make([]*TreeEntry, len(t.gogitTree.Entries))
+ for i, entry := range t.gogitTree.Entries {
+ entries[i] = &TreeEntry{
+ ID: entry.Hash,
+ gogitTreeEntry: &t.gogitTree.Entries[i],
+ ptree: t,
+ }
+ }
+
+ return entries, nil
+}
+
+// ListEntriesRecursive returns all entries of current tree recursively including all subtrees
+func (t *Tree) ListEntriesRecursive() (Entries, error) {
+ if t.gogitTree == nil {
+ err := t.loadTreeObject()
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ var entries []*TreeEntry
+ seen := map[plumbing.Hash]bool{}
+ walker := object.NewTreeWalker(t.gogitTree, true, seen)
+ for {
+ fullName, entry, err := walker.Next()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return nil, err
+ }
+ if seen[entry.Hash] {
+ continue
+ }
+
+ convertedEntry := &TreeEntry{
+ ID: entry.Hash,
+ gogitTreeEntry: &entry,
+ ptree: t,
+ fullName: fullName,
+ }
+ entries = append(entries, convertedEntry)
+ }
+
+ return entries, nil
+}
diff --git a/modules/git/tree_nogogit.go b/modules/git/tree_nogogit.go
new file mode 100644
index 0000000000..e78115b777
--- /dev/null
+++ b/modules/git/tree_nogogit.go
@@ -0,0 +1,69 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// +build !gogit
+
+package git
+
+import (
+ "strings"
+)
+
+// Tree represents a flat directory listing.
+type Tree struct {
+ ID SHA1
+ ResolvedID SHA1
+ repo *Repository
+
+ // parent tree
+ ptree *Tree
+
+ entries Entries
+ entriesParsed bool
+
+ entriesRecursive Entries
+ entriesRecursiveParsed bool
+}
+
+// ListEntries returns all entries of current tree.
+func (t *Tree) ListEntries() (Entries, error) {
+ if t.entriesParsed {
+ return t.entries, nil
+ }
+
+ stdout, err := NewCommand("ls-tree", t.ID.String()).RunInDirBytes(t.repo.Path)
+ if err != nil {
+ if strings.Contains(err.Error(), "fatal: Not a valid object name") || strings.Contains(err.Error(), "fatal: not a tree object") {
+ return nil, ErrNotExist{
+ ID: t.ID.String(),
+ }
+ }
+ return nil, err
+ }
+
+ t.entries, err = parseTreeEntries(stdout, t)
+ if err == nil {
+ t.entriesParsed = true
+ }
+
+ return t.entries, err
+}
+
+// ListEntriesRecursive returns all entries of current tree recursively including all subtrees
+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
+ }
+
+ t.entriesRecursive, err = parseTreeEntries(stdout, t)
+ if err == nil {
+ t.entriesRecursiveParsed = true
+ }
+
+ return t.entriesRecursive, err
+}
diff --git a/modules/git/utils.go b/modules/git/utils.go
index 83209924c8..d952189416 100644
--- a/modules/git/utils.go
+++ b/modules/git/utils.go
@@ -6,6 +6,7 @@ package git
import (
"fmt"
+ "io"
"os"
"strconv"
"strings"
@@ -68,11 +69,12 @@ func isExist(path string) bool {
return err == nil || os.IsExist(err)
}
-func concatenateError(err error, stderr string) error {
+// ConcatenateError concatenats an error with stderr string
+func ConcatenateError(err error, stderr string) error {
if len(stderr) == 0 {
return err
}
- return fmt.Errorf("%v - %s", err, stderr)
+ return fmt.Errorf("%w - %s", err, stderr)
}
// RefEndName return the end name of a ref name
@@ -140,3 +142,29 @@ func ParseBool(value string) (result bool, valid bool) {
}
return intValue != 0, true
}
+
+// LimitedReaderCloser is a limited reader closer
+type LimitedReaderCloser struct {
+ R io.Reader
+ C io.Closer
+ N int64
+}
+
+// Read implements io.Reader
+func (l *LimitedReaderCloser) Read(p []byte) (n int, err error) {
+ if l.N <= 0 {
+ _ = l.C.Close()
+ return 0, io.EOF
+ }
+ if int64(len(p)) > l.N {
+ p = p[0:l.N]
+ }
+ n, err = l.R.Read(p)
+ l.N -= int64(n)
+ return
+}
+
+// Close implements io.Closer
+func (l *LimitedReaderCloser) Close() error {
+ return l.C.Close()
+}