Monorepo for Tangled
0
fork

Configure Feed

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

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