Use native git variants by default with go-git variants as build tag (#13673)
* Move last commit cache back into modules/git Signed-off-by: Andrew Thornton <art27@cantab.net> * Remove go-git from the interface for last commit cache Signed-off-by: Andrew Thornton <art27@cantab.net> * move cacheref to last_commit_cache Signed-off-by: Andrew Thornton <art27@cantab.net> * Remove go-git from routers/private/hook Signed-off-by: Andrew Thornton <art27@cantab.net> * Move FindLFSFiles to pipeline Signed-off-by: Andrew Thornton <art27@cantab.net> * Make no-go-git variants Signed-off-by: Andrew Thornton <art27@cantab.net> * Submodule RefID Signed-off-by: Andrew Thornton <art27@cantab.net> * fix issue with GetCommitsInfo Signed-off-by: Andrew Thornton <art27@cantab.net> * fix GetLastCommitForPaths Signed-off-by: Andrew Thornton <art27@cantab.net> * Improve efficiency Signed-off-by: Andrew Thornton <art27@cantab.net> * More efficiency Signed-off-by: Andrew Thornton <art27@cantab.net> * even faster Signed-off-by: Andrew Thornton <art27@cantab.net> * Reduce duplication * As per @lunny Signed-off-by: Andrew Thornton <art27@cantab.net> * attempt to fix drone Signed-off-by: Andrew Thornton <art27@cantab.net> * fix test-tags Signed-off-by: Andrew Thornton <art27@cantab.net> * default to use no-go-git variants and add gogit build tag Signed-off-by: Andrew Thornton <art27@cantab.net> * placate lint Signed-off-by: Andrew Thornton <art27@cantab.net> * as per @6543 Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: techknowlogick <techknowlogick@gitea.io>
This commit is contained in:
parent
0851a89581
commit
511f6138d4
81 changed files with 3952 additions and 1492 deletions
29
.drone.yml
29
.drone.yml
|
@ -33,6 +33,16 @@ steps:
|
||||||
GOSUMDB: sum.golang.org
|
GOSUMDB: sum.golang.org
|
||||||
TAGS: bindata sqlite sqlite_unlock_notify
|
TAGS: bindata sqlite sqlite_unlock_notify
|
||||||
|
|
||||||
|
- name: lint-backend-gogit
|
||||||
|
pull: always
|
||||||
|
image: golang:1.15
|
||||||
|
commands:
|
||||||
|
- make lint-backend
|
||||||
|
environment:
|
||||||
|
GOPROXY: https://goproxy.cn # proxy.golang.org is blocked in China, this proxy is not
|
||||||
|
GOSUMDB: sum.golang.org
|
||||||
|
TAGS: bindata gogit sqlite sqlite_unlock_notify
|
||||||
|
|
||||||
- name: checks-frontend
|
- name: checks-frontend
|
||||||
image: node:14
|
image: node:14
|
||||||
commands:
|
commands:
|
||||||
|
@ -69,7 +79,7 @@ steps:
|
||||||
GOPROXY: off
|
GOPROXY: off
|
||||||
GOOS: linux
|
GOOS: linux
|
||||||
GOARCH: arm64
|
GOARCH: arm64
|
||||||
TAGS: bindata
|
TAGS: bindata gogit
|
||||||
commands:
|
commands:
|
||||||
- make backend # test cross compile
|
- make backend # test cross compile
|
||||||
- rm ./gitea # clean
|
- rm ./gitea # clean
|
||||||
|
@ -173,6 +183,17 @@ steps:
|
||||||
GITHUB_READ_TOKEN:
|
GITHUB_READ_TOKEN:
|
||||||
from_secret: github_read_token
|
from_secret: github_read_token
|
||||||
|
|
||||||
|
- name: unit-test-gogit
|
||||||
|
pull: always
|
||||||
|
image: golang:1.15
|
||||||
|
commands:
|
||||||
|
- make unit-test-coverage test-check
|
||||||
|
environment:
|
||||||
|
GOPROXY: off
|
||||||
|
TAGS: bindata gogit sqlite sqlite_unlock_notify
|
||||||
|
GITHUB_READ_TOKEN:
|
||||||
|
from_secret: github_read_token
|
||||||
|
|
||||||
- name: test-mysql
|
- name: test-mysql
|
||||||
image: golang:1.15
|
image: golang:1.15
|
||||||
commands:
|
commands:
|
||||||
|
@ -305,7 +326,8 @@ steps:
|
||||||
- timeout -s ABRT 40m make test-sqlite-migration test-sqlite
|
- timeout -s ABRT 40m make test-sqlite-migration test-sqlite
|
||||||
environment:
|
environment:
|
||||||
GOPROXY: off
|
GOPROXY: off
|
||||||
TAGS: bindata
|
TAGS: bindata gogit sqlite sqlite_unlock_notify
|
||||||
|
TEST_TAGS: gogit sqlite sqlite_unlock_notify
|
||||||
USE_REPO_TEST_DIR: 1
|
USE_REPO_TEST_DIR: 1
|
||||||
depends_on:
|
depends_on:
|
||||||
- build
|
- build
|
||||||
|
@ -318,7 +340,8 @@ steps:
|
||||||
- timeout -s ABRT 40m make test-pgsql-migration test-pgsql
|
- timeout -s ABRT 40m make test-pgsql-migration test-pgsql
|
||||||
environment:
|
environment:
|
||||||
GOPROXY: off
|
GOPROXY: off
|
||||||
TAGS: bindata
|
TAGS: bindata gogit
|
||||||
|
TEST_TAGS: gogit
|
||||||
TEST_LDAP: 1
|
TEST_LDAP: 1
|
||||||
USE_REPO_TEST_DIR: 1
|
USE_REPO_TEST_DIR: 1
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
19
Makefile
19
Makefile
|
@ -110,7 +110,10 @@ TAGS ?=
|
||||||
TAGS_SPLIT := $(subst $(COMMA), ,$(TAGS))
|
TAGS_SPLIT := $(subst $(COMMA), ,$(TAGS))
|
||||||
TAGS_EVIDENCE := $(MAKE_EVIDENCE_DIR)/tags
|
TAGS_EVIDENCE := $(MAKE_EVIDENCE_DIR)/tags
|
||||||
|
|
||||||
|
TEST_TAGS ?= sqlite sqlite_unlock_notify
|
||||||
|
|
||||||
GO_DIRS := cmd integrations models modules routers build services vendor tools
|
GO_DIRS := cmd integrations models modules routers build services vendor tools
|
||||||
|
|
||||||
GO_SOURCES := $(wildcard *.go)
|
GO_SOURCES := $(wildcard *.go)
|
||||||
GO_SOURCES += $(shell find $(GO_DIRS) -type f -name "*.go" -not -path modules/options/bindata.go -not -path modules/public/bindata.go -not -path modules/templates/bindata.go)
|
GO_SOURCES += $(shell find $(GO_DIRS) -type f -name "*.go" -not -path modules/options/bindata.go -not -path modules/public/bindata.go -not -path modules/templates/bindata.go)
|
||||||
|
|
||||||
|
@ -339,8 +342,8 @@ watch-backend: go-check
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test:
|
test:
|
||||||
@echo "Running go test..."
|
@echo "Running go test with -tags '$(TEST_TAGS)'..."
|
||||||
@$(GO) test $(GOTESTFLAGS) -mod=vendor -tags='sqlite sqlite_unlock_notify' $(GO_PACKAGES)
|
@$(GO) test $(GOTESTFLAGS) -mod=vendor -tags='$(TEST_TAGS)' $(GO_PACKAGES)
|
||||||
|
|
||||||
.PHONY: test-check
|
.PHONY: test-check
|
||||||
test-check:
|
test-check:
|
||||||
|
@ -356,8 +359,8 @@ test-check:
|
||||||
|
|
||||||
.PHONY: test\#%
|
.PHONY: test\#%
|
||||||
test\#%:
|
test\#%:
|
||||||
@echo "Running go test..."
|
@echo "Running go test with -tags '$(TEST_TAGS)'..."
|
||||||
@$(GO) test -mod=vendor -tags='sqlite sqlite_unlock_notify' -run $(subst .,/,$*) $(GO_PACKAGES)
|
@$(GO) test -mod=vendor -tags='$(TEST_TAGS)' -run $(subst .,/,$*) $(GO_PACKAGES)
|
||||||
|
|
||||||
.PHONY: coverage
|
.PHONY: coverage
|
||||||
coverage:
|
coverage:
|
||||||
|
@ -365,8 +368,8 @@ coverage:
|
||||||
|
|
||||||
.PHONY: unit-test-coverage
|
.PHONY: unit-test-coverage
|
||||||
unit-test-coverage:
|
unit-test-coverage:
|
||||||
@echo "Running unit-test-coverage..."
|
@echo "Running unit-test-coverage -tags '$(TEST_TAGS)'..."
|
||||||
@$(GO) test $(GOTESTFLAGS) -mod=vendor -tags='sqlite sqlite_unlock_notify' -cover -coverprofile coverage.out $(GO_PACKAGES) && echo "\n==>\033[32m Ok\033[m\n" || exit 1
|
@$(GO) test $(GOTESTFLAGS) -mod=vendor -tags='$(TEST_TAGS)' -cover -coverprofile coverage.out $(GO_PACKAGES) && echo "\n==>\033[32m Ok\033[m\n" || exit 1
|
||||||
|
|
||||||
.PHONY: vendor
|
.PHONY: vendor
|
||||||
vendor:
|
vendor:
|
||||||
|
@ -511,7 +514,7 @@ integrations.mssql.test: git-check $(GO_SOURCES)
|
||||||
$(GO) test $(GOTESTFLAGS) -mod=vendor -c code.gitea.io/gitea/integrations -o integrations.mssql.test
|
$(GO) test $(GOTESTFLAGS) -mod=vendor -c code.gitea.io/gitea/integrations -o integrations.mssql.test
|
||||||
|
|
||||||
integrations.sqlite.test: git-check $(GO_SOURCES)
|
integrations.sqlite.test: git-check $(GO_SOURCES)
|
||||||
$(GO) test $(GOTESTFLAGS) -mod=vendor -c code.gitea.io/gitea/integrations -o integrations.sqlite.test -tags 'sqlite sqlite_unlock_notify'
|
$(GO) test $(GOTESTFLAGS) -mod=vendor -c code.gitea.io/gitea/integrations -o integrations.sqlite.test -tags '$(TEST_TAGS)'
|
||||||
|
|
||||||
integrations.cover.test: git-check $(GO_SOURCES)
|
integrations.cover.test: git-check $(GO_SOURCES)
|
||||||
$(GO) test $(GOTESTFLAGS) -mod=vendor -c code.gitea.io/gitea/integrations -coverpkg $(shell echo $(GO_PACKAGES) | tr ' ' ',') -o integrations.cover.test
|
$(GO) test $(GOTESTFLAGS) -mod=vendor -c code.gitea.io/gitea/integrations -coverpkg $(shell echo $(GO_PACKAGES) | tr ' ' ',') -o integrations.cover.test
|
||||||
|
@ -534,7 +537,7 @@ migrations.mssql.test: $(GO_SOURCES)
|
||||||
|
|
||||||
.PHONY: migrations.sqlite.test
|
.PHONY: migrations.sqlite.test
|
||||||
migrations.sqlite.test: $(GO_SOURCES)
|
migrations.sqlite.test: $(GO_SOURCES)
|
||||||
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations/migration-test -o migrations.sqlite.test -tags 'sqlite sqlite_unlock_notify'
|
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations/migration-test -o migrations.sqlite.test -tags '$(TEST_TAGS)'
|
||||||
|
|
||||||
.PHONY: check
|
.PHONY: check
|
||||||
check: test
|
check: test
|
||||||
|
|
|
@ -101,6 +101,7 @@ Depending on requirements, the following build tags can be included.
|
||||||
- `pam`: Enable support for PAM (Linux Pluggable Authentication Modules). Can
|
- `pam`: Enable support for PAM (Linux Pluggable Authentication Modules). Can
|
||||||
be used to authenticate local users or extend authentication to methods
|
be used to authenticate local users or extend authentication to methods
|
||||||
available to PAM.
|
available to PAM.
|
||||||
|
* `gogit`: (EXPERIMENTAL) Use go-git variants of git commands.
|
||||||
|
|
||||||
Bundling assets into the binary using the `bindata` build tag is recommended for
|
Bundling assets into the binary using the `bindata` build tag is recommended for
|
||||||
production deployments. It is possible to serve the static assets directly via a reverse proxy,
|
production deployments. It is possible to serve the static assets directly via a reverse proxy,
|
||||||
|
|
23
modules/cache/cache.go
vendored
23
modules/cache/cache.go
vendored
|
@ -27,6 +27,24 @@ func newCache(cacheConfig setting.Cache) (mc.Cache, error) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache is the interface that operates the cache data.
|
||||||
|
type Cache interface {
|
||||||
|
// Put puts value into cache with key and expire time.
|
||||||
|
Put(key string, val interface{}, timeout int64) error
|
||||||
|
// Get gets cached value by given key.
|
||||||
|
Get(key string) interface{}
|
||||||
|
// Delete deletes cached value by given key.
|
||||||
|
Delete(key string) error
|
||||||
|
// Incr increases cached int-type value by given key as a counter.
|
||||||
|
Incr(key string) error
|
||||||
|
// Decr decreases cached int-type value by given key as a counter.
|
||||||
|
Decr(key string) error
|
||||||
|
// IsExist returns true if cached value exists.
|
||||||
|
IsExist(key string) bool
|
||||||
|
// Flush deletes all cached data.
|
||||||
|
Flush() error
|
||||||
|
}
|
||||||
|
|
||||||
// NewContext start cache service
|
// NewContext start cache service
|
||||||
func NewContext() error {
|
func NewContext() error {
|
||||||
var err error
|
var err error
|
||||||
|
@ -40,6 +58,11 @@ func NewContext() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCache returns the currently configured cache
|
||||||
|
func GetCache() Cache {
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
|
||||||
// GetString returns the key value from cache with callback when no key exists in cache
|
// GetString returns the key value from cache with callback when no key exists in cache
|
||||||
func GetString(key string, getFunc func() (string, error)) (string, error) {
|
func GetString(key string, getFunc func() (string, error)) (string, error) {
|
||||||
if conn == nil || setting.CacheService.TTL == 0 {
|
if conn == nil || setting.CacheService.TTL == 0 {
|
||||||
|
|
70
modules/cache/last_commit.go
vendored
70
modules/cache/last_commit.go
vendored
|
@ -1,70 +0,0 @@
|
||||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a MIT-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package cache
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha256"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
|
||||||
|
|
||||||
mc "gitea.com/macaron/cache"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LastCommitCache represents a cache to store last commit
|
|
||||||
type LastCommitCache struct {
|
|
||||||
repoPath string
|
|
||||||
ttl int64
|
|
||||||
repo *git.Repository
|
|
||||||
commitCache map[string]*object.Commit
|
|
||||||
mc.Cache
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewLastCommitCache creates a new last commit cache for repo
|
|
||||||
func NewLastCommitCache(repoPath string, gitRepo *git.Repository, ttl int64) *LastCommitCache {
|
|
||||||
return &LastCommitCache{
|
|
||||||
repoPath: repoPath,
|
|
||||||
repo: gitRepo,
|
|
||||||
commitCache: make(map[string]*object.Commit),
|
|
||||||
ttl: ttl,
|
|
||||||
Cache: conn,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c LastCommitCache) getCacheKey(repoPath, ref, entryPath string) string {
|
|
||||||
hashBytes := sha256.Sum256([]byte(fmt.Sprintf("%s:%s:%s", repoPath, ref, entryPath)))
|
|
||||||
return fmt.Sprintf("last_commit:%x", hashBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get get the last commit information by commit id and entry path
|
|
||||||
func (c LastCommitCache) Get(ref, entryPath string) (*object.Commit, error) {
|
|
||||||
v := c.Cache.Get(c.getCacheKey(c.repoPath, ref, entryPath))
|
|
||||||
if vs, ok := v.(string); ok {
|
|
||||||
log.Trace("LastCommitCache hit level 1: [%s:%s:%s]", ref, entryPath, vs)
|
|
||||||
if commit, ok := c.commitCache[vs]; ok {
|
|
||||||
log.Trace("LastCommitCache hit level 2: [%s:%s:%s]", ref, entryPath, vs)
|
|
||||||
return commit, nil
|
|
||||||
}
|
|
||||||
id, err := c.repo.ConvertToSHA1(vs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
commit, err := c.repo.GoGitRepo().CommitObject(id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
c.commitCache[vs] = commit
|
|
||||||
return commit, nil
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put put the last commit id with commit and entry path
|
|
||||||
func (c LastCommitCache) Put(ref, entryPath, commitID string) error {
|
|
||||||
log.Trace("LastCommitCache save: [%s:%s:%s]", ref, entryPath, commitID)
|
|
||||||
return c.Cache.Put(c.getCacheKey(c.repoPath, ref, entryPath), commitID, c.ttl)
|
|
||||||
}
|
|
|
@ -13,7 +13,6 @@ import (
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -21,7 +20,7 @@ func TestToCommitMeta(t *testing.T) {
|
||||||
assert.NoError(t, models.PrepareTestDatabase())
|
assert.NoError(t, models.PrepareTestDatabase())
|
||||||
headRepo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
|
headRepo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
|
||||||
sha1, _ := git.NewIDFromString("0000000000000000000000000000000000000000")
|
sha1, _ := git.NewIDFromString("0000000000000000000000000000000000000000")
|
||||||
signature := &object.Signature{Name: "Test Signature", Email: "test@email.com", When: time.Unix(0, 0)}
|
signature := &git.Signature{Name: "Test Signature", Email: "test@email.com", When: time.Unix(0, 0)}
|
||||||
tag := &git.Tag{
|
tag := &git.Tag{
|
||||||
Name: "Test Tag",
|
Name: "Test Tag",
|
||||||
ID: sha1,
|
ID: sha1,
|
||||||
|
|
243
modules/git/batch_reader_nogogit.go
Normal file
243
modules/git/batch_reader_nogogit.go
Normal file
|
@ -0,0 +1,243 @@
|
||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadBatchLine reads the header line from cat-file --batch
|
||||||
|
// We expect:
|
||||||
|
// <sha> SP <type> SP <size> LF
|
||||||
|
func ReadBatchLine(rd *bufio.Reader) (sha []byte, typ string, size int64, err error) {
|
||||||
|
sha, err = rd.ReadBytes(' ')
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sha = sha[:len(sha)-1]
|
||||||
|
|
||||||
|
typ, err = rd.ReadString(' ')
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
typ = typ[:len(typ)-1]
|
||||||
|
|
||||||
|
var sizeStr string
|
||||||
|
sizeStr, err = rd.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
size, err = strconv.ParseInt(sizeStr[:len(sizeStr)-1], 10, 64)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadTagObjectID reads a tag object ID hash from a cat-file --batch stream, throwing away the rest of the stream.
|
||||||
|
func ReadTagObjectID(rd *bufio.Reader, size int64) (string, error) {
|
||||||
|
id := ""
|
||||||
|
var n int64
|
||||||
|
headerLoop:
|
||||||
|
for {
|
||||||
|
line, err := rd.ReadBytes('\n')
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
n += int64(len(line))
|
||||||
|
idx := bytes.Index(line, []byte{' '})
|
||||||
|
if idx < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(line[:idx]) == "object" {
|
||||||
|
id = string(line[idx+1 : len(line)-1])
|
||||||
|
break headerLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discard the rest of the tag
|
||||||
|
discard := size - n
|
||||||
|
for discard > math.MaxInt32 {
|
||||||
|
_, err := rd.Discard(math.MaxInt32)
|
||||||
|
if err != nil {
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
discard -= math.MaxInt32
|
||||||
|
}
|
||||||
|
_, err := rd.Discard(int(discard))
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadTreeID reads a tree ID from a cat-file --batch stream, throwing away the rest of the stream.
|
||||||
|
func ReadTreeID(rd *bufio.Reader, size int64) (string, error) {
|
||||||
|
id := ""
|
||||||
|
var n int64
|
||||||
|
headerLoop:
|
||||||
|
for {
|
||||||
|
line, err := rd.ReadBytes('\n')
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
n += int64(len(line))
|
||||||
|
idx := bytes.Index(line, []byte{' '})
|
||||||
|
if idx < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(line[:idx]) == "tree" {
|
||||||
|
id = string(line[idx+1 : len(line)-1])
|
||||||
|
break headerLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discard the rest of the commit
|
||||||
|
discard := size - n
|
||||||
|
for discard > math.MaxInt32 {
|
||||||
|
_, err := rd.Discard(math.MaxInt32)
|
||||||
|
if err != nil {
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
discard -= math.MaxInt32
|
||||||
|
}
|
||||||
|
_, err := rd.Discard(int(discard))
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// git tree files are a list:
|
||||||
|
// <mode-in-ascii> SP <fname> NUL <20-byte SHA>
|
||||||
|
//
|
||||||
|
// Unfortunately this 20-byte notation is somewhat in conflict to all other git tools
|
||||||
|
// Therefore we need some method to convert these 20-byte SHAs to a 40-byte SHA
|
||||||
|
|
||||||
|
// constant hextable to help quickly convert between 20byte and 40byte hashes
|
||||||
|
const hextable = "0123456789abcdef"
|
||||||
|
|
||||||
|
// to40ByteSHA converts a 20-byte SHA in a 40-byte slice into a 40-byte sha in place
|
||||||
|
// without allocations. This is at least 100x quicker that hex.EncodeToString
|
||||||
|
// NB This requires that sha is a 40-byte slice
|
||||||
|
func to40ByteSHA(sha []byte) []byte {
|
||||||
|
for i := 19; i >= 0; i-- {
|
||||||
|
v := sha[i]
|
||||||
|
vhi, vlo := v>>4, v&0x0f
|
||||||
|
shi, slo := hextable[vhi], hextable[vlo]
|
||||||
|
sha[i*2], sha[i*2+1] = shi, slo
|
||||||
|
}
|
||||||
|
return sha
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseTreeLineSkipMode reads an entry from a tree in a cat-file --batch stream
|
||||||
|
// This simply skips the mode - saving a substantial amount of time and carefully avoids allocations - except where fnameBuf is too small.
|
||||||
|
// It is recommended therefore to pass in an fnameBuf large enough to avoid almost all allocations
|
||||||
|
//
|
||||||
|
// Each line is composed of:
|
||||||
|
// <mode-in-ascii-dropping-initial-zeros> SP <fname> NUL <20-byte SHA>
|
||||||
|
//
|
||||||
|
// We don't attempt to convert the 20-byte SHA to 40-byte SHA to save a lot of time
|
||||||
|
func ParseTreeLineSkipMode(rd *bufio.Reader, fnameBuf, shaBuf []byte) (fname, sha []byte, n int, err error) {
|
||||||
|
var readBytes []byte
|
||||||
|
// Skip the Mode
|
||||||
|
readBytes, err = rd.ReadSlice(' ') // NB: DOES NOT ALLOCATE SIMPLY RETURNS SLICE WITHIN READER BUFFER
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n += len(readBytes)
|
||||||
|
|
||||||
|
// Deal with the fname
|
||||||
|
readBytes, err = rd.ReadSlice('\x00')
|
||||||
|
copy(fnameBuf, readBytes)
|
||||||
|
if len(fnameBuf) > len(readBytes) {
|
||||||
|
fnameBuf = fnameBuf[:len(readBytes)] // cut the buf the correct size
|
||||||
|
} else {
|
||||||
|
fnameBuf = append(fnameBuf, readBytes[len(fnameBuf):]...) // extend the buf and copy in the missing bits
|
||||||
|
}
|
||||||
|
for err == bufio.ErrBufferFull { // Then we need to read more
|
||||||
|
readBytes, err = rd.ReadSlice('\x00')
|
||||||
|
fnameBuf = append(fnameBuf, readBytes...) // there is little point attempting to avoid allocations here so just extend
|
||||||
|
}
|
||||||
|
n += len(fnameBuf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fnameBuf = fnameBuf[:len(fnameBuf)-1] // Drop the terminal NUL
|
||||||
|
fname = fnameBuf // set the returnable fname to the slice
|
||||||
|
|
||||||
|
// Now deal with the 20-byte SHA
|
||||||
|
idx := 0
|
||||||
|
for idx < 20 {
|
||||||
|
read := 0
|
||||||
|
read, err = rd.Read(shaBuf[idx:20])
|
||||||
|
n += read
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
idx += read
|
||||||
|
}
|
||||||
|
sha = shaBuf
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseTreeLine reads an entry from a tree in a cat-file --batch stream
|
||||||
|
// This carefully avoids allocations - except where fnameBuf is too small.
|
||||||
|
// It is recommended therefore to pass in an fnameBuf large enough to avoid almost all allocations
|
||||||
|
//
|
||||||
|
// Each line is composed of:
|
||||||
|
// <mode-in-ascii-dropping-initial-zeros> SP <fname> NUL <20-byte SHA>
|
||||||
|
//
|
||||||
|
// We don't attempt to convert the 20-byte SHA to 40-byte SHA to save a lot of time
|
||||||
|
func ParseTreeLine(rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) {
|
||||||
|
var readBytes []byte
|
||||||
|
|
||||||
|
// Read the Mode
|
||||||
|
readBytes, err = rd.ReadSlice(' ')
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n += len(readBytes)
|
||||||
|
copy(modeBuf, readBytes)
|
||||||
|
if len(modeBuf) > len(readBytes) {
|
||||||
|
modeBuf = modeBuf[:len(readBytes)]
|
||||||
|
} else {
|
||||||
|
modeBuf = append(modeBuf, readBytes[len(modeBuf):]...)
|
||||||
|
|
||||||
|
}
|
||||||
|
mode = modeBuf[:len(modeBuf)-1] // Drop the SP
|
||||||
|
|
||||||
|
// Deal with the fname
|
||||||
|
readBytes, err = rd.ReadSlice('\x00')
|
||||||
|
copy(fnameBuf, readBytes)
|
||||||
|
if len(fnameBuf) > len(readBytes) {
|
||||||
|
fnameBuf = fnameBuf[:len(readBytes)]
|
||||||
|
} else {
|
||||||
|
fnameBuf = append(fnameBuf, readBytes[len(fnameBuf):]...)
|
||||||
|
}
|
||||||
|
for err == bufio.ErrBufferFull {
|
||||||
|
readBytes, err = rd.ReadSlice('\x00')
|
||||||
|
fnameBuf = append(fnameBuf, readBytes...)
|
||||||
|
}
|
||||||
|
n += len(fnameBuf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fnameBuf = fnameBuf[:len(fnameBuf)-1]
|
||||||
|
fname = fnameBuf
|
||||||
|
|
||||||
|
// Deal with the 20-byte SHA
|
||||||
|
idx := 0
|
||||||
|
for idx < 20 {
|
||||||
|
read := 0
|
||||||
|
read, err = rd.Read(shaBuf[idx:20])
|
||||||
|
n += read
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
idx += read
|
||||||
|
}
|
||||||
|
sha = shaBuf
|
||||||
|
return
|
||||||
|
}
|
|
@ -10,28 +10,9 @@ import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Blob represents a Git object.
|
// This file contains common functions between the gogit and !gogit variants for git Blobs
|
||||||
type Blob struct {
|
|
||||||
ID SHA1
|
|
||||||
|
|
||||||
gogitEncodedObj plumbing.EncodedObject
|
|
||||||
name string
|
|
||||||
}
|
|
||||||
|
|
||||||
// DataAsync gets a ReadCloser for the contents of a blob without reading it all.
|
|
||||||
// Calling the Close function on the result will discard all unread output.
|
|
||||||
func (b *Blob) DataAsync() (io.ReadCloser, error) {
|
|
||||||
return b.gogitEncodedObj.Reader()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Size returns the uncompressed size of the blob
|
|
||||||
func (b *Blob) Size() int64 {
|
|
||||||
return b.gogitEncodedObj.Size()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name returns name of the tree entry this blob object was created from (or empty string)
|
// Name returns name of the tree entry this blob object was created from (or empty string)
|
||||||
func (b *Blob) Name() string {
|
func (b *Blob) Name() string {
|
||||||
|
|
33
modules/git/blob_gogit.go
Normal file
33
modules/git/blob_gogit.go
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Blob represents a Git object.
|
||||||
|
type Blob struct {
|
||||||
|
ID SHA1
|
||||||
|
|
||||||
|
gogitEncodedObj plumbing.EncodedObject
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// DataAsync gets a ReadCloser for the contents of a blob without reading it all.
|
||||||
|
// Calling the Close function on the result will discard all unread output.
|
||||||
|
func (b *Blob) DataAsync() (io.ReadCloser, error) {
|
||||||
|
return b.gogitEncodedObj.Reader()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size returns the uncompressed size of the blob
|
||||||
|
func (b *Blob) Size() int64 {
|
||||||
|
return b.gogitEncodedObj.Size()
|
||||||
|
}
|
77
modules/git/blob_nogogit.go
Normal file
77
modules/git/blob_nogogit.go
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Blob represents a Git object.
|
||||||
|
type Blob struct {
|
||||||
|
ID SHA1
|
||||||
|
|
||||||
|
gotSize bool
|
||||||
|
size int64
|
||||||
|
repoPath string
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// DataAsync gets a ReadCloser for the contents of a blob without reading it all.
|
||||||
|
// Calling the Close function on the result will discard all unread output.
|
||||||
|
func (b *Blob) DataAsync() (io.ReadCloser, error) {
|
||||||
|
stdoutReader, stdoutWriter := io.Pipe()
|
||||||
|
var err error
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
stderr := &strings.Builder{}
|
||||||
|
err = NewCommand("cat-file", "--batch").RunInDirFullPipeline(b.repoPath, stdoutWriter, stderr, strings.NewReader(b.ID.String()+"\n"))
|
||||||
|
if err != nil {
|
||||||
|
err = ConcatenateError(err, stderr.String())
|
||||||
|
_ = stdoutWriter.CloseWithError(err)
|
||||||
|
} else {
|
||||||
|
_ = stdoutWriter.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
bufReader := bufio.NewReader(stdoutReader)
|
||||||
|
_, _, size, err := ReadBatchLine(bufReader)
|
||||||
|
if err != nil {
|
||||||
|
stdoutReader.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &LimitedReaderCloser{
|
||||||
|
R: bufReader,
|
||||||
|
C: stdoutReader,
|
||||||
|
N: int64(size),
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size returns the uncompressed size of the blob
|
||||||
|
func (b *Blob) Size() int64 {
|
||||||
|
if b.gotSize {
|
||||||
|
return b.size
|
||||||
|
}
|
||||||
|
|
||||||
|
size, err := NewCommand("cat-file", "-s", b.ID.String()).RunInDir(b.repoPath)
|
||||||
|
if err != nil {
|
||||||
|
log("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repoPath, err)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
b.size, err = strconv.ParseInt(size[:len(size)-1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
log("error whilst parsing size %s for %s in %s. Error: %v", size, b.ID.String(), b.repoPath, err)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
b.gotSize = true
|
||||||
|
|
||||||
|
return b.size
|
||||||
|
}
|
|
@ -1,13 +0,0 @@
|
||||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a MIT-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package git
|
|
||||||
|
|
||||||
import "github.com/go-git/go-git/v5/plumbing/object"
|
|
||||||
|
|
||||||
// LastCommitCache cache
|
|
||||||
type LastCommitCache interface {
|
|
||||||
Get(ref, entryPath string) (*object.Commit, error)
|
|
||||||
Put(ref, entryPath, commitID string) error
|
|
||||||
}
|
|
|
@ -189,7 +189,7 @@ func (c *Command) RunInDirTimeoutEnv(env []string, timeout time.Duration, dir st
|
||||||
stdout := new(bytes.Buffer)
|
stdout := new(bytes.Buffer)
|
||||||
stderr := new(bytes.Buffer)
|
stderr := new(bytes.Buffer)
|
||||||
if err := c.RunInDirTimeoutEnvPipeline(env, timeout, dir, stdout, stderr); err != nil {
|
if err := c.RunInDirTimeoutEnvPipeline(env, timeout, dir, stdout, stderr); err != nil {
|
||||||
return nil, concatenateError(err, stderr.String())
|
return nil, ConcatenateError(err, stderr.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
if stdout.Len() > 0 {
|
if stdout.Len() > 0 {
|
||||||
|
|
|
@ -19,8 +19,6 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Commit represents a git commit.
|
// Commit represents a git commit.
|
||||||
|
@ -43,61 +41,6 @@ type CommitGPGSignature struct {
|
||||||
Payload string //TODO check if can be reconstruct from the rest of commit information to not have duplicate data
|
Payload string //TODO check if can be reconstruct from the rest of commit information to not have duplicate data
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertPGPSignature(c *object.Commit) *CommitGPGSignature {
|
|
||||||
if c.PGPSignature == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var w strings.Builder
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if _, err = fmt.Fprintf(&w, "tree %s\n", c.TreeHash.String()); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, parent := range c.ParentHashes {
|
|
||||||
if _, err = fmt.Fprintf(&w, "parent %s\n", parent.String()); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = fmt.Fprint(&w, "author "); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = c.Author.Encode(&w); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = fmt.Fprint(&w, "\ncommitter "); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = c.Committer.Encode(&w); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = fmt.Fprintf(&w, "\n\n%s", c.Message); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &CommitGPGSignature{
|
|
||||||
Signature: c.PGPSignature,
|
|
||||||
Payload: w.String(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertCommit(c *object.Commit) *Commit {
|
|
||||||
return &Commit{
|
|
||||||
ID: c.Hash,
|
|
||||||
CommitMessage: c.Message,
|
|
||||||
Committer: &c.Committer,
|
|
||||||
Author: &c.Author,
|
|
||||||
Signature: convertPGPSignature(c),
|
|
||||||
Parents: c.ParentHashes,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Message returns the commit message. Same as retrieving CommitMessage directly.
|
// Message returns the commit message. Same as retrieving CommitMessage directly.
|
||||||
func (c *Commit) Message() string {
|
func (c *Commit) Message() string {
|
||||||
return c.CommitMessage
|
return c.CommitMessage
|
||||||
|
@ -576,7 +519,7 @@ func GetCommitFileStatus(repoPath, commitID string) (*CommitFileStatus, error) {
|
||||||
err := NewCommand("show", "--name-status", "--pretty=format:''", commitID).RunInDirPipeline(repoPath, w, stderr)
|
err := NewCommand("show", "--name-status", "--pretty=format:''", commitID).RunInDirPipeline(repoPath, w, stderr)
|
||||||
w.Close() // Close writer to exit parsing goroutine
|
w.Close() // Close writer to exit parsing goroutine
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, concatenateError(err, stderr.String())
|
return nil, ConcatenateError(err, stderr.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
<-done
|
<-done
|
||||||
|
|
70
modules/git/commit_convert_gogit.go
Normal file
70
modules/git/commit_convert_gogit.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||||
|
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/object"
|
||||||
|
)
|
||||||
|
|
||||||
|
func convertPGPSignature(c *object.Commit) *CommitGPGSignature {
|
||||||
|
if c.PGPSignature == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var w strings.Builder
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if _, err = fmt.Fprintf(&w, "tree %s\n", c.TreeHash.String()); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, parent := range c.ParentHashes {
|
||||||
|
if _, err = fmt.Fprintf(&w, "parent %s\n", parent.String()); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = fmt.Fprint(&w, "author "); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = c.Author.Encode(&w); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = fmt.Fprint(&w, "\ncommitter "); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = c.Committer.Encode(&w); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = fmt.Fprintf(&w, "\n\n%s", c.Message); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CommitGPGSignature{
|
||||||
|
Signature: c.PGPSignature,
|
||||||
|
Payload: w.String(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertCommit(c *object.Commit) *Commit {
|
||||||
|
return &Commit{
|
||||||
|
ID: c.Hash,
|
||||||
|
CommitMessage: c.Message,
|
||||||
|
Committer: &c.Committer,
|
||||||
|
Author: &c.Author,
|
||||||
|
Signature: convertPGPSignature(c),
|
||||||
|
Parents: c.ParentHashes,
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,286 +4,9 @@
|
||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
// CommitInfo describes the first commit with the provided entry
|
||||||
"path"
|
type CommitInfo struct {
|
||||||
|
Entry *TreeEntry
|
||||||
"github.com/emirpasic/gods/trees/binaryheap"
|
Commit *Commit
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
SubModuleFile *SubModuleFile
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
|
||||||
cgobject "github.com/go-git/go-git/v5/plumbing/object/commitgraph"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetCommitsInfo gets information of all commits that are corresponding to these entries
|
|
||||||
func (tes Entries) GetCommitsInfo(commit *Commit, treePath string, cache LastCommitCache) ([][]interface{}, *Commit, error) {
|
|
||||||
entryPaths := make([]string, len(tes)+1)
|
|
||||||
// Get the commit for the treePath itself
|
|
||||||
entryPaths[0] = ""
|
|
||||||
for i, entry := range tes {
|
|
||||||
entryPaths[i+1] = entry.Name()
|
|
||||||
}
|
|
||||||
|
|
||||||
commitNodeIndex, commitGraphFile := commit.repo.CommitNodeIndex()
|
|
||||||
if commitGraphFile != nil {
|
|
||||||
defer commitGraphFile.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := commitNodeIndex.Get(commit.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var revs map[string]*object.Commit
|
|
||||||
if cache != nil {
|
|
||||||
var unHitPaths []string
|
|
||||||
revs, unHitPaths, err = getLastCommitForPathsByCache(commit.ID.String(), treePath, entryPaths, cache)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
if len(unHitPaths) > 0 {
|
|
||||||
revs2, err := GetLastCommitForPaths(c, treePath, unHitPaths)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range revs2 {
|
|
||||||
if err := cache.Put(commit.ID.String(), path.Join(treePath, k), v.ID().String()); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
revs[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
revs, err = GetLastCommitForPaths(c, treePath, entryPaths)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
commit.repo.gogitStorage.Close()
|
|
||||||
|
|
||||||
commitsInfo := make([][]interface{}, len(tes))
|
|
||||||
for i, entry := range tes {
|
|
||||||
if rev, ok := revs[entry.Name()]; ok {
|
|
||||||
entryCommit := convertCommit(rev)
|
|
||||||
if entry.IsSubModule() {
|
|
||||||
subModuleURL := ""
|
|
||||||
var fullPath string
|
|
||||||
if len(treePath) > 0 {
|
|
||||||
fullPath = treePath + "/" + entry.Name()
|
|
||||||
} else {
|
|
||||||
fullPath = entry.Name()
|
|
||||||
}
|
|
||||||
if subModule, err := commit.GetSubModule(fullPath); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
} else if subModule != nil {
|
|
||||||
subModuleURL = subModule.URL
|
|
||||||
}
|
|
||||||
subModuleFile := NewSubModuleFile(entryCommit, subModuleURL, entry.ID.String())
|
|
||||||
commitsInfo[i] = []interface{}{entry, subModuleFile}
|
|
||||||
} else {
|
|
||||||
commitsInfo[i] = []interface{}{entry, entryCommit}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
commitsInfo[i] = []interface{}{entry, nil}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve the commit for the treePath itself (see above). We basically
|
|
||||||
// get it for free during the tree traversal and it's used for listing
|
|
||||||
// pages to display information about newest commit for a given path.
|
|
||||||
var treeCommit *Commit
|
|
||||||
if treePath == "" {
|
|
||||||
treeCommit = commit
|
|
||||||
} else if rev, ok := revs[""]; ok {
|
|
||||||
treeCommit = convertCommit(rev)
|
|
||||||
treeCommit.repo = commit.repo
|
|
||||||
}
|
|
||||||
return commitsInfo, treeCommit, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type commitAndPaths struct {
|
|
||||||
commit cgobject.CommitNode
|
|
||||||
// Paths that are still on the branch represented by commit
|
|
||||||
paths []string
|
|
||||||
// Set of hashes for the paths
|
|
||||||
hashes map[string]plumbing.Hash
|
|
||||||
}
|
|
||||||
|
|
||||||
func getCommitTree(c cgobject.CommitNode, treePath string) (*object.Tree, error) {
|
|
||||||
tree, err := c.Tree()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optimize deep traversals by focusing only on the specific tree
|
|
||||||
if treePath != "" {
|
|
||||||
tree, err = tree.Tree(treePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tree, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getFileHashes(c cgobject.CommitNode, treePath string, paths []string) (map[string]plumbing.Hash, error) {
|
|
||||||
tree, err := getCommitTree(c, treePath)
|
|
||||||
if err == object.ErrDirectoryNotFound {
|
|
||||||
// The whole tree didn't exist, so return empty map
|
|
||||||
return make(map[string]plumbing.Hash), nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
hashes := make(map[string]plumbing.Hash)
|
|
||||||
for _, path := range paths {
|
|
||||||
if path != "" {
|
|
||||||
entry, err := tree.FindEntry(path)
|
|
||||||
if err == nil {
|
|
||||||
hashes[path] = entry.Hash
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
hashes[path] = tree.Hash
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return hashes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getLastCommitForPathsByCache(commitID, treePath string, paths []string, cache LastCommitCache) (map[string]*object.Commit, []string, error) {
|
|
||||||
var unHitEntryPaths []string
|
|
||||||
var results = make(map[string]*object.Commit)
|
|
||||||
for _, p := range paths {
|
|
||||||
lastCommit, err := cache.Get(commitID, path.Join(treePath, p))
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
if lastCommit != nil {
|
|
||||||
results[p] = lastCommit
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
unHitEntryPaths = append(unHitEntryPaths, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
return results, unHitEntryPaths, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLastCommitForPaths returns last commit information
|
|
||||||
func GetLastCommitForPaths(c cgobject.CommitNode, treePath string, paths []string) (map[string]*object.Commit, error) {
|
|
||||||
// We do a tree traversal with nodes sorted by commit time
|
|
||||||
heap := binaryheap.NewWith(func(a, b interface{}) int {
|
|
||||||
if a.(*commitAndPaths).commit.CommitTime().Before(b.(*commitAndPaths).commit.CommitTime()) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
})
|
|
||||||
|
|
||||||
resultNodes := make(map[string]cgobject.CommitNode)
|
|
||||||
initialHashes, err := getFileHashes(c, treePath, paths)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start search from the root commit and with full set of paths
|
|
||||||
heap.Push(&commitAndPaths{c, paths, initialHashes})
|
|
||||||
|
|
||||||
for {
|
|
||||||
cIn, ok := heap.Pop()
|
|
||||||
if !ok {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
current := cIn.(*commitAndPaths)
|
|
||||||
|
|
||||||
// Load the parent commits for the one we are currently examining
|
|
||||||
numParents := current.commit.NumParents()
|
|
||||||
var parents []cgobject.CommitNode
|
|
||||||
for i := 0; i < numParents; i++ {
|
|
||||||
parent, err := current.commit.ParentNode(i)
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
parents = append(parents, parent)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Examine the current commit and set of interesting paths
|
|
||||||
pathUnchanged := make([]bool, len(current.paths))
|
|
||||||
parentHashes := make([]map[string]plumbing.Hash, len(parents))
|
|
||||||
for j, parent := range parents {
|
|
||||||
parentHashes[j], err = getFileHashes(parent, treePath, current.paths)
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, path := range current.paths {
|
|
||||||
if parentHashes[j][path] == current.hashes[path] {
|
|
||||||
pathUnchanged[i] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var remainingPaths []string
|
|
||||||
for i, path := range current.paths {
|
|
||||||
// The results could already contain some newer change for the same path,
|
|
||||||
// so don't override that and bail out on the file early.
|
|
||||||
if resultNodes[path] == nil {
|
|
||||||
if pathUnchanged[i] {
|
|
||||||
// The path existed with the same hash in at least one parent so it could
|
|
||||||
// not have been changed in this commit directly.
|
|
||||||
remainingPaths = append(remainingPaths, path)
|
|
||||||
} else {
|
|
||||||
// There are few possible cases how can we get here:
|
|
||||||
// - The path didn't exist in any parent, so it must have been created by
|
|
||||||
// this commit.
|
|
||||||
// - The path did exist in the parent commit, but the hash of the file has
|
|
||||||
// changed.
|
|
||||||
// - We are looking at a merge commit and the hash of the file doesn't
|
|
||||||
// match any of the hashes being merged. This is more common for directories,
|
|
||||||
// but it can also happen if a file is changed through conflict resolution.
|
|
||||||
resultNodes[path] = current.commit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(remainingPaths) > 0 {
|
|
||||||
// Add the parent nodes along with remaining paths to the heap for further
|
|
||||||
// processing.
|
|
||||||
for j, parent := range parents {
|
|
||||||
// Combine remainingPath with paths available on the parent branch
|
|
||||||
// and make union of them
|
|
||||||
remainingPathsForParent := make([]string, 0, len(remainingPaths))
|
|
||||||
newRemainingPaths := make([]string, 0, len(remainingPaths))
|
|
||||||
for _, path := range remainingPaths {
|
|
||||||
if parentHashes[j][path] == current.hashes[path] {
|
|
||||||
remainingPathsForParent = append(remainingPathsForParent, path)
|
|
||||||
} else {
|
|
||||||
newRemainingPaths = append(newRemainingPaths, path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if remainingPathsForParent != nil {
|
|
||||||
heap.Push(&commitAndPaths{parent, remainingPathsForParent, parentHashes[j]})
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(newRemainingPaths) == 0 {
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
remainingPaths = newRemainingPaths
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post-processing
|
|
||||||
result := make(map[string]*object.Commit)
|
|
||||||
for path, commitNode := range resultNodes {
|
|
||||||
var err error
|
|
||||||
result[path], err = commitNode.Commit()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
}
|
||||||
|
|
291
modules/git/commit_info_gogit.go
Normal file
291
modules/git/commit_info_gogit.go
Normal file
|
@ -0,0 +1,291 @@
|
||||||
|
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/emirpasic/gods/trees/binaryheap"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/object"
|
||||||
|
cgobject "github.com/go-git/go-git/v5/plumbing/object/commitgraph"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetCommitsInfo gets information of all commits that are corresponding to these entries
|
||||||
|
func (tes Entries) GetCommitsInfo(commit *Commit, treePath string, cache *LastCommitCache) ([]CommitInfo, *Commit, error) {
|
||||||
|
entryPaths := make([]string, len(tes)+1)
|
||||||
|
// Get the commit for the treePath itself
|
||||||
|
entryPaths[0] = ""
|
||||||
|
for i, entry := range tes {
|
||||||
|
entryPaths[i+1] = entry.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
commitNodeIndex, commitGraphFile := commit.repo.CommitNodeIndex()
|
||||||
|
if commitGraphFile != nil {
|
||||||
|
defer commitGraphFile.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := commitNodeIndex.Get(commit.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var revs map[string]*object.Commit
|
||||||
|
if cache != nil {
|
||||||
|
var unHitPaths []string
|
||||||
|
revs, unHitPaths, err = getLastCommitForPathsByCache(commit.ID.String(), treePath, entryPaths, cache)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if len(unHitPaths) > 0 {
|
||||||
|
revs2, err := GetLastCommitForPaths(c, treePath, unHitPaths)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range revs2 {
|
||||||
|
if err := cache.Put(commit.ID.String(), path.Join(treePath, k), v.ID().String()); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
revs[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
revs, err = GetLastCommitForPaths(c, treePath, entryPaths)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
commit.repo.gogitStorage.Close()
|
||||||
|
|
||||||
|
commitsInfo := make([]CommitInfo, len(tes))
|
||||||
|
for i, entry := range tes {
|
||||||
|
commitsInfo[i] = CommitInfo{
|
||||||
|
Entry: entry,
|
||||||
|
}
|
||||||
|
if rev, ok := revs[entry.Name()]; ok {
|
||||||
|
entryCommit := convertCommit(rev)
|
||||||
|
commitsInfo[i].Commit = entryCommit
|
||||||
|
if entry.IsSubModule() {
|
||||||
|
subModuleURL := ""
|
||||||
|
var fullPath string
|
||||||
|
if len(treePath) > 0 {
|
||||||
|
fullPath = treePath + "/" + entry.Name()
|
||||||
|
} else {
|
||||||
|
fullPath = entry.Name()
|
||||||
|
}
|
||||||
|
if subModule, err := commit.GetSubModule(fullPath); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
} else if subModule != nil {
|
||||||
|
subModuleURL = subModule.URL
|
||||||
|
}
|
||||||
|
subModuleFile := NewSubModuleFile(entryCommit, subModuleURL, entry.ID.String())
|
||||||
|
commitsInfo[i].SubModuleFile = subModuleFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the commit for the treePath itself (see above). We basically
|
||||||
|
// get it for free during the tree traversal and it's used for listing
|
||||||
|
// pages to display information about newest commit for a given path.
|
||||||
|
var treeCommit *Commit
|
||||||
|
if treePath == "" {
|
||||||
|
treeCommit = commit
|
||||||
|
} else if rev, ok := revs[""]; ok {
|
||||||
|
treeCommit = convertCommit(rev)
|
||||||
|
treeCommit.repo = commit.repo
|
||||||
|
}
|
||||||
|
return commitsInfo, treeCommit, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type commitAndPaths struct {
|
||||||
|
commit cgobject.CommitNode
|
||||||
|
// Paths that are still on the branch represented by commit
|
||||||
|
paths []string
|
||||||
|
// Set of hashes for the paths
|
||||||
|
hashes map[string]plumbing.Hash
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCommitTree(c cgobject.CommitNode, treePath string) (*object.Tree, error) {
|
||||||
|
tree, err := c.Tree()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimize deep traversals by focusing only on the specific tree
|
||||||
|
if treePath != "" {
|
||||||
|
tree, err = tree.Tree(treePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tree, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFileHashes(c cgobject.CommitNode, treePath string, paths []string) (map[string]plumbing.Hash, error) {
|
||||||
|
tree, err := getCommitTree(c, treePath)
|
||||||
|
if err == object.ErrDirectoryNotFound {
|
||||||
|
// The whole tree didn't exist, so return empty map
|
||||||
|
return make(map[string]plumbing.Hash), nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
hashes := make(map[string]plumbing.Hash)
|
||||||
|
for _, path := range paths {
|
||||||
|
if path != "" {
|
||||||
|
entry, err := tree.FindEntry(path)
|
||||||
|
if err == nil {
|
||||||
|
hashes[path] = entry.Hash
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hashes[path] = tree.Hash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hashes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLastCommitForPathsByCache(commitID, treePath string, paths []string, cache *LastCommitCache) (map[string]*object.Commit, []string, error) {
|
||||||
|
var unHitEntryPaths []string
|
||||||
|
var results = make(map[string]*object.Commit)
|
||||||
|
for _, p := range paths {
|
||||||
|
lastCommit, err := cache.Get(commitID, path.Join(treePath, p))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if lastCommit != nil {
|
||||||
|
results[p] = lastCommit.(*object.Commit)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
unHitEntryPaths = append(unHitEntryPaths, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, unHitEntryPaths, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLastCommitForPaths returns last commit information
|
||||||
|
func GetLastCommitForPaths(c cgobject.CommitNode, treePath string, paths []string) (map[string]*object.Commit, error) {
|
||||||
|
// We do a tree traversal with nodes sorted by commit time
|
||||||
|
heap := binaryheap.NewWith(func(a, b interface{}) int {
|
||||||
|
if a.(*commitAndPaths).commit.CommitTime().Before(b.(*commitAndPaths).commit.CommitTime()) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
})
|
||||||
|
|
||||||
|
resultNodes := make(map[string]cgobject.CommitNode)
|
||||||
|
initialHashes, err := getFileHashes(c, treePath, paths)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start search from the root commit and with full set of paths
|
||||||
|
heap.Push(&commitAndPaths{c, paths, initialHashes})
|
||||||
|
|
||||||
|
for {
|
||||||
|
cIn, ok := heap.Pop()
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
current := cIn.(*commitAndPaths)
|
||||||
|
|
||||||
|
// Load the parent commits for the one we are currently examining
|
||||||
|
numParents := current.commit.NumParents()
|
||||||
|
var parents []cgobject.CommitNode
|
||||||
|
for i := 0; i < numParents; i++ {
|
||||||
|
parent, err := current.commit.ParentNode(i)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
parents = append(parents, parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Examine the current commit and set of interesting paths
|
||||||
|
pathUnchanged := make([]bool, len(current.paths))
|
||||||
|
parentHashes := make([]map[string]plumbing.Hash, len(parents))
|
||||||
|
for j, parent := range parents {
|
||||||
|
parentHashes[j], err = getFileHashes(parent, treePath, current.paths)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, path := range current.paths {
|
||||||
|
if parentHashes[j][path] == current.hashes[path] {
|
||||||
|
pathUnchanged[i] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var remainingPaths []string
|
||||||
|
for i, path := range current.paths {
|
||||||
|
// The results could already contain some newer change for the same path,
|
||||||
|
// so don't override that and bail out on the file early.
|
||||||
|
if resultNodes[path] == nil {
|
||||||
|
if pathUnchanged[i] {
|
||||||
|
// The path existed with the same hash in at least one parent so it could
|
||||||
|
// not have been changed in this commit directly.
|
||||||
|
remainingPaths = append(remainingPaths, path)
|
||||||
|
} else {
|
||||||
|
// There are few possible cases how can we get here:
|
||||||
|
// - The path didn't exist in any parent, so it must have been created by
|
||||||
|
// this commit.
|
||||||
|
// - The path did exist in the parent commit, but the hash of the file has
|
||||||
|
// changed.
|
||||||
|
// - We are looking at a merge commit and the hash of the file doesn't
|
||||||
|
// match any of the hashes being merged. This is more common for directories,
|
||||||
|
// but it can also happen if a file is changed through conflict resolution.
|
||||||
|
resultNodes[path] = current.commit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(remainingPaths) > 0 {
|
||||||
|
// Add the parent nodes along with remaining paths to the heap for further
|
||||||
|
// processing.
|
||||||
|
for j, parent := range parents {
|
||||||
|
// Combine remainingPath with paths available on the parent branch
|
||||||
|
// and make union of them
|
||||||
|
remainingPathsForParent := make([]string, 0, len(remainingPaths))
|
||||||
|
newRemainingPaths := make([]string, 0, len(remainingPaths))
|
||||||
|
for _, path := range remainingPaths {
|
||||||
|
if parentHashes[j][path] == current.hashes[path] {
|
||||||
|
remainingPathsForParent = append(remainingPathsForParent, path)
|
||||||
|
} else {
|
||||||
|
newRemainingPaths = append(newRemainingPaths, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if remainingPathsForParent != nil {
|
||||||
|
heap.Push(&commitAndPaths{parent, remainingPathsForParent, parentHashes[j]})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newRemainingPaths) == 0 {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
remainingPaths = newRemainingPaths
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post-processing
|
||||||
|
result := make(map[string]*object.Commit)
|
||||||
|
for path, commitNode := range resultNodes {
|
||||||
|
var err error
|
||||||
|
result[path], err = commitNode.Commit()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
370
modules/git/commit_info_nogogit.go
Normal file
370
modules/git/commit_info_nogogit.go
Normal file
|
@ -0,0 +1,370 @@
|
||||||
|
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"path"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetCommitsInfo gets information of all commits that are corresponding to these entries
|
||||||
|
func (tes Entries) GetCommitsInfo(commit *Commit, treePath string, cache *LastCommitCache) ([]CommitInfo, *Commit, error) {
|
||||||
|
entryPaths := make([]string, len(tes)+1)
|
||||||
|
// Get the commit for the treePath itself
|
||||||
|
entryPaths[0] = ""
|
||||||
|
for i, entry := range tes {
|
||||||
|
entryPaths[i+1] = entry.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
var revs map[string]*Commit
|
||||||
|
if cache != nil {
|
||||||
|
var unHitPaths []string
|
||||||
|
revs, unHitPaths, err = getLastCommitForPathsByCache(commit.ID.String(), treePath, entryPaths, cache)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if len(unHitPaths) > 0 {
|
||||||
|
sort.Strings(unHitPaths)
|
||||||
|
commits, err := GetLastCommitForPaths(commit, treePath, unHitPaths)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, found := range commits {
|
||||||
|
if err := cache.Put(commit.ID.String(), path.Join(treePath, unHitPaths[i]), found.ID.String()); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
revs[unHitPaths[i]] = found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sort.Strings(entryPaths)
|
||||||
|
revs = map[string]*Commit{}
|
||||||
|
var foundCommits []*Commit
|
||||||
|
foundCommits, err = GetLastCommitForPaths(commit, treePath, entryPaths)
|
||||||
|
for i, found := range foundCommits {
|
||||||
|
revs[entryPaths[i]] = found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
commitsInfo := make([]CommitInfo, len(tes))
|
||||||
|
for i, entry := range tes {
|
||||||
|
commitsInfo[i] = CommitInfo{
|
||||||
|
Entry: entry,
|
||||||
|
}
|
||||||
|
if entryCommit, ok := revs[entry.Name()]; ok {
|
||||||
|
commitsInfo[i].Commit = entryCommit
|
||||||
|
if entry.IsSubModule() {
|
||||||
|
subModuleURL := ""
|
||||||
|
var fullPath string
|
||||||
|
if len(treePath) > 0 {
|
||||||
|
fullPath = treePath + "/" + entry.Name()
|
||||||
|
} else {
|
||||||
|
fullPath = entry.Name()
|
||||||
|
}
|
||||||
|
if subModule, err := commit.GetSubModule(fullPath); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
} else if subModule != nil {
|
||||||
|
subModuleURL = subModule.URL
|
||||||
|
}
|
||||||
|
subModuleFile := NewSubModuleFile(entryCommit, subModuleURL, entry.ID.String())
|
||||||
|
commitsInfo[i].SubModuleFile = subModuleFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the commit for the treePath itself (see above). We basically
|
||||||
|
// get it for free during the tree traversal and it's used for listing
|
||||||
|
// pages to display information about newest commit for a given path.
|
||||||
|
var treeCommit *Commit
|
||||||
|
var ok bool
|
||||||
|
if treePath == "" {
|
||||||
|
treeCommit = commit
|
||||||
|
} else if treeCommit, ok = revs[""]; ok {
|
||||||
|
treeCommit.repo = commit.repo
|
||||||
|
}
|
||||||
|
return commitsInfo, treeCommit, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLastCommitForPathsByCache(commitID, treePath string, paths []string, cache *LastCommitCache) (map[string]*Commit, []string, error) {
|
||||||
|
var unHitEntryPaths []string
|
||||||
|
var results = make(map[string]*Commit)
|
||||||
|
for _, p := range paths {
|
||||||
|
lastCommit, err := cache.Get(commitID, path.Join(treePath, p))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if lastCommit != nil {
|
||||||
|
results[p] = lastCommit.(*Commit)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
unHitEntryPaths = append(unHitEntryPaths, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, unHitEntryPaths, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLastCommitForPaths returns last commit information
|
||||||
|
func GetLastCommitForPaths(commit *Commit, treePath string, paths []string) ([]*Commit, error) {
|
||||||
|
// We read backwards from the commit to obtain all of the commits
|
||||||
|
|
||||||
|
// We'll do this by using rev-list to provide us with parent commits in order
|
||||||
|
revListReader, revListWriter := io.Pipe()
|
||||||
|
defer func() {
|
||||||
|
_ = revListWriter.Close()
|
||||||
|
_ = revListReader.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
stderr := strings.Builder{}
|
||||||
|
err := NewCommand("rev-list", "--format=%T", commit.ID.String()).RunInDirPipeline(commit.repo.Path, revListWriter, &stderr)
|
||||||
|
if err != nil {
|
||||||
|
_ = revListWriter.CloseWithError(ConcatenateError(err, (&stderr).String()))
|
||||||
|
} else {
|
||||||
|
_ = revListWriter.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// We feed the commits in order into cat-file --batch, followed by their trees and sub trees as necessary.
|
||||||
|
// so let's create a batch stdin and stdout
|
||||||
|
batchStdinReader, batchStdinWriter := io.Pipe()
|
||||||
|
batchStdoutReader, batchStdoutWriter := io.Pipe()
|
||||||
|
defer func() {
|
||||||
|
_ = batchStdinReader.Close()
|
||||||
|
_ = batchStdinWriter.Close()
|
||||||
|
_ = batchStdoutReader.Close()
|
||||||
|
_ = batchStdoutWriter.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
stderr := strings.Builder{}
|
||||||
|
err := NewCommand("cat-file", "--batch").RunInDirFullPipeline(commit.repo.Path, batchStdoutWriter, &stderr, batchStdinReader)
|
||||||
|
if err != nil {
|
||||||
|
_ = revListWriter.CloseWithError(ConcatenateError(err, (&stderr).String()))
|
||||||
|
} else {
|
||||||
|
_ = revListWriter.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// For simplicities sake we'll us a buffered reader
|
||||||
|
batchReader := bufio.NewReader(batchStdoutReader)
|
||||||
|
|
||||||
|
mapsize := 4096
|
||||||
|
if len(paths) > mapsize {
|
||||||
|
mapsize = len(paths)
|
||||||
|
}
|
||||||
|
|
||||||
|
path2idx := make(map[string]int, mapsize)
|
||||||
|
for i, path := range paths {
|
||||||
|
path2idx[path] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
fnameBuf := make([]byte, 4096)
|
||||||
|
modeBuf := make([]byte, 40)
|
||||||
|
|
||||||
|
allShaBuf := make([]byte, (len(paths)+1)*20)
|
||||||
|
shaBuf := make([]byte, 20)
|
||||||
|
tmpTreeID := make([]byte, 40)
|
||||||
|
|
||||||
|
// commits is the returnable commits matching the paths provided
|
||||||
|
commits := make([]string, len(paths))
|
||||||
|
// ids are the blob/tree ids for the paths
|
||||||
|
ids := make([][]byte, len(paths))
|
||||||
|
|
||||||
|
// We'll use a scanner for the revList because it's simpler than a bufio.Reader
|
||||||
|
scan := bufio.NewScanner(revListReader)
|
||||||
|
revListLoop:
|
||||||
|
for scan.Scan() {
|
||||||
|
// Get the next parent commit ID
|
||||||
|
commitID := scan.Text()
|
||||||
|
if !scan.Scan() {
|
||||||
|
break revListLoop
|
||||||
|
}
|
||||||
|
commitID = commitID[7:]
|
||||||
|
rootTreeID := scan.Text()
|
||||||
|
|
||||||
|
// push the tree to the cat-file --batch process
|
||||||
|
_, err := batchStdinWriter.Write([]byte(rootTreeID + "\n"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPath := ""
|
||||||
|
|
||||||
|
// OK if the target tree path is "" and the "" is in the paths just set this now
|
||||||
|
if treePath == "" && paths[0] == "" {
|
||||||
|
// If this is the first time we see this set the id appropriate for this paths to this tree and set the last commit to curCommit
|
||||||
|
if len(ids[0]) == 0 {
|
||||||
|
ids[0] = []byte(rootTreeID)
|
||||||
|
commits[0] = string(commitID)
|
||||||
|
} else if bytes.Equal(ids[0], []byte(rootTreeID)) {
|
||||||
|
commits[0] = string(commitID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
treeReadingLoop:
|
||||||
|
for {
|
||||||
|
_, _, size, err := ReadBatchLine(batchReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle trees
|
||||||
|
|
||||||
|
// n is counter for file position in the tree file
|
||||||
|
var n int64
|
||||||
|
|
||||||
|
// Two options: currentPath is the targetTreepath
|
||||||
|
if treePath == currentPath {
|
||||||
|
// We are in the right directory
|
||||||
|
// Parse each tree line in turn. (don't care about mode here.)
|
||||||
|
for n < size {
|
||||||
|
fname, sha, count, err := ParseTreeLineSkipMode(batchReader, fnameBuf, shaBuf)
|
||||||
|
shaBuf = sha
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n += int64(count)
|
||||||
|
idx, ok := path2idx[string(fname)]
|
||||||
|
if ok {
|
||||||
|
// Now if this is the first time round set the initial Blob(ish) SHA ID and the commit
|
||||||
|
if len(ids[idx]) == 0 {
|
||||||
|
copy(allShaBuf[20*(idx+1):20*(idx+2)], shaBuf)
|
||||||
|
ids[idx] = allShaBuf[20*(idx+1) : 20*(idx+2)]
|
||||||
|
commits[idx] = string(commitID)
|
||||||
|
} else if bytes.Equal(ids[idx], shaBuf) {
|
||||||
|
commits[idx] = string(commitID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// FIXME: is there any order to the way strings are emitted from cat-file?
|
||||||
|
// if there is - then we could skip once we've passed all of our data
|
||||||
|
}
|
||||||
|
break treeReadingLoop
|
||||||
|
}
|
||||||
|
|
||||||
|
var treeID []byte
|
||||||
|
|
||||||
|
// We're in the wrong directory
|
||||||
|
// Find target directory in this directory
|
||||||
|
idx := len(currentPath)
|
||||||
|
if idx > 0 {
|
||||||
|
idx++
|
||||||
|
}
|
||||||
|
target := strings.SplitN(treePath[idx:], "/", 2)[0]
|
||||||
|
|
||||||
|
for n < size {
|
||||||
|
// Read each tree entry in turn
|
||||||
|
mode, fname, sha, count, err := ParseTreeLine(batchReader, modeBuf, fnameBuf, shaBuf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n += int64(count)
|
||||||
|
|
||||||
|
// if we have found the target directory
|
||||||
|
if bytes.Equal(fname, []byte(target)) && bytes.Equal(mode, []byte("40000")) {
|
||||||
|
copy(tmpTreeID, sha)
|
||||||
|
treeID = tmpTreeID
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if n < size {
|
||||||
|
// Discard any remaining entries in the current tree
|
||||||
|
discard := size - n
|
||||||
|
for discard > math.MaxInt32 {
|
||||||
|
_, err := batchReader.Discard(math.MaxInt32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
discard -= math.MaxInt32
|
||||||
|
}
|
||||||
|
_, err := batchReader.Discard(int(discard))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we haven't found a treeID for the target directory our search is over
|
||||||
|
if len(treeID) == 0 {
|
||||||
|
break treeReadingLoop
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the target to the current path
|
||||||
|
if idx > 0 {
|
||||||
|
currentPath += "/"
|
||||||
|
}
|
||||||
|
currentPath += target
|
||||||
|
|
||||||
|
// if we've now found the current path check its sha id and commit status
|
||||||
|
if treePath == currentPath && paths[0] == "" {
|
||||||
|
if len(ids[0]) == 0 {
|
||||||
|
copy(allShaBuf[0:20], treeID)
|
||||||
|
ids[0] = allShaBuf[0:20]
|
||||||
|
commits[0] = string(commitID)
|
||||||
|
} else if bytes.Equal(ids[0], treeID) {
|
||||||
|
commits[0] = string(commitID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
treeID = to40ByteSHA(treeID)
|
||||||
|
_, err = batchStdinWriter.Write(treeID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = batchStdinWriter.Write([]byte("\n"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
commitsMap := make(map[string]*Commit, len(commits))
|
||||||
|
commitsMap[commit.ID.String()] = commit
|
||||||
|
|
||||||
|
commitCommits := make([]*Commit, len(commits))
|
||||||
|
for i, commitID := range commits {
|
||||||
|
c, ok := commitsMap[commitID]
|
||||||
|
if ok {
|
||||||
|
commitCommits[i] = c
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(commitID) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := batchStdinWriter.Write([]byte(commitID + "\n"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, typ, size, err := ReadBatchLine(batchReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if typ != "commit" {
|
||||||
|
return nil, fmt.Errorf("unexpected type: %s for commit id: %s", typ, commitID)
|
||||||
|
}
|
||||||
|
c, err = CommitFromReader(commit.repo, MustIDFromString(string(commitID)), io.LimitReader(batchReader, int64(size)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
commitCommits[i] = c
|
||||||
|
}
|
||||||
|
|
||||||
|
return commitCommits, scan.Err()
|
||||||
|
}
|
|
@ -58,17 +58,27 @@ func testGetCommitsInfo(t *testing.T, repo1 *Repository) {
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
commit, err := repo1.GetCommit(testCase.CommitID)
|
commit, err := repo1.GetCommit(testCase.CommitID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, commit)
|
||||||
|
assert.NotNil(t, commit.Tree)
|
||||||
|
assert.NotNil(t, commit.Tree.repo)
|
||||||
|
|
||||||
tree, err := commit.Tree.SubTree(testCase.Path)
|
tree, err := commit.Tree.SubTree(testCase.Path)
|
||||||
|
assert.NotNil(t, tree, "tree is nil for testCase CommitID %s in Path %s", testCase.CommitID, testCase.Path)
|
||||||
|
assert.NotNil(t, tree.repo, "repo is nil for testCase CommitID %s in Path %s", testCase.CommitID, testCase.Path)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
entries, err := tree.ListEntries()
|
entries, err := tree.ListEntries()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
commitsInfo, treeCommit, err := entries.GetCommitsInfo(commit, testCase.Path, nil)
|
commitsInfo, treeCommit, err := entries.GetCommitsInfo(commit, testCase.Path, nil)
|
||||||
assert.Equal(t, testCase.ExpectedTreeCommit, treeCommit.ID.String())
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
if err != nil {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
assert.Equal(t, testCase.ExpectedTreeCommit, treeCommit.ID.String())
|
||||||
assert.Len(t, commitsInfo, len(testCase.ExpectedIDs))
|
assert.Len(t, commitsInfo, len(testCase.ExpectedIDs))
|
||||||
for _, commitInfo := range commitsInfo {
|
for _, commitInfo := range commitsInfo {
|
||||||
entry := commitInfo[0].(*TreeEntry)
|
entry := commitInfo.Entry
|
||||||
commit := commitInfo[1].(*Commit)
|
commit := commitInfo.Commit
|
||||||
expectedID, ok := testCase.ExpectedIDs[entry.Name()]
|
expectedID, ok := testCase.ExpectedIDs[entry.Name()]
|
||||||
if !assert.True(t, ok) {
|
if !assert.True(t, ok) {
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -9,13 +9,13 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CommitFromReader will generate a Commit from a provided reader
|
// CommitFromReader will generate a Commit from a provided reader
|
||||||
// We will need this to interpret commits from cat-file
|
// We need this to interpret commits from cat-file or cat-file --batch
|
||||||
func CommitFromReader(gitRepo *Repository, sha plumbing.Hash, reader io.Reader) (*Commit, error) {
|
//
|
||||||
|
// If used as part of a cat-file --batch stream you need to limit the reader to the correct size
|
||||||
|
func CommitFromReader(gitRepo *Repository, sha SHA1, reader io.Reader) (*Commit, error) {
|
||||||
commit := &Commit{
|
commit := &Commit{
|
||||||
ID: sha,
|
ID: sha,
|
||||||
}
|
}
|
||||||
|
@ -26,26 +26,20 @@ func CommitFromReader(gitRepo *Repository, sha plumbing.Hash, reader io.Reader)
|
||||||
message := false
|
message := false
|
||||||
pgpsig := false
|
pgpsig := false
|
||||||
|
|
||||||
scanner := bufio.NewScanner(reader)
|
bufReader, ok := reader.(*bufio.Reader)
|
||||||
// Split by '\n' but include the '\n'
|
if !ok {
|
||||||
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
bufReader = bufio.NewReader(reader)
|
||||||
if atEOF && len(data) == 0 {
|
|
||||||
return 0, nil, nil
|
|
||||||
}
|
}
|
||||||
if i := bytes.IndexByte(data, '\n'); i >= 0 {
|
|
||||||
// We have a full newline-terminated line.
|
|
||||||
return i + 1, data[0 : i+1], nil
|
|
||||||
}
|
|
||||||
// If we're at EOF, we have a final, non-terminated line. Return it.
|
|
||||||
if atEOF {
|
|
||||||
return len(data), data, nil
|
|
||||||
}
|
|
||||||
// Request more data.
|
|
||||||
return 0, nil, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
for scanner.Scan() {
|
readLoop:
|
||||||
line := scanner.Bytes()
|
for {
|
||||||
|
line, err := bufReader.ReadBytes('\n')
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
break readLoop
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if pgpsig {
|
if pgpsig {
|
||||||
if len(line) > 0 && line[0] == ' ' {
|
if len(line) > 0 && line[0] == ' ' {
|
||||||
_, _ = signatureSB.Write(line[1:])
|
_, _ = signatureSB.Write(line[1:])
|
||||||
|
@ -72,10 +66,10 @@ func CommitFromReader(gitRepo *Repository, sha plumbing.Hash, reader io.Reader)
|
||||||
|
|
||||||
switch string(split[0]) {
|
switch string(split[0]) {
|
||||||
case "tree":
|
case "tree":
|
||||||
commit.Tree = *NewTree(gitRepo, plumbing.NewHash(string(data)))
|
commit.Tree = *NewTree(gitRepo, MustIDFromString(string(data)))
|
||||||
_, _ = payloadSB.Write(line)
|
_, _ = payloadSB.Write(line)
|
||||||
case "parent":
|
case "parent":
|
||||||
commit.Parents = append(commit.Parents, plumbing.NewHash(string(data)))
|
commit.Parents = append(commit.Parents, MustIDFromString(string(data)))
|
||||||
_, _ = payloadSB.Write(line)
|
_, _ = payloadSB.Write(line)
|
||||||
case "author":
|
case "author":
|
||||||
commit.Author = &Signature{}
|
commit.Author = &Signature{}
|
||||||
|
@ -104,5 +98,5 @@ func CommitFromReader(gitRepo *Repository, sha plumbing.Hash, reader io.Reader)
|
||||||
commit.Signature = nil
|
commit.Signature = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return commit, scanner.Err()
|
return commit, nil
|
||||||
}
|
}
|
||||||
|
|
29
modules/git/last_commit_cache.go
Normal file
29
modules/git/last_commit_cache.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cache represents a caching interface
|
||||||
|
type Cache interface {
|
||||||
|
// Put puts value into cache with key and expire time.
|
||||||
|
Put(key string, val interface{}, timeout int64) error
|
||||||
|
// Get gets cached value by given key.
|
||||||
|
Get(key string) interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LastCommitCache) getCacheKey(repoPath, ref, entryPath string) string {
|
||||||
|
hashBytes := sha256.Sum256([]byte(fmt.Sprintf("%s:%s:%s", repoPath, ref, entryPath)))
|
||||||
|
return fmt.Sprintf("last_commit:%x", hashBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put put the last commit id with commit and entry path
|
||||||
|
func (c *LastCommitCache) Put(ref, entryPath, commitID string) error {
|
||||||
|
log("LastCommitCache save: [%s:%s:%s]", ref, entryPath, commitID)
|
||||||
|
return c.cache.Put(c.getCacheKey(c.repoPath, ref, entryPath), commitID, c.ttl)
|
||||||
|
}
|
113
modules/git/last_commit_cache_gogit.go
Normal file
113
modules/git/last_commit_cache_gogit.go
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/object"
|
||||||
|
cgobject "github.com/go-git/go-git/v5/plumbing/object/commitgraph"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LastCommitCache represents a cache to store last commit
|
||||||
|
type LastCommitCache struct {
|
||||||
|
repoPath string
|
||||||
|
ttl int64
|
||||||
|
repo *Repository
|
||||||
|
commitCache map[string]*object.Commit
|
||||||
|
cache Cache
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLastCommitCache creates a new last commit cache for repo
|
||||||
|
func NewLastCommitCache(repoPath string, gitRepo *Repository, ttl int64, cache Cache) *LastCommitCache {
|
||||||
|
if cache == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &LastCommitCache{
|
||||||
|
repoPath: repoPath,
|
||||||
|
repo: gitRepo,
|
||||||
|
commitCache: make(map[string]*object.Commit),
|
||||||
|
ttl: ttl,
|
||||||
|
cache: cache,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get get the last commit information by commit id and entry path
|
||||||
|
func (c *LastCommitCache) Get(ref, entryPath string) (interface{}, error) {
|
||||||
|
v := c.cache.Get(c.getCacheKey(c.repoPath, ref, entryPath))
|
||||||
|
if vs, ok := v.(string); ok {
|
||||||
|
log("LastCommitCache hit level 1: [%s:%s:%s]", ref, entryPath, vs)
|
||||||
|
if commit, ok := c.commitCache[vs]; ok {
|
||||||
|
log("LastCommitCache hit level 2: [%s:%s:%s]", ref, entryPath, vs)
|
||||||
|
return commit, nil
|
||||||
|
}
|
||||||
|
id, err := c.repo.ConvertToSHA1(vs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
commit, err := c.repo.GoGitRepo().CommitObject(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.commitCache[vs] = commit
|
||||||
|
return commit, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CacheCommit will cache the commit from the gitRepository
|
||||||
|
func (c *LastCommitCache) CacheCommit(commit *Commit) error {
|
||||||
|
|
||||||
|
commitNodeIndex, _ := commit.repo.CommitNodeIndex()
|
||||||
|
|
||||||
|
index, err := commitNodeIndex.Get(commit.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.recursiveCache(index, &commit.Tree, "", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LastCommitCache) recursiveCache(index cgobject.CommitNode, tree *Tree, treePath string, level int) error {
|
||||||
|
if level == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := tree.ListEntries()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
entryPaths := make([]string, len(entries))
|
||||||
|
entryMap := make(map[string]*TreeEntry)
|
||||||
|
for i, entry := range entries {
|
||||||
|
entryPaths[i] = entry.Name()
|
||||||
|
entryMap[entry.Name()] = entry
|
||||||
|
}
|
||||||
|
|
||||||
|
commits, err := GetLastCommitForPaths(index, treePath, entryPaths)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for entry, cm := range commits {
|
||||||
|
if err := c.Put(index.ID().String(), path.Join(treePath, entry), cm.ID().String()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if entryMap[entry].IsDir() {
|
||||||
|
subTree, err := tree.SubTree(entry)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.recursiveCache(index, subTree, entry, level-1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
103
modules/git/last_commit_cache_nogogit.go
Normal file
103
modules/git/last_commit_cache_nogogit.go
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LastCommitCache represents a cache to store last commit
|
||||||
|
type LastCommitCache struct {
|
||||||
|
repoPath string
|
||||||
|
ttl int64
|
||||||
|
repo *Repository
|
||||||
|
commitCache map[string]*Commit
|
||||||
|
cache Cache
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLastCommitCache creates a new last commit cache for repo
|
||||||
|
func NewLastCommitCache(repoPath string, gitRepo *Repository, ttl int64, cache Cache) *LastCommitCache {
|
||||||
|
if cache == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &LastCommitCache{
|
||||||
|
repoPath: repoPath,
|
||||||
|
repo: gitRepo,
|
||||||
|
commitCache: make(map[string]*Commit),
|
||||||
|
ttl: ttl,
|
||||||
|
cache: cache,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get get the last commit information by commit id and entry path
|
||||||
|
func (c *LastCommitCache) Get(ref, entryPath string) (interface{}, error) {
|
||||||
|
v := c.cache.Get(c.getCacheKey(c.repoPath, ref, entryPath))
|
||||||
|
if vs, ok := v.(string); ok {
|
||||||
|
log("LastCommitCache hit level 1: [%s:%s:%s]", ref, entryPath, vs)
|
||||||
|
if commit, ok := c.commitCache[vs]; ok {
|
||||||
|
log("LastCommitCache hit level 2: [%s:%s:%s]", ref, entryPath, vs)
|
||||||
|
return commit, nil
|
||||||
|
}
|
||||||
|
id, err := c.repo.ConvertToSHA1(vs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
commit, err := c.repo.getCommit(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.commitCache[vs] = commit
|
||||||
|
return commit, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CacheCommit will cache the commit from the gitRepository
|
||||||
|
func (c *LastCommitCache) CacheCommit(commit *Commit) error {
|
||||||
|
return c.recursiveCache(commit, &commit.Tree, "", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LastCommitCache) recursiveCache(commit *Commit, tree *Tree, treePath string, level int) error {
|
||||||
|
if level == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := tree.ListEntries()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
entryPaths := make([]string, len(entries))
|
||||||
|
entryMap := make(map[string]*TreeEntry)
|
||||||
|
for i, entry := range entries {
|
||||||
|
entryPaths[i] = entry.Name()
|
||||||
|
entryMap[entry.Name()] = entry
|
||||||
|
}
|
||||||
|
|
||||||
|
commits, err := GetLastCommitForPaths(commit, treePath, entryPaths)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, entryCommit := range commits {
|
||||||
|
entry := entryPaths[i]
|
||||||
|
if err := c.Put(commit.ID.String(), path.Join(treePath, entryPaths[i]), entryCommit.ID.String()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if entryMap[entry].IsDir() {
|
||||||
|
subTree, err := tree.SubTree(entry)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.recursiveCache(commit, subTree, entry, level-1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -4,12 +4,6 @@
|
||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
|
||||||
"io/ioutil"
|
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NotesRef is the git ref where Gitea will look for git-notes data.
|
// NotesRef is the git ref where Gitea will look for git-notes data.
|
||||||
// The value ("refs/notes/commits") is the default ref used by git-notes.
|
// The value ("refs/notes/commits") is the default ref used by git-notes.
|
||||||
const NotesRef = "refs/notes/commits"
|
const NotesRef = "refs/notes/commits"
|
||||||
|
@ -19,62 +13,3 @@ type Note struct {
|
||||||
Message []byte
|
Message []byte
|
||||||
Commit *Commit
|
Commit *Commit
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNote retrieves the git-notes data for a given commit.
|
|
||||||
func GetNote(repo *Repository, commitID string, note *Note) error {
|
|
||||||
notes, err := repo.GetCommit(NotesRef)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
remainingCommitID := commitID
|
|
||||||
path := ""
|
|
||||||
currentTree := notes.Tree.gogitTree
|
|
||||||
var file *object.File
|
|
||||||
for len(remainingCommitID) > 2 {
|
|
||||||
file, err = currentTree.File(remainingCommitID)
|
|
||||||
if err == nil {
|
|
||||||
path += remainingCommitID
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if err == object.ErrFileNotFound {
|
|
||||||
currentTree, err = currentTree.Tree(remainingCommitID[0:2])
|
|
||||||
path += remainingCommitID[0:2] + "/"
|
|
||||||
remainingCommitID = remainingCommitID[2:]
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
blob := file.Blob
|
|
||||||
dataRc, err := blob.Reader()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer dataRc.Close()
|
|
||||||
d, err := ioutil.ReadAll(dataRc)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
note.Message = d
|
|
||||||
|
|
||||||
commitNodeIndex, commitGraphFile := repo.CommitNodeIndex()
|
|
||||||
if commitGraphFile != nil {
|
|
||||||
defer commitGraphFile.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
commitNode, err := commitNodeIndex.Get(notes.ID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
lastCommits, err := GetLastCommitForPaths(commitNode, "", []string{path})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
note.Commit = convertCommit(lastCommits[path])
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
72
modules/git/notes_gogit.go
Normal file
72
modules/git/notes_gogit.go
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/object"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetNote retrieves the git-notes data for a given commit.
|
||||||
|
func GetNote(repo *Repository, commitID string, note *Note) error {
|
||||||
|
notes, err := repo.GetCommit(NotesRef)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
remainingCommitID := commitID
|
||||||
|
path := ""
|
||||||
|
currentTree := notes.Tree.gogitTree
|
||||||
|
var file *object.File
|
||||||
|
for len(remainingCommitID) > 2 {
|
||||||
|
file, err = currentTree.File(remainingCommitID)
|
||||||
|
if err == nil {
|
||||||
|
path += remainingCommitID
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err == object.ErrFileNotFound {
|
||||||
|
currentTree, err = currentTree.Tree(remainingCommitID[0:2])
|
||||||
|
path += remainingCommitID[0:2] + "/"
|
||||||
|
remainingCommitID = remainingCommitID[2:]
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
blob := file.Blob
|
||||||
|
dataRc, err := blob.Reader()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer dataRc.Close()
|
||||||
|
d, err := ioutil.ReadAll(dataRc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
note.Message = d
|
||||||
|
|
||||||
|
commitNodeIndex, commitGraphFile := repo.CommitNodeIndex()
|
||||||
|
if commitGraphFile != nil {
|
||||||
|
defer commitGraphFile.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
commitNode, err := commitNodeIndex.Get(notes.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
lastCommits, err := GetLastCommitForPaths(commitNode, "", []string{path})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
note.Commit = convertCommit(lastCommits[path])
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
59
modules/git/notes_nogogit.go
Normal file
59
modules/git/notes_nogogit.go
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetNote retrieves the git-notes data for a given commit.
|
||||||
|
func GetNote(repo *Repository, commitID string, note *Note) error {
|
||||||
|
notes, err := repo.GetCommit(NotesRef)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
path := ""
|
||||||
|
|
||||||
|
tree := ¬es.Tree
|
||||||
|
|
||||||
|
var entry *TreeEntry
|
||||||
|
for len(commitID) > 2 {
|
||||||
|
entry, err = tree.GetTreeEntryByPath(commitID)
|
||||||
|
if err == nil {
|
||||||
|
path += commitID
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if IsErrNotExist(err) {
|
||||||
|
tree, err = tree.SubTree(commitID[0:2])
|
||||||
|
path += commitID[0:2] + "/"
|
||||||
|
commitID = commitID[2:]
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dataRc, err := entry.Blob().DataAsync()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer dataRc.Close()
|
||||||
|
d, err := ioutil.ReadAll(dataRc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
note.Message = d
|
||||||
|
|
||||||
|
lastCommits, err := GetLastCommitForPaths(notes, "", []string{path})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
note.Commit = lastCommits[0]
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -2,6 +2,8 @@
|
||||||
// Use of this source code is governed by a MIT-style
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build gogit
|
||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
|
@ -2,6 +2,8 @@
|
||||||
// Use of this source code is governed by a MIT-style
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build gogit
|
||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
78
modules/git/parse_nogogit.go
Normal file
78
modules/git/parse_nogogit.go
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseTreeEntries parses the output of a `git ls-tree` command.
|
||||||
|
func ParseTreeEntries(data []byte) ([]*TreeEntry, error) {
|
||||||
|
return parseTreeEntries(data, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) {
|
||||||
|
entries := make([]*TreeEntry, 0, 10)
|
||||||
|
for pos := 0; pos < len(data); {
|
||||||
|
// expect line to be of the form "<mode> <type> <sha>\t<filename>"
|
||||||
|
entry := new(TreeEntry)
|
||||||
|
entry.ptree = ptree
|
||||||
|
if pos+6 > len(data) {
|
||||||
|
return nil, fmt.Errorf("Invalid ls-tree output: %s", string(data))
|
||||||
|
}
|
||||||
|
switch string(data[pos : pos+6]) {
|
||||||
|
case "100644":
|
||||||
|
entry.entryMode = EntryModeBlob
|
||||||
|
pos += 12 // skip over "100644 blob "
|
||||||
|
case "100755":
|
||||||
|
entry.entryMode = EntryModeExec
|
||||||
|
pos += 12 // skip over "100755 blob "
|
||||||
|
case "120000":
|
||||||
|
entry.entryMode = EntryModeSymlink
|
||||||
|
pos += 12 // skip over "120000 blob "
|
||||||
|
case "160000":
|
||||||
|
entry.entryMode = EntryModeCommit
|
||||||
|
pos += 14 // skip over "160000 object "
|
||||||
|
case "040000":
|
||||||
|
entry.entryMode = EntryModeTree
|
||||||
|
pos += 12 // skip over "040000 tree "
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown type: %v", string(data[pos:pos+6]))
|
||||||
|
}
|
||||||
|
|
||||||
|
if pos+40 > len(data) {
|
||||||
|
return nil, fmt.Errorf("Invalid ls-tree output: %s", string(data))
|
||||||
|
}
|
||||||
|
id, err := NewIDFromString(string(data[pos : pos+40]))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Invalid ls-tree output: %v", err)
|
||||||
|
}
|
||||||
|
entry.ID = id
|
||||||
|
pos += 41 // skip over sha and trailing space
|
||||||
|
|
||||||
|
end := pos + bytes.IndexByte(data[pos:], '\n')
|
||||||
|
if end < pos {
|
||||||
|
return nil, fmt.Errorf("Invalid ls-tree output: %s", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// In case entry name is surrounded by double quotes(it happens only in git-shell).
|
||||||
|
if data[pos] == '"' {
|
||||||
|
entry.name, err = strconv.Unquote(string(data[pos:end]))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Invalid ls-tree output: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
entry.name = string(data[pos:end])
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = end + 1
|
||||||
|
entries = append(entries, entry)
|
||||||
|
}
|
||||||
|
return entries, nil
|
||||||
|
}
|
159
modules/git/pipeline/lfs.go
Normal file
159
modules/git/pipeline/lfs.go
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build gogit
|
||||||
|
|
||||||
|
package pipeline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
gogit "github.com/go-git/go-git/v5"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/object"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LFSResult represents commits found using a provided pointer file hash
|
||||||
|
type LFSResult struct {
|
||||||
|
Name string
|
||||||
|
SHA string
|
||||||
|
Summary string
|
||||||
|
When time.Time
|
||||||
|
ParentHashes []git.SHA1
|
||||||
|
BranchName string
|
||||||
|
FullCommitName string
|
||||||
|
}
|
||||||
|
|
||||||
|
type lfsResultSlice []*LFSResult
|
||||||
|
|
||||||
|
func (a lfsResultSlice) Len() int { return len(a) }
|
||||||
|
func (a lfsResultSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
|
func (a lfsResultSlice) Less(i, j int) bool { return a[j].When.After(a[i].When) }
|
||||||
|
|
||||||
|
// FindLFSFile finds commits that contain a provided pointer file hash
|
||||||
|
func FindLFSFile(repo *git.Repository, hash git.SHA1) ([]*LFSResult, error) {
|
||||||
|
resultsMap := map[string]*LFSResult{}
|
||||||
|
results := make([]*LFSResult, 0)
|
||||||
|
|
||||||
|
basePath := repo.Path
|
||||||
|
gogitRepo := repo.GoGitRepo()
|
||||||
|
|
||||||
|
commitsIter, err := gogitRepo.Log(&gogit.LogOptions{
|
||||||
|
Order: gogit.LogOrderCommitterTime,
|
||||||
|
All: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to get GoGit CommitsIter. Error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = commitsIter.ForEach(func(gitCommit *object.Commit) error {
|
||||||
|
tree, err := gitCommit.Tree()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
treeWalker := object.NewTreeWalker(tree, true, nil)
|
||||||
|
defer treeWalker.Close()
|
||||||
|
for {
|
||||||
|
name, entry, err := treeWalker.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if entry.Hash == hash {
|
||||||
|
result := LFSResult{
|
||||||
|
Name: name,
|
||||||
|
SHA: gitCommit.Hash.String(),
|
||||||
|
Summary: strings.Split(strings.TrimSpace(gitCommit.Message), "\n")[0],
|
||||||
|
When: gitCommit.Author.When,
|
||||||
|
ParentHashes: gitCommit.ParentHashes,
|
||||||
|
}
|
||||||
|
resultsMap[gitCommit.Hash.String()+":"+name] = &result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return nil, fmt.Errorf("Failure in CommitIter.ForEach: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, result := range resultsMap {
|
||||||
|
hasParent := false
|
||||||
|
for _, parentHash := range result.ParentHashes {
|
||||||
|
if _, hasParent = resultsMap[parentHash.String()+":"+result.Name]; hasParent {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasParent {
|
||||||
|
results = append(results, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(lfsResultSlice(results))
|
||||||
|
|
||||||
|
// Should really use a go-git function here but name-rev is not completed and recapitulating it is not simple
|
||||||
|
shasToNameReader, shasToNameWriter := io.Pipe()
|
||||||
|
nameRevStdinReader, nameRevStdinWriter := io.Pipe()
|
||||||
|
errChan := make(chan error, 1)
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(3)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
scanner := bufio.NewScanner(nameRevStdinReader)
|
||||||
|
i := 0
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if len(line) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result := results[i]
|
||||||
|
result.FullCommitName = line
|
||||||
|
result.BranchName = strings.Split(line, "~")[0]
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
go NameRevStdin(shasToNameReader, nameRevStdinWriter, &wg, basePath)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
defer shasToNameWriter.Close()
|
||||||
|
for _, result := range results {
|
||||||
|
i := 0
|
||||||
|
if i < len(result.SHA) {
|
||||||
|
n, err := shasToNameWriter.Write([]byte(result.SHA)[i:])
|
||||||
|
if err != nil {
|
||||||
|
errChan <- err
|
||||||
|
break
|
||||||
|
}
|
||||||
|
i += n
|
||||||
|
}
|
||||||
|
n := 0
|
||||||
|
for n < 1 {
|
||||||
|
n, err = shasToNameWriter.Write([]byte{'\n'})
|
||||||
|
if err != nil {
|
||||||
|
errChan <- err
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err, has := <-errChan:
|
||||||
|
if has {
|
||||||
|
return nil, fmt.Errorf("Unable to obtain name for LFS files. Error: %w", err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
266
modules/git/pipeline/lfs_nogogit.go
Normal file
266
modules/git/pipeline/lfs_nogogit.go
Normal file
|
@ -0,0 +1,266 @@
|
||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !gogit
|
||||||
|
|
||||||
|
package pipeline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LFSResult represents commits found using a provided pointer file hash
|
||||||
|
type LFSResult struct {
|
||||||
|
Name string
|
||||||
|
SHA string
|
||||||
|
Summary string
|
||||||
|
When time.Time
|
||||||
|
ParentHashes []git.SHA1
|
||||||
|
BranchName string
|
||||||
|
FullCommitName string
|
||||||
|
}
|
||||||
|
|
||||||
|
type lfsResultSlice []*LFSResult
|
||||||
|
|
||||||
|
func (a lfsResultSlice) Len() int { return len(a) }
|
||||||
|
func (a lfsResultSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
|
func (a lfsResultSlice) Less(i, j int) bool { return a[j].When.After(a[i].When) }
|
||||||
|
|
||||||
|
// FindLFSFile finds commits that contain a provided pointer file hash
|
||||||
|
func FindLFSFile(repo *git.Repository, hash git.SHA1) ([]*LFSResult, error) {
|
||||||
|
resultsMap := map[string]*LFSResult{}
|
||||||
|
results := make([]*LFSResult, 0)
|
||||||
|
|
||||||
|
basePath := repo.Path
|
||||||
|
|
||||||
|
hashStr := hash.String()
|
||||||
|
|
||||||
|
// Use rev-list to provide us with all commits in order
|
||||||
|
revListReader, revListWriter := io.Pipe()
|
||||||
|
defer func() {
|
||||||
|
_ = revListWriter.Close()
|
||||||
|
_ = revListReader.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
stderr := strings.Builder{}
|
||||||
|
err := git.NewCommand("rev-list", "--all").RunInDirPipeline(repo.Path, revListWriter, &stderr)
|
||||||
|
if err != nil {
|
||||||
|
_ = revListWriter.CloseWithError(git.ConcatenateError(err, (&stderr).String()))
|
||||||
|
} else {
|
||||||
|
_ = revListWriter.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Next feed the commits in order into cat-file --batch, followed by their trees and sub trees as necessary.
|
||||||
|
// so let's create a batch stdin and stdout
|
||||||
|
batchStdinReader, batchStdinWriter := io.Pipe()
|
||||||
|
batchStdoutReader, batchStdoutWriter := io.Pipe()
|
||||||
|
defer func() {
|
||||||
|
_ = batchStdinReader.Close()
|
||||||
|
_ = batchStdinWriter.Close()
|
||||||
|
_ = batchStdoutReader.Close()
|
||||||
|
_ = batchStdoutWriter.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
stderr := strings.Builder{}
|
||||||
|
err := git.NewCommand("cat-file", "--batch").RunInDirFullPipeline(repo.Path, batchStdoutWriter, &stderr, batchStdinReader)
|
||||||
|
if err != nil {
|
||||||
|
_ = revListWriter.CloseWithError(git.ConcatenateError(err, (&stderr).String()))
|
||||||
|
} else {
|
||||||
|
_ = revListWriter.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// For simplicities sake we'll us a buffered reader to read from the cat-file --batch
|
||||||
|
batchReader := bufio.NewReader(batchStdoutReader)
|
||||||
|
|
||||||
|
// We'll use a scanner for the revList because it's simpler than a bufio.Reader
|
||||||
|
scan := bufio.NewScanner(revListReader)
|
||||||
|
trees := [][]byte{}
|
||||||
|
paths := []string{}
|
||||||
|
|
||||||
|
fnameBuf := make([]byte, 4096)
|
||||||
|
modeBuf := make([]byte, 40)
|
||||||
|
workingShaBuf := make([]byte, 40)
|
||||||
|
|
||||||
|
for scan.Scan() {
|
||||||
|
// Get the next commit ID
|
||||||
|
commitID := scan.Bytes()
|
||||||
|
|
||||||
|
// push the commit to the cat-file --batch process
|
||||||
|
_, err := batchStdinWriter.Write(commitID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = batchStdinWriter.Write([]byte{'\n'})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var curCommit *git.Commit
|
||||||
|
curPath := ""
|
||||||
|
|
||||||
|
commitReadingLoop:
|
||||||
|
for {
|
||||||
|
_, typ, size, err := git.ReadBatchLine(batchReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch typ {
|
||||||
|
case "tag":
|
||||||
|
// This shouldn't happen but if it does well just get the commit and try again
|
||||||
|
id, err := git.ReadTagObjectID(batchReader, size)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = batchStdinWriter.Write([]byte(id + "\n"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
case "commit":
|
||||||
|
// Read in the commit to get its tree and in case this is one of the last used commits
|
||||||
|
curCommit, err = git.CommitFromReader(repo, git.MustIDFromString(string(commitID)), io.LimitReader(batchReader, int64(size)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := batchStdinWriter.Write([]byte(curCommit.Tree.ID.String() + "\n"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
curPath = ""
|
||||||
|
case "tree":
|
||||||
|
var n int64
|
||||||
|
for n < size {
|
||||||
|
mode, fname, sha, count, err := git.ParseTreeLine(batchReader, modeBuf, fnameBuf, workingShaBuf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n += int64(count)
|
||||||
|
if bytes.Equal(sha, []byte(hashStr)) {
|
||||||
|
result := LFSResult{
|
||||||
|
Name: curPath + string(fname),
|
||||||
|
SHA: curCommit.ID.String(),
|
||||||
|
Summary: strings.Split(strings.TrimSpace(curCommit.CommitMessage), "\n")[0],
|
||||||
|
When: curCommit.Author.When,
|
||||||
|
ParentHashes: curCommit.Parents,
|
||||||
|
}
|
||||||
|
resultsMap[curCommit.ID.String()+":"+curPath+string(fname)] = &result
|
||||||
|
} else if string(mode) == git.EntryModeTree.String() {
|
||||||
|
trees = append(trees, sha)
|
||||||
|
paths = append(paths, curPath+string(fname)+"/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(trees) > 0 {
|
||||||
|
_, err := batchStdinWriter.Write(trees[len(trees)-1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = batchStdinWriter.Write([]byte("\n"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
curPath = paths[len(paths)-1]
|
||||||
|
trees = trees[:len(trees)-1]
|
||||||
|
paths = paths[:len(paths)-1]
|
||||||
|
} else {
|
||||||
|
break commitReadingLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scan.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, result := range resultsMap {
|
||||||
|
hasParent := false
|
||||||
|
for _, parentHash := range result.ParentHashes {
|
||||||
|
if _, hasParent = resultsMap[parentHash.String()+":"+result.Name]; hasParent {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasParent {
|
||||||
|
results = append(results, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(lfsResultSlice(results))
|
||||||
|
|
||||||
|
// Should really use a go-git function here but name-rev is not completed and recapitulating it is not simple
|
||||||
|
shasToNameReader, shasToNameWriter := io.Pipe()
|
||||||
|
nameRevStdinReader, nameRevStdinWriter := io.Pipe()
|
||||||
|
errChan := make(chan error, 1)
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(3)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
scanner := bufio.NewScanner(nameRevStdinReader)
|
||||||
|
i := 0
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if len(line) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result := results[i]
|
||||||
|
result.FullCommitName = line
|
||||||
|
result.BranchName = strings.Split(line, "~")[0]
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
go NameRevStdin(shasToNameReader, nameRevStdinWriter, &wg, basePath)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
defer shasToNameWriter.Close()
|
||||||
|
for _, result := range results {
|
||||||
|
i := 0
|
||||||
|
if i < len(result.SHA) {
|
||||||
|
n, err := shasToNameWriter.Write([]byte(result.SHA)[i:])
|
||||||
|
if err != nil {
|
||||||
|
errChan <- err
|
||||||
|
break
|
||||||
|
}
|
||||||
|
i += n
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
n := 0
|
||||||
|
for n < 1 {
|
||||||
|
n, err = shasToNameWriter.Write([]byte{'\n'})
|
||||||
|
if err != nil {
|
||||||
|
errChan <- err
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err, has := <-errChan:
|
||||||
|
if has {
|
||||||
|
return nil, fmt.Errorf("Unable to obtain name for LFS files. Error: %w", err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
|
@ -9,34 +9,16 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"container/list"
|
"container/list"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
gitealog "code.gitea.io/gitea/modules/log"
|
|
||||||
"github.com/go-git/go-billy/v5/osfs"
|
|
||||||
gogit "github.com/go-git/go-git/v5"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/cache"
|
|
||||||
"github.com/go-git/go-git/v5/storage/filesystem"
|
|
||||||
"github.com/unknwon/com"
|
"github.com/unknwon/com"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Repository represents a Git repository.
|
|
||||||
type Repository struct {
|
|
||||||
Path string
|
|
||||||
|
|
||||||
tagCache *ObjectCache
|
|
||||||
|
|
||||||
gogitRepo *gogit.Repository
|
|
||||||
gogitStorage *filesystem.Storage
|
|
||||||
gpgSettings *GPGSettings
|
|
||||||
}
|
|
||||||
|
|
||||||
// GPGSettings represents the default GPG settings for this repository
|
// GPGSettings represents the default GPG settings for this repository
|
||||||
type GPGSettings struct {
|
type GPGSettings struct {
|
||||||
Sign bool
|
Sign bool
|
||||||
|
@ -93,52 +75,6 @@ func InitRepository(repoPath string, bare bool) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenRepository opens the repository at the given path.
|
|
||||||
func OpenRepository(repoPath string) (*Repository, error) {
|
|
||||||
repoPath, err := filepath.Abs(repoPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if !isDir(repoPath) {
|
|
||||||
return nil, errors.New("no such file or directory")
|
|
||||||
}
|
|
||||||
|
|
||||||
fs := osfs.New(repoPath)
|
|
||||||
_, err = fs.Stat(".git")
|
|
||||||
if err == nil {
|
|
||||||
fs, err = fs.Chroot(".git")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
storage := filesystem.NewStorageWithOptions(fs, cache.NewObjectLRUDefault(), filesystem.Options{KeepDescriptors: true})
|
|
||||||
gogitRepo, err := gogit.Open(storage, fs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Repository{
|
|
||||||
Path: repoPath,
|
|
||||||
gogitRepo: gogitRepo,
|
|
||||||
gogitStorage: storage,
|
|
||||||
tagCache: newObjectCache(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close this repository, in particular close the underlying gogitStorage if this is not nil
|
|
||||||
func (repo *Repository) Close() {
|
|
||||||
if repo == nil || repo.gogitStorage == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := repo.gogitStorage.Close(); err != nil {
|
|
||||||
gitealog.Error("Error closing storage: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GoGitRepo gets the go-git repo representation
|
|
||||||
func (repo *Repository) GoGitRepo() *gogit.Repository {
|
|
||||||
return repo.gogitRepo
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsEmpty Check if repository is empty.
|
// IsEmpty Check if repository is empty.
|
||||||
func (repo *Repository) IsEmpty() (bool, error) {
|
func (repo *Repository) IsEmpty() (bool, error) {
|
||||||
var errbuf strings.Builder
|
var errbuf strings.Builder
|
||||||
|
|
76
modules/git/repo_base_gogit.go
Normal file
76
modules/git/repo_base_gogit.go
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||||
|
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
gitealog "code.gitea.io/gitea/modules/log"
|
||||||
|
"github.com/go-git/go-billy/v5/osfs"
|
||||||
|
gogit "github.com/go-git/go-git/v5"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/cache"
|
||||||
|
"github.com/go-git/go-git/v5/storage/filesystem"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Repository represents a Git repository.
|
||||||
|
type Repository struct {
|
||||||
|
Path string
|
||||||
|
|
||||||
|
tagCache *ObjectCache
|
||||||
|
|
||||||
|
gogitRepo *gogit.Repository
|
||||||
|
gogitStorage *filesystem.Storage
|
||||||
|
gpgSettings *GPGSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenRepository opens the repository at the given path.
|
||||||
|
func OpenRepository(repoPath string) (*Repository, error) {
|
||||||
|
repoPath, err := filepath.Abs(repoPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if !isDir(repoPath) {
|
||||||
|
return nil, errors.New("no such file or directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
fs := osfs.New(repoPath)
|
||||||
|
_, err = fs.Stat(".git")
|
||||||
|
if err == nil {
|
||||||
|
fs, err = fs.Chroot(".git")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
storage := filesystem.NewStorageWithOptions(fs, cache.NewObjectLRUDefault(), filesystem.Options{KeepDescriptors: true})
|
||||||
|
gogitRepo, err := gogit.Open(storage, fs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Repository{
|
||||||
|
Path: repoPath,
|
||||||
|
gogitRepo: gogitRepo,
|
||||||
|
gogitStorage: storage,
|
||||||
|
tagCache: newObjectCache(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close this repository, in particular close the underlying gogitStorage if this is not nil
|
||||||
|
func (repo *Repository) Close() {
|
||||||
|
if repo == nil || repo.gogitStorage == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := repo.gogitStorage.Close(); err != nil {
|
||||||
|
gitealog.Error("Error closing storage: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GoGitRepo gets the go-git repo representation
|
||||||
|
func (repo *Repository) GoGitRepo() *gogit.Repository {
|
||||||
|
return repo.gogitRepo
|
||||||
|
}
|
40
modules/git/repo_base_nogogit.go
Normal file
40
modules/git/repo_base_nogogit.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||||
|
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Repository represents a Git repository.
|
||||||
|
type Repository struct {
|
||||||
|
Path string
|
||||||
|
|
||||||
|
tagCache *ObjectCache
|
||||||
|
|
||||||
|
gpgSettings *GPGSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenRepository opens the repository at the given path.
|
||||||
|
func OpenRepository(repoPath string) (*Repository, error) {
|
||||||
|
repoPath, err := filepath.Abs(repoPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if !isDir(repoPath) {
|
||||||
|
return nil, errors.New("no such file or directory")
|
||||||
|
}
|
||||||
|
return &Repository{
|
||||||
|
Path: repoPath,
|
||||||
|
tagCache: newObjectCache(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close this repository, in particular close the underlying gogitStorage if this is not nil
|
||||||
|
func (repo *Repository) Close() {
|
||||||
|
}
|
|
@ -1,25 +1,9 @@
|
||||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a MIT-style
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (repo *Repository) getBlob(id SHA1) (*Blob, error) {
|
|
||||||
encodedObj, err := repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, ErrNotExist{id.String(), ""}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Blob{
|
|
||||||
ID: id,
|
|
||||||
gogitEncodedObj: encodedObj,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBlob finds the blob object in the repository.
|
// GetBlob finds the blob object in the repository.
|
||||||
func (repo *Repository) GetBlob(idStr string) (*Blob, error) {
|
func (repo *Repository) GetBlob(idStr string) (*Blob, error) {
|
||||||
id, err := NewIDFromString(idStr)
|
id, err := NewIDFromString(idStr)
|
||||||
|
|
23
modules/git/repo_blob_gogit.go
Normal file
23
modules/git/repo_blob_gogit.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (repo *Repository) getBlob(id SHA1) (*Blob, error) {
|
||||||
|
encodedObj, err := repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrNotExist{id.String(), ""}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Blob{
|
||||||
|
ID: id,
|
||||||
|
gogitEncodedObj: encodedObj,
|
||||||
|
}, nil
|
||||||
|
}
|
17
modules/git/repo_blob_nogogit.go
Normal file
17
modules/git/repo_blob_nogogit.go
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
func (repo *Repository) getBlob(id SHA1) (*Blob, error) {
|
||||||
|
if id.IsZero() {
|
||||||
|
return nil, ErrNotExist{id.String(), ""}
|
||||||
|
}
|
||||||
|
return &Blob{
|
||||||
|
ID: id,
|
||||||
|
repoPath: repo.Path,
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -8,8 +8,6 @@ package git
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// BranchPrefix base dir of the branch information file store on git
|
// BranchPrefix base dir of the branch information file store on git
|
||||||
|
@ -26,18 +24,6 @@ func IsBranchExist(repoPath, name string) bool {
|
||||||
return IsReferenceExist(repoPath, BranchPrefix+name)
|
return IsReferenceExist(repoPath, BranchPrefix+name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsBranchExist returns true if given branch exists in current repository.
|
|
||||||
func (repo *Repository) IsBranchExist(name string) bool {
|
|
||||||
if name == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
reference, err := repo.gogitRepo.Reference(plumbing.ReferenceName(BranchPrefix+name), true)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return reference.Type() != plumbing.InvalidReference
|
|
||||||
}
|
|
||||||
|
|
||||||
// Branch represents a Git branch.
|
// Branch represents a Git branch.
|
||||||
type Branch struct {
|
type Branch struct {
|
||||||
Name string
|
Name string
|
||||||
|
@ -79,25 +65,6 @@ func (repo *Repository) GetDefaultBranch() (string, error) {
|
||||||
return NewCommand("symbolic-ref", "HEAD").RunInDir(repo.Path)
|
return NewCommand("symbolic-ref", "HEAD").RunInDir(repo.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBranches returns all branches of the repository.
|
|
||||||
func (repo *Repository) GetBranches() ([]string, error) {
|
|
||||||
var branchNames []string
|
|
||||||
|
|
||||||
branches, err := repo.gogitRepo.Branches()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = branches.ForEach(func(branch *plumbing.Reference) error {
|
|
||||||
branchNames = append(branchNames, strings.TrimPrefix(branch.Name().String(), BranchPrefix))
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO: Sort?
|
|
||||||
|
|
||||||
return branchNames, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBranch returns a branch by it's name
|
// GetBranch returns a branch by it's name
|
||||||
func (repo *Repository) GetBranch(branch string) (*Branch, error) {
|
func (repo *Repository) GetBranch(branch string) (*Branch, error) {
|
||||||
if !repo.IsBranchExist(branch) {
|
if !repo.IsBranchExist(branch) {
|
||||||
|
|
45
modules/git/repo_branch_gogit.go
Normal file
45
modules/git/repo_branch_gogit.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||||
|
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsBranchExist returns true if given branch exists in current repository.
|
||||||
|
func (repo *Repository) IsBranchExist(name string) bool {
|
||||||
|
if name == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
reference, err := repo.gogitRepo.Reference(plumbing.ReferenceName(BranchPrefix+name), true)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return reference.Type() != plumbing.InvalidReference
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBranches returns all branches of the repository.
|
||||||
|
func (repo *Repository) GetBranches() ([]string, error) {
|
||||||
|
var branchNames []string
|
||||||
|
|
||||||
|
branches, err := repo.gogitRepo.Branches()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = branches.ForEach(func(branch *plumbing.Reference) error {
|
||||||
|
branchNames = append(branchNames, strings.TrimPrefix(branch.Name().String(), BranchPrefix))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: Sort?
|
||||||
|
|
||||||
|
return branchNames, nil
|
||||||
|
}
|
82
modules/git/repo_branch_nogogit.go
Normal file
82
modules/git/repo_branch_nogogit.go
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||||
|
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsBranchExist returns true if given branch exists in current repository.
|
||||||
|
func (repo *Repository) IsBranchExist(name string) bool {
|
||||||
|
if name == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return IsReferenceExist(repo.Path, BranchPrefix+name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBranches returns all branches of the repository.
|
||||||
|
func (repo *Repository) GetBranches() ([]string, error) {
|
||||||
|
return callShowRef(repo.Path, BranchPrefix, "--heads")
|
||||||
|
}
|
||||||
|
|
||||||
|
func callShowRef(repoPath, prefix, arg string) ([]string, error) {
|
||||||
|
var branchNames []string
|
||||||
|
|
||||||
|
stdoutReader, stdoutWriter := io.Pipe()
|
||||||
|
defer func() {
|
||||||
|
_ = stdoutReader.Close()
|
||||||
|
_ = stdoutWriter.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
stderrBuilder := &strings.Builder{}
|
||||||
|
err := NewCommand("show-ref", arg).RunInDirPipeline(repoPath, stdoutWriter, stderrBuilder)
|
||||||
|
if err != nil {
|
||||||
|
if stderrBuilder.Len() == 0 {
|
||||||
|
_ = stdoutWriter.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = stdoutWriter.CloseWithError(ConcatenateError(err, stderrBuilder.String()))
|
||||||
|
} else {
|
||||||
|
_ = stdoutWriter.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
bufReader := bufio.NewReader(stdoutReader)
|
||||||
|
for {
|
||||||
|
// The output of show-ref is simply a list:
|
||||||
|
// <sha> SP <ref> LF
|
||||||
|
_, err := bufReader.ReadSlice(' ')
|
||||||
|
for err == bufio.ErrBufferFull {
|
||||||
|
// This shouldn't happen but we'll tolerate it for the sake of peace
|
||||||
|
_, err = bufReader.ReadSlice(' ')
|
||||||
|
}
|
||||||
|
if err == io.EOF {
|
||||||
|
return branchNames, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
branchName, err := bufReader.ReadString('\n')
|
||||||
|
if err == io.EOF {
|
||||||
|
// This shouldn't happen... but we'll tolerate it for the sake of peace
|
||||||
|
return branchNames, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
branchName = strings.TrimPrefix(branchName, prefix)
|
||||||
|
if len(branchName) > 0 {
|
||||||
|
branchName = branchName[:len(branchName)-1]
|
||||||
|
}
|
||||||
|
branchNames = append(branchNames, branchName)
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,36 +8,10 @@ package git
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"container/list"
|
"container/list"
|
||||||
"fmt"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetRefCommitID returns the last commit ID string of given reference (branch or tag).
|
|
||||||
func (repo *Repository) GetRefCommitID(name string) (string, error) {
|
|
||||||
ref, err := repo.gogitRepo.Reference(plumbing.ReferenceName(name), true)
|
|
||||||
if err != nil {
|
|
||||||
if err == plumbing.ErrReferenceNotFound {
|
|
||||||
return "", ErrNotExist{
|
|
||||||
ID: name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ref.Hash().String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsCommitExist returns true if given commit exists in current repository.
|
|
||||||
func (repo *Repository) IsCommitExist(name string) bool {
|
|
||||||
hash := plumbing.NewHash(name)
|
|
||||||
_, err := repo.gogitRepo.CommitObject(hash)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBranchCommitID returns last commit ID string of given branch.
|
// GetBranchCommitID returns last commit ID string of given branch.
|
||||||
func (repo *Repository) GetBranchCommitID(name string) (string, error) {
|
func (repo *Repository) GetBranchCommitID(name string) (string, error) {
|
||||||
return repo.GetRefCommitID(BranchPrefix + name)
|
return repo.GetRefCommitID(BranchPrefix + name)
|
||||||
|
@ -55,78 +29,6 @@ func (repo *Repository) GetTagCommitID(name string) (string, error) {
|
||||||
return strings.TrimSpace(stdout), nil
|
return strings.TrimSpace(stdout), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertPGPSignatureForTag(t *object.Tag) *CommitGPGSignature {
|
|
||||||
if t.PGPSignature == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var w strings.Builder
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if _, err = fmt.Fprintf(&w,
|
|
||||||
"object %s\ntype %s\ntag %s\ntagger ",
|
|
||||||
t.Target.String(), t.TargetType.Bytes(), t.Name); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = t.Tagger.Encode(&w); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = fmt.Fprintf(&w, "\n\n"); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = fmt.Fprintf(&w, t.Message); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &CommitGPGSignature{
|
|
||||||
Signature: t.PGPSignature,
|
|
||||||
Payload: strings.TrimSpace(w.String()) + "\n",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *Repository) getCommit(id SHA1) (*Commit, error) {
|
|
||||||
var tagObject *object.Tag
|
|
||||||
|
|
||||||
gogitCommit, err := repo.gogitRepo.CommitObject(id)
|
|
||||||
if err == plumbing.ErrObjectNotFound {
|
|
||||||
tagObject, err = repo.gogitRepo.TagObject(id)
|
|
||||||
if err == plumbing.ErrObjectNotFound {
|
|
||||||
return nil, ErrNotExist{
|
|
||||||
ID: id.String(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
gogitCommit, err = repo.gogitRepo.CommitObject(tagObject.Target)
|
|
||||||
}
|
|
||||||
// if we get a plumbing.ErrObjectNotFound here then the repository is broken and it should be 500
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
commit := convertCommit(gogitCommit)
|
|
||||||
commit.repo = repo
|
|
||||||
|
|
||||||
if tagObject != nil {
|
|
||||||
commit.CommitMessage = strings.TrimSpace(tagObject.Message)
|
|
||||||
commit.Author = &tagObject.Tagger
|
|
||||||
commit.Signature = convertPGPSignatureForTag(tagObject)
|
|
||||||
}
|
|
||||||
|
|
||||||
tree, err := gogitCommit.Tree()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
commit.Tree.ID = tree.Hash
|
|
||||||
commit.Tree.gogitTree = tree
|
|
||||||
|
|
||||||
return commit, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertToSHA1 returns a Hash object from a potential ID string
|
// ConvertToSHA1 returns a Hash object from a potential ID string
|
||||||
func (repo *Repository) ConvertToSHA1(commitID string) (SHA1, error) {
|
func (repo *Repository) ConvertToSHA1(commitID string) (SHA1, error) {
|
||||||
if len(commitID) != 40 {
|
if len(commitID) != 40 {
|
||||||
|
|
110
modules/git/repo_commit_gogit.go
Normal file
110
modules/git/repo_commit_gogit.go
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/object"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetRefCommitID returns the last commit ID string of given reference (branch or tag).
|
||||||
|
func (repo *Repository) GetRefCommitID(name string) (string, error) {
|
||||||
|
ref, err := repo.gogitRepo.Reference(plumbing.ReferenceName(name), true)
|
||||||
|
if err != nil {
|
||||||
|
if err == plumbing.ErrReferenceNotFound {
|
||||||
|
return "", ErrNotExist{
|
||||||
|
ID: name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ref.Hash().String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsCommitExist returns true if given commit exists in current repository.
|
||||||
|
func (repo *Repository) IsCommitExist(name string) bool {
|
||||||
|
hash := plumbing.NewHash(name)
|
||||||
|
_, err := repo.gogitRepo.CommitObject(hash)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertPGPSignatureForTag(t *object.Tag) *CommitGPGSignature {
|
||||||
|
if t.PGPSignature == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var w strings.Builder
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if _, err = fmt.Fprintf(&w,
|
||||||
|
"object %s\ntype %s\ntag %s\ntagger ",
|
||||||
|
t.Target.String(), t.TargetType.Bytes(), t.Name); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = t.Tagger.Encode(&w); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = fmt.Fprintf(&w, "\n\n"); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = fmt.Fprintf(&w, t.Message); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CommitGPGSignature{
|
||||||
|
Signature: t.PGPSignature,
|
||||||
|
Payload: strings.TrimSpace(w.String()) + "\n",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *Repository) getCommit(id SHA1) (*Commit, error) {
|
||||||
|
var tagObject *object.Tag
|
||||||
|
|
||||||
|
gogitCommit, err := repo.gogitRepo.CommitObject(id)
|
||||||
|
if err == plumbing.ErrObjectNotFound {
|
||||||
|
tagObject, err = repo.gogitRepo.TagObject(id)
|
||||||
|
if err == plumbing.ErrObjectNotFound {
|
||||||
|
return nil, ErrNotExist{
|
||||||
|
ID: id.String(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
gogitCommit, err = repo.gogitRepo.CommitObject(tagObject.Target)
|
||||||
|
}
|
||||||
|
// if we get a plumbing.ErrObjectNotFound here then the repository is broken and it should be 500
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
commit := convertCommit(gogitCommit)
|
||||||
|
commit.repo = repo
|
||||||
|
|
||||||
|
if tagObject != nil {
|
||||||
|
commit.CommitMessage = strings.TrimSpace(tagObject.Message)
|
||||||
|
commit.Author = &tagObject.Tagger
|
||||||
|
commit.Signature = convertPGPSignatureForTag(tagObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
tree, err := gogitCommit.Tree()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
commit.Tree.ID = tree.Hash
|
||||||
|
commit.Tree.gogitTree = tree
|
||||||
|
|
||||||
|
return commit, nil
|
||||||
|
}
|
109
modules/git/repo_commit_nogogit.go
Normal file
109
modules/git/repo_commit_nogogit.go
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResolveReference resolves a name to a reference
|
||||||
|
func (repo *Repository) ResolveReference(name string) (string, error) {
|
||||||
|
stdout, err := NewCommand("show-ref", "--hash", name).RunInDir(repo.Path)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "not a valid ref") {
|
||||||
|
return "", ErrNotExist{name, ""}
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
stdout = strings.TrimSpace(stdout)
|
||||||
|
if stdout == "" {
|
||||||
|
return "", ErrNotExist{name, ""}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stdout, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRefCommitID returns the last commit ID string of given reference (branch or tag).
|
||||||
|
func (repo *Repository) GetRefCommitID(name string) (string, error) {
|
||||||
|
stdout, err := NewCommand("show-ref", "--verify", "--hash", name).RunInDir(repo.Path)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "not a valid ref") {
|
||||||
|
return "", ErrNotExist{name, ""}
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimSpace(stdout), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsCommitExist returns true if given commit exists in current repository.
|
||||||
|
func (repo *Repository) IsCommitExist(name string) bool {
|
||||||
|
_, err := NewCommand("cat-file", "-e", name).RunInDir(repo.Path)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *Repository) getCommit(id SHA1) (*Commit, error) {
|
||||||
|
stdoutReader, stdoutWriter := io.Pipe()
|
||||||
|
defer func() {
|
||||||
|
_ = stdoutReader.Close()
|
||||||
|
_ = stdoutWriter.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
stderr := strings.Builder{}
|
||||||
|
err := NewCommand("cat-file", "--batch").RunInDirFullPipeline(repo.Path, stdoutWriter, &stderr, strings.NewReader(id.String()+"\n"))
|
||||||
|
if err != nil {
|
||||||
|
_ = stdoutWriter.CloseWithError(ConcatenateError(err, (&stderr).String()))
|
||||||
|
} else {
|
||||||
|
_ = stdoutWriter.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
bufReader := bufio.NewReader(stdoutReader)
|
||||||
|
_, typ, size, err := ReadBatchLine(bufReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch typ {
|
||||||
|
case "tag":
|
||||||
|
// then we need to parse the tag
|
||||||
|
// and load the commit
|
||||||
|
data, err := ioutil.ReadAll(io.LimitReader(bufReader, size))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tag, err := parseTagData(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tag.repo = repo
|
||||||
|
|
||||||
|
commit, err := tag.Commit()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
commit.CommitMessage = strings.TrimSpace(tag.Message)
|
||||||
|
commit.Author = tag.Tagger
|
||||||
|
commit.Signature = tag.Signature
|
||||||
|
|
||||||
|
return commit, nil
|
||||||
|
case "commit":
|
||||||
|
return CommitFromReader(repo, id, io.LimitReader(bufReader, size))
|
||||||
|
default:
|
||||||
|
_ = stdoutReader.CloseWithError(fmt.Errorf("unknown typ: %s", typ))
|
||||||
|
log("Unknown typ: %s", typ)
|
||||||
|
return nil, ErrNotExist{
|
||||||
|
ID: id.String(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,8 @@
|
||||||
// Use of this source code is governed by a MIT-style
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build gogit
|
||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
|
@ -4,111 +4,5 @@
|
||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/analyze"
|
|
||||||
|
|
||||||
"github.com/go-enry/go-enry/v2"
|
|
||||||
"github.com/go-git/go-git/v5"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
|
||||||
)
|
|
||||||
|
|
||||||
const fileSizeLimit int64 = 16 * 1024 // 16 KiB
|
const fileSizeLimit int64 = 16 * 1024 // 16 KiB
|
||||||
const bigFileSize int64 = 1024 * 1024 // 1 MiB
|
const bigFileSize int64 = 1024 * 1024 // 1 MiB
|
||||||
|
|
||||||
// GetLanguageStats calculates language stats for git repository at specified commit
|
|
||||||
func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, error) {
|
|
||||||
r, err := git.PlainOpen(repo.Path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
rev, err := r.ResolveRevision(plumbing.Revision(commitID))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
commit, err := r.CommitObject(*rev)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tree, err := commit.Tree()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
sizes := make(map[string]int64)
|
|
||||||
err = tree.Files().ForEach(func(f *object.File) error {
|
|
||||||
if f.Size == 0 || enry.IsVendor(f.Name) || enry.IsDotFile(f.Name) ||
|
|
||||||
enry.IsDocumentation(f.Name) || enry.IsConfiguration(f.Name) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// If content can not be read or file is too big just do detection by filename
|
|
||||||
var content []byte
|
|
||||||
if f.Size <= bigFileSize {
|
|
||||||
content, _ = readFile(f, fileSizeLimit)
|
|
||||||
}
|
|
||||||
if enry.IsGenerated(f.Name, content) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Use .gitattributes file for linguist overrides
|
|
||||||
|
|
||||||
language := analyze.GetCodeLanguage(f.Name, content)
|
|
||||||
if language == enry.OtherLanguage || language == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// group languages, such as Pug -> HTML; SCSS -> CSS
|
|
||||||
group := enry.GetLanguageGroup(language)
|
|
||||||
if group != "" {
|
|
||||||
language = group
|
|
||||||
}
|
|
||||||
|
|
||||||
sizes[language] += f.Size
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// filter special languages unless they are the only language
|
|
||||||
if len(sizes) > 1 {
|
|
||||||
for language := range sizes {
|
|
||||||
langtype := enry.GetLanguageType(language)
|
|
||||||
if langtype != enry.Programming && langtype != enry.Markup {
|
|
||||||
delete(sizes, language)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sizes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readFile(f *object.File, limit int64) ([]byte, error) {
|
|
||||||
r, err := f.Reader()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer r.Close()
|
|
||||||
|
|
||||||
if limit <= 0 {
|
|
||||||
return ioutil.ReadAll(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
size := f.Size
|
|
||||||
if limit > 0 && size > limit {
|
|
||||||
size = limit
|
|
||||||
}
|
|
||||||
buf := bytes.NewBuffer(nil)
|
|
||||||
buf.Grow(int(size))
|
|
||||||
_, err = io.Copy(buf, io.LimitReader(r, limit))
|
|
||||||
return buf.Bytes(), err
|
|
||||||
}
|
|
||||||
|
|
113
modules/git/repo_language_stats_gogit.go
Normal file
113
modules/git/repo_language_stats_gogit.go
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/analyze"
|
||||||
|
|
||||||
|
"github.com/go-enry/go-enry/v2"
|
||||||
|
"github.com/go-git/go-git/v5"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/object"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetLanguageStats calculates language stats for git repository at specified commit
|
||||||
|
func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, error) {
|
||||||
|
r, err := git.PlainOpen(repo.Path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rev, err := r.ResolveRevision(plumbing.Revision(commitID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
commit, err := r.CommitObject(*rev)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tree, err := commit.Tree()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sizes := make(map[string]int64)
|
||||||
|
err = tree.Files().ForEach(func(f *object.File) error {
|
||||||
|
if f.Size == 0 || enry.IsVendor(f.Name) || enry.IsDotFile(f.Name) ||
|
||||||
|
enry.IsDocumentation(f.Name) || enry.IsConfiguration(f.Name) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If content can not be read or file is too big just do detection by filename
|
||||||
|
var content []byte
|
||||||
|
if f.Size <= bigFileSize {
|
||||||
|
content, _ = readFile(f, fileSizeLimit)
|
||||||
|
}
|
||||||
|
if enry.IsGenerated(f.Name, content) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Use .gitattributes file for linguist overrides
|
||||||
|
|
||||||
|
language := analyze.GetCodeLanguage(f.Name, content)
|
||||||
|
if language == enry.OtherLanguage || language == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// group languages, such as Pug -> HTML; SCSS -> CSS
|
||||||
|
group := enry.GetLanguageGroup(language)
|
||||||
|
if group != "" {
|
||||||
|
language = group
|
||||||
|
}
|
||||||
|
|
||||||
|
sizes[language] += f.Size
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter special languages unless they are the only language
|
||||||
|
if len(sizes) > 1 {
|
||||||
|
for language := range sizes {
|
||||||
|
langtype := enry.GetLanguageType(language)
|
||||||
|
if langtype != enry.Programming && langtype != enry.Markup {
|
||||||
|
delete(sizes, language)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sizes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readFile(f *object.File, limit int64) ([]byte, error) {
|
||||||
|
r, err := f.Reader()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
if limit <= 0 {
|
||||||
|
return ioutil.ReadAll(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
size := f.Size
|
||||||
|
if limit > 0 && size > limit {
|
||||||
|
size = limit
|
||||||
|
}
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
buf.Grow(int(size))
|
||||||
|
_, err = io.Copy(buf, io.LimitReader(r, limit))
|
||||||
|
return buf.Bytes(), err
|
||||||
|
}
|
109
modules/git/repo_language_stats_nogogit.go
Normal file
109
modules/git/repo_language_stats_nogogit.go
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/analyze"
|
||||||
|
|
||||||
|
"github.com/go-enry/go-enry/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetLanguageStats calculates language stats for git repository at specified commit
|
||||||
|
func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, error) {
|
||||||
|
// FIXME: We can be more efficient here...
|
||||||
|
//
|
||||||
|
// We're expecting that we will be reading a lot of blobs and the trees
|
||||||
|
// Thus we should use a shared `cat-file --batch` to get all of this data
|
||||||
|
// And keep the buffers around with resets as necessary.
|
||||||
|
//
|
||||||
|
// It's more complicated so...
|
||||||
|
commit, err := repo.GetCommit(commitID)
|
||||||
|
if err != nil {
|
||||||
|
log("Unable to get commit for: %s", commitID)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tree := commit.Tree
|
||||||
|
|
||||||
|
entries, err := tree.ListEntriesRecursive()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sizes := make(map[string]int64)
|
||||||
|
for _, f := range entries {
|
||||||
|
if f.Size() == 0 || enry.IsVendor(f.Name()) || enry.IsDotFile(f.Name()) ||
|
||||||
|
enry.IsDocumentation(f.Name()) || enry.IsConfiguration(f.Name()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If content can not be read or file is too big just do detection by filename
|
||||||
|
var content []byte
|
||||||
|
if f.Size() <= bigFileSize {
|
||||||
|
content, _ = readFile(f, fileSizeLimit)
|
||||||
|
}
|
||||||
|
if enry.IsGenerated(f.Name(), content) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Use .gitattributes file for linguist overrides
|
||||||
|
// FIXME: Why can't we split this and the IsGenerated tests to avoid reading the blob unless absolutely necessary?
|
||||||
|
// - eg. do the all the detection tests using filename first before reading content.
|
||||||
|
language := analyze.GetCodeLanguage(f.Name(), content)
|
||||||
|
if language == enry.OtherLanguage || language == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// group languages, such as Pug -> HTML; SCSS -> CSS
|
||||||
|
group := enry.GetLanguageGroup(language)
|
||||||
|
if group != "" {
|
||||||
|
language = group
|
||||||
|
}
|
||||||
|
|
||||||
|
sizes[language] += f.Size()
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter special languages unless they are the only language
|
||||||
|
if len(sizes) > 1 {
|
||||||
|
for language := range sizes {
|
||||||
|
langtype := enry.GetLanguageType(language)
|
||||||
|
if langtype != enry.Programming && langtype != enry.Markup {
|
||||||
|
delete(sizes, language)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sizes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readFile(entry *TreeEntry, limit int64) ([]byte, error) {
|
||||||
|
// FIXME: We can probably be a little more efficient here... see above
|
||||||
|
r, err := entry.Blob().DataAsync()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
if limit <= 0 {
|
||||||
|
return ioutil.ReadAll(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
size := entry.Size()
|
||||||
|
if limit > 0 && size > limit {
|
||||||
|
size = limit
|
||||||
|
}
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
buf.Grow(int(size))
|
||||||
|
_, err = io.Copy(buf, io.LimitReader(r, limit))
|
||||||
|
return buf.Bytes(), err
|
||||||
|
}
|
|
@ -27,6 +27,11 @@ const (
|
||||||
ObjectBranch ObjectType = "branch"
|
ObjectBranch ObjectType = "branch"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Bytes returns the byte array for the Object Type
|
||||||
|
func (o ObjectType) Bytes() []byte {
|
||||||
|
return []byte(o)
|
||||||
|
}
|
||||||
|
|
||||||
// HashObject takes a reader and returns SHA1 hash for that reader
|
// HashObject takes a reader and returns SHA1 hash for that reader
|
||||||
func (repo *Repository) HashObject(reader io.Reader) (SHA1, error) {
|
func (repo *Repository) HashObject(reader io.Reader) (SHA1, error) {
|
||||||
idStr, err := repo.hashObject(reader)
|
idStr, err := repo.hashObject(reader)
|
||||||
|
|
|
@ -4,52 +4,7 @@
|
||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetRefs returns all references of the repository.
|
// GetRefs returns all references of the repository.
|
||||||
func (repo *Repository) GetRefs() ([]*Reference, error) {
|
func (repo *Repository) GetRefs() ([]*Reference, error) {
|
||||||
return repo.GetRefsFiltered("")
|
return repo.GetRefsFiltered("")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRefsFiltered returns all references of the repository that matches patterm exactly or starting with.
|
|
||||||
func (repo *Repository) GetRefsFiltered(pattern string) ([]*Reference, error) {
|
|
||||||
r, err := git.PlainOpen(repo.Path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
refsIter, err := r.References()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
refs := make([]*Reference, 0)
|
|
||||||
if err = refsIter.ForEach(func(ref *plumbing.Reference) error {
|
|
||||||
if ref.Name() != plumbing.HEAD && !ref.Name().IsRemote() &&
|
|
||||||
(pattern == "" || strings.HasPrefix(ref.Name().String(), pattern)) {
|
|
||||||
refType := string(ObjectCommit)
|
|
||||||
if ref.Name().IsTag() {
|
|
||||||
// tags can be of type `commit` (lightweight) or `tag` (annotated)
|
|
||||||
if tagType, _ := repo.GetTagType(ref.Hash()); err == nil {
|
|
||||||
refType = tagType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
r := &Reference{
|
|
||||||
Name: ref.Name().String(),
|
|
||||||
Object: ref.Hash(),
|
|
||||||
Type: refType,
|
|
||||||
repo: repo,
|
|
||||||
}
|
|
||||||
refs = append(refs, r)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return refs, nil
|
|
||||||
}
|
|
||||||
|
|
52
modules/git/repo_ref_gogit.go
Normal file
52
modules/git/repo_ref_gogit.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v5"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetRefsFiltered returns all references of the repository that matches patterm exactly or starting with.
|
||||||
|
func (repo *Repository) GetRefsFiltered(pattern string) ([]*Reference, error) {
|
||||||
|
r, err := git.PlainOpen(repo.Path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
refsIter, err := r.References()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
refs := make([]*Reference, 0)
|
||||||
|
if err = refsIter.ForEach(func(ref *plumbing.Reference) error {
|
||||||
|
if ref.Name() != plumbing.HEAD && !ref.Name().IsRemote() &&
|
||||||
|
(pattern == "" || strings.HasPrefix(ref.Name().String(), pattern)) {
|
||||||
|
refType := string(ObjectCommit)
|
||||||
|
if ref.Name().IsTag() {
|
||||||
|
// tags can be of type `commit` (lightweight) or `tag` (annotated)
|
||||||
|
if tagType, _ := repo.GetTagType(ref.Hash()); err == nil {
|
||||||
|
refType = tagType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r := &Reference{
|
||||||
|
Name: ref.Name().String(),
|
||||||
|
Object: ref.Hash(),
|
||||||
|
Type: refType,
|
||||||
|
repo: repo,
|
||||||
|
}
|
||||||
|
refs = append(refs, r)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return refs, nil
|
||||||
|
}
|
84
modules/git/repo_ref_nogogit.go
Normal file
84
modules/git/repo_ref_nogogit.go
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetRefsFiltered returns all references of the repository that matches patterm exactly or starting with.
|
||||||
|
func (repo *Repository) GetRefsFiltered(pattern string) ([]*Reference, error) {
|
||||||
|
stdoutReader, stdoutWriter := io.Pipe()
|
||||||
|
defer func() {
|
||||||
|
_ = stdoutReader.Close()
|
||||||
|
_ = stdoutWriter.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
stderrBuilder := &strings.Builder{}
|
||||||
|
err := NewCommand("for-each-ref").RunInDirPipeline(repo.Path, stdoutWriter, stderrBuilder)
|
||||||
|
if err != nil {
|
||||||
|
_ = stdoutWriter.CloseWithError(ConcatenateError(err, stderrBuilder.String()))
|
||||||
|
} else {
|
||||||
|
_ = stdoutWriter.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
refs := make([]*Reference, 0)
|
||||||
|
bufReader := bufio.NewReader(stdoutReader)
|
||||||
|
for {
|
||||||
|
// The output of for-each-ref is simply a list:
|
||||||
|
// <sha> SP <type> TAB <ref> LF
|
||||||
|
sha, err := bufReader.ReadString(' ')
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sha = sha[:len(sha)-1]
|
||||||
|
|
||||||
|
typ, err := bufReader.ReadString('\t')
|
||||||
|
if err == io.EOF {
|
||||||
|
// This should not happen, but we'll tolerate it
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
typ = typ[:len(typ)-1]
|
||||||
|
|
||||||
|
refName, err := bufReader.ReadString('\n')
|
||||||
|
if err == io.EOF {
|
||||||
|
// This should not happen, but we'll tolerate it
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
refName = refName[:len(refName)-1]
|
||||||
|
|
||||||
|
// refName cannot be HEAD but can be remotes or stash
|
||||||
|
if strings.HasPrefix(refName, "/refs/remotes/") || refName == "/refs/stash" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if pattern == "" || strings.HasPrefix(refName, pattern) {
|
||||||
|
r := &Reference{
|
||||||
|
Name: refName,
|
||||||
|
Object: MustIDFromString(sha),
|
||||||
|
Type: typ,
|
||||||
|
repo: repo,
|
||||||
|
}
|
||||||
|
refs = append(refs, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return refs, nil
|
||||||
|
}
|
|
@ -8,8 +8,6 @@ package git
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TagPrefix tags prefix path on the repository
|
// TagPrefix tags prefix path on the repository
|
||||||
|
@ -20,12 +18,6 @@ func IsTagExist(repoPath, name string) bool {
|
||||||
return IsReferenceExist(repoPath, TagPrefix+name)
|
return IsReferenceExist(repoPath, TagPrefix+name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsTagExist returns true if given tag exists in the repository.
|
|
||||||
func (repo *Repository) IsTagExist(name string) bool {
|
|
||||||
_, err := repo.gogitRepo.Reference(plumbing.ReferenceName(TagPrefix+name), true)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateTag create one tag in the repository
|
// CreateTag create one tag in the repository
|
||||||
func (repo *Repository) CreateTag(name, revision string) error {
|
func (repo *Repository) CreateTag(name, revision string) error {
|
||||||
_, err := NewCommand("tag", "--", name, revision).RunInDir(repo.Path)
|
_, err := NewCommand("tag", "--", name, revision).RunInDir(repo.Path)
|
||||||
|
@ -224,29 +216,6 @@ func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, error) {
|
||||||
return tags, nil
|
return tags, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTags returns all tags of the repository.
|
|
||||||
func (repo *Repository) GetTags() ([]string, error) {
|
|
||||||
var tagNames []string
|
|
||||||
|
|
||||||
tags, err := repo.gogitRepo.Tags()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = tags.ForEach(func(tag *plumbing.Reference) error {
|
|
||||||
tagNames = append(tagNames, strings.TrimPrefix(tag.Name().String(), TagPrefix))
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
// Reverse order
|
|
||||||
for i := 0; i < len(tagNames)/2; i++ {
|
|
||||||
j := len(tagNames) - i - 1
|
|
||||||
tagNames[i], tagNames[j] = tagNames[j], tagNames[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
return tagNames, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTagType gets the type of the tag, either commit (simple) or tag (annotated)
|
// GetTagType gets the type of the tag, either commit (simple) or tag (annotated)
|
||||||
func (repo *Repository) GetTagType(id SHA1) (string, error) {
|
func (repo *Repository) GetTagType(id SHA1) (string, error) {
|
||||||
// Get tag type
|
// Get tag type
|
||||||
|
|
43
modules/git/repo_tag_gogit.go
Normal file
43
modules/git/repo_tag_gogit.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsTagExist returns true if given tag exists in the repository.
|
||||||
|
func (repo *Repository) IsTagExist(name string) bool {
|
||||||
|
_, err := repo.gogitRepo.Reference(plumbing.ReferenceName(TagPrefix+name), true)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTags returns all tags of the repository.
|
||||||
|
func (repo *Repository) GetTags() ([]string, error) {
|
||||||
|
var tagNames []string
|
||||||
|
|
||||||
|
tags, err := repo.gogitRepo.Tags()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = tags.ForEach(func(tag *plumbing.Reference) error {
|
||||||
|
tagNames = append(tagNames, strings.TrimPrefix(tag.Name().String(), TagPrefix))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reverse order
|
||||||
|
for i := 0; i < len(tagNames)/2; i++ {
|
||||||
|
j := len(tagNames) - i - 1
|
||||||
|
tagNames[i], tagNames[j] = tagNames[j], tagNames[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return tagNames, nil
|
||||||
|
}
|
18
modules/git/repo_tag_nogogit.go
Normal file
18
modules/git/repo_tag_nogogit.go
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
// IsTagExist returns true if given tag exists in the repository.
|
||||||
|
func (repo *Repository) IsTagExist(name string) bool {
|
||||||
|
return IsReferenceExist(repo.Path, TagPrefix+name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTags returns all tags of the repository.
|
||||||
|
func (repo *Repository) GetTags() ([]string, error) {
|
||||||
|
return callShowRef(repo.Path, TagPrefix, "--tags")
|
||||||
|
}
|
|
@ -13,45 +13,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (repo *Repository) getTree(id SHA1) (*Tree, error) {
|
|
||||||
gogitTree, err := repo.gogitRepo.TreeObject(id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tree := NewTree(repo, id)
|
|
||||||
tree.gogitTree = gogitTree
|
|
||||||
return tree, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTree find the tree object in the repository.
|
|
||||||
func (repo *Repository) GetTree(idStr string) (*Tree, error) {
|
|
||||||
if len(idStr) != 40 {
|
|
||||||
res, err := NewCommand("rev-parse", "--verify", idStr).RunInDir(repo.Path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(res) > 0 {
|
|
||||||
idStr = res[:len(res)-1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
id, err := NewIDFromString(idStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resolvedID := id
|
|
||||||
commitObject, err := repo.gogitRepo.CommitObject(id)
|
|
||||||
if err == nil {
|
|
||||||
id = SHA1(commitObject.TreeHash)
|
|
||||||
}
|
|
||||||
treeObject, err := repo.getTree(id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
treeObject.ResolvedID = resolvedID
|
|
||||||
return treeObject, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CommitTreeOpts represents the possible options to CommitTree
|
// CommitTreeOpts represents the possible options to CommitTree
|
||||||
type CommitTreeOpts struct {
|
type CommitTreeOpts struct {
|
||||||
Parents []string
|
Parents []string
|
||||||
|
@ -102,7 +63,7 @@ func (repo *Repository) CommitTree(author *Signature, committer *Signature, tree
|
||||||
err = cmd.RunInDirTimeoutEnvFullPipeline(env, -1, repo.Path, stdout, stderr, messageBytes)
|
err = cmd.RunInDirTimeoutEnvFullPipeline(env, -1, repo.Path, stdout, stderr, messageBytes)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return SHA1{}, concatenateError(err, stderr.String())
|
return SHA1{}, ConcatenateError(err, stderr.String())
|
||||||
}
|
}
|
||||||
return NewIDFromString(strings.TrimSpace(stdout.String()))
|
return NewIDFromString(strings.TrimSpace(stdout.String()))
|
||||||
}
|
}
|
||||||
|
|
47
modules/git/repo_tree_gogit.go
Normal file
47
modules/git/repo_tree_gogit.go
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
func (repo *Repository) getTree(id SHA1) (*Tree, error) {
|
||||||
|
gogitTree, err := repo.gogitRepo.TreeObject(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tree := NewTree(repo, id)
|
||||||
|
tree.gogitTree = gogitTree
|
||||||
|
return tree, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTree find the tree object in the repository.
|
||||||
|
func (repo *Repository) GetTree(idStr string) (*Tree, error) {
|
||||||
|
if len(idStr) != 40 {
|
||||||
|
res, err := NewCommand("rev-parse", "--verify", idStr).RunInDir(repo.Path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(res) > 0 {
|
||||||
|
idStr = res[:len(res)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
id, err := NewIDFromString(idStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resolvedID := id
|
||||||
|
commitObject, err := repo.gogitRepo.CommitObject(id)
|
||||||
|
if err == nil {
|
||||||
|
id = SHA1(commitObject.TreeHash)
|
||||||
|
}
|
||||||
|
treeObject, err := repo.getTree(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
treeObject.ResolvedID = resolvedID
|
||||||
|
return treeObject, nil
|
||||||
|
}
|
98
modules/git/repo_tree_nogogit.go
Normal file
98
modules/git/repo_tree_nogogit.go
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (repo *Repository) getTree(id SHA1) (*Tree, error) {
|
||||||
|
stdoutReader, stdoutWriter := io.Pipe()
|
||||||
|
defer func() {
|
||||||
|
_ = stdoutReader.Close()
|
||||||
|
_ = stdoutWriter.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
stderr := &strings.Builder{}
|
||||||
|
err := NewCommand("cat-file", "--batch").RunInDirFullPipeline(repo.Path, stdoutWriter, stderr, strings.NewReader(id.String()+"\n"))
|
||||||
|
if err != nil {
|
||||||
|
_ = stdoutWriter.CloseWithError(ConcatenateError(err, stderr.String()))
|
||||||
|
} else {
|
||||||
|
_ = stdoutWriter.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
bufReader := bufio.NewReader(stdoutReader)
|
||||||
|
// ignore the SHA
|
||||||
|
_, typ, _, err := ReadBatchLine(bufReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch typ {
|
||||||
|
case "tag":
|
||||||
|
resolvedID := id
|
||||||
|
data, err := ioutil.ReadAll(bufReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tag, err := parseTagData(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
commit, err := tag.Commit()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
commit.Tree.ResolvedID = resolvedID
|
||||||
|
log("tag.commit.Tree: %s %v", commit.Tree.ID.String(), commit.Tree.repo)
|
||||||
|
return &commit.Tree, nil
|
||||||
|
case "commit":
|
||||||
|
commit, err := CommitFromReader(repo, id, bufReader)
|
||||||
|
if err != nil {
|
||||||
|
_ = stdoutReader.CloseWithError(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
commit.Tree.ResolvedID = commit.ID
|
||||||
|
log("commit.Tree: %s %v", commit.Tree.ID.String(), commit.Tree.repo)
|
||||||
|
return &commit.Tree, nil
|
||||||
|
case "tree":
|
||||||
|
stdoutReader.Close()
|
||||||
|
tree := NewTree(repo, id)
|
||||||
|
tree.ResolvedID = id
|
||||||
|
return tree, nil
|
||||||
|
default:
|
||||||
|
_ = stdoutReader.CloseWithError(fmt.Errorf("unknown typ: %s", typ))
|
||||||
|
return nil, ErrNotExist{
|
||||||
|
ID: id.String(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTree find the tree object in the repository.
|
||||||
|
func (repo *Repository) GetTree(idStr string) (*Tree, error) {
|
||||||
|
if len(idStr) != 40 {
|
||||||
|
res, err := NewCommand("rev-parse", "--verify", idStr).RunInDir(repo.Path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(res) > 0 {
|
||||||
|
idStr = res[:len(res)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
id, err := NewIDFromString(idStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return repo.getTree(id)
|
||||||
|
}
|
|
@ -10,8 +10,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// EmptySHA defines empty git SHA
|
// EmptySHA defines empty git SHA
|
||||||
|
@ -23,9 +21,6 @@ const EmptyTreeSHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
|
||||||
// SHAPattern can be used to determine if a string is an valid sha
|
// SHAPattern can be used to determine if a string is an valid sha
|
||||||
var SHAPattern = regexp.MustCompile(`^[0-9a-f]{4,40}$`)
|
var SHAPattern = regexp.MustCompile(`^[0-9a-f]{4,40}$`)
|
||||||
|
|
||||||
// SHA1 a git commit name
|
|
||||||
type SHA1 = plumbing.Hash
|
|
||||||
|
|
||||||
// MustID always creates a new SHA1 from a [20]byte array with no validation of input.
|
// MustID always creates a new SHA1 from a [20]byte array with no validation of input.
|
||||||
func MustID(b []byte) SHA1 {
|
func MustID(b []byte) SHA1 {
|
||||||
var id SHA1
|
var id SHA1
|
||||||
|
|
20
modules/git/sha1_gogit.go
Normal file
20
modules/git/sha1_gogit.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SHA1 a git commit name
|
||||||
|
type SHA1 = plumbing.Hash
|
||||||
|
|
||||||
|
// ComputeBlobHash compute the hash for a given blob content
|
||||||
|
func ComputeBlobHash(content []byte) SHA1 {
|
||||||
|
return plumbing.ComputeHash(plumbing.BlobObject, content)
|
||||||
|
}
|
62
modules/git/sha1_nogogit.go
Normal file
62
modules/git/sha1_nogogit.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
|
"hash"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SHA1 a git commit name
|
||||||
|
type SHA1 [20]byte
|
||||||
|
|
||||||
|
// String returns a string representation of the SHA
|
||||||
|
func (s SHA1) String() string {
|
||||||
|
return hex.EncodeToString(s[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsZero returns whether this SHA1 is all zeroes
|
||||||
|
func (s SHA1) IsZero() bool {
|
||||||
|
var empty SHA1
|
||||||
|
return s == empty
|
||||||
|
}
|
||||||
|
|
||||||
|
// ComputeBlobHash compute the hash for a given blob content
|
||||||
|
func ComputeBlobHash(content []byte) SHA1 {
|
||||||
|
return ComputeHash(ObjectBlob, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ComputeHash compute the hash for a given ObjectType and content
|
||||||
|
func ComputeHash(t ObjectType, content []byte) SHA1 {
|
||||||
|
h := NewHasher(t, int64(len(content)))
|
||||||
|
_, _ = h.Write(content)
|
||||||
|
return h.Sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hasher is a struct that will generate a SHA1
|
||||||
|
type Hasher struct {
|
||||||
|
hash.Hash
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHasher takes an object type and size and creates a hasher to generate a SHA
|
||||||
|
func NewHasher(t ObjectType, size int64) Hasher {
|
||||||
|
h := Hasher{sha1.New()}
|
||||||
|
_, _ = h.Write(t.Bytes())
|
||||||
|
_, _ = h.Write([]byte(" "))
|
||||||
|
_, _ = h.Write([]byte(strconv.FormatInt(size, 10)))
|
||||||
|
_, _ = h.Write([]byte{0})
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum generates a SHA1 for the provided hash
|
||||||
|
func (h Hasher) Sum() (sha1 SHA1) {
|
||||||
|
copy(sha1[:], h.Hash.Sum(nil))
|
||||||
|
return
|
||||||
|
}
|
|
@ -5,53 +5,7 @@
|
||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Signature represents the Author or Committer information.
|
|
||||||
type Signature = object.Signature
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// GitTimeLayout is the (default) time layout used by git.
|
// GitTimeLayout is the (default) time layout used by git.
|
||||||
GitTimeLayout = "Mon Jan _2 15:04:05 2006 -0700"
|
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
|
|
||||||
}
|
|
||||||
|
|
54
modules/git/signature_gogit.go
Normal file
54
modules/git/signature_gogit.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/object"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Signature represents the Author or Committer information.
|
||||||
|
type Signature = object.Signature
|
||||||
|
|
||||||
|
// Helper to get a signature from the commit line, which looks like these:
|
||||||
|
// author Patrick Gundlach <gundlach@speedata.de> 1378823654 +0200
|
||||||
|
// author Patrick Gundlach <gundlach@speedata.de> Thu, 07 Apr 2005 22:13:13 +0200
|
||||||
|
// but without the "author " at the beginning (this method should)
|
||||||
|
// be used for author and committer.
|
||||||
|
//
|
||||||
|
// FIXME: include timezone for timestamp!
|
||||||
|
func newSignatureFromCommitline(line []byte) (_ *Signature, err error) {
|
||||||
|
sig := new(Signature)
|
||||||
|
emailStart := bytes.IndexByte(line, '<')
|
||||||
|
sig.Name = string(line[:emailStart-1])
|
||||||
|
emailEnd := bytes.IndexByte(line, '>')
|
||||||
|
sig.Email = string(line[emailStart+1 : emailEnd])
|
||||||
|
|
||||||
|
// Check date format.
|
||||||
|
if len(line) > emailEnd+2 {
|
||||||
|
firstChar := line[emailEnd+2]
|
||||||
|
if firstChar >= 48 && firstChar <= 57 {
|
||||||
|
timestop := bytes.IndexByte(line[emailEnd+2:], ' ')
|
||||||
|
timestring := string(line[emailEnd+2 : emailEnd+2+timestop])
|
||||||
|
seconds, _ := strconv.ParseInt(timestring, 10, 64)
|
||||||
|
sig.When = time.Unix(seconds, 0)
|
||||||
|
} else {
|
||||||
|
sig.When, err = time.Parse(GitTimeLayout, string(line[emailEnd+2:]))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fall back to unix 0 time
|
||||||
|
sig.When = time.Unix(0, 0)
|
||||||
|
}
|
||||||
|
return sig, nil
|
||||||
|
}
|
95
modules/git/signature_nogogit.go
Normal file
95
modules/git/signature_nogogit.go
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Signature represents the Author or Committer information.
|
||||||
|
type Signature struct {
|
||||||
|
// Name represents a person name. It is an arbitrary string.
|
||||||
|
Name string
|
||||||
|
// Email is an email, but it cannot be assumed to be well-formed.
|
||||||
|
Email string
|
||||||
|
// When is the timestamp of the signature.
|
||||||
|
When time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Signature) String() string {
|
||||||
|
return fmt.Sprintf("%s <%s>", s.Name, s.Email)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode decodes a byte array representing a signature to signature
|
||||||
|
func (s *Signature) Decode(b []byte) {
|
||||||
|
sig, _ := newSignatureFromCommitline(b)
|
||||||
|
s.Email = sig.Email
|
||||||
|
s.Name = sig.Name
|
||||||
|
s.When = sig.When
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to get a signature from the commit line, which looks like these:
|
||||||
|
// author Patrick Gundlach <gundlach@speedata.de> 1378823654 +0200
|
||||||
|
// author Patrick Gundlach <gundlach@speedata.de> Thu, 07 Apr 2005 22:13:13 +0200
|
||||||
|
// but without the "author " at the beginning (this method should)
|
||||||
|
// be used for author and committer.
|
||||||
|
func newSignatureFromCommitline(line []byte) (sig *Signature, err error) {
|
||||||
|
sig = new(Signature)
|
||||||
|
emailStart := bytes.LastIndexByte(line, '<')
|
||||||
|
emailEnd := bytes.LastIndexByte(line, '>')
|
||||||
|
if emailStart == -1 || emailEnd == -1 || emailEnd < emailStart {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sig.Name = string(line[:emailStart-1])
|
||||||
|
sig.Email = string(line[emailStart+1 : emailEnd])
|
||||||
|
|
||||||
|
hasTime := emailEnd+2 < len(line)
|
||||||
|
if !hasTime {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check date format.
|
||||||
|
firstChar := line[emailEnd+2]
|
||||||
|
if firstChar >= 48 && firstChar <= 57 {
|
||||||
|
idx := bytes.IndexByte(line[emailEnd+2:], ' ')
|
||||||
|
if idx < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
timestring := string(line[emailEnd+2 : emailEnd+2+idx])
|
||||||
|
seconds, _ := strconv.ParseInt(timestring, 10, 64)
|
||||||
|
sig.When = time.Unix(seconds, 0)
|
||||||
|
|
||||||
|
idx += emailEnd + 3
|
||||||
|
if idx >= len(line) || idx+5 > len(line) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
timezone := string(line[idx : idx+5])
|
||||||
|
tzhours, err1 := strconv.ParseInt(timezone[0:3], 10, 64)
|
||||||
|
tzmins, err2 := strconv.ParseInt(timezone[3:], 10, 64)
|
||||||
|
if err1 != nil || err2 != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tzhours < 0 {
|
||||||
|
tzmins *= -1
|
||||||
|
}
|
||||||
|
tz := time.FixedZone("", int(tzhours*60*60+tzmins*60))
|
||||||
|
sig.When = sig.When.In(tz)
|
||||||
|
} else {
|
||||||
|
sig.When, err = time.Parse(GitTimeLayout, string(line[emailEnd+2:]))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
|
@ -10,6 +10,9 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const beginpgp = "\n-----BEGIN PGP SIGNATURE-----\n"
|
||||||
|
const endpgp = "\n-----END PGP SIGNATURE-----"
|
||||||
|
|
||||||
// Tag represents a Git tag.
|
// Tag represents a Git tag.
|
||||||
type Tag struct {
|
type Tag struct {
|
||||||
Name string
|
Name string
|
||||||
|
@ -19,6 +22,7 @@ type Tag struct {
|
||||||
Type string
|
Type string
|
||||||
Tagger *Signature
|
Tagger *Signature
|
||||||
Message string
|
Message string
|
||||||
|
Signature *CommitGPGSignature
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commit return the commit of the tag reference
|
// Commit return the commit of the tag reference
|
||||||
|
@ -60,12 +64,23 @@ l:
|
||||||
}
|
}
|
||||||
nextline += eol + 1
|
nextline += eol + 1
|
||||||
case eol == 0:
|
case eol == 0:
|
||||||
tag.Message = strings.TrimRight(string(data[nextline+1:]), "\n")
|
tag.Message = string(data[nextline+1 : len(data)-1])
|
||||||
break l
|
break l
|
||||||
default:
|
default:
|
||||||
break l
|
break l
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
idx := strings.LastIndex(tag.Message, beginpgp)
|
||||||
|
if idx > 0 {
|
||||||
|
endSigIdx := strings.Index(tag.Message[idx:], endpgp)
|
||||||
|
if endSigIdx > 0 {
|
||||||
|
tag.Signature = &CommitGPGSignature{
|
||||||
|
Signature: tag.Message[idx+1 : idx+endSigIdx+len(endpgp)],
|
||||||
|
Payload: string(data[:bytes.LastIndex(data, []byte(beginpgp))+1]),
|
||||||
|
}
|
||||||
|
tag.Message = tag.Message[:idx+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
return tag, nil
|
return tag, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,25 +6,9 @@
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Tree represents a flat directory listing.
|
|
||||||
type Tree struct {
|
|
||||||
ID SHA1
|
|
||||||
ResolvedID SHA1
|
|
||||||
repo *Repository
|
|
||||||
|
|
||||||
gogitTree *object.Tree
|
|
||||||
|
|
||||||
// parent tree
|
|
||||||
ptree *Tree
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTree create a new tree according the repository and tree id
|
// NewTree create a new tree according the repository and tree id
|
||||||
func NewTree(repo *Repository, id SHA1) *Tree {
|
func NewTree(repo *Repository, id SHA1) *Tree {
|
||||||
return &Tree{
|
return &Tree{
|
||||||
|
@ -61,70 +45,3 @@ func (t *Tree) SubTree(rpath string) (*Tree, error) {
|
||||||
}
|
}
|
||||||
return g, nil
|
return g, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tree) loadTreeObject() error {
|
|
||||||
gogitTree, err := t.repo.gogitRepo.TreeObject(t.ID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
t.gogitTree = gogitTree
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListEntries returns all entries of current tree.
|
|
||||||
func (t *Tree) ListEntries() (Entries, error) {
|
|
||||||
if t.gogitTree == nil {
|
|
||||||
err := t.loadTreeObject()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
entries := make([]*TreeEntry, len(t.gogitTree.Entries))
|
|
||||||
for i, entry := range t.gogitTree.Entries {
|
|
||||||
entries[i] = &TreeEntry{
|
|
||||||
ID: entry.Hash,
|
|
||||||
gogitTreeEntry: &t.gogitTree.Entries[i],
|
|
||||||
ptree: t,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return entries, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListEntriesRecursive returns all entries of current tree recursively including all subtrees
|
|
||||||
func (t *Tree) ListEntriesRecursive() (Entries, error) {
|
|
||||||
if t.gogitTree == nil {
|
|
||||||
err := t.loadTreeObject()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var entries []*TreeEntry
|
|
||||||
seen := map[plumbing.Hash]bool{}
|
|
||||||
walker := object.NewTreeWalker(t.gogitTree, true, seen)
|
|
||||||
for {
|
|
||||||
fullName, entry, err := walker.Next()
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if seen[entry.Hash] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
convertedEntry := &TreeEntry{
|
|
||||||
ID: entry.Hash,
|
|
||||||
gogitTreeEntry: &entry,
|
|
||||||
ptree: t,
|
|
||||||
fullName: fullName,
|
|
||||||
}
|
|
||||||
entries = append(entries, convertedEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
return entries, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -5,64 +5,6 @@
|
||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/filemode"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetTreeEntryByPath get the tree entries according the sub dir
|
|
||||||
func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) {
|
|
||||||
if len(relpath) == 0 {
|
|
||||||
return &TreeEntry{
|
|
||||||
ID: t.ID,
|
|
||||||
//Type: ObjectTree,
|
|
||||||
gogitTreeEntry: &object.TreeEntry{
|
|
||||||
Name: "",
|
|
||||||
Mode: filemode.Dir,
|
|
||||||
Hash: t.ID,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
relpath = path.Clean(relpath)
|
|
||||||
parts := strings.Split(relpath, "/")
|
|
||||||
var err error
|
|
||||||
tree := t
|
|
||||||
for i, name := range parts {
|
|
||||||
if i == len(parts)-1 {
|
|
||||||
entries, err := tree.ListEntries()
|
|
||||||
if err != nil {
|
|
||||||
if err == plumbing.ErrObjectNotFound {
|
|
||||||
return nil, ErrNotExist{
|
|
||||||
RelPath: relpath,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, v := range entries {
|
|
||||||
if v.Name() == name {
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tree, err = tree.SubTree(name)
|
|
||||||
if err != nil {
|
|
||||||
if err == plumbing.ErrObjectNotFound {
|
|
||||||
return nil, ErrNotExist{
|
|
||||||
RelPath: relpath,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, ErrNotExist{"", relpath}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBlobByPath get the blob object according the path
|
// GetBlobByPath get the blob object according the path
|
||||||
func (t *Tree) GetBlobByPath(relpath string) (*Blob, error) {
|
func (t *Tree) GetBlobByPath(relpath string) (*Blob, error) {
|
||||||
entry, err := t.GetTreeEntryByPath(relpath)
|
entry, err := t.GetTreeEntryByPath(relpath)
|
||||||
|
|
66
modules/git/tree_blob_gogit.go
Normal file
66
modules/git/tree_blob_gogit.go
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/filemode"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/object"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetTreeEntryByPath get the tree entries according the sub dir
|
||||||
|
func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) {
|
||||||
|
if len(relpath) == 0 {
|
||||||
|
return &TreeEntry{
|
||||||
|
ID: t.ID,
|
||||||
|
//Type: ObjectTree,
|
||||||
|
gogitTreeEntry: &object.TreeEntry{
|
||||||
|
Name: "",
|
||||||
|
Mode: filemode.Dir,
|
||||||
|
Hash: t.ID,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
relpath = path.Clean(relpath)
|
||||||
|
parts := strings.Split(relpath, "/")
|
||||||
|
var err error
|
||||||
|
tree := t
|
||||||
|
for i, name := range parts {
|
||||||
|
if i == len(parts)-1 {
|
||||||
|
entries, err := tree.ListEntries()
|
||||||
|
if err != nil {
|
||||||
|
if err == plumbing.ErrObjectNotFound {
|
||||||
|
return nil, ErrNotExist{
|
||||||
|
RelPath: relpath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, v := range entries {
|
||||||
|
if v.Name() == name {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tree, err = tree.SubTree(name)
|
||||||
|
if err != nil {
|
||||||
|
if err == plumbing.ErrObjectNotFound {
|
||||||
|
return nil, ErrNotExist{
|
||||||
|
RelPath: relpath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, ErrNotExist{"", relpath}
|
||||||
|
}
|
49
modules/git/tree_blob_nogogit.go
Normal file
49
modules/git/tree_blob_nogogit.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetTreeEntryByPath get the tree entries according the sub dir
|
||||||
|
func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) {
|
||||||
|
if len(relpath) == 0 {
|
||||||
|
return &TreeEntry{
|
||||||
|
ID: t.ID,
|
||||||
|
name: "",
|
||||||
|
fullName: "",
|
||||||
|
entryMode: EntryModeTree,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: This should probably use git cat-file --batch to be a bit more efficient
|
||||||
|
relpath = path.Clean(relpath)
|
||||||
|
parts := strings.Split(relpath, "/")
|
||||||
|
var err error
|
||||||
|
tree := t
|
||||||
|
for i, name := range parts {
|
||||||
|
if i == len(parts)-1 {
|
||||||
|
entries, err := tree.ListEntries()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, v := range entries {
|
||||||
|
if v.Name() == name {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tree, err = tree.SubTree(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, ErrNotExist{"", relpath}
|
||||||
|
}
|
|
@ -9,55 +9,8 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/filemode"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// EntryMode the type of the object in the git tree
|
|
||||||
type EntryMode int
|
|
||||||
|
|
||||||
// There are only a few file modes in Git. They look like unix file modes, but they can only be
|
|
||||||
// one of these.
|
|
||||||
const (
|
|
||||||
// EntryModeBlob
|
|
||||||
EntryModeBlob EntryMode = 0100644
|
|
||||||
// EntryModeExec
|
|
||||||
EntryModeExec EntryMode = 0100755
|
|
||||||
// EntryModeSymlink
|
|
||||||
EntryModeSymlink EntryMode = 0120000
|
|
||||||
// EntryModeCommit
|
|
||||||
EntryModeCommit EntryMode = 0160000
|
|
||||||
// EntryModeTree
|
|
||||||
EntryModeTree EntryMode = 0040000
|
|
||||||
)
|
|
||||||
|
|
||||||
// TreeEntry the leaf in the git tree
|
|
||||||
type TreeEntry struct {
|
|
||||||
ID SHA1
|
|
||||||
|
|
||||||
gogitTreeEntry *object.TreeEntry
|
|
||||||
ptree *Tree
|
|
||||||
|
|
||||||
size int64
|
|
||||||
sized bool
|
|
||||||
fullName string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name returns the name of the entry
|
|
||||||
func (te *TreeEntry) Name() string {
|
|
||||||
if te.fullName != "" {
|
|
||||||
return te.fullName
|
|
||||||
}
|
|
||||||
return te.gogitTreeEntry.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mode returns the mode of the entry
|
|
||||||
func (te *TreeEntry) Mode() EntryMode {
|
|
||||||
return EntryMode(te.gogitTreeEntry.Mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type returns the type of the entry (commit, tree, blob)
|
// Type returns the type of the entry (commit, tree, blob)
|
||||||
func (te *TreeEntry) Type() string {
|
func (te *TreeEntry) Type() string {
|
||||||
switch te.Mode() {
|
switch te.Mode() {
|
||||||
|
@ -70,63 +23,6 @@ func (te *TreeEntry) Type() string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Size returns the size of the entry
|
|
||||||
func (te *TreeEntry) Size() int64 {
|
|
||||||
if te.IsDir() {
|
|
||||||
return 0
|
|
||||||
} else if te.sized {
|
|
||||||
return te.size
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := te.ptree.gogitTree.TreeEntryFile(te.gogitTreeEntry)
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
te.sized = true
|
|
||||||
te.size = file.Size
|
|
||||||
return te.size
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsSubModule if the entry is a sub module
|
|
||||||
func (te *TreeEntry) IsSubModule() bool {
|
|
||||||
return te.gogitTreeEntry.Mode == filemode.Submodule
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsDir if the entry is a sub dir
|
|
||||||
func (te *TreeEntry) IsDir() bool {
|
|
||||||
return te.gogitTreeEntry.Mode == filemode.Dir
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsLink if the entry is a symlink
|
|
||||||
func (te *TreeEntry) IsLink() bool {
|
|
||||||
return te.gogitTreeEntry.Mode == filemode.Symlink
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsRegular if the entry is a regular file
|
|
||||||
func (te *TreeEntry) IsRegular() bool {
|
|
||||||
return te.gogitTreeEntry.Mode == filemode.Regular
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsExecutable if the entry is an executable file (not necessarily binary)
|
|
||||||
func (te *TreeEntry) IsExecutable() bool {
|
|
||||||
return te.gogitTreeEntry.Mode == filemode.Executable
|
|
||||||
}
|
|
||||||
|
|
||||||
// Blob returns the blob object the entry
|
|
||||||
func (te *TreeEntry) Blob() *Blob {
|
|
||||||
encodedObj, err := te.ptree.repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, te.gogitTreeEntry.Hash)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Blob{
|
|
||||||
ID: te.gogitTreeEntry.Hash,
|
|
||||||
gogitEncodedObj: encodedObj,
|
|
||||||
name: te.Name(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FollowLink returns the entry pointed to by a symlink
|
// FollowLink returns the entry pointed to by a symlink
|
||||||
func (te *TreeEntry) FollowLink() (*TreeEntry, error) {
|
func (te *TreeEntry) FollowLink() (*TreeEntry, error) {
|
||||||
if !te.IsLink() {
|
if !te.IsLink() {
|
||||||
|
|
96
modules/git/tree_entry_gogit.go
Normal file
96
modules/git/tree_entry_gogit.go
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/filemode"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/object"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TreeEntry the leaf in the git tree
|
||||||
|
type TreeEntry struct {
|
||||||
|
ID SHA1
|
||||||
|
|
||||||
|
gogitTreeEntry *object.TreeEntry
|
||||||
|
ptree *Tree
|
||||||
|
|
||||||
|
size int64
|
||||||
|
sized bool
|
||||||
|
fullName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the name of the entry
|
||||||
|
func (te *TreeEntry) Name() string {
|
||||||
|
if te.fullName != "" {
|
||||||
|
return te.fullName
|
||||||
|
}
|
||||||
|
return te.gogitTreeEntry.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode returns the mode of the entry
|
||||||
|
func (te *TreeEntry) Mode() EntryMode {
|
||||||
|
return EntryMode(te.gogitTreeEntry.Mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size returns the size of the entry
|
||||||
|
func (te *TreeEntry) Size() int64 {
|
||||||
|
if te.IsDir() {
|
||||||
|
return 0
|
||||||
|
} else if te.sized {
|
||||||
|
return te.size
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := te.ptree.gogitTree.TreeEntryFile(te.gogitTreeEntry)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
te.sized = true
|
||||||
|
te.size = file.Size
|
||||||
|
return te.size
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSubModule if the entry is a sub module
|
||||||
|
func (te *TreeEntry) IsSubModule() bool {
|
||||||
|
return te.gogitTreeEntry.Mode == filemode.Submodule
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDir if the entry is a sub dir
|
||||||
|
func (te *TreeEntry) IsDir() bool {
|
||||||
|
return te.gogitTreeEntry.Mode == filemode.Dir
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsLink if the entry is a symlink
|
||||||
|
func (te *TreeEntry) IsLink() bool {
|
||||||
|
return te.gogitTreeEntry.Mode == filemode.Symlink
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRegular if the entry is a regular file
|
||||||
|
func (te *TreeEntry) IsRegular() bool {
|
||||||
|
return te.gogitTreeEntry.Mode == filemode.Regular
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsExecutable if the entry is an executable file (not necessarily binary)
|
||||||
|
func (te *TreeEntry) IsExecutable() bool {
|
||||||
|
return te.gogitTreeEntry.Mode == filemode.Executable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blob returns the blob object the entry
|
||||||
|
func (te *TreeEntry) Blob() *Blob {
|
||||||
|
encodedObj, err := te.ptree.repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, te.gogitTreeEntry.Hash)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Blob{
|
||||||
|
ID: te.gogitTreeEntry.Hash,
|
||||||
|
gogitEncodedObj: encodedObj,
|
||||||
|
name: te.Name(),
|
||||||
|
}
|
||||||
|
}
|
36
modules/git/tree_entry_mode.go
Normal file
36
modules/git/tree_entry_mode.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import "strconv"
|
||||||
|
|
||||||
|
// EntryMode the type of the object in the git tree
|
||||||
|
type EntryMode int
|
||||||
|
|
||||||
|
// There are only a few file modes in Git. They look like unix file modes, but they can only be
|
||||||
|
// one of these.
|
||||||
|
const (
|
||||||
|
// EntryModeBlob
|
||||||
|
EntryModeBlob EntryMode = 0100644
|
||||||
|
// EntryModeExec
|
||||||
|
EntryModeExec EntryMode = 0100755
|
||||||
|
// EntryModeSymlink
|
||||||
|
EntryModeSymlink EntryMode = 0120000
|
||||||
|
// EntryModeCommit
|
||||||
|
EntryModeCommit EntryMode = 0160000
|
||||||
|
// EntryModeTree
|
||||||
|
EntryModeTree EntryMode = 0040000
|
||||||
|
)
|
||||||
|
|
||||||
|
// String converts an EntryMode to a string
|
||||||
|
func (e EntryMode) String() string {
|
||||||
|
return strconv.FormatInt(int64(e), 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToEntryMode converts a string to an EntryMode
|
||||||
|
func ToEntryMode(value string) EntryMode {
|
||||||
|
v, _ := strconv.ParseInt(value, 8, 32)
|
||||||
|
return EntryMode(v)
|
||||||
|
}
|
91
modules/git/tree_entry_nogogit.go
Normal file
91
modules/git/tree_entry_nogogit.go
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TreeEntry the leaf in the git tree
|
||||||
|
type TreeEntry struct {
|
||||||
|
ID SHA1
|
||||||
|
|
||||||
|
ptree *Tree
|
||||||
|
|
||||||
|
entryMode EntryMode
|
||||||
|
name string
|
||||||
|
|
||||||
|
size int64
|
||||||
|
sized bool
|
||||||
|
fullName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the name of the entry
|
||||||
|
func (te *TreeEntry) Name() string {
|
||||||
|
if te.fullName != "" {
|
||||||
|
return te.fullName
|
||||||
|
}
|
||||||
|
return te.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode returns the mode of the entry
|
||||||
|
func (te *TreeEntry) Mode() EntryMode {
|
||||||
|
return te.entryMode
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size returns the size of the entry
|
||||||
|
func (te *TreeEntry) Size() int64 {
|
||||||
|
if te.IsDir() {
|
||||||
|
return 0
|
||||||
|
} else if te.sized {
|
||||||
|
return te.size
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout, err := NewCommand("cat-file", "-s", te.ID.String()).RunInDir(te.ptree.repo.Path)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
te.sized = true
|
||||||
|
te.size, _ = strconv.ParseInt(strings.TrimSpace(stdout), 10, 64)
|
||||||
|
return te.size
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSubModule if the entry is a sub module
|
||||||
|
func (te *TreeEntry) IsSubModule() bool {
|
||||||
|
return te.entryMode == EntryModeCommit
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDir if the entry is a sub dir
|
||||||
|
func (te *TreeEntry) IsDir() bool {
|
||||||
|
return te.entryMode == EntryModeTree
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsLink if the entry is a symlink
|
||||||
|
func (te *TreeEntry) IsLink() bool {
|
||||||
|
return te.entryMode == EntryModeSymlink
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRegular if the entry is a regular file
|
||||||
|
func (te *TreeEntry) IsRegular() bool {
|
||||||
|
return te.entryMode == EntryModeBlob
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsExecutable if the entry is an executable file (not necessarily binary)
|
||||||
|
func (te *TreeEntry) IsExecutable() bool {
|
||||||
|
return te.entryMode == EntryModeExec
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blob returns the blob object the entry
|
||||||
|
func (te *TreeEntry) Blob() *Blob {
|
||||||
|
return &Blob{
|
||||||
|
ID: te.ID,
|
||||||
|
repoPath: te.ptree.repo.Path,
|
||||||
|
name: te.Name(),
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,8 @@
|
||||||
// Use of this source code is governed by a MIT-style
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build gogit
|
||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
94
modules/git/tree_gogit.go
Normal file
94
modules/git/tree_gogit.go
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/object"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tree represents a flat directory listing.
|
||||||
|
type Tree struct {
|
||||||
|
ID SHA1
|
||||||
|
ResolvedID SHA1
|
||||||
|
repo *Repository
|
||||||
|
|
||||||
|
gogitTree *object.Tree
|
||||||
|
|
||||||
|
// parent tree
|
||||||
|
ptree *Tree
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tree) loadTreeObject() error {
|
||||||
|
gogitTree, err := t.repo.gogitRepo.TreeObject(t.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
t.gogitTree = gogitTree
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListEntries returns all entries of current tree.
|
||||||
|
func (t *Tree) ListEntries() (Entries, error) {
|
||||||
|
if t.gogitTree == nil {
|
||||||
|
err := t.loadTreeObject()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := make([]*TreeEntry, len(t.gogitTree.Entries))
|
||||||
|
for i, entry := range t.gogitTree.Entries {
|
||||||
|
entries[i] = &TreeEntry{
|
||||||
|
ID: entry.Hash,
|
||||||
|
gogitTreeEntry: &t.gogitTree.Entries[i],
|
||||||
|
ptree: t,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListEntriesRecursive returns all entries of current tree recursively including all subtrees
|
||||||
|
func (t *Tree) ListEntriesRecursive() (Entries, error) {
|
||||||
|
if t.gogitTree == nil {
|
||||||
|
err := t.loadTreeObject()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries []*TreeEntry
|
||||||
|
seen := map[plumbing.Hash]bool{}
|
||||||
|
walker := object.NewTreeWalker(t.gogitTree, true, seen)
|
||||||
|
for {
|
||||||
|
fullName, entry, err := walker.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if seen[entry.Hash] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
convertedEntry := &TreeEntry{
|
||||||
|
ID: entry.Hash,
|
||||||
|
gogitTreeEntry: &entry,
|
||||||
|
ptree: t,
|
||||||
|
fullName: fullName,
|
||||||
|
}
|
||||||
|
entries = append(entries, convertedEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries, nil
|
||||||
|
}
|
69
modules/git/tree_nogogit.go
Normal file
69
modules/git/tree_nogogit.go
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !gogit
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tree represents a flat directory listing.
|
||||||
|
type Tree struct {
|
||||||
|
ID SHA1
|
||||||
|
ResolvedID SHA1
|
||||||
|
repo *Repository
|
||||||
|
|
||||||
|
// parent tree
|
||||||
|
ptree *Tree
|
||||||
|
|
||||||
|
entries Entries
|
||||||
|
entriesParsed bool
|
||||||
|
|
||||||
|
entriesRecursive Entries
|
||||||
|
entriesRecursiveParsed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListEntries returns all entries of current tree.
|
||||||
|
func (t *Tree) ListEntries() (Entries, error) {
|
||||||
|
if t.entriesParsed {
|
||||||
|
return t.entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout, err := NewCommand("ls-tree", t.ID.String()).RunInDirBytes(t.repo.Path)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "fatal: Not a valid object name") || strings.Contains(err.Error(), "fatal: not a tree object") {
|
||||||
|
return nil, ErrNotExist{
|
||||||
|
ID: t.ID.String(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
t.entries, err = parseTreeEntries(stdout, t)
|
||||||
|
if err == nil {
|
||||||
|
t.entriesParsed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.entries, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListEntriesRecursive returns all entries of current tree recursively including all subtrees
|
||||||
|
func (t *Tree) ListEntriesRecursive() (Entries, error) {
|
||||||
|
if t.entriesRecursiveParsed {
|
||||||
|
return t.entriesRecursive, nil
|
||||||
|
}
|
||||||
|
stdout, err := NewCommand("ls-tree", "-t", "-r", t.ID.String()).RunInDirBytes(t.repo.Path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
t.entriesRecursive, err = parseTreeEntries(stdout, t)
|
||||||
|
if err == nil {
|
||||||
|
t.entriesRecursiveParsed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.entriesRecursive, err
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -68,11 +69,12 @@ func isExist(path string) bool {
|
||||||
return err == nil || os.IsExist(err)
|
return err == nil || os.IsExist(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func concatenateError(err error, stderr string) error {
|
// ConcatenateError concatenats an error with stderr string
|
||||||
|
func ConcatenateError(err error, stderr string) error {
|
||||||
if len(stderr) == 0 {
|
if len(stderr) == 0 {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return fmt.Errorf("%v - %s", err, stderr)
|
return fmt.Errorf("%w - %s", err, stderr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RefEndName return the end name of a ref name
|
// RefEndName return the end name of a ref name
|
||||||
|
@ -140,3 +142,29 @@ func ParseBool(value string) (result bool, valid bool) {
|
||||||
}
|
}
|
||||||
return intValue != 0, true
|
return intValue != 0, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LimitedReaderCloser is a limited reader closer
|
||||||
|
type LimitedReaderCloser struct {
|
||||||
|
R io.Reader
|
||||||
|
C io.Closer
|
||||||
|
N int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read implements io.Reader
|
||||||
|
func (l *LimitedReaderCloser) Read(p []byte) (n int, err error) {
|
||||||
|
if l.N <= 0 {
|
||||||
|
_ = l.C.Close()
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
if int64(len(p)) > l.N {
|
||||||
|
p = p[0:l.N]
|
||||||
|
}
|
||||||
|
n, err = l.R.Read(p)
|
||||||
|
l.N -= int64(n)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements io.Closer
|
||||||
|
func (l *LimitedReaderCloser) Close() error {
|
||||||
|
return l.C.Close()
|
||||||
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ package stats
|
||||||
import (
|
import (
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DBIndexer implements Indexer interface to use database's like search
|
// DBIndexer implements Indexer interface to use database's like search
|
||||||
|
@ -37,6 +38,7 @@ func (db *DBIndexer) Index(id int64) error {
|
||||||
// Get latest commit for default branch
|
// Get latest commit for default branch
|
||||||
commitID, err := gitRepo.GetBranchCommitID(repo.DefaultBranch)
|
commitID, err := gitRepo.GetBranchCommitID(repo.DefaultBranch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error("Unable to get commit ID for defaultbranch %s in %s", repo.DefaultBranch, repo.RepoPath())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,6 +50,7 @@ func (db *DBIndexer) Index(id int64) error {
|
||||||
// Calculate and save language statistics to database
|
// Calculate and save language statistics to database
|
||||||
stats, err := gitRepo.GetLanguageStats(commitID)
|
stats, err := gitRepo.GetLanguageStats(commitID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error("Unable to get language stats for ID %s for defaultbranch %s in %s. Error: %v", commitID, repo.DefaultBranch, repo.RepoPath(), err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return repo.UpdateLanguageStats(commitID, stats)
|
return repo.UpdateLanguageStats(commitID, stats)
|
||||||
|
|
|
@ -5,57 +5,14 @@
|
||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"path"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
"code.gitea.io/gitea/modules/cache"
|
"code.gitea.io/gitea/modules/cache"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
cgobject "github.com/go-git/go-git/v5/plumbing/object/commitgraph"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func recusiveCache(gitRepo *git.Repository, c cgobject.CommitNode, tree *git.Tree, treePath string, ca *cache.LastCommitCache, level int) error {
|
|
||||||
if level == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
entries, err := tree.ListEntries()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
entryPaths := make([]string, len(entries))
|
|
||||||
entryMap := make(map[string]*git.TreeEntry)
|
|
||||||
for i, entry := range entries {
|
|
||||||
entryPaths[i] = entry.Name()
|
|
||||||
entryMap[entry.Name()] = entry
|
|
||||||
}
|
|
||||||
|
|
||||||
commits, err := git.GetLastCommitForPaths(c, treePath, entryPaths)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for entry, cm := range commits {
|
|
||||||
if err := ca.Put(c.ID().String(), path.Join(treePath, entry), cm.ID().String()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if entryMap[entry].IsDir() {
|
|
||||||
subTree, err := tree.SubTree(entry)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := recusiveCache(gitRepo, c, subTree, entry, ca, level-1); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getRefName(fullRefName string) string {
|
func getRefName(fullRefName string) string {
|
||||||
if strings.HasPrefix(fullRefName, git.TagPrefix) {
|
if strings.HasPrefix(fullRefName, git.TagPrefix) {
|
||||||
return fullRefName[len(git.TagPrefix):]
|
return fullRefName[len(git.TagPrefix):]
|
||||||
|
@ -84,14 +41,7 @@ func CacheRef(repo *models.Repository, gitRepo *git.Repository, fullRefName stri
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
commitNodeIndex, _ := gitRepo.CommitNodeIndex()
|
commitCache := git.NewLastCommitCache(repo.FullName(), gitRepo, int64(setting.CacheService.LastCommit.TTL.Seconds()), cache.GetCache())
|
||||||
|
|
||||||
c, err := commitNodeIndex.Get(commit.ID)
|
return commitCache.CacheCommit(commit)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ca := cache.NewLastCommitCache(repo.FullName(), gitRepo, int64(setting.CacheService.LastCommit.TTL.Seconds()))
|
|
||||||
|
|
||||||
return recusiveCache(gitRepo, c, &commit.Tree, "", ca, 1)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,6 @@ import (
|
||||||
repo_service "code.gitea.io/gitea/services/repository"
|
repo_service "code.gitea.io/gitea/services/repository"
|
||||||
|
|
||||||
"gitea.com/macaron/macaron"
|
"gitea.com/macaron/macaron"
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func verifyCommits(oldCommitID, newCommitID string, repo *git.Repository, env []string) error {
|
func verifyCommits(oldCommitID, newCommitID string, repo *git.Repository, env []string) error {
|
||||||
|
@ -82,7 +81,7 @@ func readAndVerifyCommit(sha string, repo *git.Repository, env []string) error {
|
||||||
_ = stdoutReader.Close()
|
_ = stdoutReader.Close()
|
||||||
_ = stdoutWriter.Close()
|
_ = stdoutWriter.Close()
|
||||||
}()
|
}()
|
||||||
hash := plumbing.NewHash(sha)
|
hash := git.MustIDFromString(sha)
|
||||||
|
|
||||||
return git.NewCommand("cat-file", "commit", sha).
|
return git.NewCommand("cat-file", "commit", sha).
|
||||||
RunInDirTimeoutEnvFullPipelineFunc(env, -1, repo.Path,
|
RunInDirTimeoutEnvFullPipelineFunc(env, -1, repo.Path,
|
||||||
|
|
|
@ -12,11 +12,9 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"path"
|
"path"
|
||||||
"sort"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
|
@ -29,9 +27,6 @@ import (
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/storage"
|
"code.gitea.io/gitea/modules/storage"
|
||||||
|
|
||||||
gogit "github.com/go-git/go-git/v5"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
|
||||||
"github.com/unknwon/com"
|
"github.com/unknwon/com"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -363,22 +358,6 @@ func LFSDelete(ctx *context.Context) {
|
||||||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs")
|
ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs")
|
||||||
}
|
}
|
||||||
|
|
||||||
type lfsResult struct {
|
|
||||||
Name string
|
|
||||||
SHA string
|
|
||||||
Summary string
|
|
||||||
When time.Time
|
|
||||||
ParentHashes []plumbing.Hash
|
|
||||||
BranchName string
|
|
||||||
FullCommitName string
|
|
||||||
}
|
|
||||||
|
|
||||||
type lfsResultSlice []*lfsResult
|
|
||||||
|
|
||||||
func (a lfsResultSlice) Len() int { return len(a) }
|
|
||||||
func (a lfsResultSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
||||||
func (a lfsResultSlice) Less(i, j int) bool { return a[j].When.After(a[i].When) }
|
|
||||||
|
|
||||||
// LFSFileFind guesses a sha for the provided oid (or uses the provided sha) and then finds the commits that contain this sha
|
// LFSFileFind guesses a sha for the provided oid (or uses the provided sha) and then finds the commits that contain this sha
|
||||||
func LFSFileFind(ctx *context.Context) {
|
func LFSFileFind(ctx *context.Context) {
|
||||||
if !setting.LFS.StartServer {
|
if !setting.LFS.StartServer {
|
||||||
|
@ -394,140 +373,27 @@ func LFSFileFind(ctx *context.Context) {
|
||||||
sha := ctx.Query("sha")
|
sha := ctx.Query("sha")
|
||||||
ctx.Data["Title"] = oid
|
ctx.Data["Title"] = oid
|
||||||
ctx.Data["PageIsSettingsLFS"] = true
|
ctx.Data["PageIsSettingsLFS"] = true
|
||||||
var hash plumbing.Hash
|
var hash git.SHA1
|
||||||
if len(sha) == 0 {
|
if len(sha) == 0 {
|
||||||
meta := models.LFSMetaObject{Oid: oid, Size: size}
|
meta := models.LFSMetaObject{Oid: oid, Size: size}
|
||||||
pointer := meta.Pointer()
|
pointer := meta.Pointer()
|
||||||
hash = plumbing.ComputeHash(plumbing.BlobObject, []byte(pointer))
|
hash = git.ComputeBlobHash([]byte(pointer))
|
||||||
sha = hash.String()
|
sha = hash.String()
|
||||||
} else {
|
} else {
|
||||||
hash = plumbing.NewHash(sha)
|
hash = git.MustIDFromString(sha)
|
||||||
}
|
}
|
||||||
ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
|
ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
|
||||||
ctx.Data["Oid"] = oid
|
ctx.Data["Oid"] = oid
|
||||||
ctx.Data["Size"] = size
|
ctx.Data["Size"] = size
|
||||||
ctx.Data["SHA"] = sha
|
ctx.Data["SHA"] = sha
|
||||||
|
|
||||||
resultsMap := map[string]*lfsResult{}
|
results, err := pipeline.FindLFSFile(ctx.Repo.GitRepo, hash)
|
||||||
results := make([]*lfsResult, 0)
|
|
||||||
|
|
||||||
basePath := ctx.Repo.Repository.RepoPath()
|
|
||||||
gogitRepo := ctx.Repo.GitRepo.GoGitRepo()
|
|
||||||
|
|
||||||
commitsIter, err := gogitRepo.Log(&gogit.LogOptions{
|
|
||||||
Order: gogit.LogOrderCommitterTime,
|
|
||||||
All: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to get GoGit CommitsIter: %v", err)
|
|
||||||
ctx.ServerError("LFSFind: Iterate Commits", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = commitsIter.ForEach(func(gitCommit *object.Commit) error {
|
|
||||||
tree, err := gitCommit.Tree()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
treeWalker := object.NewTreeWalker(tree, true, nil)
|
|
||||||
defer treeWalker.Close()
|
|
||||||
for {
|
|
||||||
name, entry, err := treeWalker.Next()
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if entry.Hash == hash {
|
|
||||||
result := lfsResult{
|
|
||||||
Name: name,
|
|
||||||
SHA: gitCommit.Hash.String(),
|
|
||||||
Summary: strings.Split(strings.TrimSpace(gitCommit.Message), "\n")[0],
|
|
||||||
When: gitCommit.Author.When,
|
|
||||||
ParentHashes: gitCommit.ParentHashes,
|
|
||||||
}
|
|
||||||
resultsMap[gitCommit.Hash.String()+":"+name] = &result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil && err != io.EOF {
|
if err != nil && err != io.EOF {
|
||||||
log.Error("Failure in CommitIter.ForEach: %v", err)
|
log.Error("Failure in FindLFSFile: %v", err)
|
||||||
ctx.ServerError("LFSFind: IterateCommits ForEach", err)
|
ctx.ServerError("LFSFind: FindLFSFile.", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, result := range resultsMap {
|
|
||||||
hasParent := false
|
|
||||||
for _, parentHash := range result.ParentHashes {
|
|
||||||
if _, hasParent = resultsMap[parentHash.String()+":"+result.Name]; hasParent {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !hasParent {
|
|
||||||
results = append(results, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Sort(lfsResultSlice(results))
|
|
||||||
|
|
||||||
// Should really use a go-git function here but name-rev is not completed and recapitulating it is not simple
|
|
||||||
shasToNameReader, shasToNameWriter := io.Pipe()
|
|
||||||
nameRevStdinReader, nameRevStdinWriter := io.Pipe()
|
|
||||||
errChan := make(chan error, 1)
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
wg.Add(3)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
scanner := bufio.NewScanner(nameRevStdinReader)
|
|
||||||
i := 0
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
if len(line) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
result := results[i]
|
|
||||||
result.FullCommitName = line
|
|
||||||
result.BranchName = strings.Split(line, "~")[0]
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
go pipeline.NameRevStdin(shasToNameReader, nameRevStdinWriter, &wg, basePath)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
defer shasToNameWriter.Close()
|
|
||||||
for _, result := range results {
|
|
||||||
i := 0
|
|
||||||
if i < len(result.SHA) {
|
|
||||||
n, err := shasToNameWriter.Write([]byte(result.SHA)[i:])
|
|
||||||
if err != nil {
|
|
||||||
errChan <- err
|
|
||||||
break
|
|
||||||
}
|
|
||||||
i += n
|
|
||||||
}
|
|
||||||
n := 0
|
|
||||||
for n < 1 {
|
|
||||||
n, err = shasToNameWriter.Write([]byte{'\n'})
|
|
||||||
if err != nil {
|
|
||||||
errChan <- err
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case err, has := <-errChan:
|
|
||||||
if has {
|
|
||||||
ctx.ServerError("LFSPointerFiles", err)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Data["Results"] = results
|
ctx.Data["Results"] = results
|
||||||
ctx.HTML(200, tplSettingsLFSFileFind)
|
ctx.HTML(200, tplSettingsLFSFileFind)
|
||||||
}
|
}
|
||||||
|
|
|
@ -137,9 +137,9 @@ func renderDirectory(ctx *context.Context, treeLink string) {
|
||||||
}
|
}
|
||||||
entries.CustomSort(base.NaturalSortLess)
|
entries.CustomSort(base.NaturalSortLess)
|
||||||
|
|
||||||
var c git.LastCommitCache
|
var c *git.LastCommitCache
|
||||||
if setting.CacheService.LastCommit.Enabled && ctx.Repo.CommitsCount >= setting.CacheService.LastCommit.CommitsCount {
|
if setting.CacheService.LastCommit.Enabled && ctx.Repo.CommitsCount >= setting.CacheService.LastCommit.CommitsCount {
|
||||||
c = cache.NewLastCommitCache(ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, int64(setting.CacheService.LastCommit.TTL.Seconds()))
|
c = git.NewLastCommitCache(ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, int64(setting.CacheService.LastCommit.TTL.Seconds()), cache.GetCache())
|
||||||
}
|
}
|
||||||
|
|
||||||
var latestCommit *git.Commit
|
var latestCommit *git.Commit
|
||||||
|
|
|
@ -40,18 +40,19 @@
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{range $item := .Files}}
|
{{range $item := .Files}}
|
||||||
{{$entry := index $item 0}}
|
{{$entry := $item.Entry}}
|
||||||
{{$commit := index $item 1}}
|
{{$commit := $item.Commit}}
|
||||||
|
{{$subModuleFile := $item.SubModuleFile}}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="name four wide">
|
<td class="name four wide">
|
||||||
<span class="truncate">
|
<span class="truncate">
|
||||||
{{if $entry.IsSubModule}}
|
{{if $entry.IsSubModule}}
|
||||||
{{svg "octicon-file-submodule"}}
|
{{svg "octicon-file-submodule"}}
|
||||||
{{$refURL := $commit.RefURL AppUrl $.Repository.FullName $.SSHDomain}}
|
{{$refURL := $subModuleFile.RefURL AppUrl $.Repository.FullName $.SSHDomain}}
|
||||||
{{if $refURL}}
|
{{if $refURL}}
|
||||||
<a href="{{$refURL}}">{{$entry.Name}}</a><span class="at">@</span><a href="{{$refURL}}/commit/{{$commit.RefID}}">{{ShortSha $commit.RefID}}</a>
|
<a href="{{$refURL}}">{{$entry.Name}}</a><span class="at">@</span><a href="{{$refURL}}/commit/{{$subModuleFile.RefID}}">{{ShortSha $subModuleFile.RefID}}</a>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{$entry.Name}}<span class="at">@</span>{{ShortSha $commit.RefID}}
|
{{$entry.Name}}<span class="at">@</span>{{ShortSha $subModuleFile.RefID}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{if $entry.IsDir}}
|
{{if $entry.IsDir}}
|
||||||
|
|
Loading…
Add table
Reference in a new issue