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