1
1
mirror of https://github.com/go-gitea/gitea synced 2025-07-22 18:28:37 +00:00

move code.gitea.io/git to code.gitea.io/gitea/modules/git (#6364)

* move code.gitea.io/git to code.gitea.io/gitea/modules/git

* fix imports

* fix fmt

* fix misspell

* remove wrong tests data

* fix unit tests

* fix tests

* fix tests

* fix tests

* fix tests

* fix tests

* enable Debug to trace the failure tests

* fix tests

* fix tests

* fix tests

* fix tests

* fix tests

* comment commit count tests since git clone depth is 50

* fix tests

* update from code.gitea.io/git

* revert change to makefile
This commit is contained in:
Lunny Xiao
2019-03-27 17:33:00 +08:00
committed by GitHub
parent d056bf300f
commit d578b71d61
174 changed files with 3044 additions and 145 deletions

3
modules/git/README.md Normal file
View File

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

73
modules/git/blob.go Normal file
View File

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

80
modules/git/blob_test.go Normal file
View File

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

11
modules/git/cache.go Normal file
View File

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

137
modules/git/command.go Normal file
View File

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

View File

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

349
modules/git/commit.go Normal file
View File

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

View File

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

329
modules/git/commit_info.go Normal file
View File

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

View File

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

View File

@@ -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: ]")
}
}

66
modules/git/error.go Normal file
View File

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

91
modules/git/git.go Normal file
View File

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

135
modules/git/hook.go Normal file
View File

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

81
modules/git/parse.go Normal file
View File

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

58
modules/git/parse_test.go Normal file
View File

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

18
modules/git/ref.go Normal file
View File

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

287
modules/git/repo.go Normal file
View File

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

24
modules/git/repo_blame.go Normal file
View File

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

30
modules/git/repo_blob.go Normal file
View File

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

View File

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

134
modules/git/repo_branch.go Normal file
View File

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

View File

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

388
modules/git/repo_commit.go Normal file
View File

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

View File

@@ -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: ]")
}

15
modules/git/repo_hook.go Normal file
View File

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

View File

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

89
modules/git/repo_pull.go Normal file
View File

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

View File

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

51
modules/git/repo_ref.go Normal file
View File

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

View File

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

149
modules/git/repo_tag.go Normal file
View File

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

View File

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

26
modules/git/repo_test.go Normal file
View File

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

35
modules/git/repo_tree.go Normal file
View File

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

76
modules/git/sha1.go Normal file
View File

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

58
modules/git/signature.go Normal file
View File

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

87
modules/git/submodule.go Normal file
View File

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

View File

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

89
modules/git/tag.go Normal file
View File

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

View File

@@ -0,0 +1 @@
ref: refs/heads/master

View File

@@ -0,0 +1,4 @@
[core]
repositoryformatversion = 0
filemode = true
bare = true

View File

@@ -0,0 +1 @@
Unnamed repository; edit this file 'description' to name the repository.

View File

@@ -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+"$@"}
:

View File

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

View File

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

View File

@@ -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+"$@"}
:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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]
# *~

View File

@@ -0,0 +1 @@
x<01><><EFBFBD>

View File

@@ -0,0 +1 @@
x<01><>KN1 Y<><14>#<23><1C>3 !<21>p.<2E><><EFBFBD>L<EFBFBD><4C>J<><05><><EFBFBD>,k<><6B><15><>}5<>Pl<>B<EFBFBD>5sK<73>H|><3E>d<>K<15>Kl k<>%<25><><EFBFBD>S7<53>ܛ<EFBFBD><DC9B>e<EFBFBD>Ӣ<>c<EFBFBD>Ή<EFBFBD><CE89><EFBFBD>H<EFBFBD><48>Se<53><65>~<7E>uLx<4C><78>oc<6F><63><13><><EFBFBD><EFBFBD>e<EFBFBD><65><EFBFBD>:D<>7<EFBFBD>Ә<EFBFBD>g<EFBFBD><67><EFBFBD>_B<5F>=":<3A><><EFBFBD><EFBFBD><EFBFBD>S<EFBFBD>^ET<45><54><EFBFBD><06>u<EFBFBD>p?<3F>6M^

View File

@@ -0,0 +1,3 @@
x<01><>;
1@<40>s<EFBFBD><73><EFBFBD>:<3A>f7<10><>#x<><78>L<EFBFBD><4C><EFBFBD>!F<><EFBFBD><E3BB88>+<2B>+<2B><><EFBFBD><EFBFBD>h<EFBFBD><68><EFBFBD>Z<EFBFBD>b<>H<EFBFBD><48><1F><><EFBFBD><19>J<J<08><>B
VK6 <0B><5<>J<EFBFBD><08><><EFBFBD>)J1I<17><>)<29>d#1<> T<><54>w<EFBFBD><77><15>+<2B><>3<EFBFBD><33><EFBFBD>+<2B><>`{<7B><>;<3B>=FD#ߡ<>ٿ<EFBFBD><D9BF><EFBFBD>B<EFBFBD><<3C><><<3C><><EFBFBD>3<>>=

View File

@@ -0,0 +1 @@
x<01><>1n<31>0 E3<45><14> <04>-<2D>&<04><><13><02>H&F+<2B>ա<EFBFBD>o<EFBFBD>#d<><64><1B><>/<2F><>X: <0C><>zS<7A>i<1A><>Z<EFBFBD>b1<62>ؔSa<53><61>g<EFBFBD>@<40>F<1F>35];̈<>'Ɇ%s<>D<EFBFBD><44>5<EFBFBD><35><EFBFBD><EFBFBD><EFBFBD><EFBFBD>R,<2C><>w<EFBFBD><77>_m<5F><EFBFBD>k<EFBFBD>C<43><7F><EFBFBD><EFBFBD><EFBFBD>v<EFBFBD>"?<3F><>}m<>#<23>8"<22>#|xDt<44><74><EFBFBD><EFBFBD><EFBFBD>f<EFBFBD><66>ET <20>z<EFBFBD><15><>z<EFBFBD><7A>/<2F><>N<EFBFBD>

View File

@@ -0,0 +1,2 @@
x<01><>M
1 @a<>=E.<2E><><EFBFBD><EFBFBD>ӂ.<<3C>H۔<11>j<>9<EFBFBD><39>\<5C>o<EFBFBD><6F>6<>Co<43>@<40>2<EFBFBD>DI+QA<51><41>[V

View File

@@ -0,0 +1 @@
2839944139e0de9737a044f78b0e4b40d989a9e3

View File

@@ -0,0 +1 @@
5c80b0245c1c6f8343fa418ec374b13b5d4ee658

View File

@@ -0,0 +1 @@
37991dec2c8e592043f47155ce4808d4580f9123

View File

@@ -0,0 +1 @@
3ad28a9149a2864384548f3d17ed7f38014c9e8a

98
modules/git/tree.go Normal file
View File

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

59
modules/git/tree_blob.go Normal file
View File

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

205
modules/git/tree_entry.go Normal file
View File

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

View File

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

96
modules/git/utils.go Normal file
View File

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