forked from
tangled.org/core
fork
Configure Feed
Select the types of activity you want to include in your feed.
this repo has no description
fork
Configure Feed
Select the types of activity you want to include in your feed.
1package git
2
3import (
4 "archive/tar"
5 "bytes"
6 "errors"
7 "fmt"
8 "io"
9 "io/fs"
10 "path"
11 "strconv"
12 "strings"
13 "time"
14
15 "github.com/go-git/go-git/v5"
16 "github.com/go-git/go-git/v5/config"
17 "github.com/go-git/go-git/v5/plumbing"
18 "github.com/go-git/go-git/v5/plumbing/object"
19)
20
21var (
22 ErrBinaryFile = errors.New("binary file")
23 ErrNotBinaryFile = errors.New("not binary file")
24 ErrMissingGitModules = errors.New("no .gitmodules file found")
25 ErrInvalidGitModules = errors.New("invalid .gitmodules file")
26 ErrNotSubmodule = errors.New("path is not a submodule")
27)
28
29type GitRepo struct {
30 path string
31 r *git.Repository
32 h plumbing.Hash
33}
34
35// infoWrapper wraps the property of a TreeEntry so it can export fs.FileInfo
36// to tar WriteHeader
37type infoWrapper struct {
38 name string
39 size int64
40 mode fs.FileMode
41 modTime time.Time
42 isDir bool
43}
44
45func Open(path string, ref string) (*GitRepo, error) {
46 var err error
47 g := GitRepo{path: path}
48 g.r, err = git.PlainOpen(path)
49 if err != nil {
50 return nil, fmt.Errorf("opening %s: %w", path, err)
51 }
52
53 if ref == "" {
54 head, err := g.r.Head()
55 if err != nil {
56 return nil, fmt.Errorf("getting head of %s: %w", path, err)
57 }
58 g.h = head.Hash()
59 } else {
60 hash, err := g.r.ResolveRevision(plumbing.Revision(ref))
61 if err != nil {
62 return nil, fmt.Errorf("resolving rev %s for %s: %w", ref, path, err)
63 }
64 g.h = *hash
65 }
66 return &g, nil
67}
68
69func PlainOpen(path string) (*GitRepo, error) {
70 var err error
71 g := GitRepo{path: path}
72 g.r, err = git.PlainOpen(path)
73 if err != nil {
74 return nil, fmt.Errorf("opening %s: %w", path, err)
75 }
76 return &g, nil
77}
78
79// re-open a repository and update references
80func (g *GitRepo) Refresh() error {
81 refreshed, err := PlainOpen(g.path)
82 if err != nil {
83 return err
84 }
85
86 *g = *refreshed
87 return nil
88}
89
90func (g *GitRepo) Commits(offset, limit int) ([]*object.Commit, error) {
91 commits := []*object.Commit{}
92
93 output, err := g.revList(
94 g.h.String(),
95 fmt.Sprintf("--skip=%d", offset),
96 fmt.Sprintf("--max-count=%d", limit),
97 )
98 if err != nil {
99 return nil, fmt.Errorf("commits from ref: %w", err)
100 }
101
102 lines := strings.Split(strings.TrimSpace(string(output)), "\n")
103 if len(lines) == 1 && lines[0] == "" {
104 return commits, nil
105 }
106
107 for _, item := range lines {
108 obj, err := g.r.CommitObject(plumbing.NewHash(item))
109 if err != nil {
110 continue
111 }
112 commits = append(commits, obj)
113 }
114
115 return commits, nil
116}
117
118func (g *GitRepo) TotalCommits() (int, error) {
119 output, err := g.revList(
120 g.h.String(),
121 fmt.Sprintf("--count"),
122 )
123 if err != nil {
124 return 0, fmt.Errorf("failed to run rev-list: %w", err)
125 }
126
127 count, err := strconv.Atoi(strings.TrimSpace(string(output)))
128 if err != nil {
129 return 0, err
130 }
131
132 return count, nil
133}
134
135func (g *GitRepo) Commit(h plumbing.Hash) (*object.Commit, error) {
136 return g.r.CommitObject(h)
137}
138
139func (g *GitRepo) FileContentN(path string, cap int64) ([]byte, error) {
140 c, err := g.r.CommitObject(g.h)
141 if err != nil {
142 return nil, fmt.Errorf("commit object: %w", err)
143 }
144
145 tree, err := c.Tree()
146 if err != nil {
147 return nil, fmt.Errorf("file tree: %w", err)
148 }
149
150 file, err := tree.File(path)
151 if err != nil {
152 return nil, err
153 }
154
155 isbin, _ := file.IsBinary()
156 if isbin {
157 return nil, ErrBinaryFile
158 }
159
160 reader, err := file.Reader()
161 if err != nil {
162 return nil, err
163 }
164
165 buf := new(bytes.Buffer)
166 if _, err = buf.ReadFrom(io.LimitReader(reader, cap)); err != nil {
167 return nil, err
168 }
169
170 return buf.Bytes(), nil
171}
172
173func (g *GitRepo) RawContent(path string) ([]byte, error) {
174 c, err := g.r.CommitObject(g.h)
175 if err != nil {
176 return nil, fmt.Errorf("commit object: %w", err)
177 }
178
179 tree, err := c.Tree()
180 if err != nil {
181 return nil, fmt.Errorf("file tree: %w", err)
182 }
183
184 file, err := tree.File(path)
185 if err != nil {
186 return nil, err
187 }
188
189 reader, err := file.Reader()
190 if err != nil {
191 return nil, fmt.Errorf("opening file reader: %w", err)
192 }
193 defer reader.Close()
194
195 return io.ReadAll(reader)
196}
197
198// read and parse .gitmodules
199func (g *GitRepo) Submodules() (*config.Modules, error) {
200 c, err := g.r.CommitObject(g.h)
201 if err != nil {
202 return nil, fmt.Errorf("commit object: %w", err)
203 }
204
205 tree, err := c.Tree()
206 if err != nil {
207 return nil, fmt.Errorf("tree: %w", err)
208 }
209
210 // read .gitmodules file
211 modulesEntry, err := tree.FindEntry(".gitmodules")
212 if err != nil {
213 return nil, fmt.Errorf("%w: %w", ErrMissingGitModules, err)
214 }
215
216 modulesFile, err := tree.TreeEntryFile(modulesEntry)
217 if err != nil {
218 return nil, fmt.Errorf("%w: failed to read file: %w", ErrInvalidGitModules, err)
219 }
220
221 content, err := modulesFile.Contents()
222 if err != nil {
223 return nil, fmt.Errorf("%w: failed to read contents: %w", ErrInvalidGitModules, err)
224 }
225
226 // parse .gitmodules
227 modules := config.NewModules()
228 if err = modules.Unmarshal([]byte(content)); err != nil {
229 return nil, fmt.Errorf("%w: failed to parse: %w", ErrInvalidGitModules, err)
230 }
231
232 return modules, nil
233}
234
235func (g *GitRepo) Submodule(path string) (*config.Submodule, error) {
236 modules, err := g.Submodules()
237 if err != nil {
238 return nil, err
239 }
240
241 for _, submodule := range modules.Submodules {
242 if submodule.Path == path {
243 return submodule, nil
244 }
245 }
246
247 // path is not a submodule
248 return nil, ErrNotSubmodule
249}
250
251func (g *GitRepo) Branch(name string) (*plumbing.Reference, error) {
252 ref, err := g.r.Reference(plumbing.NewBranchReferenceName(name), false)
253 if err != nil {
254 return nil, fmt.Errorf("branch: %w", err)
255 }
256
257 if !ref.Name().IsBranch() {
258 return nil, fmt.Errorf("branch: %s is not a branch", ref.Name())
259 }
260
261 return ref, nil
262}
263
264func (g *GitRepo) SetDefaultBranch(branch string) error {
265 ref := plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName(branch))
266 return g.r.Storer.SetReference(ref)
267}
268
269func (g *GitRepo) FindMainBranch() (string, error) {
270 output, err := g.revParse("--abbrev-ref", "HEAD")
271 if err != nil {
272 return "", fmt.Errorf("failed to find main branch: %w", err)
273 }
274
275 return strings.TrimSpace(string(output)), nil
276}
277
278// WriteTar writes itself from a tree into a binary tar file format.
279// prefix is root folder to be appended.
280func (g *GitRepo) WriteTar(w io.Writer, prefix string) error {
281 tw := tar.NewWriter(w)
282 defer tw.Close()
283
284 c, err := g.r.CommitObject(g.h)
285 if err != nil {
286 return fmt.Errorf("commit object: %w", err)
287 }
288
289 tree, err := c.Tree()
290 if err != nil {
291 return err
292 }
293
294 walker := object.NewTreeWalker(tree, true, nil)
295 defer walker.Close()
296
297 name, entry, err := walker.Next()
298 for ; err == nil; name, entry, err = walker.Next() {
299 info, err := newInfoWrapper(name, prefix, &entry, tree)
300 if err != nil {
301 return err
302 }
303
304 header, err := tar.FileInfoHeader(info, "")
305 if err != nil {
306 return err
307 }
308
309 err = tw.WriteHeader(header)
310 if err != nil {
311 return err
312 }
313
314 if !info.IsDir() {
315 file, err := tree.File(name)
316 if err != nil {
317 return err
318 }
319
320 reader, err := file.Blob.Reader()
321 if err != nil {
322 return err
323 }
324
325 _, err = io.Copy(tw, reader)
326 if err != nil {
327 reader.Close()
328 return err
329 }
330 reader.Close()
331 }
332 }
333
334 return nil
335}
336
337func newInfoWrapper(
338 name string,
339 prefix string,
340 entry *object.TreeEntry,
341 tree *object.Tree,
342) (*infoWrapper, error) {
343 var (
344 size int64
345 mode fs.FileMode
346 isDir bool
347 )
348
349 if entry.Mode.IsFile() {
350 file, err := tree.TreeEntryFile(entry)
351 if err != nil {
352 return nil, err
353 }
354 mode = fs.FileMode(file.Mode)
355
356 size, err = tree.Size(name)
357 if err != nil {
358 return nil, err
359 }
360 } else {
361 isDir = true
362 mode = fs.ModeDir | fs.ModePerm
363 }
364
365 fullname := path.Join(prefix, name)
366 return &infoWrapper{
367 name: fullname,
368 size: size,
369 mode: mode,
370 modTime: time.Unix(0, 0),
371 isDir: isDir,
372 }, nil
373}
374
375func (i *infoWrapper) Name() string {
376 return i.name
377}
378
379func (i *infoWrapper) Size() int64 {
380 return i.size
381}
382
383func (i *infoWrapper) Mode() fs.FileMode {
384 return i.mode
385}
386
387func (i *infoWrapper) ModTime() time.Time {
388 return i.modTime
389}
390
391func (i *infoWrapper) IsDir() bool {
392 return i.isDir
393}
394
395func (i *infoWrapper) Sys() any {
396 return nil
397}