aboutsummaryrefslogtreecommitdiffstats
path: root/modules/git
diff options
context:
space:
mode:
Diffstat (limited to 'modules/git')
-rw-r--r--modules/git/README.md3
-rw-r--r--modules/git/blob.go73
-rw-r--r--modules/git/blob_test.go80
-rw-r--r--modules/git/cache.go11
-rw-r--r--modules/git/command.go137
-rw-r--r--modules/git/command_test.go41
-rw-r--r--modules/git/commit.go349
-rw-r--r--modules/git/commit_archive.go37
-rw-r--r--modules/git/commit_info.go329
-rw-r--r--modules/git/commit_info_test.go117
-rw-r--r--modules/git/commit_test.go38
-rw-r--r--modules/git/error.go66
-rw-r--r--modules/git/git.go91
-rw-r--r--modules/git/hook.go135
-rw-r--r--modules/git/parse.go81
-rw-r--r--modules/git/parse_test.go58
-rw-r--r--modules/git/ref.go18
-rw-r--r--modules/git/repo.go287
-rw-r--r--modules/git/repo_blame.go24
-rw-r--r--modules/git/repo_blob.go30
-rw-r--r--modules/git/repo_blob_test.go66
-rw-r--r--modules/git/repo_branch.go134
-rw-r--r--modules/git/repo_branch_test.go39
-rw-r--r--modules/git/repo_commit.go388
-rw-r--r--modules/git/repo_commit_test.go59
-rw-r--r--modules/git/repo_hook.go15
-rw-r--r--modules/git/repo_object.go19
-rw-r--r--modules/git/repo_pull.go89
-rw-r--r--modules/git/repo_pull_test.go30
-rw-r--r--modules/git/repo_ref.go51
-rw-r--r--modules/git/repo_ref_test.go49
-rw-r--r--modules/git/repo_tag.go149
-rw-r--r--modules/git/repo_tag_test.go44
-rw-r--r--modules/git/repo_test.go26
-rw-r--r--modules/git/repo_tree.go35
-rw-r--r--modules/git/sha1.go76
-rw-r--r--modules/git/signature.go58
-rw-r--r--modules/git/submodule.go87
-rw-r--r--modules/git/submodule_test.go29
-rw-r--r--modules/git/tag.go89
-rw-r--r--modules/git/tests/repos/repo1_bare/HEAD1
-rw-r--r--modules/git/tests/repos/repo1_bare/config4
-rw-r--r--modules/git/tests/repos/repo1_bare/description1
-rwxr-xr-xmodules/git/tests/repos/repo1_bare/hooks/applypatch-msg.sample15
-rwxr-xr-xmodules/git/tests/repos/repo1_bare/hooks/commit-msg.sample24
-rwxr-xr-xmodules/git/tests/repos/repo1_bare/hooks/post-update.sample8
-rwxr-xr-xmodules/git/tests/repos/repo1_bare/hooks/pre-applypatch.sample14
-rwxr-xr-xmodules/git/tests/repos/repo1_bare/hooks/pre-commit.sample49
-rwxr-xr-xmodules/git/tests/repos/repo1_bare/hooks/pre-push.sample53
-rwxr-xr-xmodules/git/tests/repos/repo1_bare/hooks/pre-rebase.sample169
-rwxr-xr-xmodules/git/tests/repos/repo1_bare/hooks/pre-receive.sample24
-rwxr-xr-xmodules/git/tests/repos/repo1_bare/hooks/prepare-commit-msg.sample36
-rwxr-xr-xmodules/git/tests/repos/repo1_bare/hooks/update.sample128
-rw-r--r--modules/git/tests/repos/repo1_bare/info/exclude6
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/0b/9f291245f6c596fd30bee925fe94fe0cbadd60bin0 -> 54 bytes
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/11/93ff46343f4f6a0522e2b28b871e905178c1f0bin0 -> 23 bytes
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/15/3f451b9ee7fa1da317ab17a127e9fd9d384310bin0 -> 31 bytes
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/18/4d49c75a0b202b1d2ea2fcb5861c329321fcd6bin0 -> 56 bytes
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/21/6bf54c2f2e2916b830ebe09e8c58a6ed52d86bbin0 -> 28 bytes
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/28/39944139e0de9737a044f78b0e4b40d989a9e31
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/2e/65efe2a145dda7ee51d1741299f848e5bf752ebin0 -> 16 bytes
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/30/4c56b3bef33d0afeb8515ee803c839daf30ab8bin0 -> 85 bytes
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/34/d1da713bf7de1c535e1d7d3ca985afd84bc7e5bin0 -> 23 bytes
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/37/991dec2c8e592043f47155ce4808d4580f91231
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/38/441bf2c4d4c27efff94728c9eb33266f44a702bin0 -> 134 bytes
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/3a/d28a9149a2864384548f3d17ed7f38014c9e8abin0 -> 638 bytes
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/50/13716a9da8e66ea21059a84f1b4311424d2b7fbin0 -> 31 bytes
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/59/dfb0bb505a601006e31fed53d2e24e44fca9cabin0 -> 144 bytes
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/5c/80b0245c1c6f8343fa418ec374b13b5d4ee6583
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/62/d735f9efa9cf5b7df6bac9917b80e4779f4315bin0 -> 111 bytes
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/64/3a35374408002fcf2f0e8d42d262a1e0e2f80ebin0 -> 32 bytes
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/6c/493ff740f9380390d5c9ddef4af18697ac9375bin0 -> 21 bytes
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/6f/bd69e9823458e6c4a2fc5c0f6bc022b2f2acd11
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/7e/3b688f3369ca28ebafbda9f8ef39713dd12fc8bin0 -> 85 bytes
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/80/06ff9adbf0cb94da7dad9e537e53817f9fa5c0bin0 -> 176 bytes
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/82/26f571dcc2d2f33a7179d929b10b9c39faa631bin0 -> 71 bytes
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/83/b9c4da46ed59098a009f8640c77eac97b71dfebin0 -> 115 bytes
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/8d/92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2bin0 -> 153 bytes
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/95/bb4d39648ee7e325106df01a621c530863a653bin0 -> 121 bytes
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/98/1ff127cc331753bba28e1377c35934f1ca9b56bin0 -> 56 bytes
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/9c/9aef8dd84e02bc7ec12641deb4c930a7c301852
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/b1/4df6442ea5a1b382985a6549b85d435376c351bin0 -> 18 bytes
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/b1/fc9917b618c924cf4aa421dae74e8bf9b556d3bin0 -> 176 bytes
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/b7/5f44edbd9252c32bf9faa0c1257ffb3b126c24bin0 -> 80 bytes
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/c8/c90111bdc18b3afd2b2906007059e95ac8fdc3bin0 -> 111 bytes
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/d0/845fe2f85710b50d673dafe98236bf9f2023dabin0 -> 50 bytes
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/e2/129701f1a4d54dc44f03c93bca0a2aec7c5449bin0 -> 21 bytes
-rw-r--r--modules/git/tests/repos/repo1_bare/objects/f1/a6cb52b2d16773290cefe49ad0684b50a4f930bin0 -> 111 bytes
-rw-r--r--modules/git/tests/repos/repo1_bare/refs/heads/branch11
-rw-r--r--modules/git/tests/repos/repo1_bare/refs/heads/branch21
-rw-r--r--modules/git/tests/repos/repo1_bare/refs/heads/master1
-rw-r--r--modules/git/tests/repos/repo1_bare/refs/tags/test1
-rw-r--r--modules/git/tree.go98
-rw-r--r--modules/git/tree_blob.go59
-rw-r--r--modules/git/tree_entry.go205
-rw-r--r--modules/git/tree_entry_test.go98
-rw-r--r--modules/git/utils.go96
97 files changed, 4637 insertions, 0 deletions
diff --git a/modules/git/README.md b/modules/git/README.md
new file mode 100644
index 0000000000..4418c1b891
--- /dev/null
+++ b/modules/git/README.md
@@ -0,0 +1,3 @@
+# Git Module
+
+This module is merged from https://github.com/go-gitea/git which is a Go module to access Git through shell commands. Now it's a part of gitea's main repository for easier pull request.
diff --git a/modules/git/blob.go b/modules/git/blob.go
new file mode 100644
index 0000000000..a6e392eeb5
--- /dev/null
+++ b/modules/git/blob.go
@@ -0,0 +1,73 @@
+// Copyright 2015 The Gogs 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 (
+ "bytes"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/exec"
+)
+
+// Blob represents a Git object.
+type Blob struct {
+ repo *Repository
+ *TreeEntry
+}
+
+// Data gets content of blob all at once and wrap it as io.Reader.
+// This can be very slow and memory consuming for huge content.
+func (b *Blob) Data() (io.Reader, error) {
+ stdout := new(bytes.Buffer)
+ stderr := new(bytes.Buffer)
+
+ // Preallocate memory to save ~50% memory usage on big files.
+ stdout.Grow(int(b.Size() + 2048))
+
+ if err := b.DataPipeline(stdout, stderr); err != nil {
+ return nil, concatenateError(err, stderr.String())
+ }
+ return stdout, nil
+}
+
+// DataPipeline gets content of blob and write the result or error to stdout or stderr
+func (b *Blob) DataPipeline(stdout, stderr io.Writer) error {
+ return NewCommand("show", b.ID.String()).RunInDirPipeline(b.repo.Path, stdout, stderr)
+}
+
+type cmdReadCloser struct {
+ cmd *exec.Cmd
+ stdout io.Reader
+}
+
+func (c cmdReadCloser) Read(p []byte) (int, error) {
+ return c.stdout.Read(p)
+}
+
+func (c cmdReadCloser) Close() error {
+ io.Copy(ioutil.Discard, c.stdout)
+ return c.cmd.Wait()
+}
+
+// DataAsync gets a ReadCloser for the contents of a blob without reading it all.
+// Calling the Close function on the result will discard all unread output.
+func (b *Blob) DataAsync() (io.ReadCloser, error) {
+ cmd := exec.Command("git", "show", b.ID.String())
+ cmd.Dir = b.repo.Path
+ cmd.Stderr = os.Stderr
+
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ return nil, fmt.Errorf("StdoutPipe: %v", err)
+ }
+
+ if err = cmd.Start(); err != nil {
+ return nil, fmt.Errorf("Start: %v", err)
+ }
+
+ return cmdReadCloser{stdout: stdout, cmd: cmd}, nil
+}
diff --git a/modules/git/blob_test.go b/modules/git/blob_test.go
new file mode 100644
index 0000000000..39516c422c
--- /dev/null
+++ b/modules/git/blob_test.go
@@ -0,0 +1,80 @@
+// Copyright 2015 The Gogs 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 (
+ "bytes"
+ "io/ioutil"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+var repoSelf = &Repository{
+ Path: "./",
+}
+
+var testBlob = &Blob{
+ repo: repoSelf,
+ TreeEntry: &TreeEntry{
+ ID: MustIDFromString("a8d4b49dd073a4a38a7e58385eeff7cc52568697"),
+ ptree: &Tree{
+ repo: repoSelf,
+ },
+ },
+}
+
+func TestBlob_Data(t *testing.T) {
+ output := `Copyright (c) 2016 The Gitea Authors
+Copyright (c) 2015 The Gogs Authors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+`
+
+ r, err := testBlob.Data()
+ assert.NoError(t, err)
+ require.NotNil(t, r)
+
+ data, err := ioutil.ReadAll(r)
+ assert.NoError(t, err)
+ assert.Equal(t, output, string(data))
+}
+
+func Benchmark_Blob_Data(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ r, err := testBlob.Data()
+ if err != nil {
+ b.Fatal(err)
+ }
+ ioutil.ReadAll(r)
+ }
+}
+
+func Benchmark_Blob_DataPipeline(b *testing.B) {
+ stdout := new(bytes.Buffer)
+ for i := 0; i < b.N; i++ {
+ stdout.Reset()
+ if err := testBlob.DataPipeline(stdout, nil); err != nil {
+ b.Fatal(err)
+ }
+ }
+}
diff --git a/modules/git/cache.go b/modules/git/cache.go
new file mode 100644
index 0000000000..dbbbafae4c
--- /dev/null
+++ b/modules/git/cache.go
@@ -0,0 +1,11 @@
+// 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
+
+// LastCommitCache cache
+type LastCommitCache interface {
+ Get(repoPath, ref, entryPath string) (*Commit, error)
+ Put(repoPath, ref, entryPath string, commit *Commit) error
+}
diff --git a/modules/git/command.go b/modules/git/command.go
new file mode 100644
index 0000000000..d354635119
--- /dev/null
+++ b/modules/git/command.go
@@ -0,0 +1,137 @@
+// Copyright 2015 The Gogs 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 (
+ "bytes"
+ "context"
+ "fmt"
+ "io"
+ "os/exec"
+ "strings"
+ "time"
+)
+
+var (
+ // GlobalCommandArgs global command args for external package setting
+ GlobalCommandArgs []string
+
+ // DefaultCommandExecutionTimeout default command execution timeout duration
+ DefaultCommandExecutionTimeout = 60 * time.Second
+)
+
+// Command represents a command with its subcommands or arguments.
+type Command struct {
+ name string
+ args []string
+}
+
+func (c *Command) String() string {
+ if len(c.args) == 0 {
+ return c.name
+ }
+ return fmt.Sprintf("%s %s", c.name, strings.Join(c.args, " "))
+}
+
+// NewCommand creates and returns a new Git Command based on given command and arguments.
+func NewCommand(args ...string) *Command {
+ // Make an explicit copy of GlobalCommandArgs, otherwise append might overwrite it
+ cargs := make([]string, len(GlobalCommandArgs))
+ copy(cargs, GlobalCommandArgs)
+ return &Command{
+ name: "git",
+ args: append(cargs, args...),
+ }
+}
+
+// AddArguments adds new argument(s) to the command.
+func (c *Command) AddArguments(args ...string) *Command {
+ c.args = append(c.args, args...)
+ return c
+}
+
+// RunInDirTimeoutPipeline executes the command in given directory with given timeout,
+// it pipes stdout and stderr to given io.Writer.
+func (c *Command) RunInDirTimeoutPipeline(timeout time.Duration, dir string, stdout, stderr io.Writer) error {
+ if timeout == -1 {
+ timeout = DefaultCommandExecutionTimeout
+ }
+
+ if len(dir) == 0 {
+ log(c.String())
+ } else {
+ log("%s: %v", dir, c)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), timeout)
+ defer cancel()
+
+ cmd := exec.CommandContext(ctx, c.name, c.args...)
+ cmd.Dir = dir
+ cmd.Stdout = stdout
+ cmd.Stderr = stderr
+ if err := cmd.Start(); err != nil {
+ return err
+ }
+
+ if err := cmd.Wait(); err != nil {
+ return err
+ }
+
+ return ctx.Err()
+}
+
+// RunInDirTimeout executes the command in given directory with given timeout,
+// and returns stdout in []byte and error (combined with stderr).
+func (c *Command) RunInDirTimeout(timeout time.Duration, dir string) ([]byte, error) {
+ stdout := new(bytes.Buffer)
+ stderr := new(bytes.Buffer)
+ if err := c.RunInDirTimeoutPipeline(timeout, dir, stdout, stderr); err != nil {
+ return nil, concatenateError(err, stderr.String())
+ }
+
+ if stdout.Len() > 0 {
+ log("stdout:\n%s", stdout.Bytes()[:1024])
+ }
+ return stdout.Bytes(), nil
+}
+
+// RunInDirPipeline executes the command in given directory,
+// it pipes stdout and stderr to given io.Writer.
+func (c *Command) RunInDirPipeline(dir string, stdout, stderr io.Writer) error {
+ return c.RunInDirTimeoutPipeline(-1, dir, stdout, stderr)
+}
+
+// RunInDirBytes executes the command in given directory
+// and returns stdout in []byte and error (combined with stderr).
+func (c *Command) RunInDirBytes(dir string) ([]byte, error) {
+ return c.RunInDirTimeout(-1, dir)
+}
+
+// RunInDir executes the command in given directory
+// and returns stdout in string and error (combined with stderr).
+func (c *Command) RunInDir(dir string) (string, error) {
+ stdout, err := c.RunInDirTimeout(-1, dir)
+ if err != nil {
+ return "", err
+ }
+ return string(stdout), nil
+}
+
+// RunTimeout executes the command in default working directory with given timeout,
+// and returns stdout in string and error (combined with stderr).
+func (c *Command) RunTimeout(timeout time.Duration) (string, error) {
+ stdout, err := c.RunInDirTimeout(timeout, "")
+ if err != nil {
+ return "", err
+ }
+ return string(stdout), nil
+}
+
+// Run executes the command in default working directory
+// and returns stdout in string and error (combined with stderr).
+func (c *Command) Run() (string, error) {
+ return c.RunTimeout(-1)
+}
diff --git a/modules/git/command_test.go b/modules/git/command_test.go
new file mode 100644
index 0000000000..2f35b3c329
--- /dev/null
+++ b/modules/git/command_test.go
@@ -0,0 +1,41 @@
+// 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 race
+
+package git
+
+import (
+ "context"
+ "testing"
+ "time"
+)
+
+func TestRunInDirTimeoutPipelineNoTimeout(t *testing.T) {
+
+ maxLoops := 1000
+
+ // 'git --version' does not block so it must be finished before the timeout triggered.
+ cmd := NewCommand("--version")
+ for i := 0; i < maxLoops; i++ {
+ if err := cmd.RunInDirTimeoutPipeline(-1, "", nil, nil); err != nil {
+ t.Fatal(err)
+ }
+ }
+}
+
+func TestRunInDirTimeoutPipelineAlwaysTimeout(t *testing.T) {
+
+ maxLoops := 1000
+
+ // 'git hash-object --stdin' blocks on stdin so we can have the timeout triggered.
+ cmd := NewCommand("hash-object --stdin")
+ for i := 0; i < maxLoops; i++ {
+ if err := cmd.RunInDirTimeoutPipeline(1*time.Microsecond, "", nil, nil); err != nil {
+ if err != context.DeadlineExceeded {
+ t.Fatalf("Testing %d/%d: %v", i, maxLoops, err)
+ }
+ }
+ }
+}
diff --git a/modules/git/commit.go b/modules/git/commit.go
new file mode 100644
index 0000000000..36b8d54565
--- /dev/null
+++ b/modules/git/commit.go
@@ -0,0 +1,349 @@
+// 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.
+
+package git
+
+import (
+ "bufio"
+ "bytes"
+ "container/list"
+ "fmt"
+ "io"
+ "net/http"
+ "strconv"
+ "strings"
+)
+
+// Commit represents a git commit.
+type Commit struct {
+ Branch string // Branch this commit belongs to
+ Tree
+ ID SHA1 // The ID of this commit object
+ Author *Signature
+ Committer *Signature
+ CommitMessage string
+ Signature *CommitGPGSignature
+
+ parents []SHA1 // SHA1 strings
+ submoduleCache *ObjectCache
+}
+
+// CommitGPGSignature represents a git commit signature part.
+type CommitGPGSignature struct {
+ Signature string
+ Payload string //TODO check if can be reconstruct from the rest of commit information to not have duplicate data
+}
+
+// similar to https://github.com/git/git/blob/3bc53220cb2dcf709f7a027a3f526befd021d858/commit.c#L1128
+func newGPGSignatureFromCommitline(data []byte, signatureStart int, tag bool) (*CommitGPGSignature, error) {
+ sig := new(CommitGPGSignature)
+ signatureEnd := bytes.LastIndex(data, []byte("-----END PGP SIGNATURE-----"))
+ if signatureEnd == -1 {
+ return nil, fmt.Errorf("end of commit signature not found")
+ }
+ sig.Signature = strings.Replace(string(data[signatureStart:signatureEnd+27]), "\n ", "\n", -1)
+ if tag {
+ sig.Payload = string(data[:signatureStart-1])
+ } else {
+ sig.Payload = string(data[:signatureStart-8]) + string(data[signatureEnd+27:])
+ }
+ return sig, nil
+}
+
+// Message returns the commit message. Same as retrieving CommitMessage directly.
+func (c *Commit) Message() string {
+ return c.CommitMessage
+}
+
+// Summary returns first line of commit message.
+func (c *Commit) Summary() string {
+ return strings.Split(strings.TrimSpace(c.CommitMessage), "\n")[0]
+}
+
+// ParentID returns oid of n-th parent (0-based index).
+// It returns nil if no such parent exists.
+func (c *Commit) ParentID(n int) (SHA1, error) {
+ if n >= len(c.parents) {
+ return SHA1{}, ErrNotExist{"", ""}
+ }
+ return c.parents[n], nil
+}
+
+// Parent returns n-th parent (0-based index) of the commit.
+func (c *Commit) Parent(n int) (*Commit, error) {
+ id, err := c.ParentID(n)
+ if err != nil {
+ return nil, err
+ }
+ parent, err := c.repo.getCommit(id)
+ if err != nil {
+ return nil, err
+ }
+ return parent, nil
+}
+
+// ParentCount returns number of parents of the commit.
+// 0 if this is the root commit, otherwise 1,2, etc.
+func (c *Commit) ParentCount() int {
+ return len(c.parents)
+}
+
+func isImageFile(data []byte) (string, bool) {
+ contentType := http.DetectContentType(data)
+ if strings.Index(contentType, "image/") != -1 {
+ return contentType, true
+ }
+ return contentType, false
+}
+
+// IsImageFile is a file image type
+func (c *Commit) IsImageFile(name string) bool {
+ blob, err := c.GetBlobByPath(name)
+ if err != nil {
+ return false
+ }
+
+ dataRc, err := blob.DataAsync()
+ if err != nil {
+ return false
+ }
+ defer dataRc.Close()
+ buf := make([]byte, 1024)
+ n, _ := dataRc.Read(buf)
+ buf = buf[:n]
+ _, isImage := isImageFile(buf)
+ return isImage
+}
+
+// GetCommitByPath return the commit of relative path object.
+func (c *Commit) GetCommitByPath(relpath string) (*Commit, error) {
+ return c.repo.getCommitByPathWithID(c.ID, relpath)
+}
+
+// AddChanges marks local changes to be ready for commit.
+func AddChanges(repoPath string, all bool, files ...string) error {
+ cmd := NewCommand("add")
+ if all {
+ cmd.AddArguments("--all")
+ }
+ _, err := cmd.AddArguments(files...).RunInDir(repoPath)
+ return err
+}
+
+// CommitChangesOptions the options when a commit created
+type CommitChangesOptions struct {
+ Committer *Signature
+ Author *Signature
+ Message string
+}
+
+// CommitChanges commits local changes with given committer, author and message.
+// If author is nil, it will be the same as committer.
+func CommitChanges(repoPath string, opts CommitChangesOptions) error {
+ cmd := NewCommand()
+ if opts.Committer != nil {
+ cmd.AddArguments("-c", "user.name="+opts.Committer.Name, "-c", "user.email="+opts.Committer.Email)
+ }
+ cmd.AddArguments("commit")
+
+ if opts.Author == nil {
+ opts.Author = opts.Committer
+ }
+ if opts.Author != nil {
+ cmd.AddArguments(fmt.Sprintf("--author='%s <%s>'", opts.Author.Name, opts.Author.Email))
+ }
+ cmd.AddArguments("-m", opts.Message)
+
+ _, err := cmd.RunInDir(repoPath)
+ // No stderr but exit status 1 means nothing to commit.
+ if err != nil && err.Error() == "exit status 1" {
+ return nil
+ }
+ return err
+}
+
+func commitsCount(repoPath, revision, relpath string) (int64, error) {
+ var cmd *Command
+ cmd = NewCommand("rev-list", "--count")
+ cmd.AddArguments(revision)
+ if len(relpath) > 0 {
+ cmd.AddArguments("--", relpath)
+ }
+
+ stdout, err := cmd.RunInDir(repoPath)
+ if err != nil {
+ return 0, err
+ }
+
+ return strconv.ParseInt(strings.TrimSpace(stdout), 10, 64)
+}
+
+// CommitsCount returns number of total commits of until given revision.
+func CommitsCount(repoPath, revision string) (int64, error) {
+ return commitsCount(repoPath, revision, "")
+}
+
+// CommitsCount returns number of total commits of until current revision.
+func (c *Commit) CommitsCount() (int64, error) {
+ return CommitsCount(c.repo.Path, c.ID.String())
+}
+
+// CommitsByRange returns the specific page commits before current revision, every page's number default by CommitsRangeSize
+func (c *Commit) CommitsByRange(page int) (*list.List, error) {
+ return c.repo.commitsByRange(c.ID, page)
+}
+
+// CommitsBefore returns all the commits before current revision
+func (c *Commit) CommitsBefore() (*list.List, error) {
+ return c.repo.getCommitsBefore(c.ID)
+}
+
+// CommitsBeforeLimit returns num commits before current revision
+func (c *Commit) CommitsBeforeLimit(num int) (*list.List, error) {
+ return c.repo.getCommitsBeforeLimit(c.ID, num)
+}
+
+// CommitsBeforeUntil returns the commits between commitID to current revision
+func (c *Commit) CommitsBeforeUntil(commitID string) (*list.List, error) {
+ endCommit, err := c.repo.GetCommit(commitID)
+ if err != nil {
+ return nil, err
+ }
+ return c.repo.CommitsBetween(c, endCommit)
+}
+
+// SearchCommits returns the commits match the keyword before current revision
+func (c *Commit) SearchCommits(keyword string, all bool) (*list.List, error) {
+ return c.repo.searchCommits(c.ID, keyword, all)
+}
+
+// GetFilesChangedSinceCommit get all changed file names between pastCommit to current revision
+func (c *Commit) GetFilesChangedSinceCommit(pastCommit string) ([]string, error) {
+ return c.repo.getFilesChanged(pastCommit, c.ID.String())
+}
+
+// GetSubModules get all the sub modules of current revision git tree
+func (c *Commit) GetSubModules() (*ObjectCache, error) {
+ if c.submoduleCache != nil {
+ return c.submoduleCache, nil
+ }
+
+ entry, err := c.GetTreeEntryByPath(".gitmodules")
+ if err != nil {
+ if _, ok := err.(ErrNotExist); ok {
+ return nil, nil
+ }
+ return nil, err
+ }
+ rd, err := entry.Blob().Data()
+ if err != nil {
+ return nil, err
+ }
+
+ scanner := bufio.NewScanner(rd)
+ c.submoduleCache = newObjectCache()
+ var ismodule bool
+ var path string
+ for scanner.Scan() {
+ if strings.HasPrefix(scanner.Text(), "[submodule") {
+ ismodule = true
+ continue
+ }
+ if ismodule {
+ fields := strings.Split(scanner.Text(), "=")
+ k := strings.TrimSpace(fields[0])
+ if k == "path" {
+ path = strings.TrimSpace(fields[1])
+ } else if k == "url" {
+ c.submoduleCache.Set(path, &SubModule{path, strings.TrimSpace(fields[1])})
+ ismodule = false
+ }
+ }
+ }
+
+ return c.submoduleCache, nil
+}
+
+// GetSubModule get the sub module according entryname
+func (c *Commit) GetSubModule(entryname string) (*SubModule, error) {
+ modules, err := c.GetSubModules()
+ if err != nil {
+ return nil, err
+ }
+
+ if modules != nil {
+ module, has := modules.Get(entryname)
+ if has {
+ return module.(*SubModule), nil
+ }
+ }
+ return nil, nil
+}
+
+// CommitFileStatus represents status of files in a commit.
+type CommitFileStatus struct {
+ Added []string
+ Removed []string
+ Modified []string
+}
+
+// NewCommitFileStatus creates a CommitFileStatus
+func NewCommitFileStatus() *CommitFileStatus {
+ return &CommitFileStatus{
+ []string{}, []string{}, []string{},
+ }
+}
+
+// GetCommitFileStatus returns file status of commit in given repository.
+func GetCommitFileStatus(repoPath, commitID string) (*CommitFileStatus, error) {
+ stdout, w := io.Pipe()
+ done := make(chan struct{})
+ fileStatus := NewCommitFileStatus()
+ go func() {
+ scanner := bufio.NewScanner(stdout)
+ for scanner.Scan() {
+ fields := strings.Fields(scanner.Text())
+ if len(fields) < 2 {
+ continue
+ }
+
+ switch fields[0][0] {
+ case 'A':
+ fileStatus.Added = append(fileStatus.Added, fields[1])
+ case 'D':
+ fileStatus.Removed = append(fileStatus.Removed, fields[1])
+ case 'M':
+ fileStatus.Modified = append(fileStatus.Modified, fields[1])
+ }
+ }
+ done <- struct{}{}
+ }()
+
+ stderr := new(bytes.Buffer)
+ 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())
+ }
+
+ <-done
+ return fileStatus, nil
+}
+
+// GetFullCommitID returns full length (40) of commit ID by given short SHA in a repository.
+func GetFullCommitID(repoPath, shortID string) (string, error) {
+ if len(shortID) >= 40 {
+ return shortID, nil
+ }
+
+ commitID, err := NewCommand("rev-parse", shortID).RunInDir(repoPath)
+ if err != nil {
+ if strings.Contains(err.Error(), "exit status 128") {
+ return "", ErrNotExist{shortID, ""}
+ }
+ return "", err
+ }
+ return strings.TrimSpace(commitID), nil
+}
diff --git a/modules/git/commit_archive.go b/modules/git/commit_archive.go
new file mode 100644
index 0000000000..e13825a962
--- /dev/null
+++ b/modules/git/commit_archive.go
@@ -0,0 +1,37 @@
+// Copyright 2015 The Gogs 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 (
+ "fmt"
+ "path/filepath"
+ "strings"
+)
+
+// ArchiveType archive types
+type ArchiveType int
+
+const (
+ // ZIP zip archive type
+ ZIP ArchiveType = iota + 1
+ // TARGZ tar gz archive type
+ TARGZ
+)
+
+// CreateArchive create archive content to the target path
+func (c *Commit) CreateArchive(target string, archiveType ArchiveType) error {
+ var format string
+ switch archiveType {
+ case ZIP:
+ format = "zip"
+ case TARGZ:
+ format = "tar.gz"
+ default:
+ return fmt.Errorf("unknown format: %v", archiveType)
+ }
+
+ _, err := NewCommand("archive", "--prefix="+filepath.Base(strings.TrimSuffix(c.repo.Path, ".git"))+"/", "--format="+format, "-o", target, c.ID.String()).RunInDir(c.repo.Path)
+ return err
+}
diff --git a/modules/git/commit_info.go b/modules/git/commit_info.go
new file mode 100644
index 0000000000..971082be1f
--- /dev/null
+++ b/modules/git/commit_info.go
@@ -0,0 +1,329 @@
+// 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.
+
+package git
+
+import (
+ "bufio"
+ "context"
+ "fmt"
+ "os/exec"
+ "path"
+ "runtime"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+)
+
+const (
+ // parameters for searching for commit infos. If the untargeted search has
+ // not found any entries in the past 5 commits, and 12 or fewer entries
+ // remain, then we'll just let the targeted-searching threads finish off,
+ // and stop the untargeted search to not interfere.
+ deferToTargetedSearchColdStreak = 5
+ deferToTargetedSearchNumRemainingEntries = 12
+)
+
+// getCommitsInfoState shared state while getting commit info for entries
+type getCommitsInfoState struct {
+ lock sync.Mutex
+ /* read-only fields, can be read without the mutex */
+ // entries and entryPaths are read-only after initialization, so they can
+ // safely be read without the mutex
+ entries []*TreeEntry
+ // set of filepaths to get info for
+ entryPaths map[string]struct{}
+ treePath string
+ headCommit *Commit
+
+ /* mutable fields, must hold mutex to read or write */
+ // map from filepath to commit
+ commits map[string]*Commit
+ // set of filepaths that have been or are being searched for in a target search
+ targetedPaths map[string]struct{}
+}
+
+func (state *getCommitsInfoState) numRemainingEntries() int {
+ state.lock.Lock()
+ defer state.lock.Unlock()
+ return len(state.entries) - len(state.commits)
+}
+
+// getTargetEntryPath Returns the next path for a targeted-searching thread to
+// search for, or returns the empty string if nothing left to search for
+func (state *getCommitsInfoState) getTargetedEntryPath() string {
+ var targetedEntryPath string
+ state.lock.Lock()
+ defer state.lock.Unlock()
+ for _, entry := range state.entries {
+ entryPath := path.Join(state.treePath, entry.Name())
+ if _, ok := state.commits[entryPath]; ok {
+ continue
+ } else if _, ok = state.targetedPaths[entryPath]; ok {
+ continue
+ }
+ targetedEntryPath = entryPath
+ state.targetedPaths[entryPath] = struct{}{}
+ break
+ }
+ return targetedEntryPath
+}
+
+// repeatedly perform targeted searches for unpopulated entries
+func targetedSearch(state *getCommitsInfoState, done chan error, cache LastCommitCache) {
+ for {
+ entryPath := state.getTargetedEntryPath()
+ if len(entryPath) == 0 {
+ done <- nil
+ return
+ }
+ if cache != nil {
+ commit, err := cache.Get(state.headCommit.repo.Path, state.headCommit.ID.String(), entryPath)
+ if err == nil && commit != nil {
+ state.update(entryPath, commit)
+ continue
+ }
+ }
+ command := NewCommand("rev-list", "-1", state.headCommit.ID.String(), "--", entryPath)
+ output, err := command.RunInDir(state.headCommit.repo.Path)
+ if err != nil {
+ done <- err
+ return
+ }
+ id, err := NewIDFromString(strings.TrimSpace(output))
+ if err != nil {
+ done <- err
+ return
+ }
+ commit, err := state.headCommit.repo.getCommit(id)
+ if err != nil {
+ done <- err
+ return
+ }
+ state.update(entryPath, commit)
+ if cache != nil {
+ cache.Put(state.headCommit.repo.Path, state.headCommit.ID.String(), entryPath, commit)
+ }
+ }
+}
+
+func initGetCommitInfoState(entries Entries, headCommit *Commit, treePath string) *getCommitsInfoState {
+ entryPaths := make(map[string]struct{}, len(entries))
+ for _, entry := range entries {
+ entryPaths[path.Join(treePath, entry.Name())] = struct{}{}
+ }
+ if treePath = path.Clean(treePath); treePath == "." {
+ treePath = ""
+ }
+ return &getCommitsInfoState{
+ entries: entries,
+ entryPaths: entryPaths,
+ commits: make(map[string]*Commit, len(entries)),
+ targetedPaths: make(map[string]struct{}, len(entries)),
+ treePath: treePath,
+ headCommit: headCommit,
+ }
+}
+
+// GetCommitsInfo gets information of all commits that are corresponding to these entries
+func (tes Entries) GetCommitsInfo(commit *Commit, treePath string, cache LastCommitCache) ([][]interface{}, error) {
+ state := initGetCommitInfoState(tes, commit, treePath)
+ if err := getCommitsInfo(state, cache); err != nil {
+ return nil, err
+ }
+ if len(state.commits) < len(state.entryPaths) {
+ return nil, fmt.Errorf("could not find commits for all entries")
+ }
+
+ commitsInfo := make([][]interface{}, len(tes))
+ for i, entry := range tes {
+ commit, ok := state.commits[path.Join(treePath, entry.Name())]
+ if !ok {
+ return nil, fmt.Errorf("could not find commit for %s", entry.Name())
+ }
+ switch entry.Type {
+ case ObjectCommit:
+ subModuleURL := ""
+ if subModule, err := state.headCommit.GetSubModule(entry.Name()); err != nil {
+ return nil, err
+ } else if subModule != nil {
+ subModuleURL = subModule.URL
+ }
+ subModuleFile := NewSubModuleFile(commit, subModuleURL, entry.ID.String())
+ commitsInfo[i] = []interface{}{entry, subModuleFile}
+ default:
+ commitsInfo[i] = []interface{}{entry, commit}
+ }
+ }
+ return commitsInfo, nil
+}
+
+func (state *getCommitsInfoState) cleanEntryPath(rawEntryPath string) (string, error) {
+ if rawEntryPath[0] == '"' {
+ var err error
+ rawEntryPath, err = strconv.Unquote(rawEntryPath)
+ if err != nil {
+ return rawEntryPath, err
+ }
+ }
+ var entryNameStartIndex int
+ if len(state.treePath) > 0 {
+ entryNameStartIndex = len(state.treePath) + 1
+ }
+
+ if index := strings.IndexByte(rawEntryPath[entryNameStartIndex:], '/'); index >= 0 {
+ return rawEntryPath[:entryNameStartIndex+index], nil
+ }
+ return rawEntryPath, nil
+}
+
+// update report that the given path was last modified by the given commit.
+// Returns whether state.commits was updated
+func (state *getCommitsInfoState) update(entryPath string, commit *Commit) bool {
+ if _, ok := state.entryPaths[entryPath]; !ok {
+ return false
+ }
+
+ var updated bool
+ state.lock.Lock()
+ defer state.lock.Unlock()
+ if _, ok := state.commits[entryPath]; !ok {
+ state.commits[entryPath] = commit
+ updated = true
+ }
+ return updated
+}
+
+const getCommitsInfoPretty = "--pretty=format:%H %ct %s"
+
+func getCommitsInfo(state *getCommitsInfoState, cache LastCommitCache) error {
+ ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
+ defer cancel()
+
+ args := []string{"log", state.headCommit.ID.String(), getCommitsInfoPretty, "--name-status", "-c"}
+ if len(state.treePath) > 0 {
+ args = append(args, "--", state.treePath)
+ }
+ cmd := exec.CommandContext(ctx, "git", args...)
+ cmd.Dir = state.headCommit.repo.Path
+
+ readCloser, err := cmd.StdoutPipe()
+ if err != nil {
+ return err
+ }
+
+ if err := cmd.Start(); err != nil {
+ return err
+ }
+ // it's okay to ignore the error returned by cmd.Wait(); we expect the
+ // subprocess to sometimes have a non-zero exit status, since we may
+ // prematurely close stdout, resulting in a broken pipe.
+ defer cmd.Wait()
+
+ numThreads := runtime.NumCPU()
+ done := make(chan error, numThreads)
+ for i := 0; i < numThreads; i++ {
+ go targetedSearch(state, done, cache)
+ }
+
+ scanner := bufio.NewScanner(readCloser)
+ err = state.processGitLogOutput(scanner)
+
+ // it is important that we close stdout here; if we do not close
+ // stdout, the subprocess will keep running, and the deffered call
+ // cmd.Wait() may block for a long time.
+ if closeErr := readCloser.Close(); closeErr != nil && err == nil {
+ err = closeErr
+ }
+
+ for i := 0; i < numThreads; i++ {
+ doneErr := <-done
+ if doneErr != nil && err == nil {
+ err = doneErr
+ }
+ }
+ return err
+}
+
+func (state *getCommitsInfoState) processGitLogOutput(scanner *bufio.Scanner) error {
+ // keep a local cache of seen paths to avoid acquiring a lock for paths
+ // we've already seen
+ seenPaths := make(map[string]struct{}, len(state.entryPaths))
+ // number of consecutive commits without any finds
+ coldStreak := 0
+ var commit *Commit
+ var err error
+ for scanner.Scan() {
+ line := scanner.Text()
+ if len(line) == 0 { // in-between commits
+ numRemainingEntries := state.numRemainingEntries()
+ if numRemainingEntries == 0 {
+ break
+ }
+ if coldStreak >= deferToTargetedSearchColdStreak &&
+ numRemainingEntries <= deferToTargetedSearchNumRemainingEntries {
+ // stop this untargeted search, and let the targeted-search threads
+ // finish the work
+ break
+ }
+ continue
+ }
+ if line[0] >= 'A' && line[0] <= 'X' { // a file was changed by the current commit
+ // look for the last tab, since for copies (C) and renames (R) two
+ // filenames are printed: src, then dest
+ tabIndex := strings.LastIndexByte(line, '\t')
+ if tabIndex < 1 {
+ return fmt.Errorf("misformatted line: %s", line)
+ }
+ entryPath, err := state.cleanEntryPath(line[tabIndex+1:])
+ if err != nil {
+ return err
+ }
+ if _, ok := seenPaths[entryPath]; !ok {
+ if state.update(entryPath, commit) {
+ coldStreak = 0
+ }
+ seenPaths[entryPath] = struct{}{}
+ }
+ continue
+ }
+
+ // a new commit
+ commit, err = parseCommitInfo(line)
+ if err != nil {
+ return err
+ }
+ coldStreak++
+ }
+ return scanner.Err()
+}
+
+// parseCommitInfo parse a commit from a line of `git log` output. Expects the
+// line to be formatted according to getCommitsInfoPretty.
+func parseCommitInfo(line string) (*Commit, error) {
+ if len(line) < 43 {
+ return nil, fmt.Errorf("invalid git output: %s", line)
+ }
+ ref, err := NewIDFromString(line[:40])
+ if err != nil {
+ return nil, err
+ }
+ spaceIndex := strings.IndexByte(line[41:], ' ')
+ if spaceIndex < 0 {
+ return nil, fmt.Errorf("invalid git output: %s", line)
+ }
+ unixSeconds, err := strconv.Atoi(line[41 : 41+spaceIndex])
+ if err != nil {
+ return nil, err
+ }
+ message := line[spaceIndex+42:]
+ return &Commit{
+ ID: ref,
+ CommitMessage: message,
+ Committer: &Signature{
+ When: time.Unix(int64(unixSeconds), 0),
+ },
+ }, nil
+}
diff --git a/modules/git/commit_info_test.go b/modules/git/commit_info_test.go
new file mode 100644
index 0000000000..120a9a737c
--- /dev/null
+++ b/modules/git/commit_info_test.go
@@ -0,0 +1,117 @@
+package git
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+const testReposDir = "tests/repos/"
+const benchmarkReposDir = "benchmark/repos/"
+
+func cloneRepo(url, dir, name string) (string, error) {
+ repoDir := filepath.Join(dir, name)
+ if _, err := os.Stat(repoDir); err == nil {
+ return repoDir, nil
+ }
+ return repoDir, Clone(url, repoDir, CloneRepoOptions{
+ Mirror: false,
+ Bare: false,
+ Quiet: true,
+ Timeout: 5 * time.Minute,
+ })
+}
+
+func testGetCommitsInfo(t *testing.T, repo1 *Repository) {
+ // these test case are specific to the repo1 test repo
+ testCases := []struct {
+ CommitID string
+ Path string
+ ExpectedIDs map[string]string
+ }{
+ {"8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2", "", map[string]string{
+ "file1.txt": "95bb4d39648ee7e325106df01a621c530863a653",
+ "file2.txt": "8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2",
+ }},
+ {"2839944139e0de9737a044f78b0e4b40d989a9e3", "", map[string]string{
+ "file1.txt": "2839944139e0de9737a044f78b0e4b40d989a9e3",
+ "branch1.txt": "9c9aef8dd84e02bc7ec12641deb4c930a7c30185",
+ }},
+ {"5c80b0245c1c6f8343fa418ec374b13b5d4ee658", "branch2", map[string]string{
+ "branch2.txt": "5c80b0245c1c6f8343fa418ec374b13b5d4ee658",
+ }},
+ }
+ for _, testCase := range testCases {
+ commit, err := repo1.GetCommit(testCase.CommitID)
+ assert.NoError(t, err)
+ tree, err := commit.Tree.SubTree(testCase.Path)
+ assert.NoError(t, err)
+ entries, err := tree.ListEntries()
+ assert.NoError(t, err)
+ commitsInfo, err := entries.GetCommitsInfo(commit, testCase.Path, nil)
+ assert.NoError(t, err)
+ assert.Len(t, commitsInfo, len(testCase.ExpectedIDs))
+ for _, commitInfo := range commitsInfo {
+ entry := commitInfo[0].(*TreeEntry)
+ commit := commitInfo[1].(*Commit)
+ expectedID, ok := testCase.ExpectedIDs[entry.Name()]
+ if !assert.True(t, ok) {
+ continue
+ }
+ assert.Equal(t, expectedID, commit.ID.String())
+ }
+ }
+}
+
+func TestEntries_GetCommitsInfo(t *testing.T) {
+ bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
+ bareRepo1, err := OpenRepository(bareRepo1Path)
+ assert.NoError(t, err)
+ testGetCommitsInfo(t, bareRepo1)
+
+ clonedPath, err := cloneRepo(bareRepo1Path, testReposDir, "repo1_TestEntries_GetCommitsInfo")
+ assert.NoError(t, err)
+ defer os.RemoveAll(clonedPath)
+ clonedRepo1, err := OpenRepository(clonedPath)
+ assert.NoError(t, err)
+ testGetCommitsInfo(t, clonedRepo1)
+}
+
+func BenchmarkEntries_GetCommitsInfo(b *testing.B) {
+ benchmarks := []struct {
+ url string
+ name string
+ }{
+ {url: "https://github.com/go-gitea/gitea.git", name: "gitea"},
+ {url: "https://github.com/ethantkoenig/manyfiles.git", name: "manyfiles"},
+ {url: "https://github.com/moby/moby.git", name: "moby"},
+ {url: "https://github.com/golang/go.git", name: "go"},
+ {url: "https://github.com/torvalds/linux.git", name: "linux"},
+ }
+ for _, benchmark := range benchmarks {
+ var commit *Commit
+ var entries Entries
+ if repoPath, err := cloneRepo(benchmark.url, benchmarkReposDir, benchmark.name); err != nil {
+ b.Fatal(err)
+ } else if repo, err := OpenRepository(repoPath); err != nil {
+ b.Fatal(err)
+ } else if commit, err = repo.GetBranchCommit("master"); err != nil {
+ b.Fatal(err)
+ } else if entries, err = commit.Tree.ListEntries(); err != nil {
+ b.Fatal(err)
+ }
+ entries.Sort()
+ b.ResetTimer()
+ b.Run(benchmark.name, func(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ _, err := entries.GetCommitsInfo(commit, "", nil)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+ }
+}
diff --git a/modules/git/commit_test.go b/modules/git/commit_test.go
new file mode 100644
index 0000000000..bbe12b5aec
--- /dev/null
+++ b/modules/git/commit_test.go
@@ -0,0 +1,38 @@
+// 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.
+
+package git
+
+import (
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCommitsCount(t *testing.T) {
+ bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
+
+ commitsCount, err := CommitsCount(bareRepo1Path, "8006ff9adbf0cb94da7dad9e537e53817f9fa5c0")
+ assert.NoError(t, err)
+ assert.Equal(t, int64(3), commitsCount)
+}
+
+func TestGetFullCommitID(t *testing.T) {
+ bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
+
+ id, err := GetFullCommitID(bareRepo1Path, "8006ff9a")
+ assert.NoError(t, err)
+ assert.Equal(t, "8006ff9adbf0cb94da7dad9e537e53817f9fa5c0", id)
+}
+
+func TestGetFullCommitIDError(t *testing.T) {
+ bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
+
+ id, err := GetFullCommitID(bareRepo1Path, "unknown")
+ assert.Empty(t, id)
+ if assert.Error(t, err) {
+ assert.EqualError(t, err, "object does not exist [id: unknown, rel_path: ]")
+ }
+}
diff --git a/modules/git/error.go b/modules/git/error.go
new file mode 100644
index 0000000000..1aae5a37a2
--- /dev/null
+++ b/modules/git/error.go
@@ -0,0 +1,66 @@
+// Copyright 2015 The Gogs 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 (
+ "fmt"
+ "time"
+)
+
+// ErrExecTimeout error when exec timed out
+type ErrExecTimeout struct {
+ Duration time.Duration
+}
+
+// IsErrExecTimeout if some error is ErrExecTimeout
+func IsErrExecTimeout(err error) bool {
+ _, ok := err.(ErrExecTimeout)
+ return ok
+}
+
+func (err ErrExecTimeout) Error() string {
+ return fmt.Sprintf("execution is timeout [duration: %v]", err.Duration)
+}
+
+// ErrNotExist commit not exist error
+type ErrNotExist struct {
+ ID string
+ RelPath string
+}
+
+// IsErrNotExist if some error is ErrNotExist
+func IsErrNotExist(err error) bool {
+ _, ok := err.(ErrNotExist)
+ return ok
+}
+
+func (err ErrNotExist) Error() string {
+ return fmt.Sprintf("object does not exist [id: %s, rel_path: %s]", err.ID, err.RelPath)
+}
+
+// ErrBadLink entry.FollowLink error
+type ErrBadLink struct {
+ Name string
+ Message string
+}
+
+func (err ErrBadLink) Error() string {
+ return fmt.Sprintf("%s: %s", err.Name, err.Message)
+}
+
+// ErrUnsupportedVersion error when required git version not matched
+type ErrUnsupportedVersion struct {
+ Required string
+}
+
+// IsErrUnsupportedVersion if some error is ErrUnsupportedVersion
+func IsErrUnsupportedVersion(err error) bool {
+ _, ok := err.(ErrUnsupportedVersion)
+ return ok
+}
+
+func (err ErrUnsupportedVersion) Error() string {
+ return fmt.Sprintf("Operation requires higher version [required: %s]", err.Required)
+}
diff --git a/modules/git/git.go b/modules/git/git.go
new file mode 100644
index 0000000000..150b80fb07
--- /dev/null
+++ b/modules/git/git.go
@@ -0,0 +1,91 @@
+// 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.
+
+package git
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/mcuadros/go-version"
+)
+
+// Version return this package's current version
+func Version() string {
+ return "0.4.2"
+}
+
+var (
+ // Debug enables verbose logging on everything.
+ // This should be false in case Gogs starts in SSH mode.
+ Debug = false
+ // Prefix the log prefix
+ Prefix = "[git-module] "
+ // GitVersionRequired is the minimum Git version required
+ GitVersionRequired = "1.7.2"
+)
+
+func log(format string, args ...interface{}) {
+ if !Debug {
+ return
+ }
+
+ fmt.Print(Prefix)
+ if len(args) == 0 {
+ fmt.Println(format)
+ } else {
+ fmt.Printf(format+"\n", args...)
+ }
+}
+
+var gitVersion string
+
+// BinVersion returns current Git version from shell.
+func BinVersion() (string, error) {
+ if len(gitVersion) > 0 {
+ return gitVersion, nil
+ }
+
+ stdout, err := NewCommand("version").Run()
+ if err != nil {
+ return "", err
+ }
+
+ fields := strings.Fields(stdout)
+ if len(fields) < 3 {
+ return "", fmt.Errorf("not enough output: %s", stdout)
+ }
+
+ // Handle special case on Windows.
+ i := strings.Index(fields[2], "windows")
+ if i >= 1 {
+ gitVersion = fields[2][:i-1]
+ return gitVersion, nil
+ }
+
+ gitVersion = fields[2]
+ return gitVersion, nil
+}
+
+func init() {
+ gitVersion, err := BinVersion()
+ if err != nil {
+ panic(fmt.Sprintf("Git version missing: %v", err))
+ }
+ if version.Compare(gitVersion, GitVersionRequired, "<") {
+ panic(fmt.Sprintf("Git version not supported. Requires version > %v", GitVersionRequired))
+ }
+}
+
+// Fsck verifies the connectivity and validity of the objects in the database
+func Fsck(repoPath string, timeout time.Duration, args ...string) error {
+ // Make sure timeout makes sense.
+ if timeout <= 0 {
+ timeout = -1
+ }
+ _, err := NewCommand("fsck").AddArguments(args...).RunInDirTimeout(timeout, repoPath)
+ return err
+}
diff --git a/modules/git/hook.go b/modules/git/hook.go
new file mode 100644
index 0000000000..18c00b5838
--- /dev/null
+++ b/modules/git/hook.go
@@ -0,0 +1,135 @@
+// Copyright 2015 The Gogs 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 (
+ "errors"
+ "io/ioutil"
+ "os"
+ "path"
+ "path/filepath"
+ "strings"
+
+ "github.com/Unknwon/com"
+)
+
+// hookNames is a list of Git server hooks' name that are supported.
+var hookNames = []string{
+ "pre-receive",
+ "update",
+ "post-receive",
+}
+
+var (
+ // ErrNotValidHook error when a git hook is not valid
+ ErrNotValidHook = errors.New("not a valid Git hook")
+)
+
+// IsValidHookName returns true if given name is a valid Git hook.
+func IsValidHookName(name string) bool {
+ for _, hn := range hookNames {
+ if hn == name {
+ return true
+ }
+ }
+ return false
+}
+
+// Hook represents a Git hook.
+type Hook struct {
+ name string
+ IsActive bool // Indicates whether repository has this hook.
+ Content string // Content of hook if it's active.
+ Sample string // Sample content from Git.
+ path string // Hook file path.
+}
+
+// GetHook returns a Git hook by given name and repository.
+func GetHook(repoPath, name string) (*Hook, error) {
+ if !IsValidHookName(name) {
+ return nil, ErrNotValidHook
+ }
+ h := &Hook{
+ name: name,
+ path: path.Join(repoPath, "hooks", name+".d", name),
+ }
+ samplePath := filepath.Join(repoPath, "hooks", name+".sample")
+ if isFile(h.path) {
+ data, err := ioutil.ReadFile(h.path)
+ if err != nil {
+ return nil, err
+ }
+ h.IsActive = true
+ h.Content = string(data)
+ } else if isFile(samplePath) {
+ data, err := ioutil.ReadFile(samplePath)
+ if err != nil {
+ return nil, err
+ }
+ h.Sample = string(data)
+ }
+ return h, nil
+}
+
+// Name return the name of the hook
+func (h *Hook) Name() string {
+ return h.name
+}
+
+// Update updates hook settings.
+func (h *Hook) Update() error {
+ if len(strings.TrimSpace(h.Content)) == 0 {
+ if isExist(h.path) {
+ err := os.Remove(h.path)
+ if err != nil {
+ return err
+ }
+ }
+ h.IsActive = false
+ return nil
+ }
+ err := ioutil.WriteFile(h.path, []byte(strings.Replace(h.Content, "\r", "", -1)), os.ModePerm)
+ if err != nil {
+ return err
+ }
+ h.IsActive = true
+ return nil
+}
+
+// ListHooks returns a list of Git hooks of given repository.
+func ListHooks(repoPath string) (_ []*Hook, err error) {
+ if !isDir(path.Join(repoPath, "hooks")) {
+ return nil, errors.New("hooks path does not exist")
+ }
+
+ hooks := make([]*Hook, len(hookNames))
+ for i, name := range hookNames {
+ hooks[i], err = GetHook(repoPath, name)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return hooks, nil
+}
+
+const (
+ // HookPathUpdate hook update path
+ HookPathUpdate = "hooks/update"
+)
+
+// SetUpdateHook writes given content to update hook of the reposiotry.
+func SetUpdateHook(repoPath, content string) (err error) {
+ log("Setting update hook: %s", repoPath)
+ hookPath := path.Join(repoPath, HookPathUpdate)
+ if com.IsExist(hookPath) {
+ err = os.Remove(hookPath)
+ } else {
+ err = os.MkdirAll(path.Dir(hookPath), os.ModePerm)
+ }
+ if err != nil {
+ return err
+ }
+ return ioutil.WriteFile(hookPath, []byte(content), 0777)
+}
diff --git a/modules/git/parse.go b/modules/git/parse.go
new file mode 100644
index 0000000000..5c964f16ee
--- /dev/null
+++ b/modules/git/parse.go
@@ -0,0 +1,81 @@
+// 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.
+
+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.mode = EntryModeBlob
+ entry.Type = ObjectBlob
+ pos += 12 // skip over "100644 blob "
+ case "100755":
+ entry.mode = EntryModeExec
+ entry.Type = ObjectBlob
+ pos += 12 // skip over "100755 blob "
+ case "120000":
+ entry.mode = EntryModeSymlink
+ entry.Type = ObjectBlob
+ pos += 12 // skip over "120000 blob "
+ case "160000":
+ entry.mode = EntryModeCommit
+ entry.Type = ObjectCommit
+ pos += 14 // skip over "160000 object "
+ case "040000":
+ entry.mode = EntryModeTree
+ entry.Type = ObjectTree
+ 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/parse_test.go b/modules/git/parse_test.go
new file mode 100644
index 0000000000..66936cbdf0
--- /dev/null
+++ b/modules/git/parse_test.go
@@ -0,0 +1,58 @@
+// 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.
+
+package git
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestParseTreeEntries(t *testing.T) {
+ testCases := []struct {
+ Input string
+ Expected []*TreeEntry
+ }{
+ {
+ Input: "",
+ Expected: []*TreeEntry{},
+ },
+ {
+ Input: "100644 blob 61ab7345a1a3bbc590068ccae37b8515cfc5843c\texample/file2.txt\n",
+ Expected: []*TreeEntry{
+ {
+ mode: EntryModeBlob,
+ Type: ObjectBlob,
+ ID: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
+ name: "example/file2.txt",
+ },
+ },
+ },
+ {
+ Input: "120000 blob 61ab7345a1a3bbc590068ccae37b8515cfc5843c\t\"example/\\n.txt\"\n" +
+ "040000 tree 1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8\texample\n",
+ Expected: []*TreeEntry{
+ {
+ ID: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
+ Type: ObjectBlob,
+ mode: EntryModeSymlink,
+ name: "example/\n.txt",
+ },
+ {
+ ID: MustIDFromString("1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8"),
+ Type: ObjectTree,
+ mode: EntryModeTree,
+ name: "example",
+ },
+ },
+ },
+ }
+
+ for _, testCase := range testCases {
+ entries, err := ParseTreeEntries([]byte(testCase.Input))
+ assert.NoError(t, err)
+ assert.EqualValues(t, testCase.Expected, entries)
+ }
+}
diff --git a/modules/git/ref.go b/modules/git/ref.go
new file mode 100644
index 0000000000..67b56ac999
--- /dev/null
+++ b/modules/git/ref.go
@@ -0,0 +1,18 @@
+// 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.
+
+package git
+
+// Reference represents a Git ref.
+type Reference struct {
+ Name string
+ repo *Repository
+ Object SHA1 // The id of this commit object
+ Type string
+}
+
+// Commit return the commit of the reference
+func (ref *Reference) Commit() (*Commit, error) {
+ return ref.repo.getCommit(ref.Object)
+}
diff --git a/modules/git/repo.go b/modules/git/repo.go
new file mode 100644
index 0000000000..4306730920
--- /dev/null
+++ b/modules/git/repo.go
@@ -0,0 +1,287 @@
+// 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.
+
+package git
+
+import (
+ "bytes"
+ "container/list"
+ "errors"
+ "os"
+ "path"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/Unknwon/com"
+)
+
+// Repository represents a Git repository.
+type Repository struct {
+ Path string
+
+ commitCache *ObjectCache
+ tagCache *ObjectCache
+}
+
+const prettyLogFormat = `--pretty=format:%H`
+
+func (repo *Repository) parsePrettyFormatLogToList(logs []byte) (*list.List, error) {
+ l := list.New()
+ if len(logs) == 0 {
+ return l, nil
+ }
+
+ parts := bytes.Split(logs, []byte{'\n'})
+
+ for _, commitID := range parts {
+ commit, err := repo.GetCommit(string(commitID))
+ if err != nil {
+ return nil, err
+ }
+ l.PushBack(commit)
+ }
+
+ return l, nil
+}
+
+// IsRepoURLAccessible checks if given repository URL is accessible.
+func IsRepoURLAccessible(url string) bool {
+ _, err := NewCommand("ls-remote", "-q", "-h", url, "HEAD").Run()
+ if err != nil {
+ return false
+ }
+ return true
+}
+
+// InitRepository initializes a new Git repository.
+func InitRepository(repoPath string, bare bool) error {
+ os.MkdirAll(repoPath, os.ModePerm)
+
+ cmd := NewCommand("init")
+ if bare {
+ cmd.AddArguments("--bare")
+ }
+ _, err := cmd.RunInDir(repoPath)
+ 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")
+ }
+
+ return &Repository{
+ Path: repoPath,
+ commitCache: newObjectCache(),
+ tagCache: newObjectCache(),
+ }, nil
+}
+
+// CloneRepoOptions options when clone a repository
+type CloneRepoOptions struct {
+ Timeout time.Duration
+ Mirror bool
+ Bare bool
+ Quiet bool
+ Branch string
+}
+
+// Clone clones original repository to target path.
+func Clone(from, to string, opts CloneRepoOptions) (err error) {
+ toDir := path.Dir(to)
+ if err = os.MkdirAll(toDir, os.ModePerm); err != nil {
+ return err
+ }
+
+ cmd := NewCommand("clone")
+ if opts.Mirror {
+ cmd.AddArguments("--mirror")
+ }
+ if opts.Bare {
+ cmd.AddArguments("--bare")
+ }
+ if opts.Quiet {
+ cmd.AddArguments("--quiet")
+ }
+ if len(opts.Branch) > 0 {
+ cmd.AddArguments("-b", opts.Branch)
+ }
+ cmd.AddArguments(from, to)
+
+ if opts.Timeout <= 0 {
+ opts.Timeout = -1
+ }
+
+ _, err = cmd.RunTimeout(opts.Timeout)
+ return err
+}
+
+// PullRemoteOptions options when pull from remote
+type PullRemoteOptions struct {
+ Timeout time.Duration
+ All bool
+ Rebase bool
+ Remote string
+ Branch string
+}
+
+// Pull pulls changes from remotes.
+func Pull(repoPath string, opts PullRemoteOptions) error {
+ cmd := NewCommand("pull")
+ if opts.Rebase {
+ cmd.AddArguments("--rebase")
+ }
+ if opts.All {
+ cmd.AddArguments("--all")
+ } else {
+ cmd.AddArguments(opts.Remote)
+ cmd.AddArguments(opts.Branch)
+ }
+
+ if opts.Timeout <= 0 {
+ opts.Timeout = -1
+ }
+
+ _, err := cmd.RunInDirTimeout(opts.Timeout, repoPath)
+ return err
+}
+
+// PushOptions options when push to remote
+type PushOptions struct {
+ Remote string
+ Branch string
+ Force bool
+}
+
+// Push pushs local commits to given remote branch.
+func Push(repoPath string, opts PushOptions) error {
+ cmd := NewCommand("push")
+ if opts.Force {
+ cmd.AddArguments("-f")
+ }
+ cmd.AddArguments(opts.Remote, opts.Branch)
+ _, err := cmd.RunInDir(repoPath)
+ return err
+}
+
+// CheckoutOptions options when heck out some branch
+type CheckoutOptions struct {
+ Timeout time.Duration
+ Branch string
+ OldBranch string
+}
+
+// Checkout checkouts a branch
+func Checkout(repoPath string, opts CheckoutOptions) error {
+ cmd := NewCommand("checkout")
+ if len(opts.OldBranch) > 0 {
+ cmd.AddArguments("-b")
+ }
+
+ if opts.Timeout <= 0 {
+ opts.Timeout = -1
+ }
+
+ cmd.AddArguments(opts.Branch)
+
+ if len(opts.OldBranch) > 0 {
+ cmd.AddArguments(opts.OldBranch)
+ }
+
+ _, err := cmd.RunInDirTimeout(opts.Timeout, repoPath)
+ return err
+}
+
+// ResetHEAD resets HEAD to given revision or head of branch.
+func ResetHEAD(repoPath string, hard bool, revision string) error {
+ cmd := NewCommand("reset")
+ if hard {
+ cmd.AddArguments("--hard")
+ }
+ _, err := cmd.AddArguments(revision).RunInDir(repoPath)
+ return err
+}
+
+// MoveFile moves a file to another file or directory.
+func MoveFile(repoPath, oldTreeName, newTreeName string) error {
+ _, err := NewCommand("mv").AddArguments(oldTreeName, newTreeName).RunInDir(repoPath)
+ return err
+}
+
+// CountObject represents repository count objects report
+type CountObject struct {
+ Count int64
+ Size int64
+ InPack int64
+ Packs int64
+ SizePack int64
+ PrunePack int64
+ Garbage int64
+ SizeGarbage int64
+}
+
+const (
+ statCount = "count: "
+ statSize = "size: "
+ statInpack = "in-pack: "
+ statPacks = "packs: "
+ statSizePack = "size-pack: "
+ statPrunePackage = "prune-package: "
+ statGarbage = "garbage: "
+ statSizeGarbage = "size-garbage: "
+)
+
+// GetRepoSize returns disk consumption for repo in path
+func GetRepoSize(repoPath string) (*CountObject, error) {
+ cmd := NewCommand("count-objects", "-v")
+ stdout, err := cmd.RunInDir(repoPath)
+ if err != nil {
+ return nil, err
+ }
+
+ return parseSize(stdout), nil
+}
+
+// parseSize parses the output from count-objects and return a CountObject
+func parseSize(objects string) *CountObject {
+ repoSize := new(CountObject)
+ for _, line := range strings.Split(objects, "\n") {
+ switch {
+ case strings.HasPrefix(line, statCount):
+ repoSize.Count = com.StrTo(line[7:]).MustInt64()
+ case strings.HasPrefix(line, statSize):
+ repoSize.Size = com.StrTo(line[6:]).MustInt64() * 1024
+ case strings.HasPrefix(line, statInpack):
+ repoSize.InPack = com.StrTo(line[9:]).MustInt64()
+ case strings.HasPrefix(line, statPacks):
+ repoSize.Packs = com.StrTo(line[7:]).MustInt64()
+ case strings.HasPrefix(line, statSizePack):
+ repoSize.SizePack = com.StrTo(line[11:]).MustInt64() * 1024
+ case strings.HasPrefix(line, statPrunePackage):
+ repoSize.PrunePack = com.StrTo(line[16:]).MustInt64()
+ case strings.HasPrefix(line, statGarbage):
+ repoSize.Garbage = com.StrTo(line[9:]).MustInt64()
+ case strings.HasPrefix(line, statSizeGarbage):
+ repoSize.SizeGarbage = com.StrTo(line[14:]).MustInt64() * 1024
+ }
+ }
+ return repoSize
+}
+
+// GetLatestCommitTime returns time for latest commit in repository (across all branches)
+func GetLatestCommitTime(repoPath string) (time.Time, error) {
+ cmd := NewCommand("for-each-ref", "--sort=-committerdate", "refs/heads/", "--count", "1", "--format=%(committerdate)")
+ stdout, err := cmd.RunInDir(repoPath)
+ if err != nil {
+ return time.Time{}, err
+ }
+ commitTime := strings.TrimSpace(stdout)
+ return time.Parse(GitTimeLayout, commitTime)
+}
diff --git a/modules/git/repo_blame.go b/modules/git/repo_blame.go
new file mode 100644
index 0000000000..80ec50e472
--- /dev/null
+++ b/modules/git/repo_blame.go
@@ -0,0 +1,24 @@
+// 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.
+
+package git
+
+import "fmt"
+
+// FileBlame return the Blame object of file
+func (repo *Repository) FileBlame(revision, path, file string) ([]byte, error) {
+ return NewCommand("blame", "--root", "--", file).RunInDirBytes(path)
+}
+
+// LineBlame returns the latest commit at the given line
+func (repo *Repository) LineBlame(revision, path, file string, line uint) (*Commit, error) {
+ res, err := NewCommand("blame", fmt.Sprintf("-L %d,%d", line, line), "-p", revision, "--", file).RunInDir(path)
+ if err != nil {
+ return nil, err
+ }
+ if len(res) < 40 {
+ return nil, fmt.Errorf("invalid result of blame: %s", res)
+ }
+ return repo.GetCommit(string(res[:40]))
+}
diff --git a/modules/git/repo_blob.go b/modules/git/repo_blob.go
new file mode 100644
index 0000000000..a9445a1f7a
--- /dev/null
+++ b/modules/git/repo_blob.go
@@ -0,0 +1,30 @@
+// 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.
+
+package git
+
+func (repo *Repository) getBlob(id SHA1) (*Blob, error) {
+ if _, err := NewCommand("cat-file", "-p", id.String()).RunInDir(repo.Path); err != nil {
+ return nil, ErrNotExist{id.String(), ""}
+ }
+
+ return &Blob{
+ repo: repo,
+ TreeEntry: &TreeEntry{
+ ID: id,
+ ptree: &Tree{
+ repo: repo,
+ },
+ },
+ }, nil
+}
+
+// GetBlob finds the blob object in the repository.
+func (repo *Repository) GetBlob(idStr string) (*Blob, error) {
+ id, err := NewIDFromString(idStr)
+ if err != nil {
+ return nil, err
+ }
+ return repo.getBlob(id)
+}
diff --git a/modules/git/repo_blob_test.go b/modules/git/repo_blob_test.go
new file mode 100644
index 0000000000..074365f164
--- /dev/null
+++ b/modules/git/repo_blob_test.go
@@ -0,0 +1,66 @@
+// 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.
+
+package git
+
+import (
+ "fmt"
+ "io/ioutil"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRepository_GetBlob_Found(t *testing.T) {
+ repoPath := filepath.Join(testReposDir, "repo1_bare")
+ r, err := OpenRepository(repoPath)
+ assert.NoError(t, err)
+
+ testCases := []struct {
+ OID string
+ Data []byte
+ }{
+ {"e2129701f1a4d54dc44f03c93bca0a2aec7c5449", []byte("file1\n")},
+ {"6c493ff740f9380390d5c9ddef4af18697ac9375", []byte("file2\n")},
+ }
+
+ for _, testCase := range testCases {
+ blob, err := r.GetBlob(testCase.OID)
+ assert.NoError(t, err)
+
+ dataReader, err := blob.Data()
+ assert.NoError(t, err)
+
+ data, err := ioutil.ReadAll(dataReader)
+ assert.NoError(t, err)
+ assert.Equal(t, testCase.Data, data)
+ }
+}
+
+func TestRepository_GetBlob_NotExist(t *testing.T) {
+ repoPath := filepath.Join(testReposDir, "repo1_bare")
+ r, err := OpenRepository(repoPath)
+ assert.NoError(t, err)
+
+ testCase := "0000000000000000000000000000000000000000"
+ testError := ErrNotExist{testCase, ""}
+
+ blob, err := r.GetBlob(testCase)
+ assert.Nil(t, blob)
+ assert.EqualError(t, err, testError.Error())
+}
+
+func TestRepository_GetBlob_NoId(t *testing.T) {
+ repoPath := filepath.Join(testReposDir, "repo1_bare")
+ r, err := OpenRepository(repoPath)
+ assert.NoError(t, err)
+
+ testCase := ""
+ testError := fmt.Errorf("Length must be 40: %s", testCase)
+
+ blob, err := r.GetBlob(testCase)
+ assert.Nil(t, blob)
+ assert.EqualError(t, err, testError.Error())
+}
diff --git a/modules/git/repo_branch.go b/modules/git/repo_branch.go
new file mode 100644
index 0000000000..6414abbec5
--- /dev/null
+++ b/modules/git/repo_branch.go
@@ -0,0 +1,134 @@
+// 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.
+
+package git
+
+import (
+ "fmt"
+ "strings"
+
+ "gopkg.in/src-d/go-git.v4"
+ "gopkg.in/src-d/go-git.v4/plumbing"
+)
+
+// BranchPrefix base dir of the branch information file store on git
+const BranchPrefix = "refs/heads/"
+
+// IsReferenceExist returns true if given reference exists in the repository.
+func IsReferenceExist(repoPath, name string) bool {
+ _, err := NewCommand("show-ref", "--verify", name).RunInDir(repoPath)
+ return err == nil
+}
+
+// IsBranchExist returns true if given branch exists in the repository.
+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 {
+ return IsBranchExist(repo.Path, name)
+}
+
+// Branch represents a Git branch.
+type Branch struct {
+ Name string
+ Path string
+}
+
+// GetHEADBranch returns corresponding branch of HEAD.
+func (repo *Repository) GetHEADBranch() (*Branch, error) {
+ stdout, err := NewCommand("symbolic-ref", "HEAD").RunInDir(repo.Path)
+ if err != nil {
+ return nil, err
+ }
+ stdout = strings.TrimSpace(stdout)
+
+ if !strings.HasPrefix(stdout, BranchPrefix) {
+ return nil, fmt.Errorf("invalid HEAD branch: %v", stdout)
+ }
+
+ return &Branch{
+ Name: stdout[len(BranchPrefix):],
+ Path: stdout,
+ }, nil
+}
+
+// SetDefaultBranch sets default branch of repository.
+func (repo *Repository) SetDefaultBranch(name string) error {
+ _, err := NewCommand("symbolic-ref", "HEAD", BranchPrefix+name).RunInDir(repo.Path)
+ return err
+}
+
+// GetBranches returns all branches of the repository.
+func (repo *Repository) GetBranches() ([]string, error) {
+ r, err := git.PlainOpen(repo.Path)
+ if err != nil {
+ return nil, err
+ }
+
+ branchIter, err := r.Branches()
+ if err != nil {
+ return nil, err
+ }
+ branches := make([]string, 0)
+ if err = branchIter.ForEach(func(branch *plumbing.Reference) error {
+ branches = append(branches, branch.Name().Short())
+ return nil
+ }); err != nil {
+ return nil, err
+ }
+
+ return branches, nil
+}
+
+// DeleteBranchOptions Option(s) for delete branch
+type DeleteBranchOptions struct {
+ Force bool
+}
+
+// DeleteBranch delete a branch by name on repository.
+func (repo *Repository) DeleteBranch(name string, opts DeleteBranchOptions) error {
+ cmd := NewCommand("branch")
+
+ if opts.Force {
+ cmd.AddArguments("-D")
+ } else {
+ cmd.AddArguments("-d")
+ }
+
+ cmd.AddArguments(name)
+ _, err := cmd.RunInDir(repo.Path)
+
+ return err
+}
+
+// CreateBranch create a new branch
+func (repo *Repository) CreateBranch(branch, newBranch string) error {
+ cmd := NewCommand("branch")
+ cmd.AddArguments(branch, newBranch)
+
+ _, err := cmd.RunInDir(repo.Path)
+
+ return err
+}
+
+// AddRemote adds a new remote to repository.
+func (repo *Repository) AddRemote(name, url string, fetch bool) error {
+ cmd := NewCommand("remote", "add")
+ if fetch {
+ cmd.AddArguments("-f")
+ }
+ cmd.AddArguments(name, url)
+
+ _, err := cmd.RunInDir(repo.Path)
+ return err
+}
+
+// RemoveRemote removes a remote from repository.
+func (repo *Repository) RemoveRemote(name string) error {
+ _, err := NewCommand("remote", "remove", name).RunInDir(repo.Path)
+ return err
+}
diff --git a/modules/git/repo_branch_test.go b/modules/git/repo_branch_test.go
new file mode 100644
index 0000000000..08736d702e
--- /dev/null
+++ b/modules/git/repo_branch_test.go
@@ -0,0 +1,39 @@
+// 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.
+
+package git
+
+import (
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRepository_GetBranches(t *testing.T) {
+ bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
+ bareRepo1, err := OpenRepository(bareRepo1Path)
+ assert.NoError(t, err)
+
+ branches, err := bareRepo1.GetBranches()
+
+ assert.NoError(t, err)
+ assert.Len(t, branches, 3)
+ assert.ElementsMatch(t, []string{"branch1", "branch2", "master"}, branches)
+}
+
+func BenchmarkRepository_GetBranches(b *testing.B) {
+ bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
+ bareRepo1, err := OpenRepository(bareRepo1Path)
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ for i := 0; i < b.N; i++ {
+ _, err := bareRepo1.GetBranches()
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go
new file mode 100644
index 0000000000..146619373d
--- /dev/null
+++ b/modules/git/repo_commit.go
@@ -0,0 +1,388 @@
+// Copyright 2015 The Gogs 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 (
+ "bytes"
+ "container/list"
+ "strconv"
+ "strings"
+
+ version "github.com/mcuadros/go-version"
+)
+
+// 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", name).RunInDir(repo.Path)
+ if err != nil {
+ if strings.Contains(err.Error(), "not a valid ref") {
+ return "", ErrNotExist{name, ""}
+ }
+ return "", err
+ }
+ return strings.Split(stdout, " ")[0], nil
+}
+
+// GetBranchCommitID returns last commit ID string of given branch.
+func (repo *Repository) GetBranchCommitID(name string) (string, error) {
+ return repo.GetRefCommitID(BranchPrefix + name)
+}
+
+// GetTagCommitID returns last commit ID string of given tag.
+func (repo *Repository) GetTagCommitID(name string) (string, error) {
+ stdout, err := NewCommand("rev-list", "-n", "1", name).RunInDir(repo.Path)
+ if err != nil {
+ if strings.Contains(err.Error(), "unknown revision or path") {
+ return "", ErrNotExist{name, ""}
+ }
+ return "", err
+ }
+ return strings.TrimSpace(stdout), nil
+}
+
+// parseCommitData parses commit information from the (uncompressed) raw
+// data from the commit object.
+// \n\n separate headers from message
+func parseCommitData(data []byte) (*Commit, error) {
+ commit := new(Commit)
+ commit.parents = make([]SHA1, 0, 1)
+ // we now have the contents of the commit object. Let's investigate...
+ nextline := 0
+l:
+ for {
+ eol := bytes.IndexByte(data[nextline:], '\n')
+ switch {
+ case eol > 0:
+ line := data[nextline : nextline+eol]
+ spacepos := bytes.IndexByte(line, ' ')
+ reftype := line[:spacepos]
+ switch string(reftype) {
+ case "tree", "object":
+ id, err := NewIDFromString(string(line[spacepos+1:]))
+ if err != nil {
+ return nil, err
+ }
+ commit.Tree.ID = id
+ case "parent":
+ // A commit can have one or more parents
+ oid, err := NewIDFromString(string(line[spacepos+1:]))
+ if err != nil {
+ return nil, err
+ }
+ commit.parents = append(commit.parents, oid)
+ case "author", "tagger":
+ sig, err := newSignatureFromCommitline(line[spacepos+1:])
+ if err != nil {
+ return nil, err
+ }
+ commit.Author = sig
+ case "committer":
+ sig, err := newSignatureFromCommitline(line[spacepos+1:])
+ if err != nil {
+ return nil, err
+ }
+ commit.Committer = sig
+ case "gpgsig":
+ sig, err := newGPGSignatureFromCommitline(data, nextline+spacepos+1, false)
+ if err != nil {
+ return nil, err
+ }
+ commit.Signature = sig
+ }
+ nextline += eol + 1
+ case eol == 0:
+ cm := string(data[nextline+1:])
+
+ // Tag GPG signatures are stored below the commit message
+ sigindex := strings.Index(cm, "-----BEGIN PGP SIGNATURE-----")
+ if sigindex != -1 {
+ sig, err := newGPGSignatureFromCommitline(data, (nextline+1)+sigindex, true)
+ if err == nil && sig != nil {
+ // remove signature from commit message
+ if sigindex == 0 {
+ cm = ""
+ } else {
+ cm = cm[:sigindex-1]
+ }
+ commit.Signature = sig
+ }
+ }
+
+ commit.CommitMessage = cm
+ break l
+ default:
+ break l
+ }
+ }
+ return commit, nil
+}
+
+func (repo *Repository) getCommit(id SHA1) (*Commit, error) {
+ c, ok := repo.commitCache.Get(id.String())
+ if ok {
+ log("Hit cache: %s", id)
+ return c.(*Commit), nil
+ }
+
+ data, err := NewCommand("cat-file", "-p", id.String()).RunInDirBytes(repo.Path)
+ if err != nil {
+ if strings.Contains(err.Error(), "fatal: Not a valid object name") {
+ return nil, ErrNotExist{id.String(), ""}
+ }
+ return nil, err
+ }
+
+ commit, err := parseCommitData(data)
+ if err != nil {
+ return nil, err
+ }
+ commit.repo = repo
+ commit.ID = id
+
+ data, err = NewCommand("name-rev", id.String()).RunInDirBytes(repo.Path)
+ if err != nil {
+ return nil, err
+ }
+
+ // name-rev commitID output will be "COMMIT_ID master" or "COMMIT_ID master~12"
+ commit.Branch = strings.Split(strings.Split(string(data), " ")[1], "~")[0]
+
+ repo.commitCache.Set(id.String(), commit)
+ return commit, nil
+}
+
+// GetCommit returns commit object of by ID string.
+func (repo *Repository) GetCommit(commitID string) (*Commit, error) {
+ if len(commitID) != 40 {
+ var err error
+ actualCommitID, err := NewCommand("rev-parse", commitID).RunInDir(repo.Path)
+ if err != nil {
+ if strings.Contains(err.Error(), "unknown revision or path") {
+ return nil, ErrNotExist{commitID, ""}
+ }
+ return nil, err
+ }
+ commitID = actualCommitID
+ }
+ id, err := NewIDFromString(commitID)
+ if err != nil {
+ return nil, err
+ }
+
+ return repo.getCommit(id)
+}
+
+// GetBranchCommit returns the last commit of given branch.
+func (repo *Repository) GetBranchCommit(name string) (*Commit, error) {
+ commitID, err := repo.GetBranchCommitID(name)
+ if err != nil {
+ return nil, err
+ }
+ return repo.GetCommit(commitID)
+}
+
+// GetTagCommit get the commit of the specific tag via name
+func (repo *Repository) GetTagCommit(name string) (*Commit, error) {
+ commitID, err := repo.GetTagCommitID(name)
+ if err != nil {
+ return nil, err
+ }
+ return repo.GetCommit(commitID)
+}
+
+func (repo *Repository) getCommitByPathWithID(id SHA1, relpath string) (*Commit, error) {
+ // File name starts with ':' must be escaped.
+ if relpath[0] == ':' {
+ relpath = `\` + relpath
+ }
+
+ stdout, err := NewCommand("log", "-1", prettyLogFormat, id.String(), "--", relpath).RunInDir(repo.Path)
+ if err != nil {
+ return nil, err
+ }
+
+ id, err = NewIDFromString(stdout)
+ if err != nil {
+ return nil, err
+ }
+
+ return repo.getCommit(id)
+}
+
+// GetCommitByPath returns the last commit of relative path.
+func (repo *Repository) GetCommitByPath(relpath string) (*Commit, error) {
+ stdout, err := NewCommand("log", "-1", prettyLogFormat, "--", relpath).RunInDirBytes(repo.Path)
+ if err != nil {
+ return nil, err
+ }
+
+ commits, err := repo.parsePrettyFormatLogToList(stdout)
+ if err != nil {
+ return nil, err
+ }
+ return commits.Front().Value.(*Commit), nil
+}
+
+// CommitsRangeSize the default commits range size
+var CommitsRangeSize = 50
+
+func (repo *Repository) commitsByRange(id SHA1, page int) (*list.List, error) {
+ stdout, err := NewCommand("log", id.String(), "--skip="+strconv.Itoa((page-1)*CommitsRangeSize),
+ "--max-count="+strconv.Itoa(CommitsRangeSize), prettyLogFormat).RunInDirBytes(repo.Path)
+ if err != nil {
+ return nil, err
+ }
+ return repo.parsePrettyFormatLogToList(stdout)
+}
+
+func (repo *Repository) searchCommits(id SHA1, keyword string, all bool) (*list.List, error) {
+ cmd := NewCommand("log", id.String(), "-100", "-i", "--grep="+keyword, prettyLogFormat)
+ if all {
+ cmd.AddArguments("--all")
+ }
+ stdout, err := cmd.RunInDirBytes(repo.Path)
+ if err != nil {
+ return nil, err
+ }
+ return repo.parsePrettyFormatLogToList(stdout)
+}
+
+func (repo *Repository) getFilesChanged(id1 string, id2 string) ([]string, error) {
+ stdout, err := NewCommand("diff", "--name-only", id1, id2).RunInDirBytes(repo.Path)
+ if err != nil {
+ return nil, err
+ }
+ return strings.Split(string(stdout), "\n"), nil
+}
+
+// FileCommitsCount return the number of files at a revison
+func (repo *Repository) FileCommitsCount(revision, file string) (int64, error) {
+ return commitsCount(repo.Path, revision, file)
+}
+
+// CommitsByFileAndRange return the commits according revison file and the page
+func (repo *Repository) CommitsByFileAndRange(revision, file string, page int) (*list.List, error) {
+ stdout, err := NewCommand("log", revision, "--follow", "--skip="+strconv.Itoa((page-1)*50),
+ "--max-count="+strconv.Itoa(CommitsRangeSize), prettyLogFormat, "--", file).RunInDirBytes(repo.Path)
+ if err != nil {
+ return nil, err
+ }
+ return repo.parsePrettyFormatLogToList(stdout)
+}
+
+// FilesCountBetween return the number of files changed between two commits
+func (repo *Repository) FilesCountBetween(startCommitID, endCommitID string) (int, error) {
+ stdout, err := NewCommand("diff", "--name-only", startCommitID+"..."+endCommitID).RunInDir(repo.Path)
+ if err != nil {
+ return 0, err
+ }
+ return len(strings.Split(stdout, "\n")) - 1, nil
+}
+
+// CommitsBetween returns a list that contains commits between [last, before).
+func (repo *Repository) CommitsBetween(last *Commit, before *Commit) (*list.List, error) {
+ stdout, err := NewCommand("rev-list", before.ID.String()+"..."+last.ID.String()).RunInDirBytes(repo.Path)
+ if err != nil {
+ return nil, err
+ }
+ return repo.parsePrettyFormatLogToList(bytes.TrimSpace(stdout))
+}
+
+// CommitsBetweenIDs return commits between twoe commits
+func (repo *Repository) CommitsBetweenIDs(last, before string) (*list.List, error) {
+ lastCommit, err := repo.GetCommit(last)
+ if err != nil {
+ return nil, err
+ }
+ beforeCommit, err := repo.GetCommit(before)
+ if err != nil {
+ return nil, err
+ }
+ return repo.CommitsBetween(lastCommit, beforeCommit)
+}
+
+// CommitsCountBetween return numbers of commits between two commits
+func (repo *Repository) CommitsCountBetween(start, end string) (int64, error) {
+ return commitsCount(repo.Path, start+"..."+end, "")
+}
+
+// commitsBefore the limit is depth, not total number of returned commits.
+func (repo *Repository) commitsBefore(id SHA1, limit int) (*list.List, error) {
+ cmd := NewCommand("log")
+ if limit > 0 {
+ cmd.AddArguments("-"+strconv.Itoa(limit), prettyLogFormat, id.String())
+ } else {
+ cmd.AddArguments(prettyLogFormat, id.String())
+ }
+
+ stdout, err := cmd.RunInDirBytes(repo.Path)
+ if err != nil {
+ return nil, err
+ }
+
+ formattedLog, err := repo.parsePrettyFormatLogToList(bytes.TrimSpace(stdout))
+ if err != nil {
+ return nil, err
+ }
+
+ commits := list.New()
+ for logEntry := formattedLog.Front(); logEntry != nil; logEntry = logEntry.Next() {
+ commit := logEntry.Value.(*Commit)
+ branches, err := repo.getBranches(commit, 2)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(branches) > 1 {
+ break
+ }
+
+ commits.PushBack(commit)
+ }
+
+ return commits, nil
+}
+
+func (repo *Repository) getCommitsBefore(id SHA1) (*list.List, error) {
+ return repo.commitsBefore(id, 0)
+}
+
+func (repo *Repository) getCommitsBeforeLimit(id SHA1, num int) (*list.List, error) {
+ return repo.commitsBefore(id, num)
+}
+
+func (repo *Repository) getBranches(commit *Commit, limit int) ([]string, error) {
+ if version.Compare(gitVersion, "2.7.0", ">=") {
+ stdout, err := NewCommand("for-each-ref", "--count="+strconv.Itoa(limit), "--format=%(refname:strip=2)", "--contains", commit.ID.String(), BranchPrefix).RunInDir(repo.Path)
+ if err != nil {
+ return nil, err
+ }
+
+ branches := strings.Fields(stdout)
+ return branches, nil
+ }
+
+ stdout, err := NewCommand("branch", "--contains", commit.ID.String()).RunInDir(repo.Path)
+ if err != nil {
+ return nil, err
+ }
+
+ refs := strings.Split(stdout, "\n")
+
+ var max int
+ if len(refs) > limit {
+ max = limit
+ } else {
+ max = len(refs) - 1
+ }
+
+ branches := make([]string, max)
+ for i, ref := range refs[:max] {
+ parts := strings.Fields(ref)
+
+ branches[i] = parts[len(parts)-1]
+ }
+ return branches, nil
+}
diff --git a/modules/git/repo_commit_test.go b/modules/git/repo_commit_test.go
new file mode 100644
index 0000000000..6761a45b7a
--- /dev/null
+++ b/modules/git/repo_commit_test.go
@@ -0,0 +1,59 @@
+// 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.
+
+package git
+
+import (
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRepository_GetCommitBranches(t *testing.T) {
+ bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
+ bareRepo1, err := OpenRepository(bareRepo1Path)
+ assert.NoError(t, err)
+
+ // these test case are specific to the repo1_bare test repo
+ testCases := []struct {
+ CommitID string
+ ExpectedBranches []string
+ }{
+ {"2839944139e0de9737a044f78b0e4b40d989a9e3", []string{"branch1"}},
+ {"5c80b0245c1c6f8343fa418ec374b13b5d4ee658", []string{"branch2"}},
+ {"37991dec2c8e592043f47155ce4808d4580f9123", []string{"master"}},
+ {"95bb4d39648ee7e325106df01a621c530863a653", []string{"branch1", "branch2"}},
+ {"8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2", []string{"branch2", "master"}},
+ {"master", []string{"master"}},
+ }
+ for _, testCase := range testCases {
+ commit, err := bareRepo1.GetCommit(testCase.CommitID)
+ assert.NoError(t, err)
+ branches, err := bareRepo1.getBranches(commit, 2)
+ assert.NoError(t, err)
+ assert.Equal(t, testCase.ExpectedBranches, branches)
+ }
+}
+
+func TestGetTagCommitWithSignature(t *testing.T) {
+ bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
+ bareRepo1, err := OpenRepository(bareRepo1Path)
+ commit, err := bareRepo1.GetCommit("3ad28a9149a2864384548f3d17ed7f38014c9e8a")
+
+ assert.NoError(t, err)
+ assert.NotNil(t, commit)
+ assert.NotNil(t, commit.Signature)
+ // test that signature is not in message
+ assert.Equal(t, "tag", commit.CommitMessage)
+}
+
+func TestGetCommitWithBadCommitID(t *testing.T) {
+ bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
+ bareRepo1, err := OpenRepository(bareRepo1Path)
+ commit, err := bareRepo1.GetCommit("bad_branch")
+ assert.Nil(t, commit)
+ assert.Error(t, err)
+ assert.EqualError(t, err, "object does not exist [id: bad_branch, rel_path: ]")
+}
diff --git a/modules/git/repo_hook.go b/modules/git/repo_hook.go
new file mode 100644
index 0000000000..a652e938fa
--- /dev/null
+++ b/modules/git/repo_hook.go
@@ -0,0 +1,15 @@
+// Copyright 2015 The Gogs 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
+
+// GetHook get one hook according the name on a repository
+func (repo *Repository) GetHook(name string) (*Hook, error) {
+ return GetHook(repo.Path, name)
+}
+
+// Hooks get all the hooks on the repository
+func (repo *Repository) Hooks() ([]*Hook, error) {
+ return ListHooks(repo.Path)
+}
diff --git a/modules/git/repo_object.go b/modules/git/repo_object.go
new file mode 100644
index 0000000000..3be8400d22
--- /dev/null
+++ b/modules/git/repo_object.go
@@ -0,0 +1,19 @@
+// Copyright 2014 The Gogs 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
+
+// ObjectType git object type
+type ObjectType string
+
+const (
+ // ObjectCommit commit object type
+ ObjectCommit ObjectType = "commit"
+ // ObjectTree tree object type
+ ObjectTree ObjectType = "tree"
+ // ObjectBlob blob object type
+ ObjectBlob ObjectType = "blob"
+ // ObjectTag tag object type
+ ObjectTag ObjectType = "tag"
+)
diff --git a/modules/git/repo_pull.go b/modules/git/repo_pull.go
new file mode 100644
index 0000000000..c6d97a6fd1
--- /dev/null
+++ b/modules/git/repo_pull.go
@@ -0,0 +1,89 @@
+// Copyright 2015 The Gogs 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 (
+ "bytes"
+ "container/list"
+ "fmt"
+ "io"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// PullRequestInfo represents needed information for a pull request.
+type PullRequestInfo struct {
+ MergeBase string
+ Commits *list.List
+ NumFiles int
+}
+
+// GetMergeBase checks and returns merge base of two branches.
+func (repo *Repository) GetMergeBase(base, head string) (string, error) {
+ stdout, err := NewCommand("merge-base", base, head).RunInDir(repo.Path)
+ return strings.TrimSpace(stdout), err
+}
+
+// GetPullRequestInfo generates and returns pull request information
+// between base and head branches of repositories.
+func (repo *Repository) GetPullRequestInfo(basePath, baseBranch, headBranch string) (_ *PullRequestInfo, err error) {
+ var remoteBranch string
+
+ // We don't need a temporary remote for same repository.
+ if repo.Path != basePath {
+ // Add a temporary remote
+ tmpRemote := strconv.FormatInt(time.Now().UnixNano(), 10)
+ if err = repo.AddRemote(tmpRemote, basePath, true); err != nil {
+ return nil, fmt.Errorf("AddRemote: %v", err)
+ }
+ defer repo.RemoveRemote(tmpRemote)
+
+ remoteBranch = "remotes/" + tmpRemote + "/" + baseBranch
+ } else {
+ remoteBranch = baseBranch
+ }
+
+ prInfo := new(PullRequestInfo)
+ prInfo.MergeBase, err = repo.GetMergeBase(remoteBranch, headBranch)
+ if err != nil {
+ return nil, fmt.Errorf("GetMergeBase: %v", err)
+ }
+
+ logs, err := NewCommand("log", prInfo.MergeBase+"..."+headBranch, prettyLogFormat).RunInDirBytes(repo.Path)
+ if err != nil {
+ return nil, err
+ }
+ prInfo.Commits, err = repo.parsePrettyFormatLogToList(logs)
+ if err != nil {
+ return nil, fmt.Errorf("parsePrettyFormatLogToList: %v", err)
+ }
+
+ // Count number of changed files.
+ stdout, err := NewCommand("diff", "--name-only", remoteBranch+"..."+headBranch).RunInDir(repo.Path)
+ if err != nil {
+ return nil, err
+ }
+ prInfo.NumFiles = len(strings.Split(stdout, "\n")) - 1
+
+ return prInfo, nil
+}
+
+// GetPatch generates and returns patch data between given revisions.
+func (repo *Repository) GetPatch(base, head string) ([]byte, error) {
+ return NewCommand("diff", "-p", "--binary", base, head).RunInDirBytes(repo.Path)
+}
+
+// GetFormatPatch generates and returns format-patch data between given revisions.
+func (repo *Repository) GetFormatPatch(base, head string) (io.Reader, error) {
+ stdout := new(bytes.Buffer)
+ stderr := new(bytes.Buffer)
+
+ if err := NewCommand("format-patch", "--binary", "--stdout", base+"..."+head).
+ RunInDirPipeline(repo.Path, stdout, stderr); err != nil {
+ return nil, concatenateError(err, stderr.String())
+ }
+ return stdout, nil
+}
diff --git a/modules/git/repo_pull_test.go b/modules/git/repo_pull_test.go
new file mode 100644
index 0000000000..e194788773
--- /dev/null
+++ b/modules/git/repo_pull_test.go
@@ -0,0 +1,30 @@
+// 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.
+
+package git
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGetFormatPatch(t *testing.T) {
+ bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
+ clonedPath, err := cloneRepo(bareRepo1Path, testReposDir, "repo1_TestGetFormatPatch")
+ assert.NoError(t, err)
+ defer os.RemoveAll(clonedPath)
+ repo, err := OpenRepository(clonedPath)
+ assert.NoError(t, err)
+ rd, err := repo.GetFormatPatch("8d92fc95^", "8d92fc95")
+ assert.NoError(t, err)
+ patchb, err := ioutil.ReadAll(rd)
+ assert.NoError(t, err)
+ patch := string(patchb)
+ assert.Regexp(t, "^From 8d92fc95", patch)
+ assert.Contains(t, patch, "Subject: [PATCH] Add file2.txt")
+}
diff --git a/modules/git/repo_ref.go b/modules/git/repo_ref.go
new file mode 100644
index 0000000000..e1ab46e090
--- /dev/null
+++ b/modules/git/repo_ref.go
@@ -0,0 +1,51 @@
+// 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.
+
+package git
+
+import (
+ "strings"
+
+ "gopkg.in/src-d/go-git.v4"
+ "gopkg.in/src-d/go-git.v4/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)) {
+ r := &Reference{
+ Name: ref.Name().String(),
+ Object: SHA1(ref.Hash()),
+ Type: string(ObjectCommit),
+ repo: repo,
+ }
+ if ref.Name().IsTag() {
+ r.Type = string(ObjectTag)
+ }
+ refs = append(refs, r)
+ }
+ return nil
+ }); err != nil {
+ return nil, err
+ }
+
+ return refs, nil
+}
diff --git a/modules/git/repo_ref_test.go b/modules/git/repo_ref_test.go
new file mode 100644
index 0000000000..2a3ea26a76
--- /dev/null
+++ b/modules/git/repo_ref_test.go
@@ -0,0 +1,49 @@
+// 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.
+
+package git
+
+import (
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRepository_GetRefs(t *testing.T) {
+ bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
+ bareRepo1, err := OpenRepository(bareRepo1Path)
+ assert.NoError(t, err)
+
+ refs, err := bareRepo1.GetRefs()
+
+ assert.NoError(t, err)
+ assert.Len(t, refs, 4)
+
+ expectedRefs := []string{
+ BranchPrefix + "branch1",
+ BranchPrefix + "branch2",
+ BranchPrefix + "master",
+ TagPrefix + "test",
+ }
+
+ for _, ref := range refs {
+ assert.Contains(t, expectedRefs, ref.Name)
+ }
+}
+
+func TestRepository_GetRefsFiltered(t *testing.T) {
+ bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
+ bareRepo1, err := OpenRepository(bareRepo1Path)
+ assert.NoError(t, err)
+
+ refs, err := bareRepo1.GetRefsFiltered(TagPrefix)
+
+ assert.NoError(t, err)
+ if assert.Len(t, refs, 1) {
+ assert.Equal(t, TagPrefix+"test", refs[0].Name)
+ assert.Equal(t, "tag", refs[0].Type)
+ assert.Equal(t, "3ad28a9149a2864384548f3d17ed7f38014c9e8a", refs[0].Object.String())
+ }
+}
diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go
new file mode 100644
index 0000000000..84825d7dc3
--- /dev/null
+++ b/modules/git/repo_tag.go
@@ -0,0 +1,149 @@
+// Copyright 2015 The Gogs 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 (
+ "strings"
+
+ "github.com/mcuadros/go-version"
+)
+
+// TagPrefix tags prefix path on the repository
+const TagPrefix = "refs/tags/"
+
+// IsTagExist returns true if given tag exists in the repository.
+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 {
+ return IsTagExist(repo.Path, name)
+}
+
+// CreateTag create one tag in the repository
+func (repo *Repository) CreateTag(name, revision string) error {
+ _, err := NewCommand("tag", name, revision).RunInDir(repo.Path)
+ return err
+}
+
+func (repo *Repository) getTag(id SHA1) (*Tag, error) {
+ t, ok := repo.tagCache.Get(id.String())
+ if ok {
+ log("Hit cache: %s", id)
+ return t.(*Tag), nil
+ }
+
+ // Get tag type
+ tp, err := NewCommand("cat-file", "-t", id.String()).RunInDir(repo.Path)
+ if err != nil {
+ return nil, err
+ }
+ tp = strings.TrimSpace(tp)
+
+ // Tag is a commit.
+ if ObjectType(tp) == ObjectCommit {
+ tag := &Tag{
+ ID: id,
+ Object: id,
+ Type: string(ObjectCommit),
+ repo: repo,
+ }
+
+ repo.tagCache.Set(id.String(), tag)
+ return tag, nil
+ }
+
+ // Tag with message.
+ data, err := NewCommand("cat-file", "-p", id.String()).RunInDirBytes(repo.Path)
+ if err != nil {
+ return nil, err
+ }
+
+ tag, err := parseTagData(data)
+ if err != nil {
+ return nil, err
+ }
+
+ tag.ID = id
+ tag.repo = repo
+
+ repo.tagCache.Set(id.String(), tag)
+ return tag, nil
+}
+
+// GetTag returns a Git tag by given name.
+func (repo *Repository) GetTag(name string) (*Tag, error) {
+ idStr, err := repo.GetTagCommitID(name)
+ if err != nil {
+ return nil, err
+ }
+
+ id, err := NewIDFromString(idStr)
+ if err != nil {
+ return nil, err
+ }
+
+ tag, err := repo.getTag(id)
+ if err != nil {
+ return nil, err
+ }
+ tag.Name = name
+ return tag, nil
+}
+
+// GetTagInfos returns all tag infos of the repository.
+func (repo *Repository) GetTagInfos() ([]*Tag, error) {
+ // TODO this a slow implementation, makes one git command per tag
+ stdout, err := NewCommand("tag").RunInDir(repo.Path)
+ if err != nil {
+ return nil, err
+ }
+
+ tagNames := strings.Split(stdout, "\n")
+ var tags = make([]*Tag, 0, len(tagNames))
+ for _, tagName := range tagNames {
+ tagName = strings.TrimSpace(tagName)
+ if len(tagName) == 0 {
+ continue
+ }
+
+ tag, err := repo.GetTag(tagName)
+ if err != nil {
+ return nil, err
+ }
+ tags = append(tags, tag)
+ }
+ sortTagsByTime(tags)
+ return tags, nil
+}
+
+// GetTags returns all tags of the repository.
+func (repo *Repository) GetTags() ([]string, error) {
+ cmd := NewCommand("tag", "-l")
+ if version.Compare(gitVersion, "2.0.0", ">=") {
+ cmd.AddArguments("--sort=-v:refname")
+ }
+
+ stdout, err := cmd.RunInDir(repo.Path)
+ if err != nil {
+ return nil, err
+ }
+
+ tags := strings.Split(stdout, "\n")
+ tags = tags[:len(tags)-1]
+
+ if version.Compare(gitVersion, "2.0.0", "<") {
+ version.Sort(tags)
+
+ // Reverse order
+ for i := 0; i < len(tags)/2; i++ {
+ j := len(tags) - i - 1
+ tags[i], tags[j] = tags[j], tags[i]
+ }
+ }
+
+ return tags, nil
+}
diff --git a/modules/git/repo_tag_test.go b/modules/git/repo_tag_test.go
new file mode 100644
index 0000000000..ccb2d57ac2
--- /dev/null
+++ b/modules/git/repo_tag_test.go
@@ -0,0 +1,44 @@
+// 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 (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRepository_GetTags(t *testing.T) {
+ bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
+ bareRepo1, err := OpenRepository(bareRepo1Path)
+ assert.NoError(t, err)
+
+ tags, err := bareRepo1.GetTagInfos()
+ assert.NoError(t, err)
+ assert.Len(t, tags, 1)
+ assert.EqualValues(t, "test", tags[0].Name)
+ assert.EqualValues(t, "37991dec2c8e592043f47155ce4808d4580f9123", tags[0].ID.String())
+ assert.EqualValues(t, "commit", tags[0].Type)
+}
+
+func TestRepository_GetTag(t *testing.T) {
+ bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
+
+ clonedPath, err := cloneRepo(bareRepo1Path, testReposDir, "repo1_TestRepository_GetTag")
+ assert.NoError(t, err)
+ defer os.RemoveAll(clonedPath)
+
+ bareRepo1, err := OpenRepository(clonedPath)
+ assert.NoError(t, err)
+
+ tag, err := bareRepo1.GetTag("test")
+ assert.NoError(t, err)
+ assert.NotNil(t, tag)
+ assert.EqualValues(t, "test", tag.Name)
+ assert.EqualValues(t, "37991dec2c8e592043f47155ce4808d4580f9123", tag.ID.String())
+ assert.EqualValues(t, "commit", tag.Type)
+}
diff --git a/modules/git/repo_test.go b/modules/git/repo_test.go
new file mode 100644
index 0000000000..c5ce6f4447
--- /dev/null
+++ b/modules/git/repo_test.go
@@ -0,0 +1,26 @@
+// 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.
+
+package git
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGetLatestCommitTime(t *testing.T) {
+ lct, err := GetLatestCommitTime(".")
+ assert.NoError(t, err)
+ // Time is in the past
+ now := time.Now()
+ assert.True(t, lct.Unix() < now.Unix(), "%d not smaller than %d", lct, now)
+ // Time is after Mon Oct 23 03:52:09 2017 +0300
+ // which is the time of commit
+ // d47b98c44c9a6472e44ab80efe65235e11c6da2a
+ refTime, err := time.Parse("Mon Jan 02 15:04:05 2006 -0700", "Mon Oct 23 03:52:09 2017 +0300")
+ assert.NoError(t, err)
+ assert.True(t, lct.Unix() > refTime.Unix(), "%d not greater than %d", lct, refTime)
+}
diff --git a/modules/git/repo_tree.go b/modules/git/repo_tree.go
new file mode 100644
index 0000000000..3fa491d529
--- /dev/null
+++ b/modules/git/repo_tree.go
@@ -0,0 +1,35 @@
+// Copyright 2015 The Gogs 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
+
+func (repo *Repository) getTree(id SHA1) (*Tree, error) {
+ treePath := filepathFromSHA1(repo.Path, id.String())
+ if isFile(treePath) {
+ _, err := NewCommand("ls-tree", id.String()).RunInDir(repo.Path)
+ if err != nil {
+ return nil, ErrNotExist{id.String(), ""}
+ }
+ }
+
+ return NewTree(repo, id), 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", 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
new file mode 100644
index 0000000000..6c9d53949d
--- /dev/null
+++ b/modules/git/sha1.go
@@ -0,0 +1,76 @@
+// Copyright 2015 The Gogs 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 (
+ "bytes"
+ "encoding/hex"
+ "fmt"
+ "strings"
+)
+
+// EmptySHA defines empty git SHA
+const EmptySHA = "0000000000000000000000000000000000000000"
+
+// SHA1 a git commit name
+type SHA1 [20]byte
+
+// Equal returns true if s has the same SHA1 as caller.
+// Support 40-length-string, []byte, SHA1.
+func (id SHA1) Equal(s2 interface{}) bool {
+ switch v := s2.(type) {
+ case string:
+ if len(v) != 40 {
+ return false
+ }
+ return v == id.String()
+ case []byte:
+ return bytes.Equal(v, id[:])
+ case SHA1:
+ return v == id
+ default:
+ return false
+ }
+}
+
+// String returns string (hex) representation of the Oid.
+func (id SHA1) String() string {
+ return hex.EncodeToString(id[:])
+}
+
+// MustID always creates a new SHA1 from a [20]byte array with no validation of input.
+func MustID(b []byte) SHA1 {
+ var id SHA1
+ copy(id[:], b)
+ return id
+}
+
+// NewID creates a new SHA1 from a [20]byte array.
+func NewID(b []byte) (SHA1, error) {
+ if len(b) != 20 {
+ return SHA1{}, fmt.Errorf("Length must be 20: %v", b)
+ }
+ return MustID(b), nil
+}
+
+// MustIDFromString always creates a new sha from a ID with no validation of input.
+func MustIDFromString(s string) SHA1 {
+ b, _ := hex.DecodeString(s)
+ return MustID(b)
+}
+
+// NewIDFromString creates a new SHA1 from a ID string of length 40.
+func NewIDFromString(s string) (SHA1, error) {
+ var id SHA1
+ s = strings.TrimSpace(s)
+ if len(s) != 40 {
+ return id, fmt.Errorf("Length must be 40: %s", s)
+ }
+ b, err := hex.DecodeString(s)
+ if err != nil {
+ return id, err
+ }
+ return NewID(b)
+}
diff --git a/modules/git/signature.go b/modules/git/signature.go
new file mode 100644
index 0000000000..e6ab247fd7
--- /dev/null
+++ b/modules/git/signature.go
@@ -0,0 +1,58 @@
+// Copyright 2015 The Gogs 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 (
+ "bytes"
+ "strconv"
+ "time"
+)
+
+// Signature represents the Author or Committer information.
+type Signature struct {
+ Email string
+ Name string
+ When time.Time
+}
+
+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/submodule.go b/modules/git/submodule.go
new file mode 100644
index 0000000000..294df3986a
--- /dev/null
+++ b/modules/git/submodule.go
@@ -0,0 +1,87 @@
+// Copyright 2015 The Gogs 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 "strings"
+
+// SubModule submodule is a reference on git repository
+type SubModule struct {
+ Name string
+ URL string
+}
+
+// SubModuleFile represents a file with submodule type.
+type SubModuleFile struct {
+ *Commit
+
+ refURL string
+ refID string
+}
+
+// NewSubModuleFile create a new submodule file
+func NewSubModuleFile(c *Commit, refURL, refID string) *SubModuleFile {
+ return &SubModuleFile{
+ Commit: c,
+ refURL: refURL,
+ refID: refID,
+ }
+}
+
+func getRefURL(refURL, urlPrefix, parentPath string) string {
+ if refURL == "" {
+ return ""
+ }
+
+ url := strings.TrimSuffix(refURL, ".git")
+
+ // git://xxx/user/repo
+ if strings.HasPrefix(url, "git://") {
+ return "http://" + strings.TrimPrefix(url, "git://")
+ }
+
+ // http[s]://xxx/user/repo
+ if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
+ return url
+ }
+
+ // Relative url prefix check (according to git submodule documentation)
+ if strings.HasPrefix(url, "./") || strings.HasPrefix(url, "../") {
+ // ...construct and return correct submodule url here...
+ idx := strings.Index(parentPath, "/src/")
+ if idx == -1 {
+ return url
+ }
+ return strings.TrimSuffix(urlPrefix, "/") + parentPath[:idx] + "/" + url
+ }
+
+ // sysuser@xxx:user/repo
+ i := strings.Index(url, "@")
+ j := strings.LastIndex(url, ":")
+
+ // Only process when i < j because git+ssh://git@git.forwardbias.in/npploader.git
+ if i > -1 && j > -1 && i < j {
+ // fix problem with reverse proxy works only with local server
+ if strings.Contains(urlPrefix, url[i+1:j]) {
+ return urlPrefix + url[j+1:]
+ }
+ if strings.HasPrefix(url, "ssh://") || strings.HasPrefix(url, "git+ssh://") {
+ k := strings.Index(url[j+1:], "/")
+ return "http://" + url[i+1:j] + "/" + url[j+1:][k+1:]
+ }
+ return "http://" + url[i+1:j] + "/" + url[j+1:]
+ }
+
+ return url
+}
+
+// RefURL guesses and returns reference URL.
+func (sf *SubModuleFile) RefURL(urlPrefix string, parentPath string) string {
+ return getRefURL(sf.refURL, urlPrefix, parentPath)
+}
+
+// RefID returns reference ID.
+func (sf *SubModuleFile) RefID() string {
+ return sf.refID
+}
diff --git a/modules/git/submodule_test.go b/modules/git/submodule_test.go
new file mode 100644
index 0000000000..6a3bb7ec7b
--- /dev/null
+++ b/modules/git/submodule_test.go
@@ -0,0 +1,29 @@
+// 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.
+
+package git
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGetRefURL(t *testing.T) {
+ var kases = []struct {
+ refURL string
+ prefixURL string
+ parentPath string
+ expect string
+ }{
+ {"git://github.com/user1/repo1", "/", "/", "http://github.com/user1/repo1"},
+ {"https://localhost/user1/repo1.git", "/", "/", "https://localhost/user1/repo1"},
+ {"git@github.com/user1/repo1.git", "/", "/", "git@github.com/user1/repo1"},
+ {"ssh://git@git.zefie.net:2222/zefie/lge_g6_kernel_scripts.git", "/", "/", "http://git.zefie.net/zefie/lge_g6_kernel_scripts"},
+ }
+
+ for _, kase := range kases {
+ assert.EqualValues(t, kase.expect, getRefURL(kase.refURL, kase.prefixURL, kase.parentPath))
+ }
+}
diff --git a/modules/git/tag.go b/modules/git/tag.go
new file mode 100644
index 0000000000..500fd27491
--- /dev/null
+++ b/modules/git/tag.go
@@ -0,0 +1,89 @@
+// Copyright 2015 The Gogs 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 (
+ "bytes"
+ "sort"
+)
+
+// 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
+}
+
+// Commit return the commit of the tag reference
+func (tag *Tag) Commit() (*Commit, error) {
+ return tag.repo.getCommit(tag.Object)
+}
+
+// Parse commit information from the (uncompressed) raw
+// data from the commit object.
+// \n\n separate headers from message
+func parseTagData(data []byte) (*Tag, error) {
+ tag := new(Tag)
+ // we now have the contents of the commit object. Let's investigate...
+ nextline := 0
+l:
+ for {
+ eol := bytes.IndexByte(data[nextline:], '\n')
+ switch {
+ case eol > 0:
+ line := data[nextline : nextline+eol]
+ spacepos := bytes.IndexByte(line, ' ')
+ reftype := line[:spacepos]
+ switch string(reftype) {
+ case "object":
+ id, err := NewIDFromString(string(line[spacepos+1:]))
+ if err != nil {
+ return nil, err
+ }
+ tag.Object = id
+ case "type":
+ // A commit can have one or more parents
+ tag.Type = string(line[spacepos+1:])
+ case "tagger":
+ sig, err := newSignatureFromCommitline(line[spacepos+1:])
+ if err != nil {
+ return nil, err
+ }
+ tag.Tagger = sig
+ }
+ nextline += eol + 1
+ case eol == 0:
+ tag.Message = string(data[nextline+1:])
+ break l
+ default:
+ break l
+ }
+ }
+ return tag, nil
+}
+
+type tagSorter []*Tag
+
+func (ts tagSorter) Len() int {
+ return len([]*Tag(ts))
+}
+
+func (ts tagSorter) Less(i, j int) bool {
+ return []*Tag(ts)[i].Tagger.When.After([]*Tag(ts)[j].Tagger.When)
+}
+
+func (ts tagSorter) Swap(i, j int) {
+ []*Tag(ts)[i], []*Tag(ts)[j] = []*Tag(ts)[j], []*Tag(ts)[i]
+}
+
+// sortTagsByTime
+func sortTagsByTime(tags []*Tag) {
+ sorter := tagSorter(tags)
+ sort.Sort(sorter)
+}
diff --git a/modules/git/tests/repos/repo1_bare/HEAD b/modules/git/tests/repos/repo1_bare/HEAD
new file mode 100644
index 0000000000..cb089cd89a
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/modules/git/tests/repos/repo1_bare/config b/modules/git/tests/repos/repo1_bare/config
new file mode 100644
index 0000000000..07d359d07c
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/config
@@ -0,0 +1,4 @@
+[core]
+ repositoryformatversion = 0
+ filemode = true
+ bare = true
diff --git a/modules/git/tests/repos/repo1_bare/description b/modules/git/tests/repos/repo1_bare/description
new file mode 100644
index 0000000000..498b267a8c
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/modules/git/tests/repos/repo1_bare/hooks/applypatch-msg.sample b/modules/git/tests/repos/repo1_bare/hooks/applypatch-msg.sample
new file mode 100755
index 0000000000..a5d7b84a67
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/hooks/applypatch-msg.sample
@@ -0,0 +1,15 @@
+#!/bin/sh
+#
+# An example hook script to check the commit log message taken by
+# applypatch from an e-mail message.
+#
+# The hook should exit with non-zero status after issuing an
+# appropriate message if it wants to stop the commit. The hook is
+# allowed to edit the commit message file.
+#
+# To enable this hook, rename this file to "applypatch-msg".
+
+. git-sh-setup
+commitmsg="$(git rev-parse --git-path hooks/commit-msg)"
+test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"}
+:
diff --git a/modules/git/tests/repos/repo1_bare/hooks/commit-msg.sample b/modules/git/tests/repos/repo1_bare/hooks/commit-msg.sample
new file mode 100755
index 0000000000..b58d1184a9
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/hooks/commit-msg.sample
@@ -0,0 +1,24 @@
+#!/bin/sh
+#
+# An example hook script to check the commit log message.
+# Called by "git commit" with one argument, the name of the file
+# that has the commit message. The hook should exit with non-zero
+# status after issuing an appropriate message if it wants to stop the
+# commit. The hook is allowed to edit the commit message file.
+#
+# To enable this hook, rename this file to "commit-msg".
+
+# Uncomment the below to add a Signed-off-by line to the message.
+# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
+# hook is more suited to it.
+#
+# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
+# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
+
+# This example catches duplicate Signed-off-by lines.
+
+test "" = "$(grep '^Signed-off-by: ' "$1" |
+ sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
+ echo >&2 Duplicate Signed-off-by lines.
+ exit 1
+}
diff --git a/modules/git/tests/repos/repo1_bare/hooks/post-update.sample b/modules/git/tests/repos/repo1_bare/hooks/post-update.sample
new file mode 100755
index 0000000000..ec17ec1939
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/hooks/post-update.sample
@@ -0,0 +1,8 @@
+#!/bin/sh
+#
+# An example hook script to prepare a packed repository for use over
+# dumb transports.
+#
+# To enable this hook, rename this file to "post-update".
+
+exec git update-server-info
diff --git a/modules/git/tests/repos/repo1_bare/hooks/pre-applypatch.sample b/modules/git/tests/repos/repo1_bare/hooks/pre-applypatch.sample
new file mode 100755
index 0000000000..4142082bcb
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/hooks/pre-applypatch.sample
@@ -0,0 +1,14 @@
+#!/bin/sh
+#
+# An example hook script to verify what is about to be committed
+# by applypatch from an e-mail message.
+#
+# The hook should exit with non-zero status after issuing an
+# appropriate message if it wants to stop the commit.
+#
+# To enable this hook, rename this file to "pre-applypatch".
+
+. git-sh-setup
+precommit="$(git rev-parse --git-path hooks/pre-commit)"
+test -x "$precommit" && exec "$precommit" ${1+"$@"}
+:
diff --git a/modules/git/tests/repos/repo1_bare/hooks/pre-commit.sample b/modules/git/tests/repos/repo1_bare/hooks/pre-commit.sample
new file mode 100755
index 0000000000..68d62d5446
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/hooks/pre-commit.sample
@@ -0,0 +1,49 @@
+#!/bin/sh
+#
+# An example hook script to verify what is about to be committed.
+# Called by "git commit" with no arguments. The hook should
+# exit with non-zero status after issuing an appropriate message if
+# it wants to stop the commit.
+#
+# To enable this hook, rename this file to "pre-commit".
+
+if git rev-parse --verify HEAD >/dev/null 2>&1
+then
+ against=HEAD
+else
+ # Initial commit: diff against an empty tree object
+ against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
+fi
+
+# If you want to allow non-ASCII filenames set this variable to true.
+allownonascii=$(git config --bool hooks.allownonascii)
+
+# Redirect output to stderr.
+exec 1>&2
+
+# Cross platform projects tend to avoid non-ASCII filenames; prevent
+# them from being added to the repository. We exploit the fact that the
+# printable range starts at the space character and ends with tilde.
+if [ "$allownonascii" != "true" ] &&
+ # Note that the use of brackets around a tr range is ok here, (it's
+ # even required, for portability to Solaris 10's /usr/bin/tr), since
+ # the square bracket bytes happen to fall in the designated range.
+ test $(git diff --cached --name-only --diff-filter=A -z $against |
+ LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
+then
+ cat <<\EOF
+Error: Attempt to add a non-ASCII file name.
+
+This can cause problems if you want to work with people on other platforms.
+
+To be portable it is advisable to rename the file.
+
+If you know what you are doing you can disable this check using:
+
+ git config hooks.allownonascii true
+EOF
+ exit 1
+fi
+
+# If there are whitespace errors, print the offending file names and fail.
+exec git diff-index --check --cached $against --
diff --git a/modules/git/tests/repos/repo1_bare/hooks/pre-push.sample b/modules/git/tests/repos/repo1_bare/hooks/pre-push.sample
new file mode 100755
index 0000000000..6187dbf439
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/hooks/pre-push.sample
@@ -0,0 +1,53 @@
+#!/bin/sh
+
+# An example hook script to verify what is about to be pushed. Called by "git
+# push" after it has checked the remote status, but before anything has been
+# pushed. If this script exits with a non-zero status nothing will be pushed.
+#
+# This hook is called with the following parameters:
+#
+# $1 -- Name of the remote to which the push is being done
+# $2 -- URL to which the push is being done
+#
+# If pushing without using a named remote those arguments will be equal.
+#
+# Information about the commits which are being pushed is supplied as lines to
+# the standard input in the form:
+#
+# <local ref> <local sha1> <remote ref> <remote sha1>
+#
+# This sample shows how to prevent push of commits where the log message starts
+# with "WIP" (work in progress).
+
+remote="$1"
+url="$2"
+
+z40=0000000000000000000000000000000000000000
+
+while read local_ref local_sha remote_ref remote_sha
+do
+ if [ "$local_sha" = $z40 ]
+ then
+ # Handle delete
+ :
+ else
+ if [ "$remote_sha" = $z40 ]
+ then
+ # New branch, examine all commits
+ range="$local_sha"
+ else
+ # Update to existing branch, examine new commits
+ range="$remote_sha..$local_sha"
+ fi
+
+ # Check for WIP commit
+ commit=`git rev-list -n 1 --grep '^WIP' "$range"`
+ if [ -n "$commit" ]
+ then
+ echo >&2 "Found WIP commit in $local_ref, not pushing"
+ exit 1
+ fi
+ fi
+done
+
+exit 0
diff --git a/modules/git/tests/repos/repo1_bare/hooks/pre-rebase.sample b/modules/git/tests/repos/repo1_bare/hooks/pre-rebase.sample
new file mode 100755
index 0000000000..33730ca647
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/hooks/pre-rebase.sample
@@ -0,0 +1,169 @@
+#!/bin/sh
+#
+# Copyright (c) 2006, 2008 Junio C Hamano
+#
+# The "pre-rebase" hook is run just before "git rebase" starts doing
+# its job, and can prevent the command from running by exiting with
+# non-zero status.
+#
+# The hook is called with the following parameters:
+#
+# $1 -- the upstream the series was forked from.
+# $2 -- the branch being rebased (or empty when rebasing the current branch).
+#
+# This sample shows how to prevent topic branches that are already
+# merged to 'next' branch from getting rebased, because allowing it
+# would result in rebasing already published history.
+
+publish=next
+basebranch="$1"
+if test "$#" = 2
+then
+ topic="refs/heads/$2"
+else
+ topic=`git symbolic-ref HEAD` ||
+ exit 0 ;# we do not interrupt rebasing detached HEAD
+fi
+
+case "$topic" in
+refs/heads/??/*)
+ ;;
+*)
+ exit 0 ;# we do not interrupt others.
+ ;;
+esac
+
+# Now we are dealing with a topic branch being rebased
+# on top of master. Is it OK to rebase it?
+
+# Does the topic really exist?
+git show-ref -q "$topic" || {
+ echo >&2 "No such branch $topic"
+ exit 1
+}
+
+# Is topic fully merged to master?
+not_in_master=`git rev-list --pretty=oneline ^master "$topic"`
+if test -z "$not_in_master"
+then
+ echo >&2 "$topic is fully merged to master; better remove it."
+ exit 1 ;# we could allow it, but there is no point.
+fi
+
+# Is topic ever merged to next? If so you should not be rebasing it.
+only_next_1=`git rev-list ^master "^$topic" ${publish} | sort`
+only_next_2=`git rev-list ^master ${publish} | sort`
+if test "$only_next_1" = "$only_next_2"
+then
+ not_in_topic=`git rev-list "^$topic" master`
+ if test -z "$not_in_topic"
+ then
+ echo >&2 "$topic is already up-to-date with master"
+ exit 1 ;# we could allow it, but there is no point.
+ else
+ exit 0
+ fi
+else
+ not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"`
+ /usr/bin/perl -e '
+ my $topic = $ARGV[0];
+ my $msg = "* $topic has commits already merged to public branch:\n";
+ my (%not_in_next) = map {
+ /^([0-9a-f]+) /;
+ ($1 => 1);
+ } split(/\n/, $ARGV[1]);
+ for my $elem (map {
+ /^([0-9a-f]+) (.*)$/;
+ [$1 => $2];
+ } split(/\n/, $ARGV[2])) {
+ if (!exists $not_in_next{$elem->[0]}) {
+ if ($msg) {
+ print STDERR $msg;
+ undef $msg;
+ }
+ print STDERR " $elem->[1]\n";
+ }
+ }
+ ' "$topic" "$not_in_next" "$not_in_master"
+ exit 1
+fi
+
+<<\DOC_END
+
+This sample hook safeguards topic branches that have been
+published from being rewound.
+
+The workflow assumed here is:
+
+ * Once a topic branch forks from "master", "master" is never
+ merged into it again (either directly or indirectly).
+
+ * Once a topic branch is fully cooked and merged into "master",
+ it is deleted. If you need to build on top of it to correct
+ earlier mistakes, a new topic branch is created by forking at
+ the tip of the "master". This is not strictly necessary, but
+ it makes it easier to keep your history simple.
+
+ * Whenever you need to test or publish your changes to topic
+ branches, merge them into "next" branch.
+
+The script, being an example, hardcodes the publish branch name
+to be "next", but it is trivial to make it configurable via
+$GIT_DIR/config mechanism.
+
+With this workflow, you would want to know:
+
+(1) ... if a topic branch has ever been merged to "next". Young
+ topic branches can have stupid mistakes you would rather
+ clean up before publishing, and things that have not been
+ merged into other branches can be easily rebased without
+ affecting other people. But once it is published, you would
+ not want to rewind it.
+
+(2) ... if a topic branch has been fully merged to "master".
+ Then you can delete it. More importantly, you should not
+ build on top of it -- other people may already want to
+ change things related to the topic as patches against your
+ "master", so if you need further changes, it is better to
+ fork the topic (perhaps with the same name) afresh from the
+ tip of "master".
+
+Let's look at this example:
+
+ o---o---o---o---o---o---o---o---o---o "next"
+ / / / /
+ / a---a---b A / /
+ / / / /
+ / / c---c---c---c B /
+ / / / \ /
+ / / / b---b C \ /
+ / / / / \ /
+ ---o---o---o---o---o---o---o---o---o---o---o "master"
+
+
+A, B and C are topic branches.
+
+ * A has one fix since it was merged up to "next".
+
+ * B has finished. It has been fully merged up to "master" and "next",
+ and is ready to be deleted.
+
+ * C has not merged to "next" at all.
+
+We would want to allow C to be rebased, refuse A, and encourage
+B to be deleted.
+
+To compute (1):
+
+ git rev-list ^master ^topic next
+ git rev-list ^master next
+
+ if these match, topic has not merged in next at all.
+
+To compute (2):
+
+ git rev-list master..topic
+
+ if this is empty, it is fully merged to "master".
+
+DOC_END
diff --git a/modules/git/tests/repos/repo1_bare/hooks/pre-receive.sample b/modules/git/tests/repos/repo1_bare/hooks/pre-receive.sample
new file mode 100755
index 0000000000..a1fd29ec14
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/hooks/pre-receive.sample
@@ -0,0 +1,24 @@
+#!/bin/sh
+#
+# An example hook script to make use of push options.
+# The example simply echoes all push options that start with 'echoback='
+# and rejects all pushes when the "reject" push option is used.
+#
+# To enable this hook, rename this file to "pre-receive".
+
+if test -n "$GIT_PUSH_OPTION_COUNT"
+then
+ i=0
+ while test "$i" -lt "$GIT_PUSH_OPTION_COUNT"
+ do
+ eval "value=\$GIT_PUSH_OPTION_$i"
+ case "$value" in
+ echoback=*)
+ echo "echo from the pre-receive-hook: ${value#*=}" >&2
+ ;;
+ reject)
+ exit 1
+ esac
+ i=$((i + 1))
+ done
+fi
diff --git a/modules/git/tests/repos/repo1_bare/hooks/prepare-commit-msg.sample b/modules/git/tests/repos/repo1_bare/hooks/prepare-commit-msg.sample
new file mode 100755
index 0000000000..f093a02ec4
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/hooks/prepare-commit-msg.sample
@@ -0,0 +1,36 @@
+#!/bin/sh
+#
+# An example hook script to prepare the commit log message.
+# Called by "git commit" with the name of the file that has the
+# commit message, followed by the description of the commit
+# message's source. The hook's purpose is to edit the commit
+# message file. If the hook fails with a non-zero status,
+# the commit is aborted.
+#
+# To enable this hook, rename this file to "prepare-commit-msg".
+
+# This hook includes three examples. The first comments out the
+# "Conflicts:" part of a merge commit.
+#
+# The second includes the output of "git diff --name-status -r"
+# into the message, just before the "git status" output. It is
+# commented because it doesn't cope with --amend or with squashed
+# commits.
+#
+# The third example adds a Signed-off-by line to the message, that can
+# still be edited. This is rarely a good idea.
+
+case "$2,$3" in
+ merge,)
+ /usr/bin/perl -i.bak -ne 's/^/# /, s/^# #/#/ if /^Conflicts/ .. /#/; print' "$1" ;;
+
+# ,|template,)
+# /usr/bin/perl -i.bak -pe '
+# print "\n" . `git diff --cached --name-status -r`
+# if /^#/ && $first++ == 0' "$1" ;;
+
+ *) ;;
+esac
+
+# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
+# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
diff --git a/modules/git/tests/repos/repo1_bare/hooks/update.sample b/modules/git/tests/repos/repo1_bare/hooks/update.sample
new file mode 100755
index 0000000000..80ba94135c
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/hooks/update.sample
@@ -0,0 +1,128 @@
+#!/bin/sh
+#
+# An example hook script to block unannotated tags from entering.
+# Called by "git receive-pack" with arguments: refname sha1-old sha1-new
+#
+# To enable this hook, rename this file to "update".
+#
+# Config
+# ------
+# hooks.allowunannotated
+# This boolean sets whether unannotated tags will be allowed into the
+# repository. By default they won't be.
+# hooks.allowdeletetag
+# This boolean sets whether deleting tags will be allowed in the
+# repository. By default they won't be.
+# hooks.allowmodifytag
+# This boolean sets whether a tag may be modified after creation. By default
+# it won't be.
+# hooks.allowdeletebranch
+# This boolean sets whether deleting branches will be allowed in the
+# repository. By default they won't be.
+# hooks.denycreatebranch
+# This boolean sets whether remotely creating branches will be denied
+# in the repository. By default this is allowed.
+#
+
+# --- Command line
+refname="$1"
+oldrev="$2"
+newrev="$3"
+
+# --- Safety check
+if [ -z "$GIT_DIR" ]; then
+ echo "Don't run this script from the command line." >&2
+ echo " (if you want, you could supply GIT_DIR then run" >&2
+ echo " $0 <ref> <oldrev> <newrev>)" >&2
+ exit 1
+fi
+
+if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
+ echo "usage: $0 <ref> <oldrev> <newrev>" >&2
+ exit 1
+fi
+
+# --- Config
+allowunannotated=$(git config --bool hooks.allowunannotated)
+allowdeletebranch=$(git config --bool hooks.allowdeletebranch)
+denycreatebranch=$(git config --bool hooks.denycreatebranch)
+allowdeletetag=$(git config --bool hooks.allowdeletetag)
+allowmodifytag=$(git config --bool hooks.allowmodifytag)
+
+# check for no description
+projectdesc=$(sed -e '1q' "$GIT_DIR/description")
+case "$projectdesc" in
+"Unnamed repository"* | "")
+ echo "*** Project description file hasn't been set" >&2
+ exit 1
+ ;;
+esac
+
+# --- Check types
+# if $newrev is 0000...0000, it's a commit to delete a ref.
+zero="0000000000000000000000000000000000000000"
+if [ "$newrev" = "$zero" ]; then
+ newrev_type=delete
+else
+ newrev_type=$(git cat-file -t $newrev)
+fi
+
+case "$refname","$newrev_type" in
+ refs/tags/*,commit)
+ # un-annotated tag
+ short_refname=${refname##refs/tags/}
+ if [ "$allowunannotated" != "true" ]; then
+ echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2
+ echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
+ exit 1
+ fi
+ ;;
+ refs/tags/*,delete)
+ # delete tag
+ if [ "$allowdeletetag" != "true" ]; then
+ echo "*** Deleting a tag is not allowed in this repository" >&2
+ exit 1
+ fi
+ ;;
+ refs/tags/*,tag)
+ # annotated tag
+ if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1
+ then
+ echo "*** Tag '$refname' already exists." >&2
+ echo "*** Modifying a tag is not allowed in this repository." >&2
+ exit 1
+ fi
+ ;;
+ refs/heads/*,commit)
+ # branch
+ if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then
+ echo "*** Creating a branch is not allowed in this repository" >&2
+ exit 1
+ fi
+ ;;
+ refs/heads/*,delete)
+ # delete branch
+ if [ "$allowdeletebranch" != "true" ]; then
+ echo "*** Deleting a branch is not allowed in this repository" >&2
+ exit 1
+ fi
+ ;;
+ refs/remotes/*,commit)
+ # tracking branch
+ ;;
+ refs/remotes/*,delete)
+ # delete tracking branch
+ if [ "$allowdeletebranch" != "true" ]; then
+ echo "*** Deleting a tracking branch is not allowed in this repository" >&2
+ exit 1
+ fi
+ ;;
+ *)
+ # Anything else (is there anything else?)
+ echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2
+ exit 1
+ ;;
+esac
+
+# --- Finished
+exit 0
diff --git a/modules/git/tests/repos/repo1_bare/info/exclude b/modules/git/tests/repos/repo1_bare/info/exclude
new file mode 100644
index 0000000000..a5196d1be8
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/modules/git/tests/repos/repo1_bare/objects/0b/9f291245f6c596fd30bee925fe94fe0cbadd60 b/modules/git/tests/repos/repo1_bare/objects/0b/9f291245f6c596fd30bee925fe94fe0cbadd60
new file mode 100644
index 0000000000..11de5add68
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/0b/9f291245f6c596fd30bee925fe94fe0cbadd60
Binary files differ
diff --git a/modules/git/tests/repos/repo1_bare/objects/11/93ff46343f4f6a0522e2b28b871e905178c1f0 b/modules/git/tests/repos/repo1_bare/objects/11/93ff46343f4f6a0522e2b28b871e905178c1f0
new file mode 100644
index 0000000000..3541cd14f0
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/11/93ff46343f4f6a0522e2b28b871e905178c1f0
Binary files differ
diff --git a/modules/git/tests/repos/repo1_bare/objects/15/3f451b9ee7fa1da317ab17a127e9fd9d384310 b/modules/git/tests/repos/repo1_bare/objects/15/3f451b9ee7fa1da317ab17a127e9fd9d384310
new file mode 100644
index 0000000000..8db3c790b5
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/15/3f451b9ee7fa1da317ab17a127e9fd9d384310
Binary files differ
diff --git a/modules/git/tests/repos/repo1_bare/objects/18/4d49c75a0b202b1d2ea2fcb5861c329321fcd6 b/modules/git/tests/repos/repo1_bare/objects/18/4d49c75a0b202b1d2ea2fcb5861c329321fcd6
new file mode 100644
index 0000000000..45e014ea85
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/18/4d49c75a0b202b1d2ea2fcb5861c329321fcd6
Binary files differ
diff --git a/modules/git/tests/repos/repo1_bare/objects/21/6bf54c2f2e2916b830ebe09e8c58a6ed52d86b b/modules/git/tests/repos/repo1_bare/objects/21/6bf54c2f2e2916b830ebe09e8c58a6ed52d86b
new file mode 100644
index 0000000000..8c0b1b3285
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/21/6bf54c2f2e2916b830ebe09e8c58a6ed52d86b
Binary files differ
diff --git a/modules/git/tests/repos/repo1_bare/objects/28/39944139e0de9737a044f78b0e4b40d989a9e3 b/modules/git/tests/repos/repo1_bare/objects/28/39944139e0de9737a044f78b0e4b40d989a9e3
new file mode 100644
index 0000000000..e22e656fbe
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/28/39944139e0de9737a044f78b0e4b40d989a9e3
@@ -0,0 +1 @@
+x 0 @QΙ" il q 8BX'uSo6*摨",ɓTsI\I+r,|2[j얅V*u>KT?P4Rt@On[,\`oώ3]TzK߻e; \ No newline at end of file
diff --git a/modules/git/tests/repos/repo1_bare/objects/2e/65efe2a145dda7ee51d1741299f848e5bf752e b/modules/git/tests/repos/repo1_bare/objects/2e/65efe2a145dda7ee51d1741299f848e5bf752e
new file mode 100644
index 0000000000..3e46ba4a32
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/2e/65efe2a145dda7ee51d1741299f848e5bf752e
Binary files differ
diff --git a/modules/git/tests/repos/repo1_bare/objects/30/4c56b3bef33d0afeb8515ee803c839daf30ab8 b/modules/git/tests/repos/repo1_bare/objects/30/4c56b3bef33d0afeb8515ee803c839daf30ab8
new file mode 100644
index 0000000000..3a5c6c1962
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/30/4c56b3bef33d0afeb8515ee803c839daf30ab8
Binary files differ
diff --git a/modules/git/tests/repos/repo1_bare/objects/34/d1da713bf7de1c535e1d7d3ca985afd84bc7e5 b/modules/git/tests/repos/repo1_bare/objects/34/d1da713bf7de1c535e1d7d3ca985afd84bc7e5
new file mode 100644
index 0000000000..29f2d4fd00
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/34/d1da713bf7de1c535e1d7d3ca985afd84bc7e5
Binary files differ
diff --git a/modules/git/tests/repos/repo1_bare/objects/37/991dec2c8e592043f47155ce4808d4580f9123 b/modules/git/tests/repos/repo1_bare/objects/37/991dec2c8e592043f47155ce4808d4580f9123
new file mode 100644
index 0000000000..3658e95141
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/37/991dec2c8e592043f47155ce4808d4580f9123
@@ -0,0 +1 @@
+xKN1 Y#3 !p.LJ,k}5PlB5sKH|>dKKl k%S7ܛeӢcΉHSe~uLxoce:D7Әg_B=":S^ETup?6M^ \ No newline at end of file
diff --git a/modules/git/tests/repos/repo1_bare/objects/38/441bf2c4d4c27efff94728c9eb33266f44a702 b/modules/git/tests/repos/repo1_bare/objects/38/441bf2c4d4c27efff94728c9eb33266f44a702
new file mode 100644
index 0000000000..c3d484aee0
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/38/441bf2c4d4c27efff94728c9eb33266f44a702
Binary files differ
diff --git a/modules/git/tests/repos/repo1_bare/objects/3a/d28a9149a2864384548f3d17ed7f38014c9e8a b/modules/git/tests/repos/repo1_bare/objects/3a/d28a9149a2864384548f3d17ed7f38014c9e8a
new file mode 100644
index 0000000000..ee2652b1c5
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/3a/d28a9149a2864384548f3d17ed7f38014c9e8a
Binary files differ
diff --git a/modules/git/tests/repos/repo1_bare/objects/50/13716a9da8e66ea21059a84f1b4311424d2b7f b/modules/git/tests/repos/repo1_bare/objects/50/13716a9da8e66ea21059a84f1b4311424d2b7f
new file mode 100644
index 0000000000..b96905930a
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/50/13716a9da8e66ea21059a84f1b4311424d2b7f
Binary files differ
diff --git a/modules/git/tests/repos/repo1_bare/objects/59/dfb0bb505a601006e31fed53d2e24e44fca9ca b/modules/git/tests/repos/repo1_bare/objects/59/dfb0bb505a601006e31fed53d2e24e44fca9ca
new file mode 100644
index 0000000000..16292715bd
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/59/dfb0bb505a601006e31fed53d2e24e44fca9ca
Binary files differ
diff --git a/modules/git/tests/repos/repo1_bare/objects/5c/80b0245c1c6f8343fa418ec374b13b5d4ee658 b/modules/git/tests/repos/repo1_bare/objects/5c/80b0245c1c6f8343fa418ec374b13b5d4ee658
new file mode 100644
index 0000000000..234d41bbc0
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/5c/80b0245c1c6f8343fa418ec374b13b5d4ee658
@@ -0,0 +1,3 @@
+x;
+1@s:f7#xL!F㻈++hZbHJ<JB
+VK6 <5J)J1I)d#1 Tw+3+`{;=FD#ߡٿB<<3>= \ No newline at end of file
diff --git a/modules/git/tests/repos/repo1_bare/objects/62/d735f9efa9cf5b7df6bac9917b80e4779f4315 b/modules/git/tests/repos/repo1_bare/objects/62/d735f9efa9cf5b7df6bac9917b80e4779f4315
new file mode 100644
index 0000000000..15b958a2a2
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/62/d735f9efa9cf5b7df6bac9917b80e4779f4315
Binary files differ
diff --git a/modules/git/tests/repos/repo1_bare/objects/64/3a35374408002fcf2f0e8d42d262a1e0e2f80e b/modules/git/tests/repos/repo1_bare/objects/64/3a35374408002fcf2f0e8d42d262a1e0e2f80e
new file mode 100644
index 0000000000..eb0ad47575
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/64/3a35374408002fcf2f0e8d42d262a1e0e2f80e
Binary files differ
diff --git a/modules/git/tests/repos/repo1_bare/objects/6c/493ff740f9380390d5c9ddef4af18697ac9375 b/modules/git/tests/repos/repo1_bare/objects/6c/493ff740f9380390d5c9ddef4af18697ac9375
new file mode 100644
index 0000000000..7d217a7850
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/6c/493ff740f9380390d5c9ddef4af18697ac9375
Binary files differ
diff --git a/modules/git/tests/repos/repo1_bare/objects/6f/bd69e9823458e6c4a2fc5c0f6bc022b2f2acd1 b/modules/git/tests/repos/repo1_bare/objects/6f/bd69e9823458e6c4a2fc5c0f6bc022b2f2acd1
new file mode 100644
index 0000000000..5b2cfb21cb
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/6f/bd69e9823458e6c4a2fc5c0f6bc022b2f2acd1
@@ -0,0 +1 @@
+x1n0 E3 -&H&F+աo#d/X: zSiZb1ؔSag@F35];̈'Ɇ%sD5R,w_m೶kCv"?}m#8"#|xDtfET zz/N \ No newline at end of file
diff --git a/modules/git/tests/repos/repo1_bare/objects/7e/3b688f3369ca28ebafbda9f8ef39713dd12fc8 b/modules/git/tests/repos/repo1_bare/objects/7e/3b688f3369ca28ebafbda9f8ef39713dd12fc8
new file mode 100644
index 0000000000..113089d22b
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/7e/3b688f3369ca28ebafbda9f8ef39713dd12fc8
Binary files differ
diff --git a/modules/git/tests/repos/repo1_bare/objects/80/06ff9adbf0cb94da7dad9e537e53817f9fa5c0 b/modules/git/tests/repos/repo1_bare/objects/80/06ff9adbf0cb94da7dad9e537e53817f9fa5c0
new file mode 100644
index 0000000000..808fe6efb6
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/80/06ff9adbf0cb94da7dad9e537e53817f9fa5c0
Binary files differ
diff --git a/modules/git/tests/repos/repo1_bare/objects/82/26f571dcc2d2f33a7179d929b10b9c39faa631 b/modules/git/tests/repos/repo1_bare/objects/82/26f571dcc2d2f33a7179d929b10b9c39faa631
new file mode 100644
index 0000000000..6a194fb3a6
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/82/26f571dcc2d2f33a7179d929b10b9c39faa631
Binary files differ
diff --git a/modules/git/tests/repos/repo1_bare/objects/83/b9c4da46ed59098a009f8640c77eac97b71dfe b/modules/git/tests/repos/repo1_bare/objects/83/b9c4da46ed59098a009f8640c77eac97b71dfe
new file mode 100644
index 0000000000..8602ab5402
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/83/b9c4da46ed59098a009f8640c77eac97b71dfe
Binary files differ
diff --git a/modules/git/tests/repos/repo1_bare/objects/8d/92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2 b/modules/git/tests/repos/repo1_bare/objects/8d/92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2
new file mode 100644
index 0000000000..431a481078
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/8d/92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2
Binary files differ
diff --git a/modules/git/tests/repos/repo1_bare/objects/95/bb4d39648ee7e325106df01a621c530863a653 b/modules/git/tests/repos/repo1_bare/objects/95/bb4d39648ee7e325106df01a621c530863a653
new file mode 100644
index 0000000000..6bb6a25648
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/95/bb4d39648ee7e325106df01a621c530863a653
Binary files differ
diff --git a/modules/git/tests/repos/repo1_bare/objects/98/1ff127cc331753bba28e1377c35934f1ca9b56 b/modules/git/tests/repos/repo1_bare/objects/98/1ff127cc331753bba28e1377c35934f1ca9b56
new file mode 100644
index 0000000000..ae6c93ac81
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/98/1ff127cc331753bba28e1377c35934f1ca9b56
Binary files differ
diff --git a/modules/git/tests/repos/repo1_bare/objects/9c/9aef8dd84e02bc7ec12641deb4c930a7c30185 b/modules/git/tests/repos/repo1_bare/objects/9c/9aef8dd84e02bc7ec12641deb4c930a7c30185
new file mode 100644
index 0000000000..8a263d0d6f
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/9c/9aef8dd84e02bc7ec12641deb4c930a7c30185
@@ -0,0 +1,2 @@
+xM
+1 @a=E.ӂ.<H۔j9\o:Ϗ6Co@2DI+QA[V H9P,R %IӥC\]PKE+~ 'LF}Zv liddPֿ\Kdɓ=ܲ:c \ No newline at end of file
diff --git a/modules/git/tests/repos/repo1_bare/objects/b1/4df6442ea5a1b382985a6549b85d435376c351 b/modules/git/tests/repos/repo1_bare/objects/b1/4df6442ea5a1b382985a6549b85d435376c351
new file mode 100644
index 0000000000..02fe24f0ae
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/b1/4df6442ea5a1b382985a6549b85d435376c351
Binary files differ
diff --git a/modules/git/tests/repos/repo1_bare/objects/b1/fc9917b618c924cf4aa421dae74e8bf9b556d3 b/modules/git/tests/repos/repo1_bare/objects/b1/fc9917b618c924cf4aa421dae74e8bf9b556d3
new file mode 100644
index 0000000000..aacc5efc9d
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/b1/fc9917b618c924cf4aa421dae74e8bf9b556d3
Binary files differ
diff --git a/modules/git/tests/repos/repo1_bare/objects/b7/5f44edbd9252c32bf9faa0c1257ffb3b126c24 b/modules/git/tests/repos/repo1_bare/objects/b7/5f44edbd9252c32bf9faa0c1257ffb3b126c24
new file mode 100644
index 0000000000..6c2e007e9f
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/b7/5f44edbd9252c32bf9faa0c1257ffb3b126c24
Binary files differ
diff --git a/modules/git/tests/repos/repo1_bare/objects/c8/c90111bdc18b3afd2b2906007059e95ac8fdc3 b/modules/git/tests/repos/repo1_bare/objects/c8/c90111bdc18b3afd2b2906007059e95ac8fdc3
new file mode 100644
index 0000000000..57c5d7ce84
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/c8/c90111bdc18b3afd2b2906007059e95ac8fdc3
Binary files differ
diff --git a/modules/git/tests/repos/repo1_bare/objects/d0/845fe2f85710b50d673dafe98236bf9f2023da b/modules/git/tests/repos/repo1_bare/objects/d0/845fe2f85710b50d673dafe98236bf9f2023da
new file mode 100644
index 0000000000..d29ca5af88
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/d0/845fe2f85710b50d673dafe98236bf9f2023da
Binary files differ
diff --git a/modules/git/tests/repos/repo1_bare/objects/e2/129701f1a4d54dc44f03c93bca0a2aec7c5449 b/modules/git/tests/repos/repo1_bare/objects/e2/129701f1a4d54dc44f03c93bca0a2aec7c5449
new file mode 100644
index 0000000000..08245d0012
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/e2/129701f1a4d54dc44f03c93bca0a2aec7c5449
Binary files differ
diff --git a/modules/git/tests/repos/repo1_bare/objects/f1/a6cb52b2d16773290cefe49ad0684b50a4f930 b/modules/git/tests/repos/repo1_bare/objects/f1/a6cb52b2d16773290cefe49ad0684b50a4f930
new file mode 100644
index 0000000000..6a412c76b1
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/objects/f1/a6cb52b2d16773290cefe49ad0684b50a4f930
Binary files differ
diff --git a/modules/git/tests/repos/repo1_bare/refs/heads/branch1 b/modules/git/tests/repos/repo1_bare/refs/heads/branch1
new file mode 100644
index 0000000000..eb33bd0936
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/refs/heads/branch1
@@ -0,0 +1 @@
+2839944139e0de9737a044f78b0e4b40d989a9e3
diff --git a/modules/git/tests/repos/repo1_bare/refs/heads/branch2 b/modules/git/tests/repos/repo1_bare/refs/heads/branch2
new file mode 100644
index 0000000000..0475e61f8c
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/refs/heads/branch2
@@ -0,0 +1 @@
+5c80b0245c1c6f8343fa418ec374b13b5d4ee658
diff --git a/modules/git/tests/repos/repo1_bare/refs/heads/master b/modules/git/tests/repos/repo1_bare/refs/heads/master
new file mode 100644
index 0000000000..4804d9d8fe
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/refs/heads/master
@@ -0,0 +1 @@
+37991dec2c8e592043f47155ce4808d4580f9123
diff --git a/modules/git/tests/repos/repo1_bare/refs/tags/test b/modules/git/tests/repos/repo1_bare/refs/tags/test
new file mode 100644
index 0000000000..ee311722e5
--- /dev/null
+++ b/modules/git/tests/repos/repo1_bare/refs/tags/test
@@ -0,0 +1 @@
+3ad28a9149a2864384548f3d17ed7f38014c9e8a
diff --git a/modules/git/tree.go b/modules/git/tree.go
new file mode 100644
index 0000000000..5ec22a3a6f
--- /dev/null
+++ b/modules/git/tree.go
@@ -0,0 +1,98 @@
+// Copyright 2015 The Gogs 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 (
+ "strings"
+)
+
+// Tree represents a flat directory listing.
+type Tree struct {
+ ID SHA1
+ repo *Repository
+
+ // parent tree
+ ptree *Tree
+
+ entries Entries
+ entriesParsed bool
+
+ entriesRecursive Entries
+ entriesRecursiveParsed bool
+}
+
+// NewTree create a new tree according the repository and commit id
+func NewTree(repo *Repository, id SHA1) *Tree {
+ return &Tree{
+ ID: id,
+ repo: repo,
+ }
+}
+
+// SubTree get a sub tree by the sub dir path
+func (t *Tree) SubTree(rpath string) (*Tree, error) {
+ if len(rpath) == 0 {
+ return t, nil
+ }
+
+ paths := strings.Split(rpath, "/")
+ var (
+ err error
+ g = t
+ p = t
+ te *TreeEntry
+ )
+ for _, name := range paths {
+ te, err = p.GetTreeEntryByPath(name)
+ if err != nil {
+ return nil, err
+ }
+
+ g, err = t.repo.getTree(te.ID)
+ if err != nil {
+ return nil, err
+ }
+ g.ptree = p
+ p = g
+ }
+ return g, nil
+}
+
+// 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 {
+ 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/tree_blob.go b/modules/git/tree_blob.go
new file mode 100644
index 0000000000..a37f6b2279
--- /dev/null
+++ b/modules/git/tree_blob.go
@@ -0,0 +1,59 @@
+// Copyright 2015 The Gogs 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 (
+ "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,
+ Type: ObjectTree,
+ mode: EntryModeTree,
+ }, 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 {
+ 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}
+}
+
+// GetBlobByPath get the blob object according the path
+func (t *Tree) GetBlobByPath(relpath string) (*Blob, error) {
+ entry, err := t.GetTreeEntryByPath(relpath)
+ if err != nil {
+ return nil, err
+ }
+
+ if !entry.IsDir() {
+ return entry.Blob(), nil
+ }
+
+ return nil, ErrNotExist{"", relpath}
+}
diff --git a/modules/git/tree_entry.go b/modules/git/tree_entry.go
new file mode 100644
index 0000000000..5b74e9a695
--- /dev/null
+++ b/modules/git/tree_entry.go
@@ -0,0 +1,205 @@
+// Copyright 2015 The Gogs 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 (
+ "io"
+ "sort"
+ "strconv"
+ "strings"
+)
+
+// 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 = 0x0100644
+ // EntryModeExec
+ EntryModeExec EntryMode = 0x0100755
+ // EntryModeSymlink
+ EntryModeSymlink EntryMode = 0x0120000
+ // EntryModeCommit
+ EntryModeCommit EntryMode = 0x0160000
+ // EntryModeTree
+ EntryModeTree EntryMode = 0x0040000
+)
+
+// TreeEntry the leaf in the git tree
+type TreeEntry struct {
+ ID SHA1
+ Type ObjectType
+
+ mode EntryMode
+ name string
+
+ ptree *Tree
+
+ committed bool
+
+ size int64
+ sized bool
+}
+
+// Name returns the name of the entry
+func (te *TreeEntry) Name() string {
+ return te.name
+}
+
+// Mode returns the mode of the entry
+func (te *TreeEntry) Mode() EntryMode {
+ return te.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
+ }
+
+ 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.mode == EntryModeCommit
+}
+
+// IsDir if the entry is a sub dir
+func (te *TreeEntry) IsDir() bool {
+ return te.mode == EntryModeTree
+}
+
+// IsLink if the entry is a symlink
+func (te *TreeEntry) IsLink() bool {
+ return te.mode == EntryModeSymlink
+}
+
+// Blob retrun the blob object the entry
+func (te *TreeEntry) Blob() *Blob {
+ return &Blob{
+ repo: te.ptree.repo,
+ TreeEntry: te,
+ }
+}
+
+// FollowLink returns the entry pointed to by a symlink
+func (te *TreeEntry) FollowLink() (*TreeEntry, error) {
+ if !te.IsLink() {
+ return nil, ErrBadLink{te.Name(), "not a symlink"}
+ }
+
+ // read the link
+ r, err := te.Blob().Data()
+ if err != nil {
+ return nil, err
+ }
+ buf := make([]byte, te.Size())
+ _, err = io.ReadFull(r, buf)
+ if err != nil {
+ return nil, err
+ }
+
+ lnk := string(buf)
+ t := te.ptree
+
+ // traverse up directories
+ for ; t != nil && strings.HasPrefix(lnk, "../"); lnk = lnk[3:] {
+ t = t.ptree
+ }
+
+ if t == nil {
+ return nil, ErrBadLink{te.Name(), "points outside of repo"}
+ }
+
+ target, err := t.GetTreeEntryByPath(lnk)
+ if err != nil {
+ if IsErrNotExist(err) {
+ return nil, ErrBadLink{te.Name(), "broken link"}
+ }
+ return nil, err
+ }
+ return target, nil
+}
+
+// GetSubJumpablePathName return the full path of subdirectory jumpable ( contains only one directory )
+func (te *TreeEntry) GetSubJumpablePathName() string {
+ if te.IsSubModule() || !te.IsDir() {
+ return ""
+ }
+ tree, err := te.ptree.SubTree(te.name)
+ if err != nil {
+ return te.name
+ }
+ entries, _ := tree.ListEntries()
+ if len(entries) == 1 && entries[0].IsDir() {
+ name := entries[0].GetSubJumpablePathName()
+ if name != "" {
+ return te.name + "/" + name
+ }
+ }
+ return te.name
+}
+
+// Entries a list of entry
+type Entries []*TreeEntry
+
+type customSortableEntries struct {
+ Comparer func(s1, s2 string) bool
+ Entries
+}
+
+var sorter = []func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool{
+ func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool {
+ return (t1.IsDir() || t1.IsSubModule()) && !t2.IsDir() && !t2.IsSubModule()
+ },
+ func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool {
+ return cmp(t1.name, t2.name)
+ },
+}
+
+func (ctes customSortableEntries) Len() int { return len(ctes.Entries) }
+
+func (ctes customSortableEntries) Swap(i, j int) {
+ ctes.Entries[i], ctes.Entries[j] = ctes.Entries[j], ctes.Entries[i]
+}
+
+func (ctes customSortableEntries) Less(i, j int) bool {
+ t1, t2 := ctes.Entries[i], ctes.Entries[j]
+ var k int
+ for k = 0; k < len(sorter)-1; k++ {
+ s := sorter[k]
+ switch {
+ case s(t1, t2, ctes.Comparer):
+ return true
+ case s(t2, t1, ctes.Comparer):
+ return false
+ }
+ }
+ return sorter[k](t1, t2, ctes.Comparer)
+}
+
+// Sort sort the list of entry
+func (tes Entries) Sort() {
+ sort.Sort(customSortableEntries{func(s1, s2 string) bool {
+ return s1 < s2
+ }, tes})
+}
+
+// CustomSort customizable string comparing sort entry list
+func (tes Entries) CustomSort(cmp func(s1, s2 string) bool) {
+ sort.Sort(customSortableEntries{cmp, tes})
+}
diff --git a/modules/git/tree_entry_test.go b/modules/git/tree_entry_test.go
new file mode 100644
index 0000000000..52920bc00e
--- /dev/null
+++ b/modules/git/tree_entry_test.go
@@ -0,0 +1,98 @@
+// 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.
+
+package git
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func getTestEntries() Entries {
+ return Entries{
+ &TreeEntry{name: "v1.0", mode: EntryModeTree},
+ &TreeEntry{name: "v2.0", mode: EntryModeTree},
+ &TreeEntry{name: "v2.1", mode: EntryModeTree},
+ &TreeEntry{name: "v2.12", mode: EntryModeTree},
+ &TreeEntry{name: "v2.2", mode: EntryModeTree},
+ &TreeEntry{name: "v12.0", mode: EntryModeTree},
+ &TreeEntry{name: "abc", mode: EntryModeBlob},
+ &TreeEntry{name: "bcd", mode: EntryModeBlob},
+ }
+}
+
+func TestEntriesSort(t *testing.T) {
+ entries := getTestEntries()
+ entries.Sort()
+ assert.Equal(t, "v1.0", entries[0].Name())
+ assert.Equal(t, "v12.0", entries[1].Name())
+ assert.Equal(t, "v2.0", entries[2].Name())
+ assert.Equal(t, "v2.1", entries[3].Name())
+ assert.Equal(t, "v2.12", entries[4].Name())
+ assert.Equal(t, "v2.2", entries[5].Name())
+ assert.Equal(t, "abc", entries[6].Name())
+ assert.Equal(t, "bcd", entries[7].Name())
+}
+
+func TestEntriesCustomSort(t *testing.T) {
+ entries := getTestEntries()
+ entries.CustomSort(func(s1, s2 string) bool {
+ return s1 > s2
+ })
+ assert.Equal(t, "v2.2", entries[0].Name())
+ assert.Equal(t, "v2.12", entries[1].Name())
+ assert.Equal(t, "v2.1", entries[2].Name())
+ assert.Equal(t, "v2.0", entries[3].Name())
+ assert.Equal(t, "v12.0", entries[4].Name())
+ assert.Equal(t, "v1.0", entries[5].Name())
+ assert.Equal(t, "bcd", entries[6].Name())
+ assert.Equal(t, "abc", entries[7].Name())
+}
+
+func TestFollowLink(t *testing.T) {
+ r, err := OpenRepository("tests/repos/repo1_bare")
+ assert.NoError(t, err)
+
+ commit, err := r.GetCommit("37991dec2c8e592043f47155ce4808d4580f9123")
+ assert.NoError(t, err)
+
+ // get the symlink
+ lnk, err := commit.Tree.GetTreeEntryByPath("foo/bar/link_to_hello")
+ assert.NoError(t, err)
+ assert.True(t, lnk.IsLink())
+
+ // should be able to dereference to target
+ target, err := lnk.FollowLink()
+ assert.NoError(t, err)
+ assert.Equal(t, target.Name(), "hello")
+ assert.False(t, target.IsLink())
+ assert.Equal(t, target.ID.String(), "b14df6442ea5a1b382985a6549b85d435376c351")
+
+ // should error when called on normal file
+ target, err = commit.Tree.GetTreeEntryByPath("file1.txt")
+ assert.NoError(t, err)
+ _, err = target.FollowLink()
+ assert.Equal(t, err.Error(), "file1.txt: not a symlink")
+
+ // should error for broken links
+ target, err = commit.Tree.GetTreeEntryByPath("foo/broken_link")
+ assert.NoError(t, err)
+ assert.True(t, target.IsLink())
+ _, err = target.FollowLink()
+ assert.Equal(t, err.Error(), "broken_link: broken link")
+
+ // should error for external links
+ target, err = commit.Tree.GetTreeEntryByPath("foo/outside_repo")
+ assert.NoError(t, err)
+ assert.True(t, target.IsLink())
+ _, err = target.FollowLink()
+ assert.Equal(t, err.Error(), "outside_repo: points outside of repo")
+
+ // testing fix for short link bug
+ target, err = commit.Tree.GetTreeEntryByPath("foo/link_short")
+ assert.NoError(t, err)
+ _, err = target.FollowLink()
+ assert.Equal(t, err.Error(), "link_short: broken link")
+}
diff --git a/modules/git/utils.go b/modules/git/utils.go
new file mode 100644
index 0000000000..8f010321cf
--- /dev/null
+++ b/modules/git/utils.go
@@ -0,0 +1,96 @@
+// Copyright 2015 The Gogs 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 (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+)
+
+// ObjectCache provides thread-safe cache opeations.
+type ObjectCache struct {
+ lock sync.RWMutex
+ cache map[string]interface{}
+}
+
+func newObjectCache() *ObjectCache {
+ return &ObjectCache{
+ cache: make(map[string]interface{}, 10),
+ }
+}
+
+// Set add obj to cache
+func (oc *ObjectCache) Set(id string, obj interface{}) {
+ oc.lock.Lock()
+ defer oc.lock.Unlock()
+
+ oc.cache[id] = obj
+}
+
+// Get get cached obj by id
+func (oc *ObjectCache) Get(id string) (interface{}, bool) {
+ oc.lock.RLock()
+ defer oc.lock.RUnlock()
+
+ obj, has := oc.cache[id]
+ return obj, has
+}
+
+// isDir returns true if given path is a directory,
+// or returns false when it's a file or does not exist.
+func isDir(dir string) bool {
+ f, e := os.Stat(dir)
+ if e != nil {
+ return false
+ }
+ return f.IsDir()
+}
+
+// isFile returns true if given path is a file,
+// or returns false when it's a directory or does not exist.
+func isFile(filePath string) bool {
+ f, e := os.Stat(filePath)
+ if e != nil {
+ return false
+ }
+ return !f.IsDir()
+}
+
+// isExist checks whether a file or directory exists.
+// It returns false when the file or directory does not exist.
+func isExist(path string) bool {
+ _, err := os.Stat(path)
+ return err == nil || os.IsExist(err)
+}
+
+func concatenateError(err error, stderr string) error {
+ if len(stderr) == 0 {
+ return err
+ }
+ return fmt.Errorf("%v - %s", err, stderr)
+}
+
+// If the object is stored in its own file (i.e not in a pack file),
+// this function returns the full path to the object file.
+// It does not test if the file exists.
+func filepathFromSHA1(rootdir, sha1 string) string {
+ return filepath.Join(rootdir, "objects", sha1[:2], sha1[2:])
+}
+
+// RefEndName return the end name of a ref name
+func RefEndName(refStr string) string {
+ if strings.HasPrefix(refStr, BranchPrefix) {
+ return refStr[len(BranchPrefix):]
+ }
+
+ if strings.HasPrefix(refStr, TagPrefix) {
+ return refStr[len(TagPrefix):]
+ }
+
+ return refStr
+}