1package repo
2
3import (
4 "encoding/json"
5 "fmt"
6 "log"
7 "net/http"
8 "slices"
9 "strings"
10
11 "tangled.sh/tangled.sh/core/appview/commitverify"
12 "tangled.sh/tangled.sh/core/appview/db"
13 "tangled.sh/tangled.sh/core/appview/oauth"
14 "tangled.sh/tangled.sh/core/appview/pages"
15 "tangled.sh/tangled.sh/core/appview/pages/repoinfo"
16 "tangled.sh/tangled.sh/core/appview/reporesolver"
17 "tangled.sh/tangled.sh/core/knotclient"
18 "tangled.sh/tangled.sh/core/types"
19
20 "github.com/go-chi/chi/v5"
21)
22
23func (rp *Repo) RepoIndex(w http.ResponseWriter, r *http.Request) {
24 ref := chi.URLParam(r, "ref")
25 f, err := rp.repoResolver.Resolve(r)
26 if err != nil {
27 log.Println("failed to fully resolve repo", err)
28 return
29 }
30
31 us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
32 if err != nil {
33 log.Printf("failed to create unsigned client for %s", f.Knot)
34 rp.pages.Error503(w)
35 return
36 }
37
38 result, err := us.Index(f.OwnerDid(), f.RepoName, ref)
39 if err != nil {
40 rp.pages.Error503(w)
41 log.Println("failed to reach knotserver", err)
42 return
43 }
44
45 tagMap := make(map[string][]string)
46 for _, tag := range result.Tags {
47 hash := tag.Hash
48 if tag.Tag != nil {
49 hash = tag.Tag.Target.String()
50 }
51 tagMap[hash] = append(tagMap[hash], tag.Name)
52 }
53
54 for _, branch := range result.Branches {
55 hash := branch.Hash
56 tagMap[hash] = append(tagMap[hash], branch.Name)
57 }
58
59 slices.SortFunc(result.Branches, func(a, b types.Branch) int {
60 if a.Name == result.Ref {
61 return -1
62 }
63 if a.IsDefault {
64 return -1
65 }
66 if b.IsDefault {
67 return 1
68 }
69 if a.Commit != nil && b.Commit != nil {
70 if a.Commit.Committer.When.Before(b.Commit.Committer.When) {
71 return 1
72 } else {
73 return -1
74 }
75 }
76 return strings.Compare(a.Name, b.Name) * -1
77 })
78
79 commitCount := len(result.Commits)
80 branchCount := len(result.Branches)
81 tagCount := len(result.Tags)
82 fileCount := len(result.Files)
83
84 commitCount, branchCount, tagCount = balanceIndexItems(commitCount, branchCount, tagCount, fileCount)
85 commitsTrunc := result.Commits[:min(commitCount, len(result.Commits))]
86 tagsTrunc := result.Tags[:min(tagCount, len(result.Tags))]
87 branchesTrunc := result.Branches[:min(branchCount, len(result.Branches))]
88
89 emails := uniqueEmails(commitsTrunc)
90 emailToDidMap, err := db.GetEmailToDid(rp.db, emails, true)
91 if err != nil {
92 log.Println("failed to get email to did map", err)
93 }
94
95 vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, commitsTrunc)
96 if err != nil {
97 log.Println(err)
98 }
99
100 user := rp.oauth.GetUser(r)
101 repoInfo := f.RepoInfo(user)
102
103 secret, err := db.GetRegistrationKey(rp.db, f.Knot)
104 if err != nil {
105 log.Printf("failed to get registration key for %s: %s", f.Knot, err)
106 rp.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
107 }
108
109 signedClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev)
110 if err != nil {
111 log.Printf("failed to create signed client for %s: %s", f.Knot, err)
112 return
113 }
114
115 var forkInfo *types.ForkInfo
116 if user != nil && (repoInfo.Roles.IsOwner() || repoInfo.Roles.IsCollaborator()) {
117 forkInfo, err = getForkInfo(repoInfo, rp, f, user, signedClient)
118 if err != nil {
119 log.Printf("Failed to fetch fork information: %v", err)
120 return
121 }
122 }
123
124 repoLanguages, err := signedClient.RepoLanguages(f.OwnerDid(), f.RepoName, ref)
125 if err != nil {
126 log.Printf("failed to compute language percentages: %s", err)
127 // non-fatal
128 }
129
130 var shas []string
131 for _, c := range commitsTrunc {
132 shas = append(shas, c.Hash.String())
133 }
134 pipelines, err := rp.getPipelineStatuses(repoInfo, shas)
135 if err != nil {
136 log.Printf("failed to fetch pipeline statuses: %s", err)
137 // non-fatal
138 }
139
140 rp.pages.RepoIndexPage(w, pages.RepoIndexParams{
141 LoggedInUser: user,
142 RepoInfo: repoInfo,
143 TagMap: tagMap,
144 RepoIndexResponse: *result,
145 CommitsTrunc: commitsTrunc,
146 TagsTrunc: tagsTrunc,
147 ForkInfo: forkInfo,
148 BranchesTrunc: branchesTrunc,
149 EmailToDidOrHandle: emailToDidOrHandle(rp, emailToDidMap),
150 VerifiedCommits: vc,
151 Languages: repoLanguages,
152 Pipelines: pipelines,
153 })
154 return
155}
156
157func getForkInfo(
158 repoInfo repoinfo.RepoInfo,
159 rp *Repo,
160 f *reporesolver.ResolvedRepo,
161 user *oauth.User,
162 signedClient *knotclient.SignedClient,
163) (*types.ForkInfo, error) {
164 if user == nil {
165 return nil, nil
166 }
167
168 forkInfo := types.ForkInfo{
169 IsFork: repoInfo.Source != nil,
170 Status: types.UpToDate,
171 }
172
173 if !forkInfo.IsFork {
174 forkInfo.IsFork = false
175 return &forkInfo, nil
176 }
177
178 us, err := knotclient.NewUnsignedClient(repoInfo.Source.Knot, rp.config.Core.Dev)
179 if err != nil {
180 log.Printf("failed to create unsigned client for %s", repoInfo.Source.Knot)
181 return nil, err
182 }
183
184 result, err := us.Branches(repoInfo.Source.Did, repoInfo.Source.Name)
185 if err != nil {
186 log.Println("failed to reach knotserver", err)
187 return nil, err
188 }
189
190 if !slices.ContainsFunc(result.Branches, func(branch types.Branch) bool {
191 return branch.Name == f.Ref
192 }) {
193 forkInfo.Status = types.MissingBranch
194 return &forkInfo, nil
195 }
196
197 newHiddenRefResp, err := signedClient.NewHiddenRef(user.Did, repoInfo.Name, f.Ref, f.Ref)
198 if err != nil || newHiddenRefResp.StatusCode != http.StatusNoContent {
199 log.Printf("failed to update tracking branch: %s", err)
200 return nil, err
201 }
202
203 hiddenRef := fmt.Sprintf("hidden/%s/%s", f.Ref, f.Ref)
204
205 var status types.AncestorCheckResponse
206 forkSyncableResp, err := signedClient.RepoForkAheadBehind(user.Did, string(f.RepoAt), repoInfo.Name, f.Ref, hiddenRef)
207 if err != nil {
208 log.Printf("failed to check if fork is ahead/behind: %s", err)
209 return nil, err
210 }
211
212 if err := json.NewDecoder(forkSyncableResp.Body).Decode(&status); err != nil {
213 log.Printf("failed to decode fork status: %s", err)
214 return nil, err
215 }
216
217 forkInfo.Status = status.Status
218 return &forkInfo, nil
219}