Monorepo for Tangled
0

Configure Feed

Select the types of activity you want to include in your feed.

knotmirror/xrpc,types: include total branch/tag counts on response

Signed-off-by: Seongmin Lee <git@boltless.me>

author
Seongmin Lee
committer
Tangled
date (Jul 21, 2026, 5:47 PM +0300) commit 81105734 parent 13d8cece change-id pkmzrvtz
+142 -12
+113 -9
knotmirror/xrpc/git_list_branches.go
··· 1 1 package xrpc 2 2 3 3 import ( 4 + "bytes" 4 5 "context" 5 6 "fmt" 7 + "io" 6 8 "net/http" 7 9 "os" 10 + "os/exec" 8 11 "path/filepath" 9 12 "strconv" 13 + "strings" 10 14 11 15 "github.com/bluesky-social/indigo/atproto/atclient" 12 16 "github.com/bluesky-social/indigo/atproto/syntax" 13 - "tangled.org/core/knotserver/git" 17 + "github.com/go-git/go-git/v5/plumbing" 18 + "tangled.org/core/knotmirror/xrpc/gitea" 14 19 "tangled.org/core/types" 15 20 ) 21 + 22 + const fieldSeparator = "\x1f" // ASCII Unit Separator 16 23 17 24 func (x *Xrpc) ListBranches(w http.ResponseWriter, r *http.Request) { 18 25 var ( ··· 63 70 return nil, fmt.Errorf("resolving repo did: %w", err) 64 71 } 65 72 66 - gr, err := git.PlainOpen(repoPath) 67 - if err != nil { 68 - return nil, fmt.Errorf("opening git repo: %w", err) 73 + // ignore error: an empty default branch just means nothing is marked default 74 + defaultBranch := func(repoPath string) string { 75 + out, err := exec.Command("git", "-C", repoPath, "rev-parse", "--abbrev-ref", "HEAD").Output() 76 + if err != nil { 77 + return "" 78 + } 79 + return string(bytes.TrimSpace(out)) 80 + }(repoPath) 81 + 82 + // -> [](name, oid) 83 + type branchRef struct { 84 + name string 85 + oid string 69 86 } 87 + refs, err := func(repoPath string) ([]branchRef, error) { 88 + out, err := exec.Command( 89 + "git", 90 + "-C", repoPath, 91 + "for-each-ref", 92 + "--format=%(refname:short)"+fieldSeparator+"%(objectname)", 93 + "--sort=-creatordate", 94 + // for-each-ref has no skip flag, so fetch offset+limit and slice below 95 + fmt.Sprintf("--count=%d", cursor+int64(limit)), 96 + "refs/heads", 97 + ).Output() 98 + if err != nil { 99 + return nil, err 100 + } 70 101 71 - branches, err := gr.Branches(&git.BranchesOptions{ 72 - Limit: limit, 73 - Offset: int(cursor), 74 - }) 102 + out = bytes.TrimSpace(out) 103 + if len(out) == 0 { 104 + return nil, nil 105 + } 106 + lines := strings.Split(string(out), "\n") 107 + if int(cursor) >= len(lines) { 108 + return nil, nil 109 + } 110 + lines = lines[cursor:] 111 + 112 + refs := make([]branchRef, 0, len(lines)) 113 + for _, line := range lines { 114 + name, oid, ok := strings.Cut(line, fieldSeparator) 115 + if !ok { 116 + continue 117 + } 118 + refs = append(refs, branchRef{name: name, oid: oid}) 119 + } 120 + return refs, nil 121 + }(repoPath) 75 122 if err != nil { 76 123 return nil, fmt.Errorf("listing git branches: %w", err) 77 124 } 78 125 126 + // hydrate each ref's commit 127 + branches, err := func(repoPath string, refs []branchRef) ([]types.Branch, error) { 128 + bw, br, cancel := gitea.CatFileBatch(ctx, repoPath) 129 + defer cancel() 130 + branches := make([]types.Branch, 0, len(refs)) 131 + for _, ref := range refs { 132 + if _, err := bw.Write([]byte(ref.oid + "\n")); err != nil { 133 + return nil, err 134 + } 135 + _, typ, size, err := gitea.ReadBatchLine(br) 136 + if err != nil { 137 + return nil, err 138 + } 139 + if typ != "commit" { 140 + if err := gitea.DiscardFull(br, size+1); err != nil { 141 + return nil, err 142 + } 143 + return nil, fmt.Errorf("unexpected type: %s for commit id: %s", typ, ref.oid) 144 + } 145 + c, err := gitea.ReadCommit(plumbing.NewHash(ref.oid), io.LimitReader(br, size)) 146 + if err != nil { 147 + return nil, err 148 + } 149 + if _, err := br.Discard(1); err != nil { 150 + return nil, err 151 + } 152 + branches = append(branches, types.Branch{ 153 + IsDefault: ref.name == defaultBranch, 154 + Reference: types.Reference{ 155 + Name: ref.name, 156 + Hash: ref.oid, 157 + }, 158 + Commit: c, 159 + }) 160 + } 161 + return branches, nil 162 + }(repoPath, refs) 163 + if err != nil { 164 + return nil, fmt.Errorf("hydrating branch commits: %w", err) 165 + } 166 + 167 + // -> total 168 + total, err := func(repoPath string) (int, error) { 169 + out, err := exec.Command("git", "-C", repoPath, "for-each-ref", "--format=%(refname)", "refs/heads").Output() 170 + if err != nil { 171 + return 0, err 172 + } 173 + out = bytes.TrimSpace(out) 174 + if len(out) == 0 { 175 + return 0, nil 176 + } 177 + return bytes.Count(out, []byte{'\n'}) + 1, nil 178 + }(repoPath) 179 + if err != nil { 180 + return nil, fmt.Errorf("counting git branches: %w", err) 181 + } 182 + 79 183 return &types.RepoBranchesResponse{ 80 - // TODO: include default branch and cursor 81 184 Branches: branches, 185 + Total: total, 82 186 }, nil 83 187 } 84 188
+3
knotmirror/xrpc/git_list_commits.go
··· 110 110 return nil, fmt.Errorf("unexpected type: %s for commit id: %s", typ, commitId) 111 111 } 112 112 c, err := gitea.ReadCommit(plumbing.NewHash(string(commitId)), io.LimitReader(br, size)) 113 + if err != nil { 114 + return nil, err 115 + } 113 116 if _, err := br.Discard(1); err != nil { 114 117 return nil, err 115 118 }
+21 -2
knotmirror/xrpc/git_list_tags.go
··· 1 1 package xrpc 2 2 3 3 import ( 4 + "bytes" 4 5 "context" 5 6 "fmt" 6 7 "net/http" 8 + "os/exec" 7 9 "strconv" 8 10 9 11 "github.com/bluesky-social/indigo/atproto/atclient" ··· 60 62 func (x *Xrpc) listTags(ctx context.Context, repo syntax.DID, limit int, cursor int64) (*types.RepoTagsResponse, error) { 61 63 repoPath, err := x.makeRepoPath(ctx, repo) 62 64 if err != nil { 63 - return nil, fmt.Errorf("failed to resolve repo did: %w", err) 65 + return nil, fmt.Errorf("resolving repo did: %w", err) 64 66 } 65 67 66 68 gr, err := git.PlainOpen(repoPath) ··· 92 94 } 93 95 } 94 96 97 + // -> total 98 + total, err := func(repoPath string) (int, error) { 99 + out, err := exec.Command("git", "-C", repoPath, "for-each-ref", "--format=%(refname)", "refs/tags").Output() 100 + if err != nil { 101 + return 0, err 102 + } 103 + out = bytes.TrimSpace(out) 104 + if len(out) == 0 { 105 + return 0, nil 106 + } 107 + return bytes.Count(out, []byte{'\n'}) + 1, nil 108 + }(repoPath) 109 + if err != nil { 110 + return nil, fmt.Errorf("counting git tags: %w", err) 111 + } 112 + 95 113 return &types.RepoTagsResponse{ 96 - Tags: rtags, 114 + Tags: rtags, 115 + Total: total, 97 116 }, nil 98 117 }
+5 -1
types/repo.go
··· 13 13 Commits []Commit 14 14 Files []NiceTree 15 15 Branches []Branch 16 + TotalBranches int 16 17 Tags []*TagReference 18 + TotalTags int 17 19 TotalCommits int 18 20 } 19 21 ··· 56 58 } 57 59 58 60 type RepoTagsResponse struct { 59 - Tags []*TagReference `json:"tags,omitempty"` 61 + Tags []*TagReference `json:"tags,omitempty"` 62 + Total int `json:"total,omitempty"` 60 63 } 61 64 62 65 type RepoTagResponse struct { ··· 65 68 66 69 type RepoBranchesResponse struct { 67 70 Branches []Branch `json:"branches,omitempty"` 71 + Total int `json:"total,omitempty"` 68 72 } 69 73 70 74 type RepoBranchResponse struct {