Monorepo for Tangled tangled.org
2

Configure Feed

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

1package repo 2 3import ( 4 "errors" 5 "fmt" 6 "net/http" 7 "net/url" 8 "slices" 9 "sort" 10 "strings" 11 "sync" 12 "time" 13 14 "context" 15 "encoding/json" 16 17 "github.com/bluesky-social/indigo/atproto/syntax" 18 indigoxrpc "github.com/bluesky-social/indigo/xrpc" 19 "github.com/go-git/go-git/v5/plumbing" 20 "tangled.org/core/api/tangled" 21 "tangled.org/core/appview/commitverify" 22 "tangled.org/core/appview/db" 23 "tangled.org/core/appview/models" 24 "tangled.org/core/appview/pages" 25 "tangled.org/core/appview/pages/markup" 26 "tangled.org/core/types" 27 28 "github.com/go-chi/chi/v5" 29 "github.com/go-enry/go-enry/v2" 30) 31 32func (rp *Repo) Index(w http.ResponseWriter, r *http.Request) { 33 l := rp.logger.With("handler", "RepoIndex") 34 35 ref := chi.URLParam(r, "ref") 36 ref, _ = url.PathUnescape(ref) 37 38 f, err := rp.repoResolver.Resolve(r) 39 if err != nil { 40 l.Error("failed to fully resolve repo", "err", err) 41 return 42 } 43 44 user := rp.oauth.GetMultiAccountUser(r) 45 46 if user != nil { 47 userDid := user.Did 48 repoDid := f.RepoDid 49 go func() { 50 if err := db.UpsertRecentLink(rp.db, userDid, models.RecentLinkTypeRepo, repoDid); err != nil { 51 l.Error("failed to upsert recent link", "err", err) 52 } 53 }() 54 } 55 56 // Build index response from multiple XRPC calls 57 result, err := rp.buildIndexResponse(r.Context(), f, ref) 58 if err != nil { 59 l.Error("failed to build index response", "err", err) 60 rp.pages.RepoIndexPage(w, pages.RepoIndexParams{ 61 BaseParams: pages.BaseParamsFromContext(r.Context()), 62 KnotUnreachable: true, 63 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 64 }) 65 return 66 } 67 68 tagMap := make(map[string][]string) 69 for _, tag := range result.Tags { 70 hash := tag.Hash 71 if tag.Tag != nil { 72 hash = tag.Tag.Target.String() 73 } 74 tagMap[hash] = append(tagMap[hash], tag.Name) 75 } 76 77 for _, branch := range result.Branches { 78 hash := branch.Hash 79 tagMap[hash] = append(tagMap[hash], branch.Name) 80 } 81 82 sortFiles(result.Files) 83 84 slices.SortFunc(result.Branches, func(a, b types.Branch) int { 85 if a.Name == result.Ref { 86 return -1 87 } 88 if a.IsDefault { 89 return -1 90 } 91 if b.IsDefault { 92 return 1 93 } 94 if a.Commit != nil && b.Commit != nil { 95 if a.Commit.Committer.When.Before(b.Commit.Committer.When) { 96 return 1 97 } else { 98 return -1 99 } 100 } 101 return strings.Compare(a.Name, b.Name) * -1 102 }) 103 104 commitCount := len(result.Commits) 105 branchCount := len(result.Branches) 106 tagCount := len(result.Tags) 107 fileCount := len(result.Files) 108 109 commitCount, branchCount, tagCount = balanceIndexItems(commitCount, branchCount, tagCount, fileCount) 110 commitsTrunc := result.Commits[:min(commitCount, len(result.Commits))] 111 tagsTrunc := result.Tags[:min(tagCount, len(result.Tags))] 112 branchesTrunc := result.Branches[:min(branchCount, len(result.Branches))] 113 114 emails := uniqueEmails(commitsTrunc) 115 emailToDidMap, err := db.GetEmailToDid(rp.db, emails, true) 116 if err != nil { 117 l.Error("failed to get email to did map", "err", err) 118 } 119 120 vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, commitsTrunc) 121 if err != nil { 122 l.Error("failed to GetVerifiedObjectCommits", "err", err) 123 } 124 125 var languageInfo []types.RepoLanguageDetails 126 if !result.IsEmpty { 127 langs, err := rp.getLanguageInfo(r.Context(), syntax.DID(f.RepoDid), result.Ref) 128 if err != nil { 129 l.Warn("failed to compute language percentages", "err", err) 130 // non-fatal 131 } else if ref == "" { // when request didn't specified ref, we are fetching default branch. 132 if err := func(repo syntax.DID, ref string, langs []*tangled.GitTempListLanguages_Language) error { 133 tx, err := rp.db.Begin() 134 if err != nil { 135 return err 136 } 137 defer tx.Rollback() 138 139 var mlangs []models.RepoLanguage 140 for _, lang := range langs { 141 mlangs = append(mlangs, models.RepoLanguage{ 142 RepoDid: repo, 143 Ref: ref, 144 IsDefaultRef: true, 145 Language: lang.Name, 146 Bytes: lang.Size, 147 }) 148 } 149 150 if err := db.UpdateRepoLanguages(tx, syntax.DID(f.RepoDid), ref, mlangs); err != nil { 151 return err 152 } 153 154 return tx.Commit() 155 }(syntax.DID(f.RepoDid), result.Ref, langs); err != nil { 156 l.Error("failed to populate appview repo languages index", "err", err) 157 // non-fatal 158 } 159 languageInfo = makeLanguageStats(langs) 160 } 161 } 162 163 var shas []string 164 for _, c := range commitsTrunc { 165 shas = append(shas, c.Hash.String()) 166 } 167 pipelines, err := getPipelineStatuses(rp.db, f, shas) 168 if err != nil { 169 l.Error("failed to fetch pipeline statuses", "err", err) 170 // non-fatal 171 } 172 173 rp.pages.RepoIndexPage(w, pages.RepoIndexParams{ 174 BaseParams: pages.BaseParamsFromContext(r.Context()), 175 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 176 TagMap: tagMap, 177 RepoIndexResponse: *result, 178 CommitsTrunc: commitsTrunc, 179 TagsTrunc: tagsTrunc, 180 // ForkInfo: forkInfo, // TODO: reinstate this after xrpc properly lands 181 BranchesTrunc: branchesTrunc, 182 EmailToDid: emailToDidMap, 183 VerifiedCommits: vc, 184 Languages: languageInfo, 185 Pipelines: pipelines, 186 }) 187} 188 189func (rp *Repo) getLanguageInfo( 190 ctx context.Context, 191 repoId syntax.DID, 192 ref string, 193) ([]*tangled.GitTempListLanguages_Language, error) { 194 // non-fatal, fetch langs from knotmirror via XRPC 195 xrpcc := &indigoxrpc.Client{ 196 Host: rp.config.KnotMirror.Url, 197 Client: http.DefaultClient, 198 } 199 out, err := tangled.GitTempListLanguages(ctx, xrpcc, ref, repoId.String()) 200 if err != nil { 201 return nil, fmt.Errorf("calling knotmirror git.listLanguages: %w", err) 202 } 203 204 if out == nil || out.Languages == nil { 205 return nil, nil 206 } 207 208 return out.Languages, nil 209} 210 211func makeLanguageStats(langs []*tangled.GitTempListLanguages_Language) []types.RepoLanguageDetails { 212 if len(langs) == 0 { 213 return nil 214 } 215 var total int64 216 for _, lang := range langs { 217 total += lang.Size 218 } 219 220 var languageStats []types.RepoLanguageDetails 221 for _, l := range langs { 222 languageStats = append(languageStats, types.RepoLanguageDetails{ 223 Name: l.Name, 224 Color: enry.GetColor(l.Name), 225 Percentage: float32(l.Size) / float32(total) * 100, 226 }) 227 } 228 229 sort.Slice(languageStats, func(i, j int) bool { 230 if languageStats[i].Name == enry.OtherLanguage { 231 return false 232 } 233 if languageStats[j].Name == enry.OtherLanguage { 234 return true 235 } 236 if languageStats[i].Percentage != languageStats[j].Percentage { 237 return languageStats[i].Percentage > languageStats[j].Percentage 238 } 239 return languageStats[i].Name < languageStats[j].Name 240 }) 241 return languageStats 242} 243 244// buildIndexResponse creates a RepoIndexResponse by combining multiple xrpc calls in parallel 245func (rp *Repo) buildIndexResponse(ctx context.Context, repo *models.Repo, ref string) (*types.RepoIndexResponse, error) { 246 xrpcc := &indigoxrpc.Client{Host: rp.config.KnotMirror.Url} 247 248 branchesBytes, err := tangled.GitTempListBranches(ctx, xrpcc, "", 0, repo.RepoDid) 249 if err != nil { 250 return nil, fmt.Errorf("calling knotmirror git.listBranches: %w", err) 251 } 252 253 var branchesResp types.RepoBranchesResponse 254 if err := json.Unmarshal(branchesBytes, &branchesResp); err != nil { 255 return nil, fmt.Errorf("failed to unmarshal branches response: %w", err) 256 } 257 258 // if no ref specified, use default branch or first available 259 if ref == "" { 260 for _, branch := range branchesResp.Branches { 261 if branch.IsDefault { 262 ref = branch.Name 263 break 264 } 265 } 266 } 267 268 // if ref is still empty, this means the default branch is not set 269 if ref == "" { 270 return &types.RepoIndexResponse{ 271 IsEmpty: true, 272 Branches: branchesResp.Branches, 273 }, nil 274 } 275 276 // now run the remaining queries in parallel 277 var wg sync.WaitGroup 278 var errs error 279 280 var ( 281 tagsResp types.RepoTagsResponse 282 treeResp *tangled.GitTempGetTree_Output 283 logResp types.RepoLogResponse 284 readmeContent string 285 readmeFileName string 286 ) 287 288 // tags 289 wg.Go(func() { 290 tagsBytes, err := tangled.GitTempListTags(ctx, xrpcc, "", 0, repo.RepoDid) 291 if err != nil { 292 errs = errors.Join(errs, fmt.Errorf("failed to call git.ListTags: %w", err)) 293 return 294 } 295 296 if err := json.Unmarshal(tagsBytes, &tagsResp); err != nil { 297 errs = errors.Join(errs, fmt.Errorf("failed to unmarshal git.ListTags: %w", err)) 298 } 299 }) 300 301 // tree/files 302 wg.Go(func() { 303 resp, err := tangled.GitTempGetTree(ctx, xrpcc, "", ref, repo.RepoDid) 304 if err != nil { 305 errs = errors.Join(errs, fmt.Errorf("failed to call git.GetTree: %w", err)) 306 return 307 } 308 treeResp = resp 309 310 for _, file := range resp.Files { 311 if markup.IsReadmeFile(file.Name, file.Mode) { 312 readmeFileName = file.Name 313 break 314 } 315 } 316 317 if readmeFileName != "" { 318 bytes, err := tangled.GitTempGetBlob(ctx, xrpcc, readmeFileName, ref, repo.RepoDid) 319 if err != nil { 320 errs = errors.Join(errs, fmt.Errorf("failed to call git.getBlob: %w", err)) 321 return 322 } 323 readmeContent = string(bytes) 324 } 325 }) 326 327 // commits 328 wg.Go(func() { 329 logBytes, err := tangled.GitTempListCommits(ctx, xrpcc, "", 50, ref, repo.RepoDid) 330 if err != nil { 331 errs = errors.Join(errs, fmt.Errorf("failed to call git.ListCommits: %w", err)) 332 return 333 } 334 335 if err := json.Unmarshal(logBytes, &logResp); err != nil { 336 errs = errors.Join(errs, fmt.Errorf("failed to unmarshal git.ListCommits: %w", err)) 337 } 338 }) 339 340 wg.Wait() 341 342 if errs != nil { 343 return nil, errs 344 } 345 346 var files []types.NiceTree 347 if treeResp != nil && treeResp.Files != nil { 348 for _, file := range treeResp.Files { 349 niceFile := types.NiceTree{ 350 Name: file.Name, 351 Mode: file.Mode, 352 Size: file.Size, 353 } 354 355 if file.Last_commit != nil { 356 when, _ := time.Parse(time.RFC3339, file.Last_commit.When) 357 niceFile.LastCommit = &types.LastCommitInfo{ 358 Hash: plumbing.NewHash(file.Last_commit.Hash), 359 Message: file.Last_commit.Message, 360 When: when, 361 } 362 } 363 files = append(files, niceFile) 364 } 365 } 366 367 result := &types.RepoIndexResponse{ 368 IsEmpty: false, 369 Ref: ref, 370 Readme: readmeContent, 371 ReadmeFileName: readmeFileName, 372 Commits: logResp.Commits, 373 Files: files, 374 Branches: branchesResp.Branches, 375 Tags: tagsResp.Tags, 376 TotalCommits: logResp.Total, 377 } 378 379 return result, nil 380}