forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
1package repo 2 3import ( 4 "encoding/json" 5 "fmt" 6 "log" 7 "net/http" 8 "slices" 9 "sort" 10 "strings" 11 12 "tangled.sh/tangled.sh/core/appview/commitverify" 13 "tangled.sh/tangled.sh/core/appview/db" 14 "tangled.sh/tangled.sh/core/appview/oauth" 15 "tangled.sh/tangled.sh/core/appview/pages" 16 "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 17 "tangled.sh/tangled.sh/core/appview/reporesolver" 18 "tangled.sh/tangled.sh/core/knotclient" 19 "tangled.sh/tangled.sh/core/types" 20 21 "github.com/go-chi/chi/v5" 22 "github.com/go-enry/go-enry/v2" 23) 24 25func (rp *Repo) RepoIndex(w http.ResponseWriter, r *http.Request) { 26 ref := chi.URLParam(r, "ref") 27 f, err := rp.repoResolver.Resolve(r) 28 if err != nil { 29 log.Println("failed to fully resolve repo", err) 30 return 31 } 32 33 us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev) 34 if err != nil { 35 log.Printf("failed to create unsigned client for %s", f.Knot) 36 rp.pages.Error503(w) 37 return 38 } 39 40 result, err := us.Index(f.OwnerDid(), f.RepoName, ref) 41 if err != nil { 42 rp.pages.Error503(w) 43 log.Println("failed to reach knotserver", err) 44 return 45 } 46 47 tagMap := make(map[string][]string) 48 for _, tag := range result.Tags { 49 hash := tag.Hash 50 if tag.Tag != nil { 51 hash = tag.Tag.Target.String() 52 } 53 tagMap[hash] = append(tagMap[hash], tag.Name) 54 } 55 56 for _, branch := range result.Branches { 57 hash := branch.Hash 58 tagMap[hash] = append(tagMap[hash], branch.Name) 59 } 60 61 sortFiles(result.Files) 62 63 slices.SortFunc(result.Branches, func(a, b types.Branch) int { 64 if a.Name == result.Ref { 65 return -1 66 } 67 if a.IsDefault { 68 return -1 69 } 70 if b.IsDefault { 71 return 1 72 } 73 if a.Commit != nil && b.Commit != nil { 74 if a.Commit.Committer.When.Before(b.Commit.Committer.When) { 75 return 1 76 } else { 77 return -1 78 } 79 } 80 return strings.Compare(a.Name, b.Name) * -1 81 }) 82 83 commitCount := len(result.Commits) 84 branchCount := len(result.Branches) 85 tagCount := len(result.Tags) 86 fileCount := len(result.Files) 87 88 commitCount, branchCount, tagCount = balanceIndexItems(commitCount, branchCount, tagCount, fileCount) 89 commitsTrunc := result.Commits[:min(commitCount, len(result.Commits))] 90 tagsTrunc := result.Tags[:min(tagCount, len(result.Tags))] 91 branchesTrunc := result.Branches[:min(branchCount, len(result.Branches))] 92 93 emails := uniqueEmails(commitsTrunc) 94 emailToDidMap, err := db.GetEmailToDid(rp.db, emails, true) 95 if err != nil { 96 log.Println("failed to get email to did map", err) 97 } 98 99 vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, commitsTrunc) 100 if err != nil { 101 log.Println(err) 102 } 103 104 user := rp.oauth.GetUser(r) 105 repoInfo := f.RepoInfo(user) 106 107 secret, err := db.GetRegistrationKey(rp.db, f.Knot) 108 if err != nil { 109 log.Printf("failed to get registration key for %s: %s", f.Knot, err) 110 rp.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 111 } 112 113 signedClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev) 114 if err != nil { 115 log.Printf("failed to create signed client for %s: %s", f.Knot, err) 116 return 117 } 118 119 var forkInfo *types.ForkInfo 120 if user != nil && (repoInfo.Roles.IsOwner() || repoInfo.Roles.IsCollaborator()) { 121 forkInfo, err = getForkInfo(repoInfo, rp, f, user, signedClient) 122 if err != nil { 123 log.Printf("Failed to fetch fork information: %v", err) 124 return 125 } 126 } 127 128 // TODO: a bit dirty 129 languageInfo, err := rp.getLanguageInfo(f, signedClient, chi.URLParam(r, "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, 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 f *reporesolver.ResolvedRepo, 163 signedClient *knotclient.SignedClient, 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", f.Ref), 171 ) 172 173 if err != nil || langs == nil { 174 // non-fatal, fetch langs from ks 175 ls, err := signedClient.RepoLanguages(f.OwnerDid(), f.RepoName, f.Ref) 176 if err != nil { 177 return nil, err 178 } 179 if ls == nil { 180 return nil, nil 181 } 182 183 for l, s := range ls.Languages { 184 langs = append(langs, db.RepoLanguage{ 185 RepoAt: f.RepoAt, 186 Ref: f.Ref, 187 IsDefaultRef: isDefaultRef, 188 Language: l, 189 Bytes: s, 190 }) 191 } 192 193 // update appview's cache 194 err = db.InsertRepoLanguages(rp.db, langs) 195 if err != nil { 196 // non-fatal 197 log.Println("failed to cache lang results", err) 198 } 199 } 200 201 var total int64 202 for _, l := range langs { 203 total += l.Bytes 204 } 205 206 var languageStats []types.RepoLanguageDetails 207 for _, l := range langs { 208 percentage := float32(l.Bytes) / float32(total) * 100 209 color := enry.GetColor(l.Language) 210 languageStats = append(languageStats, types.RepoLanguageDetails{ 211 Name: l.Language, 212 Percentage: percentage, 213 Color: color, 214 }) 215 } 216 217 sort.Slice(languageStats, func(i, j int) bool { 218 if languageStats[i].Name == enry.OtherLanguage { 219 return false 220 } 221 if languageStats[j].Name == enry.OtherLanguage { 222 return true 223 } 224 if languageStats[i].Percentage != languageStats[j].Percentage { 225 return languageStats[i].Percentage > languageStats[j].Percentage 226 } 227 return languageStats[i].Name < languageStats[j].Name 228 }) 229 230 return languageStats, nil 231} 232 233func getForkInfo( 234 repoInfo repoinfo.RepoInfo, 235 rp *Repo, 236 f *reporesolver.ResolvedRepo, 237 user *oauth.User, 238 signedClient *knotclient.SignedClient, 239) (*types.ForkInfo, error) { 240 if user == nil { 241 return nil, nil 242 } 243 244 forkInfo := types.ForkInfo{ 245 IsFork: repoInfo.Source != nil, 246 Status: types.UpToDate, 247 } 248 249 if !forkInfo.IsFork { 250 forkInfo.IsFork = false 251 return &forkInfo, nil 252 } 253 254 us, err := knotclient.NewUnsignedClient(repoInfo.Source.Knot, rp.config.Core.Dev) 255 if err != nil { 256 log.Printf("failed to create unsigned client for %s", repoInfo.Source.Knot) 257 return nil, err 258 } 259 260 result, err := us.Branches(repoInfo.Source.Did, repoInfo.Source.Name) 261 if err != nil { 262 log.Println("failed to reach knotserver", err) 263 return nil, err 264 } 265 266 if !slices.ContainsFunc(result.Branches, func(branch types.Branch) bool { 267 return branch.Name == f.Ref 268 }) { 269 forkInfo.Status = types.MissingBranch 270 return &forkInfo, nil 271 } 272 273 newHiddenRefResp, err := signedClient.NewHiddenRef(user.Did, repoInfo.Name, f.Ref, f.Ref) 274 if err != nil || newHiddenRefResp.StatusCode != http.StatusNoContent { 275 log.Printf("failed to update tracking branch: %s", err) 276 return nil, err 277 } 278 279 hiddenRef := fmt.Sprintf("hidden/%s/%s", f.Ref, f.Ref) 280 281 var status types.AncestorCheckResponse 282 forkSyncableResp, err := signedClient.RepoForkAheadBehind(user.Did, string(f.RepoAt), repoInfo.Name, f.Ref, hiddenRef) 283 if err != nil { 284 log.Printf("failed to check if fork is ahead/behind: %s", err) 285 return nil, err 286 } 287 288 if err := json.NewDecoder(forkSyncableResp.Body).Decode(&status); err != nil { 289 log.Printf("failed to decode fork status: %s", err) 290 return nil, err 291 } 292 293 forkInfo.Status = status.Status 294 return &forkInfo, nil 295}