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