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