1package git
2
3import (
4 "archive/tar"
5 "bytes"
6 "errors"
7 "fmt"
8 "io"
9 "io/fs"
10 "path"
11 "time"
12
13 "github.com/go-git/go-git/v5"
14 "github.com/go-git/go-git/v5/plumbing"
15 "github.com/go-git/go-git/v5/plumbing/object"
16)
17
18var ()
19
20var (
21 MissingRefErr = errors.New("Reference not found")
22 TreeForFileErr = errors.New("Trying to get tree of a file")
23)
24
25type (
26 GitRepository struct {
27 path string
28 repository *git.Repository
29
30 ref plumbing.Hash
31 // this is setRef when ref is setRef
32 setRef bool
33 }
34 infoWrapper struct {
35 name string
36 size int64
37 mode fs.FileMode
38 modTime time.Time
39 isDir bool
40 }
41)
42
43func OpenRepository(dir string) (*GitRepository, error) {
44 g := &GitRepository{
45 path: dir,
46 }
47
48 repo, err := git.PlainOpen(dir)
49 if err != nil {
50 return nil, err
51 }
52 g.repository = repo
53
54 return g, nil
55}
56
57func (g *GitRepository) SetRef(ref string) error {
58 if ref == "" {
59 head, err := g.repository.Head()
60 if err != nil {
61 return errors.Join(MissingRefErr, err)
62 }
63 g.ref = head.Hash()
64 } else {
65 hash, err := g.repository.ResolveRevision(plumbing.Revision(ref))
66 if err != nil {
67 return errors.Join(MissingRefErr, err)
68 }
69 g.ref = *hash
70 }
71 g.setRef = true
72 return nil
73}
74
75func (g *GitRepository) Path() string {
76 return g.path
77}
78
79func (g *GitRepository) LastCommit() (*object.Commit, error) {
80 err := g.validateRef()
81 if err != nil {
82 return nil, err
83 }
84
85 c, err := g.repository.CommitObject(g.ref)
86 if err != nil {
87 return nil, err
88 }
89 return c, nil
90}
91
92func (g *GitRepository) Commits(count int) ([]*object.Commit, error) {
93 err := g.validateRef()
94 if err != nil {
95 return nil, err
96 }
97
98 ci, err := g.repository.Log(&git.LogOptions{From: g.ref})
99 if err != nil {
100 return nil, fmt.Errorf("commits from ref: %w", err)
101 }
102
103 commits := []*object.Commit{}
104 // TODO: for now only load first 1000
105 for x := 0; x < count; x++ {
106 c, err := ci.Next()
107 if err != nil && errors.Is(err, io.EOF) {
108 break
109 } else if err != nil {
110 return nil, err
111 }
112 commits = append(commits, c)
113 }
114
115 return commits, nil
116}
117
118func (g *GitRepository) Head() (*plumbing.Reference, error) {
119 return g.repository.Head()
120}
121
122func (g *GitRepository) Tags() ([]*plumbing.Reference, error) {
123 ti, err := g.repository.Tags()
124 if err != nil {
125 return nil, err
126 }
127
128 tags := []*plumbing.Reference{}
129 err = ti.ForEach(func(t *plumbing.Reference) error {
130 tags = append(tags, t)
131 return nil
132 })
133 if err != nil {
134 return nil, err
135 }
136
137 return tags, nil
138}
139
140func (g *GitRepository) Branches() ([]*plumbing.Reference, error) {
141 bs, err := g.repository.Branches()
142 if err != nil {
143 return nil, err
144 }
145
146 branches := []*plumbing.Reference{}
147 err = bs.ForEach(func(ref *plumbing.Reference) error {
148 branches = append(branches, ref)
149 return nil
150 })
151 if err != nil {
152 return nil, err
153 }
154
155 return branches, nil
156}
157
158func (g *GitRepository) Tree(path string) (*object.Tree, error) {
159 err := g.validateRef()
160 if err != nil {
161 return nil, err
162 }
163
164 c, err := g.repository.CommitObject(g.ref)
165 if err != nil {
166 return nil, err
167 }
168
169 tree, err := c.Tree()
170 if err != nil {
171 return nil, err
172 }
173
174 if path == "" {
175 return tree, nil
176 } else {
177 o, err := tree.FindEntry(path)
178 if err != nil {
179 return nil, err
180 }
181
182 if !o.Mode.IsFile() {
183 subtree, err := tree.Tree(path)
184 if err != nil {
185 return nil, err
186 }
187 return subtree, nil
188 } else {
189 return nil, TreeForFileErr
190 }
191 }
192}
193
194func (g *GitRepository) validateRef() error {
195 if !g.setRef {
196 return g.SetRef("")
197 }
198 return nil
199}
200
201func (g *GitRepository) IsBinary(path string) (bool, error) {
202 tree, err := g.Tree("")
203 if err != nil {
204 return false, err
205 }
206
207 file, err := tree.File(path)
208 if err != nil {
209 return false, err
210 }
211
212 return file.IsBinary()
213}
214
215func (g *GitRepository) FileContent(path string) ([]byte, error) {
216 err := g.validateRef()
217 if err != nil {
218 return nil, err
219 }
220
221 c, err := g.repository.CommitObject(g.ref)
222 if err != nil {
223 return nil, err
224 }
225
226 tree, err := c.Tree()
227 if err != nil {
228 return nil, err
229 }
230
231 file, err := tree.File(path)
232 if err != nil {
233 return nil, err
234 }
235
236 r, err := file.Blob.Reader()
237 if err != nil {
238 return nil, err
239 }
240 defer r.Close()
241
242 var buf bytes.Buffer
243 _, err = io.Copy(&buf, r)
244 if err != nil {
245 return nil, err
246 }
247
248 return buf.Bytes(), nil
249}
250
251func (g *GitRepository) WriteTar(w io.Writer, prefix string) error {
252 tw := tar.NewWriter(w)
253 defer tw.Close()
254
255 tree, err := g.Tree("")
256 if err != nil {
257 return err
258 }
259
260 walker := object.NewTreeWalker(tree, true, nil)
261 defer walker.Close()
262
263 name, entry, err := walker.Next()
264 for ; err == nil; name, entry, err = walker.Next() {
265 info, err := newInfoWrapper(name, prefix, &entry, tree)
266 if err != nil {
267 return err
268 }
269
270 header, err := tar.FileInfoHeader(info, "")
271 if err != nil {
272 return err
273 }
274
275 err = tw.WriteHeader(header)
276 if err != nil {
277 return err
278 }
279
280 if !info.IsDir() {
281 file, err := tree.File(name)
282 if err != nil {
283 return err
284 }
285
286 reader, err := file.Blob.Reader()
287 if err != nil {
288 return err
289 }
290
291 _, err = io.Copy(tw, reader)
292 if err != nil {
293 reader.Close()
294 return err
295 }
296 reader.Close()
297 }
298 }
299
300 return nil
301}
302
303func newInfoWrapper(
304 filename string,
305 prefix string,
306 entry *object.TreeEntry,
307 tree *object.Tree,
308) (*infoWrapper, error) {
309 var (
310 size int64
311 mode fs.FileMode
312 isDir bool
313 )
314
315 if entry.Mode.IsFile() {
316 file, err := tree.TreeEntryFile(entry)
317 if err != nil {
318 return nil, err
319 }
320 mode = fs.FileMode(file.Mode)
321
322 size, err = tree.Size(filename)
323 if err != nil {
324 return nil, err
325 }
326 } else {
327 isDir = true
328 mode = fs.ModeDir | fs.ModePerm
329 }
330
331 fullname := path.Join(prefix, filename)
332 return &infoWrapper{
333 name: fullname,
334 size: size,
335 mode: mode,
336 modTime: time.Unix(0, 0),
337 isDir: isDir,
338 }, nil
339}
340
341func (i *infoWrapper) Name() string {
342 return i.name
343}
344
345func (i *infoWrapper) Size() int64 {
346 return i.size
347}
348
349func (i *infoWrapper) Mode() fs.FileMode {
350 return i.mode
351}
352
353func (i *infoWrapper) ModTime() time.Time {
354 return i.modTime
355}
356
357func (i *infoWrapper) IsDir() bool {
358 return i.isDir
359}
360
361func (i *infoWrapper) Sys() any {
362 return nil
363}