1// Copyright 2017 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5//go:build !gogit
6// +build !gogit
7
8package git
9
10import (
11	"context"
12	"fmt"
13	"io"
14	"path"
15	"sort"
16
17	"code.gitea.io/gitea/modules/log"
18)
19
20// GetCommitsInfo gets information of all commits that are corresponding to these entries
21func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath string, cache *LastCommitCache) ([]CommitInfo, *Commit, error) {
22	entryPaths := make([]string, len(tes)+1)
23	// Get the commit for the treePath itself
24	entryPaths[0] = ""
25	for i, entry := range tes {
26		entryPaths[i+1] = entry.Name()
27	}
28
29	var err error
30
31	var revs map[string]*Commit
32	if cache != nil {
33		var unHitPaths []string
34		revs, unHitPaths, err = getLastCommitForPathsByCache(ctx, commit.ID.String(), treePath, entryPaths, cache)
35		if err != nil {
36			return nil, nil, err
37		}
38		if len(unHitPaths) > 0 {
39			sort.Strings(unHitPaths)
40			commits, err := GetLastCommitForPaths(ctx, cache, commit, treePath, unHitPaths)
41			if err != nil {
42				return nil, nil, err
43			}
44
45			for pth, found := range commits {
46				revs[pth] = found
47			}
48		}
49	} else {
50		sort.Strings(entryPaths)
51		revs, err = GetLastCommitForPaths(ctx, nil, commit, treePath, entryPaths)
52	}
53	if err != nil {
54		return nil, nil, err
55	}
56
57	commitsInfo := make([]CommitInfo, len(tes))
58	for i, entry := range tes {
59		commitsInfo[i] = CommitInfo{
60			Entry: entry,
61		}
62
63		// Check if we have found a commit for this entry in time
64		if entryCommit, ok := revs[entry.Name()]; ok {
65			commitsInfo[i].Commit = entryCommit
66		} else {
67			log.Debug("missing commit for %s", entry.Name())
68		}
69
70		// If the entry if a submodule add a submodule file for this
71		if entry.IsSubModule() {
72			subModuleURL := ""
73			var fullPath string
74			if len(treePath) > 0 {
75				fullPath = treePath + "/" + entry.Name()
76			} else {
77				fullPath = entry.Name()
78			}
79			if subModule, err := commit.GetSubModule(fullPath); err != nil {
80				return nil, nil, err
81			} else if subModule != nil {
82				subModuleURL = subModule.URL
83			}
84			subModuleFile := NewSubModuleFile(commitsInfo[i].Commit, subModuleURL, entry.ID.String())
85			commitsInfo[i].SubModuleFile = subModuleFile
86		}
87	}
88
89	// Retrieve the commit for the treePath itself (see above). We basically
90	// get it for free during the tree traversal and it's used for listing
91	// pages to display information about newest commit for a given path.
92	var treeCommit *Commit
93	var ok bool
94	if treePath == "" {
95		treeCommit = commit
96	} else if treeCommit, ok = revs[""]; ok {
97		treeCommit.repo = commit.repo
98	}
99	return commitsInfo, treeCommit, nil
100}
101
102func getLastCommitForPathsByCache(ctx context.Context, commitID, treePath string, paths []string, cache *LastCommitCache) (map[string]*Commit, []string, error) {
103	wr, rd, cancel := cache.repo.CatFileBatch(ctx)
104	defer cancel()
105
106	var unHitEntryPaths []string
107	var results = make(map[string]*Commit)
108	for _, p := range paths {
109		lastCommit, err := cache.Get(commitID, path.Join(treePath, p), wr, rd)
110		if err != nil {
111			return nil, nil, err
112		}
113		if lastCommit != nil {
114			results[p] = lastCommit.(*Commit)
115			continue
116		}
117
118		unHitEntryPaths = append(unHitEntryPaths, p)
119	}
120
121	return results, unHitEntryPaths, nil
122}
123
124// GetLastCommitForPaths returns last commit information
125func GetLastCommitForPaths(ctx context.Context, cache *LastCommitCache, commit *Commit, treePath string, paths []string) (map[string]*Commit, error) {
126	// We read backwards from the commit to obtain all of the commits
127	revs, err := WalkGitLog(ctx, cache, commit.repo, commit, treePath, paths...)
128	if err != nil {
129		return nil, err
130	}
131
132	batchStdinWriter, batchReader, cancel := commit.repo.CatFileBatch(ctx)
133	defer cancel()
134
135	commitsMap := map[string]*Commit{}
136	commitsMap[commit.ID.String()] = commit
137
138	commitCommits := map[string]*Commit{}
139	for path, commitID := range revs {
140		c, ok := commitsMap[commitID]
141		if ok {
142			commitCommits[path] = c
143			continue
144		}
145
146		if len(commitID) == 0 {
147			continue
148		}
149
150		_, err := batchStdinWriter.Write([]byte(commitID + "\n"))
151		if err != nil {
152			return nil, err
153		}
154		_, typ, size, err := ReadBatchLine(batchReader)
155		if err != nil {
156			return nil, err
157		}
158		if typ != "commit" {
159			return nil, fmt.Errorf("unexpected type: %s for commit id: %s", typ, commitID)
160		}
161		c, err = CommitFromReader(commit.repo, MustIDFromString(string(commitID)), io.LimitReader(batchReader, int64(size)))
162		if err != nil {
163			return nil, err
164		}
165		if _, err := batchReader.Discard(1); err != nil {
166			return nil, err
167		}
168		commitCommits[path] = c
169	}
170
171	return commitCommits, nil
172}
173