aboutsummaryrefslogtreecommitdiffstats
path: root/modules/git/commit_info_nogogit.go
blob: f34bef9f018c832a285f9d4e8c697e20307220be (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
// 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"
	"context"
	"fmt"
	"io"
	"math"
	"path"
	"sort"
	"strings"
)

// GetCommitsInfo gets information of all commits that are corresponding to these entries
func (tes Entries) GetCommitsInfo(ctx context.Context, 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(ctx, commit.ID.String(), treePath, entryPaths, cache)
		if err != nil {
			return nil, nil, err
		}
		if len(unHitPaths) > 0 {
			sort.Strings(unHitPaths)
			commits, err := GetLastCommitForPaths(ctx, 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(ctx, 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(ctx context.Context, commitID, treePath string, paths []string, cache *LastCommitCache) (map[string]*Commit, []string, error) {
	wr, rd, cancel := cache.repo.CatFileBatch()
	defer cancel()

	var unHitEntryPaths []string
	var results = make(map[string]*Commit)
	for _, p := range paths {
		lastCommit, err := cache.Get(commitID, path.Join(treePath, p), wr, rd)
		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(ctx context.Context, 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()).SetParentContext(ctx).RunInDirPipeline(commit.repo.Path, revListWriter, &stderr)
		if err != nil {
			_ = revListWriter.CloseWithError(ConcatenateError(err, (&stderr).String()))
		} else {
			_ = revListWriter.Close()
		}
	}()

	batchStdinWriter, batchReader, cancel := commit.repo.CatFileBatch()
	defer cancel()

	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 {
			select {
			case <-ctx.Done():
				return nil, ctx.Err()
			default:
			}
			_, _, 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
				}
				if _, err := batchReader.Discard(1); err != nil {
					return nil, err
				}

				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 _, err := batchReader.Discard(1); 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, treeID)
			_, err = batchStdinWriter.Write(treeID)
			if err != nil {
				return nil, err
			}
			_, err = batchStdinWriter.Write([]byte("\n"))
			if err != nil {
				return nil, err
			}
		}
	}
	if scan.Err() != nil {
		return nil, scan.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
		}
		if _, err := batchReader.Discard(1); err != nil {
			return nil, err
		}
		commitCommits[i] = c
	}

	return commitCommits, scan.Err()
}