this repo has no description
1package repo 2 3import ( 4 "fmt" 5 "log" 6 "net/http" 7 "slices" 8 "sort" 9 "strings" 10 "sync" 11 "time" 12 13 "context" 14 "encoding/json" 15 16 indigoxrpc "github.com/bluesky-social/indigo/xrpc" 17 "github.com/go-git/go-git/v5/plumbing" 18 "tangled.sh/tangled.sh/core/api/tangled" 19 "tangled.sh/tangled.sh/core/appview/commitverify" 20 "tangled.sh/tangled.sh/core/appview/db" 21 "tangled.sh/tangled.sh/core/appview/pages" 22 "tangled.sh/tangled.sh/core/appview/pages/markup" 23 "tangled.sh/tangled.sh/core/appview/reporesolver" 24 "tangled.sh/tangled.sh/core/appview/xrpcclient" 25 "tangled.sh/tangled.sh/core/types" 26 27 "github.com/go-chi/chi/v5" 28 "github.com/go-enry/go-enry/v2" 29) 30 31func (rp *Repo) RepoIndex(w http.ResponseWriter, r *http.Request) { 32 ref := chi.URLParam(r, "ref") 33 34 f, err := rp.repoResolver.Resolve(r) 35 if err != nil { 36 log.Println("failed to fully resolve repo", err) 37 return 38 } 39 40 scheme := "http" 41 if !rp.config.Core.Dev { 42 scheme = "https" 43 } 44 host := fmt.Sprintf("%s://%s", scheme, f.Knot) 45 xrpcc := &indigoxrpc.Client{ 46 Host: host, 47 } 48 49 // Build index response from multiple XRPC calls 50 result, err := rp.buildIndexResponse(r.Context(), xrpcc, f, ref) 51 if err != nil { 52 rp.pages.Error503(w) 53 log.Println("failed to build index response", err) 54 return 55 } 56 57 tagMap := make(map[string][]string) 58 for _, tag := range result.Tags { 59 hash := tag.Hash 60 if tag.Tag != nil { 61 hash = tag.Tag.Target.String() 62 } 63 tagMap[hash] = append(tagMap[hash], tag.Name) 64 } 65 66 for _, branch := range result.Branches { 67 hash := branch.Hash 68 tagMap[hash] = append(tagMap[hash], branch.Name) 69 } 70 71 sortFiles(result.Files) 72 73 slices.SortFunc(result.Branches, func(a, b types.Branch) int { 74 if a.Name == result.Ref { 75 return -1 76 } 77 if a.IsDefault { 78 return -1 79 } 80 if b.IsDefault { 81 return 1 82 } 83 if a.Commit != nil && b.Commit != nil { 84 if a.Commit.Committer.When.Before(b.Commit.Committer.When) { 85 return 1 86 } else { 87 return -1 88 } 89 } 90 return strings.Compare(a.Name, b.Name) * -1 91 }) 92 93 commitCount := len(result.Commits) 94 branchCount := len(result.Branches) 95 tagCount := len(result.Tags) 96 fileCount := len(result.Files) 97 98 commitCount, branchCount, tagCount = balanceIndexItems(commitCount, branchCount, tagCount, fileCount) 99 commitsTrunc := result.Commits[:min(commitCount, len(result.Commits))] 100 tagsTrunc := result.Tags[:min(tagCount, len(result.Tags))] 101 branchesTrunc := result.Branches[:min(branchCount, len(result.Branches))] 102 103 emails := uniqueEmails(commitsTrunc) 104 emailToDidMap, err := db.GetEmailToDid(rp.db, emails, true) 105 if err != nil { 106 log.Println("failed to get email to did map", err) 107 } 108 109 vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, commitsTrunc) 110 if err != nil { 111 log.Println(err) 112 } 113 114 user := rp.oauth.GetUser(r) 115 repoInfo := f.RepoInfo(user) 116 117 // TODO: a bit dirty 118 languageInfo, err := rp.getLanguageInfo(r.Context(), f, xrpcc, result.Ref, ref == "") 119 if err != nil { 120 log.Printf("failed to compute language percentages: %s", err) 121 // non-fatal 122 } 123 124 var shas []string 125 for _, c := range commitsTrunc { 126 shas = append(shas, c.Hash.String()) 127 } 128 pipelines, err := getPipelineStatuses(rp.db, repoInfo, shas) 129 if err != nil { 130 log.Printf("failed to fetch pipeline statuses: %s", err) 131 // non-fatal 132 } 133 134 rp.pages.RepoIndexPage(w, pages.RepoIndexParams{ 135 LoggedInUser: user, 136 RepoInfo: repoInfo, 137 TagMap: tagMap, 138 RepoIndexResponse: *result, 139 CommitsTrunc: commitsTrunc, 140 TagsTrunc: tagsTrunc, 141 // ForkInfo: forkInfo, // TODO: reinstate this after xrpc properly lands 142 BranchesTrunc: branchesTrunc, 143 EmailToDidOrHandle: emailToDidOrHandle(rp, emailToDidMap), 144 VerifiedCommits: vc, 145 Languages: languageInfo, 146 Pipelines: pipelines, 147 }) 148} 149 150func (rp *Repo) getLanguageInfo( 151 ctx context.Context, 152 f *reporesolver.ResolvedRepo, 153 xrpcc *indigoxrpc.Client, 154 currentRef string, 155 isDefaultRef bool, 156) ([]types.RepoLanguageDetails, error) { 157 // first attempt to fetch from db 158 langs, err := db.GetRepoLanguages( 159 rp.db, 160 db.FilterEq("repo_at", f.RepoAt()), 161 db.FilterEq("ref", currentRef), 162 ) 163 164 if err != nil || langs == nil { 165 // non-fatal, fetch langs from ks via XRPC 166 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 167 ls, err := tangled.RepoLanguages(ctx, xrpcc, currentRef, repo) 168 if err != nil { 169 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 170 log.Println("failed to call XRPC repo.languages", xrpcerr) 171 return nil, xrpcerr 172 } 173 return nil, 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, db.RepoLanguage{ 182 RepoAt: f.RepoAt(), 183 Ref: currentRef, 184 IsDefaultRef: isDefaultRef, 185 Language: lang.Name, 186 Bytes: lang.Size, 187 }) 188 } 189 190 // update appview's cache 191 err = db.InsertRepoLanguages(rp.db, langs) 192 if err != nil { 193 // non-fatal 194 log.Println("failed to cache lang results", err) 195 } 196 } 197 198 var total int64 199 for _, l := range langs { 200 total += l.Bytes 201 } 202 203 var languageStats []types.RepoLanguageDetails 204 for _, l := range langs { 205 percentage := float32(l.Bytes) / float32(total) * 100 206 color := enry.GetColor(l.Language) 207 languageStats = append(languageStats, types.RepoLanguageDetails{ 208 Name: l.Language, 209 Percentage: percentage, 210 Color: color, 211 }) 212 } 213 214 sort.Slice(languageStats, func(i, j int) bool { 215 if languageStats[i].Name == enry.OtherLanguage { 216 return false 217 } 218 if languageStats[j].Name == enry.OtherLanguage { 219 return true 220 } 221 if languageStats[i].Percentage != languageStats[j].Percentage { 222 return languageStats[i].Percentage > languageStats[j].Percentage 223 } 224 return languageStats[i].Name < languageStats[j].Name 225 }) 226 227 return languageStats, nil 228} 229 230// buildIndexResponse creates a RepoIndexResponse by combining multiple xrpc calls in parallel 231func (rp *Repo) buildIndexResponse(ctx context.Context, xrpcc *indigoxrpc.Client, f *reporesolver.ResolvedRepo, ref string) (*types.RepoIndexResponse, error) { 232 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 233 234 // first get branches to determine the ref if not specified 235 branchesBytes, err := tangled.RepoBranches(ctx, xrpcc, "", 0, repo) 236 if err != nil { 237 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 238 log.Println("failed to call XRPC repo.branches", xrpcerr) 239 return nil, xrpcerr 240 } 241 return nil, err 242 } 243 244 var branchesResp types.RepoBranchesResponse 245 if err := json.Unmarshal(branchesBytes, &branchesResp); err != nil { 246 return nil, err 247 } 248 249 // if no ref specified, use default branch or first available 250 if ref == "" && len(branchesResp.Branches) > 0 { 251 for _, branch := range branchesResp.Branches { 252 if branch.IsDefault { 253 ref = branch.Name 254 break 255 } 256 } 257 if ref == "" { 258 ref = branchesResp.Branches[0].Name 259 } 260 } 261 262 // check if repo is empty 263 if len(branchesResp.Branches) == 0 { 264 return &types.RepoIndexResponse{ 265 IsEmpty: true, 266 Branches: branchesResp.Branches, 267 }, nil 268 } 269 270 // now run the remaining queries in parallel 271 var wg sync.WaitGroup 272 var mu sync.Mutex 273 var errs []error 274 275 var ( 276 tagsResp types.RepoTagsResponse 277 treeResp *tangled.RepoTree_Output 278 logResp types.RepoLogResponse 279 readmeContent string 280 readmeFileName string 281 ) 282 283 // tags 284 wg.Add(1) 285 go func() { 286 defer wg.Done() 287 tagsBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repo) 288 if err != nil { 289 mu.Lock() 290 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 291 log.Println("failed to call XRPC repo.tags", xrpcerr) 292 errs = append(errs, xrpcerr) 293 } else { 294 errs = append(errs, err) 295 } 296 mu.Unlock() 297 return 298 } 299 300 if err := json.Unmarshal(tagsBytes, &tagsResp); err != nil { 301 mu.Lock() 302 errs = append(errs, err) 303 mu.Unlock() 304 } 305 }() 306 307 // tree/files 308 wg.Add(1) 309 go func() { 310 defer wg.Done() 311 resp, err := tangled.RepoTree(ctx, xrpcc, "", ref, repo) 312 if err != nil { 313 mu.Lock() 314 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 315 log.Println("failed to call XRPC repo.tree", xrpcerr) 316 errs = append(errs, xrpcerr) 317 } else { 318 errs = append(errs, err) 319 } 320 mu.Unlock() 321 return 322 } 323 treeResp = resp 324 }() 325 326 // commits 327 wg.Add(1) 328 go func() { 329 defer wg.Done() 330 logBytes, err := tangled.RepoLog(ctx, xrpcc, "", 50, "", ref, repo) 331 if err != nil { 332 mu.Lock() 333 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 334 log.Println("failed to call XRPC repo.log", xrpcerr) 335 errs = append(errs, xrpcerr) 336 } else { 337 errs = append(errs, err) 338 } 339 mu.Unlock() 340 return 341 } 342 343 if err := json.Unmarshal(logBytes, &logResp); err != nil { 344 mu.Lock() 345 errs = append(errs, err) 346 mu.Unlock() 347 } 348 }() 349 350 // readme content 351 wg.Add(1) 352 go func() { 353 defer wg.Done() 354 for _, filename := range markup.ReadmeFilenames { 355 blobResp, err := tangled.RepoBlob(ctx, xrpcc, filename, false, ref, repo) 356 if err != nil { 357 continue 358 } 359 360 if blobResp == nil { 361 continue 362 } 363 364 readmeContent = blobResp.Content 365 readmeFileName = filename 366 break 367 } 368 }() 369 370 wg.Wait() 371 372 if len(errs) > 0 { 373 return nil, errs[0] // return first error 374 } 375 376 var files []types.NiceTree 377 if treeResp != nil && treeResp.Files != nil { 378 for _, file := range treeResp.Files { 379 niceFile := types.NiceTree{ 380 IsFile: file.Is_file, 381 IsSubtree: file.Is_subtree, 382 Name: file.Name, 383 Mode: file.Mode, 384 Size: file.Size, 385 } 386 if file.Last_commit != nil { 387 when, _ := time.Parse(time.RFC3339, file.Last_commit.When) 388 niceFile.LastCommit = &types.LastCommitInfo{ 389 Hash: plumbing.NewHash(file.Last_commit.Hash), 390 Message: file.Last_commit.Message, 391 When: when, 392 } 393 } 394 files = append(files, niceFile) 395 } 396 } 397 398 result := &types.RepoIndexResponse{ 399 IsEmpty: false, 400 Ref: ref, 401 Readme: readmeContent, 402 ReadmeFileName: readmeFileName, 403 Commits: logResp.Commits, 404 Description: logResp.Description, 405 Files: files, 406 Branches: branchesResp.Branches, 407 Tags: tagsResp.Tags, 408 TotalCommits: logResp.Total, 409 } 410 411 return result, nil 412}