apps/kepler/git.go 5.6 K raw
1
package main
2
3
import (
4
	"errors"
5
	"io"
6
	"sort"
7
	"strings"
8
9
	"github.com/go-git/go-git/v5"
10
	"github.com/go-git/go-git/v5/plumbing"
11
	"github.com/go-git/go-git/v5/plumbing/filemode"
12
	"github.com/go-git/go-git/v5/plumbing/object"
13
)
14
15
func resolveRef(repo *git.Repository, ref string) (*object.Commit, error) {
16
	if ref == "" {
17
		ref = defaultBranchName(repo)
18
	}
19
	hash, err := repo.ResolveRevision(plumbing.Revision(ref))
20
	if err != nil {
21
		return nil, err
22
	}
23
	return repo.CommitObject(*hash)
24
}
25
26
func listBranches(repo *git.Repository) ([]RefInfo, error) {
27
	iter, err := repo.Branches()
28
	if err != nil {
29
		return nil, err
30
	}
31
	var out []RefInfo
32
	_ = iter.ForEach(func(r *plumbing.Reference) error {
33
		info := RefInfo{Name: r.Name().Short(), SHA: r.Hash().String()}
34
		if c, err := repo.CommitObject(r.Hash()); err == nil {
35
			info.Time = c.Author.When
36
			info.Author = c.Author.Name
37
		}
38
		out = append(out, info)
39
		return nil
40
	})
41
	sort.Slice(out, func(i, j int) bool { return out[i].Time.After(out[j].Time) })
42
	return out, nil
43
}
44
45
func listTags(repo *git.Repository) ([]RefInfo, error) {
46
	iter, err := repo.Tags()
47
	if err != nil {
48
		return nil, err
49
	}
50
	var out []RefInfo
51
	_ = iter.ForEach(func(r *plumbing.Reference) error {
52
		info := RefInfo{Name: r.Name().Short(), SHA: r.Hash().String()}
53
		if tag, err := repo.TagObject(r.Hash()); err == nil {
54
			info.Time = tag.Tagger.When
55
			info.Author = tag.Tagger.Name
56
			if c, err := tag.Commit(); err == nil {
57
				info.SHA = c.Hash.String()
58
			}
59
		} else if c, err := repo.CommitObject(r.Hash()); err == nil {
60
			info.Time = c.Author.When
61
			info.Author = c.Author.Name
62
		}
63
		out = append(out, info)
64
		return nil
65
	})
66
	sort.Slice(out, func(i, j int) bool { return out[i].Time.After(out[j].Time) })
67
	return out, nil
68
}
69
70
func commitLog(repo *git.Repository, ref string, page, perPage int) ([]CommitInfo, bool, error) {
71
	head, err := resolveRef(repo, ref)
72
	if err != nil {
73
		return nil, false, err
74
	}
75
	iter, err := repo.Log(&git.LogOptions{From: head.Hash})
76
	if err != nil {
77
		return nil, false, err
78
	}
79
	defer iter.Close()
80
81
	skip := page * perPage
82
	var out []CommitInfo
83
	i := 0
84
	hasNext := false
85
	err = iter.ForEach(func(c *object.Commit) error {
86
		if i < skip {
87
			i++
88
			return nil
89
		}
90
		if len(out) >= perPage {
91
			hasNext = true
92
			return io.EOF
93
		}
94
		out = append(out, toCommitInfo(c))
95
		i++
96
		return nil
97
	})
98
	if err != nil && !errors.Is(err, io.EOF) {
99
		return nil, false, err
100
	}
101
	return out, hasNext, nil
102
}
103
104
func toCommitInfo(c *object.Commit) CommitInfo {
105
	subject, body := splitCommitMessage(c.Message)
106
	parent := ""
107
	if c.NumParents() > 0 {
108
		parent = c.ParentHashes[0].String()
109
	}
110
	return CommitInfo{
111
		SHA:       c.Hash.String(),
112
		ShortSHA:  c.Hash.String()[:8],
113
		Author:    c.Author.Name,
114
		Email:     c.Author.Email,
115
		When:      c.Author.When,
116
		Subject:   subject,
117
		Body:      body,
118
		ParentSHA: parent,
119
	}
120
}
121
122
func splitCommitMessage(msg string) (subject, body string) {
123
	msg = strings.TrimRight(msg, "\n")
124
	if i := strings.Index(msg, "\n"); i >= 0 {
125
		return strings.TrimSpace(msg[:i]), strings.TrimSpace(msg[i+1:])
126
	}
127
	return msg, ""
128
}
129
130
func treeAt(repo *git.Repository, ref, path string) ([]TreeEntry, *object.Tree, error) {
131
	c, err := resolveRef(repo, ref)
132
	if err != nil {
133
		return nil, nil, err
134
	}
135
	root, err := c.Tree()
136
	if err != nil {
137
		return nil, nil, err
138
	}
139
	tree := root
140
	if path != "" {
141
		t, err := root.Tree(path)
142
		if err != nil {
143
			return nil, nil, err
144
		}
145
		tree = t
146
	}
147
	var out []TreeEntry
148
	for _, e := range tree.Entries {
149
		entryPath := e.Name
150
		if path != "" {
151
			entryPath = path + "/" + e.Name
152
		}
153
		te := TreeEntry{
154
			Name:  e.Name,
155
			Path:  entryPath,
156
			IsDir: e.Mode == filemode.Dir,
157
			Mode:  e.Mode.String(),
158
		}
159
		if !te.IsDir {
160
			if f, err := tree.File(e.Name); err == nil {
161
				te.Size = f.Size
162
			}
163
		}
164
		out = append(out, te)
165
	}
166
	sort.Slice(out, func(i, j int) bool {
167
		if out[i].IsDir != out[j].IsDir {
168
			return out[i].IsDir
169
		}
170
		return out[i].Name < out[j].Name
171
	})
172
	return out, tree, nil
173
}
174
175
func blobAt(repo *git.Repository, ref, path string) (string, int64, bool, error) {
176
	c, err := resolveRef(repo, ref)
177
	if err != nil {
178
		return "", 0, false, err
179
	}
180
	tree, err := c.Tree()
181
	if err != nil {
182
		return "", 0, false, err
183
	}
184
	f, err := tree.File(path)
185
	if err != nil {
186
		return "", 0, false, err
187
	}
188
	bin, err := f.IsBinary()
189
	if err != nil {
190
		return "", 0, false, err
191
	}
192
	if bin {
193
		return "", f.Size, true, nil
194
	}
195
	content, err := f.Contents()
196
	if err != nil {
197
		return "", 0, false, err
198
	}
199
	return content, f.Size, false, nil
200
}
201
202
func blobBytes(repo *git.Repository, ref, path string) ([]byte, error) {
203
	c, err := resolveRef(repo, ref)
204
	if err != nil {
205
		return nil, err
206
	}
207
	tree, err := c.Tree()
208
	if err != nil {
209
		return nil, err
210
	}
211
	f, err := tree.File(path)
212
	if err != nil {
213
		return nil, err
214
	}
215
	r, err := f.Blob.Reader()
216
	if err != nil {
217
		return nil, err
218
	}
219
	defer r.Close()
220
	return io.ReadAll(r)
221
}
222
223
func commitDiff(repo *git.Repository, sha string) (CommitInfo, []FilePatch, DiffStats, error) {
224
	h := plumbing.NewHash(sha)
225
	c, err := repo.CommitObject(h)
226
	if err != nil {
227
		return CommitInfo{}, nil, DiffStats{}, err
228
	}
229
	info := toCommitInfo(c)
230
231
	var parentTree *object.Tree
232
	if c.NumParents() > 0 {
233
		p, err := c.Parent(0)
234
		if err != nil {
235
			return info, nil, DiffStats{}, err
236
		}
237
		parentTree, err = p.Tree()
238
		if err != nil {
239
			return info, nil, DiffStats{}, err
240
		}
241
	}
242
	thisTree, err := c.Tree()
243
	if err != nil {
244
		return info, nil, DiffStats{}, err
245
	}
246
	patch, err := diffTrees(parentTree, thisTree)
247
	if err != nil {
248
		return info, nil, DiffStats{}, err
249
	}
250
	files, stats := convertPatch(patch)
251
	return info, files, stats, nil
252
}
253
254
func diffTrees(from, to *object.Tree) (*object.Patch, error) {
255
	if from == nil {
256
		return (&object.Tree{}).Patch(to)
257
	}
258
	return from.Patch(to)
259
}