291 lines
7.1 KiB
Go
291 lines
7.1 KiB
Go
// Copyright 2015 The Gogs Authors. All rights reserved.
|
|
// Use of this source code is governed by a MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package git
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// EntryMode the type of the object in the git tree
|
|
type EntryMode int
|
|
|
|
// There are only a few file modes in Git. They look like unix file modes, but they can only be
|
|
// one of these.
|
|
const (
|
|
// EntryModeBlob
|
|
EntryModeBlob EntryMode = 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
|
|
Type ObjectType
|
|
|
|
mode EntryMode
|
|
name string
|
|
|
|
ptree *Tree
|
|
|
|
commited bool
|
|
|
|
size int64
|
|
sized bool
|
|
}
|
|
|
|
// Name returns the name of the entry
|
|
func (te *TreeEntry) Name() string {
|
|
return te.name
|
|
}
|
|
|
|
// Size returns the size of the entry
|
|
func (te *TreeEntry) Size() int64 {
|
|
if te.IsDir() {
|
|
return 0
|
|
} else if te.sized {
|
|
return te.size
|
|
}
|
|
|
|
stdout, err := NewCommand("cat-file", "-s", te.ID.String()).RunInDir(te.ptree.repo.Path)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
|
|
te.sized = true
|
|
te.size, _ = strconv.ParseInt(strings.TrimSpace(stdout), 10, 64)
|
|
return te.size
|
|
}
|
|
|
|
// IsSubModule if the entry is a sub module
|
|
func (te *TreeEntry) IsSubModule() bool {
|
|
return te.mode == EntryModeCommit
|
|
}
|
|
|
|
// IsDir if the entry is a sub dir
|
|
func (te *TreeEntry) IsDir() bool {
|
|
return te.mode == EntryModeTree
|
|
}
|
|
|
|
// IsLink if the entry is a symlink
|
|
func (te *TreeEntry) IsLink() bool {
|
|
return te.mode == EntryModeSymlink
|
|
}
|
|
|
|
// Blob retrun the blob object the entry
|
|
func (te *TreeEntry) Blob() *Blob {
|
|
return &Blob{
|
|
repo: te.ptree.repo,
|
|
TreeEntry: te,
|
|
}
|
|
}
|
|
|
|
// GetSubJumpablePathName return the full path of subdirectory jumpable ( contains only one directory )
|
|
func (te *TreeEntry) GetSubJumpablePathName() string {
|
|
if te.IsSubModule() || !te.IsDir() {
|
|
return ""
|
|
}
|
|
tree, err := te.ptree.SubTree(te.name)
|
|
if err != nil {
|
|
return te.name
|
|
}
|
|
entries, _ := tree.ListEntries()
|
|
if len(entries) == 1 && entries[0].IsDir() {
|
|
name := entries[0].GetSubJumpablePathName()
|
|
if name != "" {
|
|
return te.name + "/" + name
|
|
}
|
|
}
|
|
return te.name
|
|
}
|
|
|
|
// Entries a list of entry
|
|
type Entries []*TreeEntry
|
|
|
|
var sorter = []func(t1, t2 *TreeEntry) bool{
|
|
func(t1, t2 *TreeEntry) bool {
|
|
return (t1.IsDir() || t1.IsSubModule()) && !t2.IsDir() && !t2.IsSubModule()
|
|
},
|
|
func(t1, t2 *TreeEntry) bool {
|
|
return t1.name < t2.name
|
|
},
|
|
}
|
|
|
|
func (tes Entries) Len() int { return len(tes) }
|
|
func (tes Entries) Swap(i, j int) { tes[i], tes[j] = tes[j], tes[i] }
|
|
func (tes Entries) Less(i, j int) bool {
|
|
t1, t2 := tes[i], tes[j]
|
|
var k int
|
|
for k = 0; k < len(sorter)-1; k++ {
|
|
s := sorter[k]
|
|
switch {
|
|
case s(t1, t2):
|
|
return true
|
|
case s(t2, t1):
|
|
return false
|
|
}
|
|
}
|
|
return sorter[k](t1, t2)
|
|
}
|
|
|
|
// Sort sort the list of entry
|
|
func (tes Entries) Sort() {
|
|
sort.Sort(tes)
|
|
}
|
|
|
|
// getCommitInfoState transient state for getting commit info for entries
|
|
type getCommitInfoState struct {
|
|
entries map[string]*TreeEntry // map from filepath to entry
|
|
commits map[string]*Commit // map from entry name to commit
|
|
lastCommitHash string
|
|
lastCommit *Commit
|
|
treePath string
|
|
headCommit *Commit
|
|
nextSearchSize int // next number of commits to search for
|
|
}
|
|
|
|
func initGetCommitInfoState(entries Entries, headCommit *Commit, treePath string) *getCommitInfoState {
|
|
entriesByPath := make(map[string]*TreeEntry, len(entries))
|
|
for _, entry := range entries {
|
|
entriesByPath[filepath.Join(treePath, entry.Name())] = entry
|
|
}
|
|
return &getCommitInfoState{
|
|
entries: entriesByPath,
|
|
commits: make(map[string]*Commit, len(entriesByPath)),
|
|
treePath: treePath,
|
|
headCommit: headCommit,
|
|
nextSearchSize: 16,
|
|
}
|
|
}
|
|
|
|
// GetCommitsInfo gets information of all commits that are corresponding to these entries
|
|
func (tes Entries) GetCommitsInfo(commit *Commit, treePath string) ([][]interface{}, error) {
|
|
state := initGetCommitInfoState(tes, commit, treePath)
|
|
if err := getCommitsInfo(state); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
commitsInfo := make([][]interface{}, len(tes))
|
|
for i, entry := range tes {
|
|
commit = state.commits[filepath.Join(treePath, entry.Name())]
|
|
switch entry.Type {
|
|
case ObjectCommit:
|
|
subModuleURL := ""
|
|
if subModule, err := state.headCommit.GetSubModule(entry.Name()); err != nil {
|
|
return nil, err
|
|
} else if subModule != nil {
|
|
subModuleURL = subModule.URL
|
|
}
|
|
subModuleFile := NewSubModuleFile(commit, subModuleURL, entry.ID.String())
|
|
commitsInfo[i] = []interface{}{entry, subModuleFile}
|
|
default:
|
|
commitsInfo[i] = []interface{}{entry, commit}
|
|
}
|
|
}
|
|
return commitsInfo, nil
|
|
}
|
|
|
|
func (state *getCommitInfoState) nextCommit(hash string) {
|
|
state.lastCommitHash = hash
|
|
state.lastCommit = nil
|
|
}
|
|
|
|
func (state *getCommitInfoState) commit() (*Commit, error) {
|
|
var err error
|
|
if state.lastCommit == nil {
|
|
state.lastCommit, err = state.headCommit.repo.GetCommit(state.lastCommitHash)
|
|
}
|
|
return state.lastCommit, err
|
|
}
|
|
|
|
func (state *getCommitInfoState) update(path string) error {
|
|
relPath, err := filepath.Rel(state.treePath, path)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var entryPath string
|
|
if index := strings.IndexRune(relPath, os.PathSeparator); index >= 0 {
|
|
entryPath = filepath.Join(state.treePath, relPath[:index])
|
|
} else {
|
|
entryPath = path
|
|
}
|
|
if _, ok := state.entries[entryPath]; !ok {
|
|
return nil
|
|
} else if _, ok := state.commits[entryPath]; ok {
|
|
return nil
|
|
}
|
|
state.commits[entryPath], err = state.commit()
|
|
return err
|
|
}
|
|
|
|
func getCommitsInfo(state *getCommitInfoState) error {
|
|
for len(state.entries) > len(state.commits) {
|
|
if err := getNextCommitInfos(state); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getNextCommitInfos(state *getCommitInfoState) error {
|
|
logOutput, err := logCommand(state.lastCommitHash, state).RunInDir(state.headCommit.repo.Path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
lines := strings.Split(logOutput, "\n")
|
|
i := 0
|
|
for i < len(lines) {
|
|
state.nextCommit(lines[i])
|
|
i++
|
|
for ; i < len(lines); i++ {
|
|
path := lines[i]
|
|
if path == "" {
|
|
break
|
|
}
|
|
state.update(path)
|
|
}
|
|
i++ // skip blank line
|
|
if len(state.entries) == len(state.commits) {
|
|
break
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func logCommand(exclusiveStartHash string, state *getCommitInfoState) *Command {
|
|
var commitHash string
|
|
if len(exclusiveStartHash) == 0 {
|
|
commitHash = state.headCommit.ID.String()
|
|
} else {
|
|
commitHash = exclusiveStartHash + "^"
|
|
}
|
|
var command *Command
|
|
numRemainingEntries := len(state.entries) - len(state.commits)
|
|
if numRemainingEntries < 32 {
|
|
searchSize := (numRemainingEntries + 1) / 2
|
|
command = NewCommand("log", prettyLogFormat, "--name-only",
|
|
"-"+strconv.Itoa(searchSize), commitHash, "--")
|
|
for path, entry := range state.entries {
|
|
if _, ok := state.commits[entry.Name()]; !ok {
|
|
command.AddArguments(path)
|
|
}
|
|
}
|
|
} else {
|
|
command = NewCommand("log", prettyLogFormat, "--name-only",
|
|
"-"+strconv.Itoa(state.nextSearchSize), commitHash, "--", state.treePath)
|
|
}
|
|
state.nextSearchSize += state.nextSearchSize
|
|
return command
|
|
}
|