all repos

mugit @ e2cbacb4e9ab1dbae47ef104e6e11344e62e88a0

🐮 git server that your cow will love

mugit/internal/git/tree.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
git: get last commit from git cli, improves performance..., 2 months ago
1
package git
2
3
import (
4
	"bufio"
5
	"context"
6
	"errors"
7
	"fmt"
8
	"io"
9
	"mime"
10
	"path"
11
	"path/filepath"
12
	"strings"
13
	"time"
14
15
	"github.com/go-git/go-git/v5/plumbing"
16
	"github.com/go-git/go-git/v5/plumbing/object"
17
)
18
19
type NiceTree struct {
20
	IsFile bool
21
	Name   string
22
	Commit *Commit
23
	Mode   string
24
	Size   int64
25
}
26
27
func (g *Repo) makeNiceTree(t *object.Tree, parent string) []NiceTree {
28
	var nts []NiceTree
29
30
	ctx, cancel := context.WithTimeout(context.TODO(), 2*time.Second)
31
	defer cancel()
32
33
	cms, err := g.lastCommitForFilesInTree(ctx, t, parent)
34
	if err != nil {
35
		return nts
36
	}
37
38
	for _, e := range t.Entries {
39
		fpath := path.Join(parent, e.Name)
40
		mode, _ := e.Mode.ToOSFileMode()
41
		sz, _ := t.Size(e.Name)
42
		nts = append(nts, NiceTree{
43
			Commit: cms[fpath],
44
			Name:   e.Name,
45
			Mode:   mode.String(),
46
			IsFile: e.Mode.IsFile(),
47
			Size:   sz,
48
		})
49
	}
50
	return nts
51
}
52
53
func (g *Repo) FileTree(path string) ([]NiceTree, error) {
54
	c, err := g.r.CommitObject(g.h)
55
	if err != nil {
56
		return nil, fmt.Errorf("commit object: %w", err)
57
	}
58
59
	tree, err := c.Tree()
60
	if err != nil {
61
		return nil, fmt.Errorf("file tree: %w", err)
62
	}
63
64
	var files []NiceTree
65
	if path == "" {
66
		files = g.makeNiceTree(tree, path)
67
	} else {
68
		o, err := tree.FindEntry(path)
69
		if err != nil {
70
			return nil, err
71
		}
72
73
		if !o.Mode.IsFile() {
74
			subtree, err := tree.Tree(path)
75
			if err != nil {
76
				return nil, err
77
			}
78
			files = g.makeNiceTree(subtree, path)
79
		}
80
	}
81
82
	return files, nil
83
}
84
85
type FileContent struct {
86
	IsBinary bool
87
	IsImage  bool
88
	Content  []byte
89
	Mime     string
90
	Size     int64
91
}
92
93
func (fc *FileContent) String() string {
94
	if fc.IsBinary || fc.IsImage {
95
		return ""
96
	}
97
	return string(fc.Content)
98
}
99
100
func (g *Repo) FileContent(path string) (*FileContent, error) {
101
	c, err := g.r.CommitObject(g.h)
102
	if err != nil {
103
		return &FileContent{}, fmt.Errorf("commit object: %w", err)
104
	}
105
106
	tree, err := c.Tree()
107
	if err != nil {
108
		return &FileContent{}, fmt.Errorf("file tree: %w", err)
109
	}
110
111
	file, err := tree.File(path)
112
	if err != nil {
113
		if errors.Is(err, object.ErrFileNotFound) {
114
			return &FileContent{}, ErrFileNotFound
115
		}
116
		return &FileContent{}, err
117
	}
118
119
	reader, err := file.Reader()
120
	if err != nil {
121
		return nil, fmt.Errorf("file reader: %w", err)
122
	}
123
	defer reader.Close()
124
125
	content, err := io.ReadAll(reader)
126
	if err != nil {
127
		return nil, fmt.Errorf("read file: %w", err)
128
	}
129
130
	isBin, _ := file.IsBinary()
131
	mimeType := mime.TypeByExtension(filepath.Ext(path))
132
	if mimeType == "" {
133
		mimeType = "text/plain"
134
		if isBin {
135
			mimeType = "application/octet-stream"
136
		}
137
	}
138
139
	return &FileContent{
140
		IsBinary: isBin,
141
		IsImage:  strings.HasPrefix(mimeType, "image/"),
142
		Content:  content,
143
		Mime:     mimeType,
144
		Size:     file.Size,
145
	}, nil
146
}
147
148
type logCommit struct {
149
	Commit
150
	hash  plumbing.Hash
151
	files []string
152
}
153
154
func (g *Repo) lastCommitForFilesInTree(ctx context.Context, subtree *object.Tree, parent string) (map[string]*Commit, error) {
155
	filesToDo := make(map[string]struct{})
156
	filesDone := make(map[string]*Commit)
157
	for _, e := range subtree.Entries {
158
		fpath := path.Clean(path.Join(parent, e.Name))
159
		filesToDo[fpath] = struct{}{}
160
	}
161
162
	if len(filesToDo) == 0 {
163
		return filesDone, nil
164
	}
165
166
	ctx, cancel := context.WithCancel(ctx)
167
	defer cancel()
168
169
	pathSpec := "."
170
	if parent != "" {
171
		pathSpec = parent
172
	}
173
174
	output, err := g.streamingGitLog(ctx, "--pretty=format:%H,%ad,%ae,%an,%ce,%cn,%s", "--date=iso", "--name-only", "--", pathSpec)
175
	if err != nil {
176
		return nil, err
177
	}
178
	defer output.Close() // Ensure the git process is properly cleaned up
179
180
	var current logCommit
181
	reader := bufio.NewReader(output)
182
	for {
183
		line, err := reader.ReadString('\n')
184
		if err != nil && err != io.EOF {
185
			return nil, err
186
		}
187
188
		line = strings.TrimSpace(line)
189
		fmt.Println("LINE", line)
190
191
		if line == "" {
192
			if !current.hash.IsZero() {
193
				c := current.Commit
194
				// we have a fully parsed commit
195
				for _, f := range current.files {
196
					if _, ok := filesToDo[f]; ok {
197
						filesDone[f] = &c
198
						delete(filesToDo, f)
199
					}
200
				}
201
202
				if len(filesToDo) == 0 {
203
					cancel()
204
					break
205
				}
206
207
				current = logCommit{}
208
			}
209
		} else if current.hash.IsZero() {
210
			parts := strings.SplitN(line, ",", 7)
211
			if len(parts) == 7 {
212
				current.hash = plumbing.NewHash(parts[0])
213
214
				// NOTE: this is copy-paste of [newCommit]
215
				current.Hash = parts[0]
216
				current.HashShort = parts[0][:7]
217
				current.Committed, _ = time.Parse("2006-01-02 15:04:05 -0700", parts[1])
218
				current.AuthorEmail = parts[2]
219
				current.AuthorName = parts[3]
220
				current.CommitterEmail = parts[4]
221
				current.CommitterName = parts[5]
222
				current.Message = parts[6]
223
			}
224
		} else {
225
			// all ancestors along this path should also be included
226
			file := path.Clean(line)
227
			ancestors := ancestors(file)
228
			current.files = append(current.files, file)
229
			current.files = append(current.files, ancestors...)
230
		}
231
232
		if err == io.EOF {
233
			break
234
		}
235
	}
236
237
	return filesDone, nil
238
}
239
240
func ancestors(p string) []string {
241
	var ancestors []string
242
	for {
243
		p = path.Dir(p)
244
		if p == "." || p == "/" {
245
			break
246
		}
247
		ancestors = append(ancestors, p)
248
	}
249
	return ancestors
250
}