forked from tangled.org/core
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 user := rp.oauth.GetUser(r) 51 repoInfo := f.RepoInfo(user) 52 53 // Build index response from multiple XRPC calls 54 result, err := rp.buildIndexResponse(r.Context(), xrpcc, f, ref) 55 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 56 if errors.Is(xrpcerr, xrpcclient.ErrXrpcUnsupported) { 57 log.Println("failed to call XRPC repo.index", err) 58 rp.pages.RepoIndexPage(w, pages.RepoIndexParams{ 59 LoggedInUser: user, 60 NeedsKnotUpgrade: true, 61 RepoInfo: repoInfo, 62 }) 63 return 64 } else { 65 rp.pages.Error503(w) 66 log.Println("failed to build index response", err) 67 return 68 } 69 } 70 71 tagMap := make(map[string][]string) 72 for _, tag := range result.Tags { 73 hash := tag.Hash 74 if tag.Tag != nil { 75 hash = tag.Tag.Target.String() 76 } 77 tagMap[hash] = append(tagMap[hash], tag.Name) 78 } 79 80 for _, branch := range result.Branches { 81 hash := branch.Hash 82 tagMap[hash] = append(tagMap[hash], branch.Name) 83 } 84 85 sortFiles(result.Files) 86 87 slices.SortFunc(result.Branches, func(a, b types.Branch) int { 88 if a.Name == result.Ref { 89 return -1 90 } 91 if a.IsDefault { 92 return -1 93 } 94 if b.IsDefault { 95 return 1 96 } 97 if a.Commit != nil && b.Commit != nil { 98 if a.Commit.Committer.When.Before(b.Commit.Committer.When) { 99 return 1 100 } else { 101 return -1 102 } 103 } 104 return strings.Compare(a.Name, b.Name) * -1 105 }) 106 107 commitCount := len(result.Commits) 108 branchCount := len(result.Branches) 109 tagCount := len(result.Tags) 110 fileCount := len(result.Files) 111 112 commitCount, branchCount, tagCount = balanceIndexItems(commitCount, branchCount, tagCount, fileCount) 113 commitsTrunc := result.Commits[:min(commitCount, len(result.Commits))] 114 tagsTrunc := result.Tags[:min(tagCount, len(result.Tags))] 115 branchesTrunc := result.Branches[:min(branchCount, len(result.Branches))] 116 117 emails := uniqueEmails(commitsTrunc) 118 emailToDidMap, err := db.GetEmailToDid(rp.db, emails, true) 119 if err != nil { 120 log.Println("failed to get email to did map", err) 121 } 122 123 vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, commitsTrunc) 124 if err != nil { 125 log.Println(err) 126 } 127 128 // TODO: a bit dirty 129 languageInfo, err := rp.getLanguageInfo(r.Context(), f, xrpcc, result.Ref, ref == "") 130 if err != nil { 131 log.Printf("failed to compute language percentages: %s", err) 132 // non-fatal 133 } 134 135 var shas []string 136 for _, c := range commitsTrunc { 137 shas = append(shas, c.Hash.String()) 138 } 139 pipelines, err := getPipelineStatuses(rp.db, repoInfo, shas) 140 if err != nil { 141 log.Printf("failed to fetch pipeline statuses: %s", err) 142 // non-fatal 143 } 144 145 rp.pages.RepoIndexPage(w, pages.RepoIndexParams{ 146 LoggedInUser: user, 147 RepoInfo: repoInfo, 148 TagMap: tagMap, 149 RepoIndexResponse: *result, 150 CommitsTrunc: commitsTrunc, 151 TagsTrunc: tagsTrunc, 152 // ForkInfo: forkInfo, // TODO: reinstate this after xrpc properly lands 153 BranchesTrunc: branchesTrunc, 154 EmailToDidOrHandle: emailToDidOrHandle(rp, emailToDidMap), 155 VerifiedCommits: vc, 156 Languages: languageInfo, 157 Pipelines: pipelines, 158 }) 159} 160 161func (rp *Repo) getLanguageInfo( 162 ctx context.Context, 163 f *reporesolver.ResolvedRepo, 164 xrpcc *indigoxrpc.Client, 165 currentRef string, 166 isDefaultRef bool, 167) ([]types.RepoLanguageDetails, error) { 168 // first attempt to fetch from db 169 langs, err := db.GetRepoLanguages( 170 rp.db, 171 db.FilterEq("repo_at", f.RepoAt()), 172 db.FilterEq("ref", currentRef), 173 ) 174 175 if err != nil || langs == nil { 176 // non-fatal, fetch langs from ks via XRPC 177 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 178 ls, err := tangled.RepoLanguages(ctx, xrpcc, currentRef, repo) 179 if err != nil { 180 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 181 log.Println("failed to call XRPC repo.languages", xrpcerr) 182 return nil, xrpcerr 183 } 184 return nil, err 185 } 186 187 if ls == nil || ls.Languages == nil { 188 return nil, nil 189 } 190 191 for _, lang := range ls.Languages { 192 langs = append(langs, db.RepoLanguage{ 193 RepoAt: f.RepoAt(), 194 Ref: currentRef, 195 IsDefaultRef: isDefaultRef, 196 Language: lang.Name, 197 Bytes: lang.Size, 198 }) 199 } 200 201 // update appview's cache 202 err = db.InsertRepoLanguages(rp.db, langs) 203 if err != nil { 204 // non-fatal 205 log.Println("failed to cache lang results", 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, xrpcc *indigoxrpc.Client, f *reporesolver.ResolvedRepo, ref string) (*types.RepoIndexResponse, error) { 243 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 244 245 // first get branches to determine the ref if not specified 246 branchesBytes, err := tangled.RepoBranches(ctx, xrpcc, "", 0, repo) 247 if err != nil { 248 return nil, err 249 } 250 251 var branchesResp types.RepoBranchesResponse 252 if err := json.Unmarshal(branchesBytes, &branchesResp); err != nil { 253 return nil, err 254 } 255 256 // if no ref specified, use default branch or first available 257 if ref == "" && len(branchesResp.Branches) > 0 { 258 for _, branch := range branchesResp.Branches { 259 if branch.IsDefault { 260 ref = branch.Name 261 break 262 } 263 } 264 if ref == "" { 265 ref = branchesResp.Branches[0].Name 266 } 267 } 268 269 // check if repo is empty 270 if len(branchesResp.Branches) == 0 { 271 return &types.RepoIndexResponse{ 272 IsEmpty: true, 273 Branches: branchesResp.Branches, 274 }, nil 275 } 276 277 // now run the remaining queries in parallel 278 var wg sync.WaitGroup 279 var errs error 280 281 var ( 282 tagsResp types.RepoTagsResponse 283 treeResp *tangled.RepoTree_Output 284 logResp types.RepoLogResponse 285 readmeContent string 286 readmeFileName string 287 ) 288 289 // tags 290 wg.Add(1) 291 go func() { 292 defer wg.Done() 293 tagsBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repo) 294 if err != nil { 295 errs = errors.Join(errs, err) 296 return 297 } 298 299 if err := json.Unmarshal(tagsBytes, &tagsResp); err != nil { 300 errs = errors.Join(errs, err) 301 } 302 }() 303 304 // tree/files 305 wg.Add(1) 306 go func() { 307 defer wg.Done() 308 resp, err := tangled.RepoTree(ctx, xrpcc, "", ref, repo) 309 if err != nil { 310 errs = errors.Join(errs, err) 311 return 312 } 313 treeResp = resp 314 }() 315 316 // commits 317 wg.Add(1) 318 go func() { 319 defer wg.Done() 320 logBytes, err := tangled.RepoLog(ctx, xrpcc, "", 50, "", ref, repo) 321 if err != nil { 322 errs = errors.Join(errs, err) 323 return 324 } 325 326 if err := json.Unmarshal(logBytes, &logResp); err != nil { 327 errs = errors.Join(errs, err) 328 } 329 }() 330 331 // readme content 332 wg.Add(1) 333 go func() { 334 defer wg.Done() 335 for _, filename := range markup.ReadmeFilenames { 336 blobResp, err := tangled.RepoBlob(ctx, xrpcc, filename, false, ref, repo) 337 if err != nil { 338 continue 339 } 340 341 if blobResp == nil { 342 continue 343 } 344 345 readmeContent = blobResp.Content 346 readmeFileName = filename 347 break 348 } 349 }() 350 351 wg.Wait() 352 353 if errs != nil { 354 return nil, errs 355 } 356 357 var files []types.NiceTree 358 if treeResp != nil && treeResp.Files != nil { 359 for _, file := range treeResp.Files { 360 niceFile := types.NiceTree{ 361 IsFile: file.Is_file, 362 IsSubtree: file.Is_subtree, 363 Name: file.Name, 364 Mode: file.Mode, 365 Size: file.Size, 366 } 367 if file.Last_commit != nil { 368 when, _ := time.Parse(time.RFC3339, file.Last_commit.When) 369 niceFile.LastCommit = &types.LastCommitInfo{ 370 Hash: plumbing.NewHash(file.Last_commit.Hash), 371 Message: file.Last_commit.Message, 372 When: when, 373 } 374 } 375 files = append(files, niceFile) 376 } 377 } 378 379 result := &types.RepoIndexResponse{ 380 IsEmpty: false, 381 Ref: ref, 382 Readme: readmeContent, 383 ReadmeFileName: readmeFileName, 384 Commits: logResp.Commits, 385 Description: logResp.Description, 386 Files: files, 387 Branches: branchesResp.Branches, 388 Tags: tagsResp.Tags, 389 TotalCommits: logResp.Total, 390 } 391 392 return result, nil 393}