forked from
tangled.org/core
this repo has no description
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
79func (g *GitRepo) Hash() plumbing.Hash {
80 return g.h
81}
82
83// re-open a repository and update references
84func (g *GitRepo) Refresh() error {
85 refreshed, err := PlainOpen(g.path)
86 if err != nil {
87 return err
88 }
89
90 *g = *refreshed
91 return nil
92}
93
94func (g *GitRepo) Commits(offset, limit int) ([]*object.Commit, error) {
95 commits := []*object.Commit{}
96
97 output, err := g.revList(
98 g.h.String(),
99 fmt.Sprintf("--skip=%d", offset),
100 fmt.Sprintf("--max-count=%d", limit),
101 )
102 if err != nil {
103 return nil, fmt.Errorf("commits from ref: %w", err)
104 }
105
106 lines := strings.Split(strings.TrimSpace(string(output)), "\n")
107 if len(lines) == 1 && lines[0] == "" {
108 return commits, nil
109 }
110
111 for _, item := range lines {
112 obj, err := g.r.CommitObject(plumbing.NewHash(item))
113 if err != nil {
114 continue
115 }
116 commits = append(commits, obj)
117 }
118
119 return commits, nil
120}
121
122func (g *GitRepo) TotalCommits() (int, error) {
123 output, err := g.revList(
124 g.h.String(),
125 "--count",
126 )
127 if err != nil {
128 return 0, fmt.Errorf("failed to run rev-list: %w", err)
129 }
130
131 count, err := strconv.Atoi(strings.TrimSpace(string(output)))
132 if err != nil {
133 return 0, err
134 }
135
136 return count, nil
137}
138
139func (g *GitRepo) Commit(h plumbing.Hash) (*object.Commit, error) {
140 return g.r.CommitObject(h)
141}
142
143func (g *GitRepo) FileContentN(path string, cap int64) ([]byte, error) {
144 c, err := g.r.CommitObject(g.h)
145 if err != nil {
146 return nil, fmt.Errorf("commit object: %w", err)
147 }
148
149 tree, err := c.Tree()
150 if err != nil {
151 return nil, fmt.Errorf("file tree: %w", err)
152 }
153
154 file, err := tree.File(path)
155 if err != nil {
156 return nil, err
157 }
158
159 isbin, _ := file.IsBinary()
160 if isbin {
161 return nil, ErrBinaryFile
162 }
163
164 reader, err := file.Reader()
165 if err != nil {
166 return nil, err
167 }
168
169 buf := new(bytes.Buffer)
170 if _, err = buf.ReadFrom(io.LimitReader(reader, cap)); err != nil {
171 return nil, err
172 }
173
174 return buf.Bytes(), nil
175}
176
177func (g *GitRepo) RawContent(path string) ([]byte, error) {
178 c, err := g.r.CommitObject(g.h)
179 if err != nil {
180 return nil, fmt.Errorf("commit object: %w", err)
181 }
182
183 tree, err := c.Tree()
184 if err != nil {
185 return nil, fmt.Errorf("file tree: %w", err)
186 }
187
188 file, err := tree.File(path)
189 if err != nil {
190 return nil, err
191 }
192
193 reader, err := file.Reader()
194 if err != nil {
195 return nil, fmt.Errorf("opening file reader: %w", err)
196 }
197 defer reader.Close()
198
199 return io.ReadAll(reader)
200}
201
202func (g *GitRepo) File(path string) (*object.File, error) {
203 c, err := g.r.CommitObject(g.h)
204 if err != nil {
205 return nil, fmt.Errorf("commit object: %w", err)
206 }
207
208 tree, err := c.Tree()
209 if err != nil {
210 return nil, fmt.Errorf("file tree: %w", err)
211 }
212
213 return tree.File(path)
214}
215
216// read and parse .gitmodules
217func (g *GitRepo) Submodules() (*config.Modules, error) {
218 c, err := g.r.CommitObject(g.h)
219 if err != nil {
220 return nil, fmt.Errorf("commit object: %w", err)
221 }
222
223 tree, err := c.Tree()
224 if err != nil {
225 return nil, fmt.Errorf("tree: %w", err)
226 }
227
228 // read .gitmodules file
229 modulesEntry, err := tree.FindEntry(".gitmodules")
230 if err != nil {
231 return nil, fmt.Errorf("%w: %w", ErrMissingGitModules, err)
232 }
233
234 modulesFile, err := tree.TreeEntryFile(modulesEntry)
235 if err != nil {
236 return nil, fmt.Errorf("%w: failed to read file: %w", ErrInvalidGitModules, err)
237 }
238
239 content, err := modulesFile.Contents()
240 if err != nil {
241 return nil, fmt.Errorf("%w: failed to read contents: %w", ErrInvalidGitModules, err)
242 }
243
244 // parse .gitmodules
245 modules := config.NewModules()
246 if err = modules.Unmarshal([]byte(content)); err != nil {
247 return nil, fmt.Errorf("%w: failed to parse: %w", ErrInvalidGitModules, err)
248 }
249
250 return modules, nil
251}
252
253func (g *GitRepo) Submodule(path string) (*config.Submodule, error) {
254 modules, err := g.Submodules()
255 if err != nil {
256 return nil, err
257 }
258
259 for _, submodule := range modules.Submodules {
260 if submodule.Path == path {
261 return submodule, nil
262 }
263 }
264
265 // path is not a submodule
266 return nil, ErrNotSubmodule
267}
268
269func (g *GitRepo) SetDefaultBranch(branch string) error {
270 ref := plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName(branch))
271 return g.r.Storer.SetReference(ref)
272}
273
274func (g *GitRepo) FindMainBranch() (string, error) {
275 output, err := g.revParse("--abbrev-ref", "HEAD")
276 if err != nil {
277 return "", fmt.Errorf("failed to find main branch: %w", err)
278 }
279
280 return strings.TrimSpace(string(output)), nil
281}
282
283// WriteTar writes itself from a tree into a binary tar file format.
284// prefix is root folder to be appended.
285func (g *GitRepo) WriteTar(w io.Writer, prefix string) error {
286 tw := tar.NewWriter(w)
287 defer tw.Close()
288
289 c, err := g.r.CommitObject(g.h)
290 if err != nil {
291 return fmt.Errorf("commit object: %w", err)
292 }
293
294 tree, err := c.Tree()
295 if err != nil {
296 return err
297 }
298
299 walker := object.NewTreeWalker(tree, true, nil)
300 defer walker.Close()
301
302 name, entry, err := walker.Next()
303 for ; err == nil; name, entry, err = walker.Next() {
304 info, err := newInfoWrapper(name, prefix, &entry, tree)
305 if err != nil {
306 return err
307 }
308
309 header, err := tar.FileInfoHeader(info, "")
310 if err != nil {
311 return err
312 }
313
314 err = tw.WriteHeader(header)
315 if err != nil {
316 return err
317 }
318
319 if !info.IsDir() {
320 file, err := tree.File(name)
321 if err != nil {
322 return err
323 }
324
325 reader, err := file.Blob.Reader()
326 if err != nil {
327 return err
328 }
329
330 _, err = io.Copy(tw, reader)
331 if err != nil {
332 reader.Close()
333 return err
334 }
335 reader.Close()
336 }
337 }
338
339 return nil
340}
341
342func newInfoWrapper(
343 name string,
344 prefix string,
345 entry *object.TreeEntry,
346 tree *object.Tree,
347) (*infoWrapper, error) {
348 var (
349 size int64
350 mode fs.FileMode
351 isDir bool
352 )
353
354 if entry.Mode.IsFile() {
355 file, err := tree.TreeEntryFile(entry)
356 if err != nil {
357 return nil, err
358 }
359 mode = fs.FileMode(file.Mode)
360
361 size, err = tree.Size(name)
362 if err != nil {
363 return nil, err
364 }
365 } else {
366 isDir = true
367 mode = fs.ModeDir | fs.ModePerm
368 }
369
370 fullname := path.Join(prefix, name)
371 return &infoWrapper{
372 name: fullname,
373 size: size,
374 mode: mode,
375 modTime: time.Unix(0, 0),
376 isDir: isDir,
377 }, nil
378}
379
380func (i *infoWrapper) Name() string {
381 return i.name
382}
383
384func (i *infoWrapper) Size() int64 {
385 return i.size
386}
387
388func (i *infoWrapper) Mode() fs.FileMode {
389 return i.mode
390}
391
392func (i *infoWrapper) ModTime() time.Time {
393 return i.modTime
394}
395
396func (i *infoWrapper) IsDir() bool {
397 return i.isDir
398}
399
400func (i *infoWrapper) Sys() any {
401 return nil
402}