Monorepo for Tangled
0

Configure Feed

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

appview: use gitmirror

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

Seongmin Lee (Jul 14, 2026, 5:57 AM +0900) dea62b67 89360ce7

+352 -2
+2 -1
appview/config/config.go
··· 50 50 } 51 51 52 52 type KnotMirrorConfig struct { 53 - Url string `env:"URL, default=https://mirror.tangled.network"` 53 + Url string `env:"URL, default=https://mirror.tangled.network"` 54 + V2Host string `env:"V2_HOST, required"` 54 55 } 55 56 56 57 type JetstreamConfig struct {
+7
appview/models/pull.go
··· 87 87 Repo *Repo 88 88 } 89 89 90 + func (p *Pull) SourceRepoDid() syntax.DID { 91 + if p.PullSource != nil && p.PullSource.RepoDid != nil { 92 + return *p.PullSource.RepoDid 93 + } 94 + return p.RepoDid 95 + } 96 + 90 97 // NOTE: This method does not include patch blob in returned atproto record 91 98 func (p Pull) AsRecord() tangled.RepoPull { 92 99 mentions := make([]string, len(p.Mentions))
+45
appview/pages/pages.go
··· 1456 1456 VouchSkips map[syntax.DID]bool 1457 1457 } 1458 1458 1459 + type PullPageBaseParams struct { 1460 + BaseParams 1461 + Pull *models.Pull 1462 + 1463 + Backlinks []models.RichReferenceLink 1464 + Comments []models.Comment 1465 + Commits []types.Commit // all commits between <target>..<pr/head> 1466 + 1467 + LabelDefs map[string]*models.LabelDefinition 1468 + Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData 1469 + UserReacted map[syntax.ATURI]map[models.ReactionKind]bool 1470 + VouchRelationships map[syntax.DID]*models.VouchRelationship 1471 + VouchSkips map[syntax.DID]bool 1472 + 1473 + // diff, branch-delete-status, merge-check, resubmit-check, pipelines will be lazy-loaded. 1474 + } 1475 + 1476 + // /pulls/123/2/1a2b3c..d4e5f6 1477 + type PullDiffParams struct { 1478 + PullPageBaseParams 1479 + Version int 1480 + BaseCommitId string 1481 + HeadCommitId string 1482 + 1483 + ErrorMsg string 1484 + } 1485 + 1486 + // /pulls/123/1..2/abcdef 1487 + type PullInterdiffParams struct { 1488 + PullPageBaseParams 1489 + Version1 int 1490 + Version2 int 1491 + ChangeId string // optional change-id filter 1492 + 1493 + ErrorMsg string 1494 + } 1495 + 1496 + func (p *Pages) PullDiff(w io.Writer, params PullDiffParams) error { 1497 + panic("unimplemented") 1498 + } 1499 + 1500 + func (p *Pages) PullInterdiff(w io.Writer, params PullInterdiffParams) error { 1501 + panic("unimplemented") 1502 + } 1503 + 1459 1504 func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 1460 1505 params.Active = "pulls" 1461 1506 return p.executeRepo("repo/pulls/pull", w, params)
+280
appview/pulls/pull2.go
··· 1 + package pulls 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "net/http" 7 + "strconv" 8 + 9 + "github.com/bluesky-social/indigo/atproto/syntax" 10 + "github.com/bluesky-social/indigo/lex/util" 11 + indigoxrpc "github.com/bluesky-social/indigo/xrpc" 12 + "github.com/go-chi/chi/v5" 13 + "golang.org/x/sync/errgroup" 14 + "tangled.org/core/api/tangled" 15 + "tangled.org/core/appview/models" 16 + "tangled.org/core/appview/pages" 17 + "tangled.org/core/types" 18 + ) 19 + 20 + // NOTE: parsing object in middleware is bad pattern 21 + // you will have to check if object exist in context "just in case" 22 + // so it's better to make helper function that can read the url pattern instead. 23 + 24 + 25 + // A -- B -- C 26 + // (master) (pr/123/0) 27 + // 28 + // A -- B -- C 29 + // \ (pr/123/0) 30 + // `-- D <- B' <- C' 31 + // (master) (pr/123/1) 32 + 33 + // 1. rebase B<-C to D 34 + // 2. compare tree of C and D 35 + 36 + // PullInterDiff is router for /pulls/{pull}/{version}..{version}/{change} 37 + // 38 + // Examples: 39 + // - /pulls/123/0..2/all 40 + // - /pulls/123/0..2/nrpytyzw 41 + func (s *Pulls) PullInterDiff(w http.ResponseWriter, r *http.Request) { 42 + l := s.logger.With("handler", "PullRound") 43 + ctx := r.Context() 44 + 45 + pull, ok := r.Context().Value("pull").(*models.Pull) 46 + if !ok { 47 + l.Error("failed to get pull") 48 + s.pages.Error500(w) 49 + return 50 + } 51 + 52 + var ( 53 + version1 = 0 54 + version2 = 0 55 + changeId = chi.URLParam(r, "*") 56 + ) 57 + if changeId == "all" { 58 + changeId = "" 59 + } 60 + 61 + // defer render 62 + var params pages.PullInterdiffParams 63 + params.Pull = pull 64 + params.Version1 = version1 65 + params.Version2 = version2 66 + params.ChangeId = changeId 67 + defer s.pages.PullInterdiff(w, params) 68 + 69 + // 1. resolve target branch -> (branch, commit) 70 + xrpcc := &indigoxrpc.Client{Host: s.config.KnotMirror.Url} 71 + branch, err := tangled.GitTempGetBranch(ctx, xrpcc, pull.TargetBranch, pull.RepoDid.String()) 72 + if err != nil { 73 + panic("unimplemented") 74 + } 75 + 76 + base := branch.Hash 77 + head1 := "" // pull.Versions[version1].Head 78 + head2 := "" // pull.Versions[version2].Head 79 + 80 + // 1. log commits from base..head1 and base..head2 81 + var commits1, commits2 []types.Commit 82 + g, gctx := errgroup.WithContext(ctx) 83 + g.Go(func() error { 84 + commits1, err = getTempListCommits(gctx, xrpcc, pull.SourceRepoDid(), base, head1) 85 + return err 86 + }) 87 + g.Go(func() error { 88 + commits2, err = getTempListCommits(gctx, xrpcc, pull.SourceRepoDid(), base, head2) 89 + return err 90 + }) 91 + if err := g.Wait(); err != nil { 92 + params.ErrorMsg = "something something" 93 + panic("unimplemented") 94 + } 95 + 96 + if changeId != "" { 97 + // interdiff by change-id 98 + var old, new *types.Commit 99 + for _, commit := range commits1 { 100 + if commit.ChangeId == changeId { 101 + old = &commit 102 + break 103 + } 104 + } 105 + for _, commit := range commits2 { 106 + if commit.ChangeId == changeId { 107 + new = &commit 108 + break 109 + } 110 + } 111 + _, _ = old, new 112 + panic("unimplemented") 113 + } else { 114 + // interdiff of two commit ranges 115 + panic("unimplemented") 116 + } 117 + } 118 + 119 + // PullDiff is router for /pulls/{pull}/{version}/{commit}..{commit} 120 + // 121 + // Examples: 122 + // - /pulls/123/latest 123 + // - /pulls/123/2/head 124 + // - /pulls/123/2/base..head 125 + // - /pulls/123/2/a53ab251e..d8add468c 126 + // - /pulls/123/2/d8add468c 127 + func (s *Pulls) PullDiff(w http.ResponseWriter, r *http.Request) { 128 + l := s.logger.With("handler", "PullRound") 129 + ctx := r.Context() 130 + 131 + pull, ok := r.Context().Value("pull").(*models.Pull) 132 + if !ok { 133 + l.Error("failed to get pull") 134 + s.pages.Error500(w) 135 + return 136 + } 137 + 138 + var err error 139 + 140 + var version int 141 + var versionRaw = chi.URLParam(r, "version") 142 + if versionRaw == "latest" { 143 + version = pull.LastRoundNumber() 144 + } else { 145 + version, err = strconv.Atoi(versionRaw) 146 + if err != nil { 147 + // invalid version number. redirect 148 + http.Redirect(w, r, 149 + fmt.Sprintf("/%s/pulls/%d/latest", pull.Repo.RepoIdentifier(), pull.ID), 150 + http.StatusSeeOther, 151 + ) 152 + return 153 + } 154 + } 155 + 156 + var range_ = chi.URLParam(r, "*") 157 + base, head, err := parseRevRange(range_) 158 + if err != nil { 159 + http.Redirect(w, r, 160 + fmt.Sprintf("/%s/pulls/%d/%s", pull.Repo.RepoIdentifier(), pull.ID, versionRaw), 161 + http.StatusSeeOther, 162 + ) 163 + return 164 + } 165 + 166 + // defer render 167 + var params pages.PullDiffParams 168 + params.Pull = pull 169 + params.Version = version 170 + defer s.pages.PullDiff(w, params) 171 + 172 + // 1. resolve target branch -> (branch, commit) 173 + xrpcc := &indigoxrpc.Client{Host: s.config.KnotMirror.Url} 174 + branch, err := tangled.GitTempGetBranch(ctx, xrpcc, pull.TargetBranch, pull.RepoDid.String()) 175 + if err != nil { 176 + l.Warn("Failed to resolve target branch", "branch", pull.TargetBranch, "err", err) 177 + params.ErrorMsg = fmt.Sprintf("Failed to resolve target branch %q", pull.TargetBranch) 178 + return 179 + } 180 + 181 + if base == "base" { 182 + base = branch.Hash 183 + } 184 + if head == "head" { 185 + head = pull.HEAD() 186 + } 187 + 188 + sourceRepoDid := pull.SourceRepoDid() 189 + 190 + // 2. list diverged commits using knotmirror (BASE..HEAD) -> ([]commit) 191 + // - knotmirror needs on-demand fetch implementation for this 192 + commits, err := getTempListCommits(ctx, xrpcc, sourceRepoDid, base, head) 193 + if err != nil { 194 + panic("unimplemented") 195 + } 196 + 197 + // 3. list every commits in UI. They will be lazy-loaded 198 + params.Commits = commits 199 + } 200 + 201 + // htmx fragment. render diff between commits 202 + func (s *Pulls) PullDiffFragment(w http.ResponseWriter, r *http.Request) { 203 + // var ( 204 + // base = r.URL.Query().Get("base") // base commit ID 205 + // head = r.URL.Query().Get("head") // head commit ID 206 + // unified = r.URL.Query().Get("view") == "unified" 207 + // ) 208 + 209 + // 1. get commit object 210 + // 2. get diff between parent..commit (knotmirror), parse that diff 211 + // 3. fetch each file entries (& run syntax highlight) <- skip this part for stage 1. we will do this at stage 2. 212 + // 4. render diff 213 + } 214 + 215 + // htmx fragment. render interdiff between changes 216 + func (s *Pulls) PullInterdiffFragment(w http.ResponseWriter, r *http.Request) { 217 + // var ( 218 + // base1 = r.URL.Query().Get("base1") // base1 commit ID 219 + // base2 = r.URL.Query().Get("base2") // base2 commit ID 220 + // head1 = r.URL.Query().Get("head1") // head1 commit ID 221 + // head2 = r.URL.Query().Get("head2") // head1 commit ID 222 + // unified = r.URL.Query().Get("view") == "unified" 223 + // ) 224 + 225 + // 1. compute interdiff. (knotmirror) 226 + // 2. return rich diff data. (knotmirror) 227 + // 3. load old/new blobs & run syntax highlight 228 + // 4. render diff 229 + } 230 + 231 + // parseRevRange parses <head>..<base> string. 232 + // base and head will default to "base" and "head" when omitted. 233 + func parseRevRange(range_ string) (base string, head string, err error) { 234 + panic("unimplemented") 235 + } 236 + 237 + // parseVersionRange parses <version>..<version> string. 238 + // Each versions will default to "base" and "latest" when omitted. 239 + func parseVersionRange(range_ string) (base string, head string, err error) { 240 + panic("unimplemented") 241 + } 242 + 243 + func getTempListCommits(ctx context.Context, xrpcc util.LexClient, repo syntax.DID, base, head string) ([]types.Commit, error) { 244 + panic("unimplemented") 245 + // raw, err := tangled.GitTempListCommits(ctx, xrpcc, "", 1000, head, repo.String()) 246 + // if err != nil { 247 + // return nil, err 248 + // } 249 + // 250 + // var xrpcResp types.RepoLogResponse 251 + // if err := json.Unmarshal(raw, &xrpcResp); err != nil { 252 + // return nil, fmt.Errorf("failed to decode XRPC response: %w", err) 253 + // } 254 + // 255 + // return xrpcResp.Commits, nil 256 + } 257 + 258 + // htmx fragment. render interdiff between commits 259 + func (s *Pulls) PullInterDiffFragment(w http.ResponseWriter, r *http.Request) { 260 + panic("unimplemented") 261 + } 262 + 263 + // gitmirror 264 + // - git.ListCommitsSinceMergeBase(repo, base, head) 265 + // - git.Diff(repo, base, head, mode) 266 + // - git.Interdiff(repo, 267 + 268 + // for interdiff, we want: from{start,end}, to{start,end} 269 + // 1. squash from.start ~ from.end into one commit 270 + // 2. rebase that commit to to.start.parent() 271 + // 3. diff from_squashed.tree and to.end.tree 272 + 273 + // we want git log BASE..HEAD (only commits in HEAD) diverged=false 274 + // and git diff BASE...HEAD (changes from HEAD since merge-base) absolute=false 275 + 276 + // commands.go:56 picks comparison type: 277 + // - diff BASE..HEAD (COMPARISON_TYPE_ONLY_IN_HEAD) = direct 278 + // - diff BASE...HEAD (COMPARISON_TYPE_INTERSECTION) = merge-base. Server resolves merge-base via g.MergeBase() first (diff.go:43) then diffs. 279 + // we want second one. we should compute merge-base first. 280 + // we can have
+4
appview/pulls/pulls.go
··· 19 19 "tangled.org/core/appview/oauth" 20 20 "tangled.org/core/appview/pages" 21 21 "tangled.org/core/appview/reporesolver" 22 + knotmirror "tangled.org/core/gitmirror/proto/gen" 22 23 "tangled.org/core/idresolver" 23 24 "tangled.org/core/ogre" 24 25 "tangled.org/core/patchutil" ··· 49 50 indexer *pulls_indexer.Indexer 50 51 ogreClient *ogre.Client 51 52 diffCache *expirable.LRU[string, types.DiffRenderer] 53 + gitmirror knotmirror.GitMirrorServiceClient 52 54 } 53 55 54 56 func New( ··· 63 65 acl *knotacl.Service, 64 66 indexer *pulls_indexer.Indexer, 65 67 logger *slog.Logger, 68 + gitmirror knotmirror.GitMirrorServiceClient, 66 69 ) *Pulls { 67 70 return &Pulls{ 68 71 oauth: oauth, ··· 78 81 indexer: indexer, 79 82 ogreClient: ogre.NewClient(config.Ogre.Host), 80 83 diffCache: expirable.NewLRU[string, types.DiffRenderer](diffCacheSize, nil, diffCacheTTL), 84 + gitmirror: gitmirror, 81 85 } 82 86 } 83 87
+1
appview/state/router.go
··· 400 400 s.aclService, 401 401 s.indexer.Pulls, 402 402 log.SubLogger(s.logger, "pulls"), 403 + s.gitmirror, 403 404 ) 404 405 return pulls.Router(mw) 405 406 }
+11
appview/state/state.go
··· 10 10 "strings" 11 11 "time" 12 12 13 + "google.golang.org/grpc" 14 + "google.golang.org/grpc/credentials/insecure" 13 15 "tangled.org/core/api/tangled" 14 16 "tangled.org/core/appview" 15 17 "tangled.org/core/appview/bsky" ··· 35 37 "tangled.org/core/appview/reporesolver" 36 38 "tangled.org/core/consts" 37 39 "tangled.org/core/eventconsumer" 40 + knotmirror "tangled.org/core/gitmirror/proto/gen" 38 41 "tangled.org/core/idresolver" 39 42 "tangled.org/core/jetstream" 40 43 "tangled.org/core/log" ··· 73 76 logger *slog.Logger 74 77 cfClient *cloudflare.Client 75 78 codesearch *codesearch.CodeSearch 79 + gitmirror knotmirror.GitMirrorServiceClient 76 80 } 77 81 78 82 func Make(ctx context.Context, config *config.Config) (*State, error) { ··· 109 113 if err != nil { 110 114 return nil, fmt.Errorf("failed to create posthog client: %w", err) 111 115 } 116 + 117 + conn, err := grpc.NewClient(config.KnotMirror.V2Host, grpc.WithTransportCredentials(insecure.NewCredentials())) 118 + if err != nil { 119 + return nil, fmt.Errorf("failed to create gitmirror client: %w", err) 120 + } 121 + gitmirror := knotmirror.NewGitMirrorServiceClient(conn) 112 122 113 123 pages := pages.NewPages(config, res, d, rdb, log.SubLogger(logger, "pages")) 114 124 knotcompat.UseNativeLatch(knotacl.NewLatch(d, log.SubLogger(logger, "knotacl-latch"))) ··· 236 246 logger: logger, 237 247 cfClient: cfClient, 238 248 codesearch: &codesearch.CodeSearch{Host: config.CodeSearch.ZoektUrl}, 249 + gitmirror: gitmirror, 239 250 } 240 251 241 252 // fetch initial bluesky posts if configured
+1
docker-compose.yml
··· 348 348 TANGLED_JETSTREAM_ENDPOINT: wss://jetstream.tngl.boltless.dev/subscribe 349 349 TANGLED_REDIS_ADDR: redis:6379 350 350 TANGLED_KNOTMIRROR_URL: https://mirror.tngl.boltless.dev 351 + TANGLED_KNOTMIRROR_V2_HOST: gitmirror:9000 351 352 TANGLED_CODESEARCH_ZOEKT_URL: https://zoekt.tngl.boltless.dev 352 353 ports: 353 354 - "3000:3000"
+1 -1
go.mod
··· 84 84 golang.org/x/sync v0.20.0 85 85 golang.org/x/sys v0.45.0 86 86 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da 87 + google.golang.org/grpc v1.80.0 87 88 google.golang.org/protobuf v1.36.11 88 89 gopkg.in/yaml.v3 v3.0.1 89 90 ) ··· 313 314 golang.org/x/tools v0.44.0 // indirect 314 315 google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect 315 316 google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect 316 - google.golang.org/grpc v1.80.0 // indirect 317 317 gopkg.in/fsnotify.v1 v1.4.7 // indirect 318 318 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 319 319 gopkg.in/warnings.v0 v0.1.2 // indirect