diff options
Diffstat (limited to 'pkg/git/git.go')
-rw-r--r-- | pkg/git/git.go | 265 |
1 files changed, 251 insertions, 14 deletions
diff --git a/pkg/git/git.go b/pkg/git/git.go index 6221e33..83f3f93 100644 --- a/pkg/git/git.go +++ b/pkg/git/git.go @@ -3,10 +3,13 @@ package git import ( "archive/tar" "bytes" + "context" "errors" "fmt" "io" "io/fs" + "log/slog" + "os/exec" "path" "sort" "time" @@ -16,11 +19,10 @@ import ( "github.com/go-git/go-git/v5/plumbing/object" ) -var () - var ( MissingRefErr = errors.New("Reference not found") TreeForFileErr = errors.New("Trying to get tree of a file") + eofIter = errors.New("End of a iterator") ) type ( @@ -34,6 +36,10 @@ type ( ref *plumbing.Reference tag *object.Tag } + CommitReference struct { + commit *object.Commit + refs []*plumbing.Reference + } infoWrapper struct { name string size int64 @@ -83,7 +89,7 @@ func (g *GitRepository) Path() string { return g.path } -func (g *GitRepository) LastCommit() (*object.Commit, error) { +func (g *GitRepository) LastCommit() (*CommitReference, error) { err := g.validateRef() if err != nil { return nil, err @@ -93,39 +99,157 @@ func (g *GitRepository) LastCommit() (*object.Commit, error) { if err != nil { return nil, err } - return c, nil + + iter, err := g.repository.Tags() + if err != nil { + return nil, err + } + + commitRef := &CommitReference{commit: c} + if err := iter.ForEach(func(ref *plumbing.Reference) error { + obj, err := g.repository.TagObject(ref.Hash()) + switch err { + case nil: + if obj.Target == commitRef.commit.Hash { + commitRef.AddReference(ref) + } + case plumbing.ErrObjectNotFound: + if commitRef.commit.Hash == ref.Hash() { + commitRef.AddReference(ref) + } + default: + return err + } + + return nil + }); err != nil { + return nil, err + } + + return commitRef, nil } -func (g *GitRepository) Commits(count int) ([]*object.Commit, error) { +func (g *GitRepository) Commits(count int, from string) ([]*CommitReference, *object.Commit, error) { err := g.validateRef() if err != nil { - return nil, err + return nil, nil, err + } + + opts := &git.LogOptions{Order: git.LogOrderCommitterTime} + + if from != "" { + hash, err := g.repository.ResolveRevision(plumbing.Revision(from)) + if err != nil { + return nil, nil, errors.Join(MissingRefErr, err) + } + opts.From = *hash } - ci, err := g.repository.Log(&git.LogOptions{From: g.ref}) + ci, err := g.repository.Log(opts) if err != nil { - return nil, fmt.Errorf("commits from ref: %w", err) + return nil, nil, fmt.Errorf("commits from ref: %w", err) } - commits := []*object.Commit{} - // TODO: for now only load first 1000 - for x := 0; x < count; x++ { + commitRefs := []*CommitReference{} + var next *object.Commit + + // iterate one more item so we can fetch the next commit + for x := 0; x < (count + 1); x++ { c, err := ci.Next() if err != nil && errors.Is(err, io.EOF) { break } else if err != nil { - return nil, err + return nil, nil, err } - commits = append(commits, c) + if x == count { + next = c + } else { + commitRefs = append(commitRefs, &CommitReference{commit: c}) + } + } + + // new we fetch for possible tags for each commit + iter, err := g.repository.References() + if err != nil { + return nil, nil, err + } + + if err := iter.ForEach(func(ref *plumbing.Reference) error { + for _, c := range commitRefs { + obj, err := g.repository.TagObject(ref.Hash()) + switch err { + case nil: + if obj.Target == c.commit.Hash { + c.AddReference(ref) + } + case plumbing.ErrObjectNotFound: + if c.commit.Hash == ref.Hash() { + c.AddReference(ref) + } + default: + return err + } + } + return nil + }); err != nil { + return nil, nil, err } - return commits, nil + return commitRefs, next, nil } func (g *GitRepository) Head() (*plumbing.Reference, error) { return g.repository.Head() } +func (g *GitRepository) Tag() (*object.Commit, *TagReference, error) { + err := g.validateRef() + if err != nil { + return nil, nil, err + } + + c, err := g.repository.CommitObject(g.ref) + if err != nil { + return nil, nil, err + } + + var tagReference *TagReference + + iter, err := g.repository.Tags() + if err != nil { + return nil, nil, err + } + + if err := iter.ForEach(func(ref *plumbing.Reference) error { + obj, err := g.repository.TagObject(ref.Hash()) + switch err { + case nil: + if obj.Target == c.Hash { + tagReference = &TagReference{ + ref: ref, + tag: obj, + } + return eofIter + } + return nil + case plumbing.ErrObjectNotFound: + if c.Hash == ref.Hash() { + tagReference = &TagReference{ + ref: ref, + } + return eofIter + } + return nil + default: + return err + } + }); err != nil && !errors.Is(eofIter, err) { + return nil, nil, err + } + + return c, tagReference, nil +} + func (g *GitRepository) Tags() ([]*TagReference, error) { iter, err := g.repository.Tags() if err != nil { @@ -311,6 +435,68 @@ func (g *GitRepository) FileContent(path string) ([]byte, error) { return buf.Bytes(), nil } +func (g *GitRepository) WriteInfoRefs(ctx context.Context, w io.Writer) error { + cmd := exec.CommandContext( + ctx, + "git-upload-pack", + "--stateless-rpc", + "--advertise-refs", + ".", + ) + + cmd.Dir = g.path + cmd.Env = []string{ + // TODO: get this from header. + "GIT_PROTOCOL=version=2", + } + + var errBuff bytes.Buffer + cmd.Stderr = &errBuff + cmd.Stdout = w + + err := packLine(w, "# service=git-upload-pack\n") + if err != nil { + return err + } + + err = packFlush(w) + if err != nil { + return err + } + + err = cmd.Run() + if err != nil { + slog.Error("Error upload pack refs", "message", errBuff.String()) + return err + } + return nil +} + +func (g *GitRepository) WriteUploadPack(ctx context.Context, r io.Reader, w io.Writer) error { + cmd := exec.CommandContext( + ctx, + "git-upload-pack", + "--stateless-rpc", + ".", + ) + cmd.Dir = g.Path() + cmd.Env = []string{ + // TODO: get this from header. + "GIT_PROTOCOL=version=2", + } + var errBuff bytes.Buffer + cmd.Stderr = &errBuff + cmd.Stdout = w + cmd.Stdin = r + + if err := cmd.Run(); err != nil { + slog.ErrorContext(ctx, "Git upload pack failed", "error", err, "message", errBuff.String()) + return err + } + + return nil +} + func (g *GitRepository) WriteTar(w io.Writer, prefix string) error { tw := tar.NewWriter(w) defer tw.Close() @@ -438,7 +624,22 @@ func (t *TagReference) Message() string { return t.tag.Message } return "" +} + +func (c *CommitReference) Commit() *object.Commit { + return c.commit +} + +func (c *CommitReference) HasReference() bool { + return len(c.refs) > 0 +} + +func (c *CommitReference) References() []*plumbing.Reference { + return c.refs +} +func (c *CommitReference) AddReference(ref *plumbing.Reference) { + c.refs = append(c.refs, ref) } func (self *tagList) Len() int { @@ -477,3 +678,39 @@ func (self *tagList) Less(i, j int) bool { return dateI.After(dateJ) } + +func packLine(w io.Writer, s string) error { + _, err := fmt.Fprintf(w, "%04x%s", len(s)+4, s) + return err +} + +func packFlush(w io.Writer) error { + _, err := fmt.Fprint(w, "0000") + return err +} + +type debugReader struct { + r io.Reader +} + +func (d *debugReader) Read(p []byte) (n int, err error) { + r, err := d.r.Read(p) + if err != nil { + if errors.Is(io.EOF, err) { + fmt.Printf("READ: EOF\n") + } + return r, err + } + + fmt.Printf("READ: %s\n", p[:r]) + return r, nil +} + +type debugWriter struct { + w io.Writer +} + +func (d *debugWriter) Write(p []byte) (n int, err error) { + fmt.Printf("WRITE: %s\n", p) + return d.w.Write(p) +} |