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