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