all repos

mugit @ afcfaf3e038adfaa00c7299ea3c0197e9d861a57

🐮 git server that your cow will love

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
git: remove context.TODO from makeNiceTree, 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(ctx context.Context, t *object.Tree, parent string) []NiceTree {
28
	var nts []NiceTree
29
30
	ctx, cancel := context.WithTimeout(ctx, 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(ctx context.Context, 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(ctx, 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(ctx, 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
		if line == "" {
190
			if !current.hash.IsZero() {
191
				c := current.Commit
192
				// we have a fully parsed commit
193
				for _, f := range current.files {
194
					if _, ok := filesToDo[f]; ok {
195
						filesDone[f] = &c
196
						delete(filesToDo, f)
197
					}
198
				}
199
200
				if len(filesToDo) == 0 {
201
					cancel()
202
					break
203
				}
204
205
				current = logCommit{}
206
			}
207
		} else if current.hash.IsZero() {
208
			parts := strings.SplitN(line, ",", 7)
209
			if len(parts) == 7 {
210
				current.hash = plumbing.NewHash(parts[0])
211
212
				// NOTE: this is copy-paste of [newCommit]
213
				current.Hash = parts[0]
214
				current.HashShort = parts[0][:7]
215
				current.Committed, _ = time.Parse("2006-01-02 15:04:05 -0700", parts[1])
216
				current.AuthorEmail = parts[2]
217
				current.AuthorName = parts[3]
218
				current.CommitterEmail = parts[4]
219
				current.CommitterName = parts[5]
220
				current.Message = parts[6]
221
			}
222
		} else {
223
			// all ancestors along this path should also be included
224
			file := path.Clean(line)
225
			ancestors := ancestors(file)
226
			current.files = append(current.files, file)
227
			current.files = append(current.files, ancestors...)
228
		}
229
230
		if err == io.EOF {
231
			break
232
		}
233
	}
234
235
	return filesDone, nil
236
}
237
238
func ancestors(p string) []string {
239
	var ancestors []string
240
	for {
241
		p = path.Dir(p)
242
		if p == "." || p == "/" {
243
			break
244
		}
245
		ancestors = append(ancestors, p)
246
	}
247
	return ancestors
248
}