diff --git a/knotmirror/xrpc/git_list_branches.go b/knotmirror/xrpc/git_list_branches.go index 48cb367b..df60f153 100644 --- a/knotmirror/xrpc/git_list_branches.go +++ b/knotmirror/xrpc/git_list_branches.go @@ -1,19 +1,26 @@ package xrpc import ( + "bytes" "context" "fmt" + "io" "net/http" "os" + "os/exec" "path/filepath" "strconv" + "strings" "github.com/bluesky-social/indigo/atproto/atclient" "github.com/bluesky-social/indigo/atproto/syntax" - "tangled.org/core/knotserver/git" + "github.com/go-git/go-git/v5/plumbing" + "tangled.org/core/knotmirror/xrpc/gitea" "tangled.org/core/types" ) +const fieldSeparator = "\x1f" // ASCII Unit Separator + func (x *Xrpc) ListBranches(w http.ResponseWriter, r *http.Request) { var ( repoQuery = r.URL.Query().Get("repo") @@ -63,22 +70,119 @@ func (x *Xrpc) listBranches(ctx context.Context, repo syntax.DID, limit int, cur return nil, fmt.Errorf("resolving repo did: %w", err) } - gr, err := git.PlainOpen(repoPath) - if err != nil { - return nil, fmt.Errorf("opening git repo: %w", err) + // ignore error: an empty default branch just means nothing is marked default + defaultBranch := func(repoPath string) string { + out, err := exec.Command("git", "-C", repoPath, "rev-parse", "--abbrev-ref", "HEAD").Output() + if err != nil { + return "" + } + return string(bytes.TrimSpace(out)) + }(repoPath) + + // -> [](name, oid) + type branchRef struct { + name string + oid string } + refs, err := func(repoPath string) ([]branchRef, error) { + out, err := exec.Command( + "git", + "-C", repoPath, + "for-each-ref", + "--format=%(refname:short)"+fieldSeparator+"%(objectname)", + "--sort=-creatordate", + // for-each-ref has no skip flag, so fetch offset+limit and slice below + fmt.Sprintf("--count=%d", cursor+int64(limit)), + "refs/heads", + ).Output() + if err != nil { + return nil, err + } - branches, err := gr.Branches(&git.BranchesOptions{ - Limit: limit, - Offset: int(cursor), - }) + out = bytes.TrimSpace(out) + if len(out) == 0 { + return nil, nil + } + lines := strings.Split(string(out), "\n") + if int(cursor) >= len(lines) { + return nil, nil + } + lines = lines[cursor:] + + refs := make([]branchRef, 0, len(lines)) + for _, line := range lines { + name, oid, ok := strings.Cut(line, fieldSeparator) + if !ok { + continue + } + refs = append(refs, branchRef{name: name, oid: oid}) + } + return refs, nil + }(repoPath) if err != nil { return nil, fmt.Errorf("listing git branches: %w", err) } + // hydrate each ref's commit + branches, err := func(repoPath string, refs []branchRef) ([]types.Branch, error) { + bw, br, cancel := gitea.CatFileBatch(ctx, repoPath) + defer cancel() + branches := make([]types.Branch, 0, len(refs)) + for _, ref := range refs { + if _, err := bw.Write([]byte(ref.oid + "\n")); err != nil { + return nil, err + } + _, typ, size, err := gitea.ReadBatchLine(br) + if err != nil { + return nil, err + } + if typ != "commit" { + if err := gitea.DiscardFull(br, size+1); err != nil { + return nil, err + } + return nil, fmt.Errorf("unexpected type: %s for commit id: %s", typ, ref.oid) + } + c, err := gitea.ReadCommit(plumbing.NewHash(ref.oid), io.LimitReader(br, size)) + if err != nil { + return nil, err + } + if _, err := br.Discard(1); err != nil { + return nil, err + } + branches = append(branches, types.Branch{ + IsDefault: ref.name == defaultBranch, + Reference: types.Reference{ + Name: ref.name, + Hash: ref.oid, + }, + Commit: c, + }) + } + return branches, nil + }(repoPath, refs) + if err != nil { + return nil, fmt.Errorf("hydrating branch commits: %w", err) + } + + // -> total + total, err := func(repoPath string) (int, error) { + out, err := exec.Command("git", "-C", repoPath, "for-each-ref", "--format=%(refname)", "refs/heads").Output() + if err != nil { + return 0, err + } + out = bytes.TrimSpace(out) + if len(out) == 0 { + return 0, nil + } + return bytes.Count(out, []byte{'\n'}) + 1, nil + }(repoPath) + if err != nil { + return nil, fmt.Errorf("counting git branches: %w", err) + } + return &types.RepoBranchesResponse{ - // TODO: include default branch and cursor Branches: branches, + Total: total, }, nil } diff --git a/knotmirror/xrpc/git_list_commits.go b/knotmirror/xrpc/git_list_commits.go index 2090b997..1151cdf5 100644 --- a/knotmirror/xrpc/git_list_commits.go +++ b/knotmirror/xrpc/git_list_commits.go @@ -110,6 +110,9 @@ func (x *Xrpc) listCommits(ctx context.Context, repo syntax.DID, ref string, lim return nil, fmt.Errorf("unexpected type: %s for commit id: %s", typ, commitId) } c, err := gitea.ReadCommit(plumbing.NewHash(string(commitId)), io.LimitReader(br, size)) + if err != nil { + return nil, err + } if _, err := br.Discard(1); err != nil { return nil, err } diff --git a/knotmirror/xrpc/git_list_tags.go b/knotmirror/xrpc/git_list_tags.go index c0b94d75..6cd352ab 100644 --- a/knotmirror/xrpc/git_list_tags.go +++ b/knotmirror/xrpc/git_list_tags.go @@ -1,9 +1,11 @@ package xrpc import ( + "bytes" "context" "fmt" "net/http" + "os/exec" "strconv" "github.com/bluesky-social/indigo/atproto/atclient" @@ -60,7 +62,7 @@ func (x *Xrpc) ListTags(w http.ResponseWriter, r *http.Request) { func (x *Xrpc) listTags(ctx context.Context, repo syntax.DID, limit int, cursor int64) (*types.RepoTagsResponse, error) { repoPath, err := x.makeRepoPath(ctx, repo) if err != nil { - return nil, fmt.Errorf("failed to resolve repo did: %w", err) + return nil, fmt.Errorf("resolving repo did: %w", err) } gr, err := git.PlainOpen(repoPath) @@ -92,7 +94,24 @@ func (x *Xrpc) listTags(ctx context.Context, repo syntax.DID, limit int, cursor } } + // -> total + total, err := func(repoPath string) (int, error) { + out, err := exec.Command("git", "-C", repoPath, "for-each-ref", "--format=%(refname)", "refs/tags").Output() + if err != nil { + return 0, err + } + out = bytes.TrimSpace(out) + if len(out) == 0 { + return 0, nil + } + return bytes.Count(out, []byte{'\n'}) + 1, nil + }(repoPath) + if err != nil { + return nil, fmt.Errorf("counting git tags: %w", err) + } + return &types.RepoTagsResponse{ - Tags: rtags, + Tags: rtags, + Total: total, }, nil } diff --git a/types/repo.go b/types/repo.go index 2c53769c..e1f2936c 100644 --- a/types/repo.go +++ b/types/repo.go @@ -13,7 +13,9 @@ type RepoIndexResponse struct { Commits []Commit Files []NiceTree Branches []Branch + TotalBranches int Tags []*TagReference + TotalTags int TotalCommits int } @@ -56,7 +58,8 @@ type Branch struct { } type RepoTagsResponse struct { - Tags []*TagReference `json:"tags,omitempty"` + Tags []*TagReference `json:"tags,omitempty"` + Total int `json:"total,omitempty"` } type RepoTagResponse struct { @@ -65,6 +68,7 @@ type RepoTagResponse struct { type RepoBranchesResponse struct { Branches []Branch `json:"branches,omitempty"` + Total int `json:"total,omitempty"` } type RepoBranchResponse struct {