+12
appview/pages/markup/format.go
+12
appview/pages/markup/format.go
···
13
FormatMarkdown: []string{".md", ".markdown", ".mdown", ".mkdn", ".mkd"},
14
}
15
16
+
// ReadmeFilenames contains the list of common README filenames to search for,
17
+
// in order of preference. Only includes well-supported formats.
18
+
var ReadmeFilenames = []string{
19
+
"README.md", "readme.md",
20
+
"README",
21
+
"readme",
22
+
"README.markdown",
23
+
"readme.markdown",
24
+
"README.txt",
25
+
"readme.txt",
26
+
}
27
+
28
func GetFormat(filename string) Format {
29
for format, extensions := range FileTypes {
30
for _, extension := range extensions {
+6
-1
appview/pages/pages.go
+6
-1
appview/pages/pages.go
···
763
ShowRendered bool
764
RenderToggle bool
765
RenderedContents template.HTML
766
+
*tangled.RepoBlob_Output
767
+
// Computed fields for template compatibility
768
+
Contents string
769
+
Lines int
770
+
SizeHint uint64
771
+
IsBinary bool
772
}
773
774
func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error {
+6
appview/pages/templates/repo/fragments/diff.html
+6
appview/pages/templates/repo/fragments/diff.html
···
11
{{ $last := sub (len $diff) 1 }}
12
13
<div class="flex flex-col gap-4">
14
{{ range $idx, $hunk := $diff }}
15
{{ with $hunk }}
16
<details open id="file-{{ .Name.New }}" class="group border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm" tabindex="{{ add $idx 1 }}">
···
49
</div>
50
</details>
51
{{ end }}
52
{{ end }}
53
</div>
54
{{ end }}
···
11
{{ $last := sub (len $diff) 1 }}
12
13
<div class="flex flex-col gap-4">
14
+
{{ if eq (len $diff) 0 }}
15
+
<div class="text-center text-gray-500 dark:text-gray-400 py-8">
16
+
<p>No differences found between the selected revisions.</p>
17
+
</div>
18
+
{{ else }}
19
{{ range $idx, $hunk := $diff }}
20
{{ with $hunk }}
21
<details open id="file-{{ .Name.New }}" class="group border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm" tabindex="{{ add $idx 1 }}">
···
54
</div>
55
</details>
56
{{ end }}
57
+
{{ end }}
58
{{ end }}
59
</div>
60
{{ end }}
+26
-8
appview/repo/artifact.go
+26
-8
appview/repo/artifact.go
···
1
package repo
2
3
import (
4
"fmt"
5
"log"
6
"net/http"
···
9
10
comatproto "github.com/bluesky-social/indigo/api/atproto"
11
lexutil "github.com/bluesky-social/indigo/lex/util"
12
"github.com/dustin/go-humanize"
13
"github.com/go-chi/chi/v5"
14
"github.com/go-git/go-git/v5/plumbing"
···
17
"tangled.sh/tangled.sh/core/appview/db"
18
"tangled.sh/tangled.sh/core/appview/pages"
19
"tangled.sh/tangled.sh/core/appview/reporesolver"
20
-
"tangled.sh/tangled.sh/core/knotclient"
21
"tangled.sh/tangled.sh/core/tid"
22
"tangled.sh/tangled.sh/core/types"
23
)
···
33
return
34
}
35
36
-
tag, err := rp.resolveTag(f, tagParam)
37
if err != nil {
38
log.Println("failed to resolve tag", err)
39
rp.pages.Notice(w, "upload", "failed to upload artifact, error in tag resolution")
···
140
return
141
}
142
143
-
tag, err := rp.resolveTag(f, tagParam)
144
if err != nil {
145
log.Println("failed to resolve tag", err)
146
rp.pages.Notice(w, "upload", "failed to upload artifact, error in tag resolution")
···
259
w.Write([]byte{})
260
}
261
262
-
func (rp *Repo) resolveTag(f *reporesolver.ResolvedRepo, tagParam string) (*types.TagReference, error) {
263
tagParam, err := url.QueryUnescape(tagParam)
264
if err != nil {
265
return nil, err
266
}
267
268
-
us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
269
-
if err != nil {
270
-
return nil, err
271
}
272
273
-
result, err := us.Tags(f.OwnerDid(), f.Name)
274
if err != nil {
275
log.Println("failed to reach knotserver", err)
276
return nil, err
277
}
278
···
1
package repo
2
3
import (
4
+
"context"
5
+
"encoding/json"
6
"fmt"
7
"log"
8
"net/http"
···
11
12
comatproto "github.com/bluesky-social/indigo/api/atproto"
13
lexutil "github.com/bluesky-social/indigo/lex/util"
14
+
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
15
"github.com/dustin/go-humanize"
16
"github.com/go-chi/chi/v5"
17
"github.com/go-git/go-git/v5/plumbing"
···
20
"tangled.sh/tangled.sh/core/appview/db"
21
"tangled.sh/tangled.sh/core/appview/pages"
22
"tangled.sh/tangled.sh/core/appview/reporesolver"
23
+
"tangled.sh/tangled.sh/core/appview/xrpcclient"
24
"tangled.sh/tangled.sh/core/tid"
25
"tangled.sh/tangled.sh/core/types"
26
)
···
36
return
37
}
38
39
+
tag, err := rp.resolveTag(r.Context(), f, tagParam)
40
if err != nil {
41
log.Println("failed to resolve tag", err)
42
rp.pages.Notice(w, "upload", "failed to upload artifact, error in tag resolution")
···
143
return
144
}
145
146
+
tag, err := rp.resolveTag(r.Context(), f, tagParam)
147
if err != nil {
148
log.Println("failed to resolve tag", err)
149
rp.pages.Notice(w, "upload", "failed to upload artifact, error in tag resolution")
···
262
w.Write([]byte{})
263
}
264
265
+
func (rp *Repo) resolveTag(ctx context.Context, f *reporesolver.ResolvedRepo, tagParam string) (*types.TagReference, error) {
266
tagParam, err := url.QueryUnescape(tagParam)
267
if err != nil {
268
return nil, err
269
}
270
271
+
scheme := "http"
272
+
if !rp.config.Core.Dev {
273
+
scheme = "https"
274
+
}
275
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
276
+
xrpcc := &indigoxrpc.Client{
277
+
Host: host,
278
}
279
280
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
281
+
xrpcBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repo)
282
if err != nil {
283
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
284
+
log.Println("failed to call XRPC repo.tags", xrpcerr)
285
+
return nil, xrpcerr
286
+
}
287
log.Println("failed to reach knotserver", err)
288
+
return nil, err
289
+
}
290
+
291
+
var result types.RepoTagsResponse
292
+
if err := json.Unmarshal(xrpcBytes, &result); err != nil {
293
+
log.Println("failed to decode XRPC tags response", err)
294
return nil, err
295
}
296
+220
-16
appview/repo/index.go
+220
-16
appview/repo/index.go
···
1
package repo
2
3
import (
4
"log"
5
"net/http"
6
"slices"
7
"sort"
8
"strings"
9
10
"tangled.sh/tangled.sh/core/appview/commitverify"
11
"tangled.sh/tangled.sh/core/appview/db"
12
"tangled.sh/tangled.sh/core/appview/pages"
13
"tangled.sh/tangled.sh/core/appview/reporesolver"
14
-
"tangled.sh/tangled.sh/core/knotclient"
15
"tangled.sh/tangled.sh/core/types"
16
17
"github.com/go-chi/chi/v5"
···
27
return
28
}
29
30
-
us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
31
-
if err != nil {
32
-
log.Printf("failed to create unsigned client for %s", f.Knot)
33
-
rp.pages.Error503(w)
34
-
return
35
}
36
37
-
result, err := us.Index(f.OwnerDid(), f.Name, ref)
38
if err != nil {
39
rp.pages.Error503(w)
40
-
log.Println("failed to reach knotserver", err)
41
return
42
}
43
···
102
repoInfo := f.RepoInfo(user)
103
104
// TODO: a bit dirty
105
-
languageInfo, err := rp.getLanguageInfo(f, us, result.Ref, ref == "")
106
if err != nil {
107
log.Printf("failed to compute language percentages: %s", err)
108
// non-fatal
···
135
}
136
137
func (rp *Repo) getLanguageInfo(
138
f *reporesolver.ResolvedRepo,
139
-
us *knotclient.UnsignedClient,
140
currentRef string,
141
isDefaultRef bool,
142
) ([]types.RepoLanguageDetails, error) {
···
148
)
149
150
if err != nil || langs == nil {
151
-
// non-fatal, fetch langs from ks
152
-
ls, err := us.RepoLanguages(f.OwnerDid(), f.Name, currentRef)
153
if err != nil {
154
return nil, err
155
}
156
-
if ls == nil {
157
return nil, nil
158
}
159
160
-
for l, s := range ls.Languages {
161
langs = append(langs, db.RepoLanguage{
162
RepoAt: f.RepoAt(),
163
Ref: currentRef,
164
IsDefaultRef: isDefaultRef,
165
-
Language: l,
166
-
Bytes: s,
167
})
168
}
169
···
206
207
return languageStats, nil
208
}
···
1
package repo
2
3
import (
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"
···
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
···
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
···
148
}
149
150
func (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) {
···
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
···
226
227
return languageStats, nil
228
}
229
+
230
+
// buildIndexResponse creates a RepoIndexResponse by combining multiple xrpc calls in parallel
231
+
func (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
+
}
+358
-153
appview/repo/repo.go
+358
-153
appview/repo/repo.go
···
11
"log/slog"
12
"net/http"
13
"net/url"
14
"path/filepath"
15
"slices"
16
"strconv"
···
19
20
comatproto "github.com/bluesky-social/indigo/api/atproto"
21
lexutil "github.com/bluesky-social/indigo/lex/util"
22
"tangled.sh/tangled.sh/core/api/tangled"
23
"tangled.sh/tangled.sh/core/appview/commitverify"
24
"tangled.sh/tangled.sh/core/appview/config"
···
31
xrpcclient "tangled.sh/tangled.sh/core/appview/xrpcclient"
32
"tangled.sh/tangled.sh/core/eventconsumer"
33
"tangled.sh/tangled.sh/core/idresolver"
34
-
"tangled.sh/tangled.sh/core/knotclient"
35
"tangled.sh/tangled.sh/core/patchutil"
36
"tangled.sh/tangled.sh/core/rbac"
37
"tangled.sh/tangled.sh/core/tid"
···
92
return
93
}
94
95
-
var uri string
96
-
if rp.config.Core.Dev {
97
-
uri = "http"
98
-
} else {
99
-
uri = "https"
100
}
101
-
url := fmt.Sprintf("%s://%s/%s/%s/archive/%s.tar.gz", uri, f.Knot, f.OwnerDid(), f.Name, url.PathEscape(refParam))
102
103
-
http.Redirect(w, r, url, http.StatusFound)
104
}
105
106
func (rp *Repo) RepoLog(w http.ResponseWriter, r *http.Request) {
···
120
121
ref := chi.URLParam(r, "ref")
122
123
-
us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
124
if err != nil {
125
-
log.Println("failed to create unsigned client", err)
126
return
127
}
128
129
-
repolog, err := us.Log(f.OwnerDid(), f.Name, ref, page)
130
-
if err != nil {
131
rp.pages.Error503(w)
132
-
log.Println("failed to reach knotserver", err)
133
return
134
}
135
136
-
tagResult, err := us.Tags(f.OwnerDid(), f.Name)
137
if err != nil {
138
-
rp.pages.Error503(w)
139
-
log.Println("failed to reach knotserver", err)
140
-
return
141
}
142
143
tagMap := make(map[string][]string)
144
-
for _, tag := range tagResult.Tags {
145
-
hash := tag.Hash
146
-
if tag.Tag != nil {
147
-
hash = tag.Tag.Target.String()
148
}
149
-
tagMap[hash] = append(tagMap[hash], tag.Name)
150
}
151
152
-
branchResult, err := us.Branches(f.OwnerDid(), f.Name)
153
if err != nil {
154
-
rp.pages.Error503(w)
155
-
log.Println("failed to reach knotserver", err)
156
-
return
157
}
158
159
-
for _, branch := range branchResult.Branches {
160
-
hash := branch.Hash
161
-
tagMap[hash] = append(tagMap[hash], branch.Name)
162
}
163
164
user := rp.oauth.GetUser(r)
165
166
-
emailToDidMap, err := db.GetEmailToDid(rp.db, uniqueEmails(repolog.Commits), true)
167
if err != nil {
168
log.Println("failed to fetch email to did mapping", err)
169
}
170
171
-
vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, repolog.Commits)
172
if err != nil {
173
log.Println(err)
174
}
···
176
repoInfo := f.RepoInfo(user)
177
178
var shas []string
179
-
for _, c := range repolog.Commits {
180
shas = append(shas, c.Hash.String())
181
}
182
pipelines, err := getPipelineStatuses(rp.db, repoInfo, shas)
···
189
LoggedInUser: user,
190
TagMap: tagMap,
191
RepoInfo: repoInfo,
192
-
RepoLogResponse: *repolog,
193
EmailToDidOrHandle: emailToDidOrHandle(rp, emailToDidMap),
194
VerifiedCommits: vc,
195
Pipelines: pipelines,
···
301
return
302
}
303
ref := chi.URLParam(r, "ref")
304
-
protocol := "http"
305
-
if !rp.config.Core.Dev {
306
-
protocol = "https"
307
-
}
308
309
var diffOpts types.DiffOpts
310
if d := r.URL.Query().Get("diff"); d == "split" {
···
316
return
317
}
318
319
-
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref))
320
-
if err != nil {
321
-
rp.pages.Error503(w)
322
-
log.Println("failed to reach knotserver", err)
323
-
return
324
}
325
326
-
body, err := io.ReadAll(resp.Body)
327
if err != nil {
328
-
log.Printf("Error reading response body: %v", err)
329
return
330
}
331
332
var result types.RepoCommitResponse
333
-
err = json.Unmarshal(body, &result)
334
-
if err != nil {
335
-
log.Println("failed to parse response:", err)
336
return
337
}
338
···
378
379
ref := chi.URLParam(r, "ref")
380
treePath := chi.URLParam(r, "*")
381
-
protocol := "http"
382
-
if !rp.config.Core.Dev {
383
-
protocol = "https"
384
-
}
385
386
// if the tree path has a trailing slash, let's strip it
387
// so we don't 404
388
treePath = strings.TrimSuffix(treePath, "/")
389
390
-
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, treePath))
391
-
if err != nil {
392
-
rp.pages.Error503(w)
393
-
log.Println("failed to reach knotserver", err)
394
-
return
395
}
396
397
-
// uhhh so knotserver returns a 500 if the entry isn't found in
398
-
// the requested tree path, so let's stick to not-OK here.
399
-
// we can fix this once we build out the xrpc apis for these operations.
400
-
if resp.StatusCode != http.StatusOK {
401
rp.pages.Error404(w)
402
return
403
}
404
405
-
body, err := io.ReadAll(resp.Body)
406
-
if err != nil {
407
-
log.Printf("Error reading response body: %v", err)
408
-
return
409
}
410
411
-
var result types.RepoTreeResponse
412
-
err = json.Unmarshal(body, &result)
413
-
if err != nil {
414
-
log.Println("failed to parse response:", err)
415
-
return
416
}
417
418
// redirects tree paths trying to access a blob; in this case the result.Files is unpopulated,
···
451
return
452
}
453
454
-
us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
455
if err != nil {
456
-
log.Println("failed to create unsigned client", err)
457
return
458
}
459
460
-
result, err := us.Tags(f.OwnerDid(), f.Name)
461
-
if err != nil {
462
rp.pages.Error503(w)
463
-
log.Println("failed to reach knotserver", err)
464
return
465
}
466
···
496
rp.pages.RepoTags(w, pages.RepoTagsParams{
497
LoggedInUser: user,
498
RepoInfo: f.RepoInfo(user),
499
-
RepoTagsResponse: *result,
500
ArtifactMap: artifactMap,
501
DanglingArtifacts: danglingArtifacts,
502
})
···
509
return
510
}
511
512
-
us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
513
if err != nil {
514
-
log.Println("failed to create unsigned client", err)
515
return
516
}
517
518
-
result, err := us.Branches(f.OwnerDid(), f.Name)
519
-
if err != nil {
520
rp.pages.Error503(w)
521
-
log.Println("failed to reach knotserver", err)
522
return
523
}
524
···
528
rp.pages.RepoBranches(w, pages.RepoBranchesParams{
529
LoggedInUser: user,
530
RepoInfo: f.RepoInfo(user),
531
-
RepoBranchesResponse: *result,
532
})
533
}
534
···
541
542
ref := chi.URLParam(r, "ref")
543
filePath := chi.URLParam(r, "*")
544
-
protocol := "http"
545
if !rp.config.Core.Dev {
546
-
protocol = "https"
547
-
}
548
-
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, filePath))
549
-
if err != nil {
550
-
rp.pages.Error503(w)
551
-
log.Println("failed to reach knotserver", err)
552
-
return
553
}
554
-
555
-
if resp.StatusCode == http.StatusNotFound {
556
-
rp.pages.Error404(w)
557
-
return
558
}
559
560
-
body, err := io.ReadAll(resp.Body)
561
if err != nil {
562
-
log.Printf("Error reading response body: %v", err)
563
return
564
}
565
566
-
var result types.RepoBlobResponse
567
-
err = json.Unmarshal(body, &result)
568
-
if err != nil {
569
-
log.Println("failed to parse response:", err)
570
-
return
571
-
}
572
573
var breadcrumbs [][]string
574
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)})
···
581
showRendered := false
582
renderToggle := false
583
584
-
if markup.GetFormat(result.Path) == markup.FormatMarkdown {
585
renderToggle = true
586
showRendered = r.URL.Query().Get("code") != "true"
587
}
···
591
var isVideo bool
592
var contentSrc string
593
594
-
if result.IsBinary {
595
-
ext := strings.ToLower(filepath.Ext(result.Path))
596
switch ext {
597
case ".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp":
598
isImage = true
···
602
unsupported = true
603
}
604
605
-
// fetch the actual binary content like in RepoBlobRaw
606
607
-
blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Name, ref, filePath)
608
contentSrc = blobURL
609
if !rp.config.Core.Dev {
610
contentSrc = markup.GenerateCamoURL(rp.config.Camo.Host, rp.config.Camo.SharedSecret, blobURL)
611
}
612
}
613
614
user := rp.oauth.GetUser(r)
615
rp.pages.RepoBlob(w, pages.RepoBlobParams{
616
-
LoggedInUser: user,
617
-
RepoInfo: f.RepoInfo(user),
618
-
RepoBlobResponse: result,
619
-
BreadCrumbs: breadcrumbs,
620
-
ShowRendered: showRendered,
621
-
RenderToggle: renderToggle,
622
-
Unsupported: unsupported,
623
-
IsImage: isImage,
624
-
IsVideo: isVideo,
625
-
ContentSrc: contentSrc,
626
})
627
}
628
···
637
ref := chi.URLParam(r, "ref")
638
filePath := chi.URLParam(r, "*")
639
640
-
protocol := "http"
641
if !rp.config.Core.Dev {
642
-
protocol = "https"
643
}
644
645
-
blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, filePath)
646
647
req, err := http.NewRequest("GET", blobURL, nil)
648
if err != nil {
···
685
return
686
}
687
688
-
// Safely serve content based on type
689
if strings.HasPrefix(contentType, "text/") || isTextualMimeType(contentType) {
690
-
// Serve all textual content as text/plain for security
691
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
692
w.Write(body)
693
} else if strings.HasPrefix(contentType, "image/") || strings.HasPrefix(contentType, "video/") {
694
-
// Serve images and videos with their original content type
695
w.Header().Set("Content-Type", contentType)
696
w.Write(body)
697
} else {
698
-
// Block potentially dangerous content types
699
w.WriteHeader(http.StatusUnsupportedMediaType)
700
w.Write([]byte("unsupported content type"))
701
return
···
716
"message/",
717
}
718
719
-
for _, t := range textualTypes {
720
-
if mimeType == t {
721
-
return true
722
-
}
723
-
}
724
-
return false
725
}
726
727
// modify the spindle configured for this repo
···
1227
f, err := rp.repoResolver.Resolve(r)
1228
user := rp.oauth.GetUser(r)
1229
1230
-
us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
1231
if err != nil {
1232
-
log.Println("failed to create unsigned client", err)
1233
return
1234
}
1235
1236
-
result, err := us.Branches(f.OwnerDid(), f.Name)
1237
-
if err != nil {
1238
rp.pages.Error503(w)
1239
-
log.Println("failed to reach knotserver", err)
1240
return
1241
}
1242
···
1607
return
1608
}
1609
1610
-
us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
1611
if err != nil {
1612
-
log.Printf("failed to create unsigned client for %s", f.Knot)
1613
-
rp.pages.Error503(w)
1614
return
1615
}
1616
1617
-
result, err := us.Branches(f.OwnerDid(), f.Name)
1618
-
if err != nil {
1619
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
1620
-
log.Println("failed to reach knotserver", err)
1621
return
1622
}
1623
-
branches := result.Branches
1624
1625
sortBranches(branches)
1626
···
1644
head = queryHead
1645
}
1646
1647
-
tags, err := us.Tags(f.OwnerDid(), f.Name)
1648
if err != nil {
1649
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
1650
-
log.Println("failed to reach knotserver", err)
1651
return
1652
}
1653
···
1699
return
1700
}
1701
1702
-
us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
1703
if err != nil {
1704
-
log.Printf("failed to create unsigned client for %s", f.Knot)
1705
-
rp.pages.Error503(w)
1706
return
1707
}
1708
1709
-
branches, err := us.Branches(f.OwnerDid(), f.Name)
1710
if err != nil {
1711
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
1712
-
log.Println("failed to reach knotserver", err)
1713
return
1714
}
1715
1716
-
tags, err := us.Tags(f.OwnerDid(), f.Name)
1717
if err != nil {
1718
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
1719
-
log.Println("failed to reach knotserver", err)
1720
return
1721
}
1722
1723
-
formatPatch, err := us.Compare(f.OwnerDid(), f.Name, base, head)
1724
-
if err != nil {
1725
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
1726
-
log.Println("failed to compare", err)
1727
return
1728
}
1729
diff := patchutil.AsNiceDiff(formatPatch.Patch, base)
1730
1731
repoinfo := f.RepoInfo(user)
···
11
"log/slog"
12
"net/http"
13
"net/url"
14
+
"path"
15
"path/filepath"
16
"slices"
17
"strconv"
···
20
21
comatproto "github.com/bluesky-social/indigo/api/atproto"
22
lexutil "github.com/bluesky-social/indigo/lex/util"
23
+
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
24
"tangled.sh/tangled.sh/core/api/tangled"
25
"tangled.sh/tangled.sh/core/appview/commitverify"
26
"tangled.sh/tangled.sh/core/appview/config"
···
33
xrpcclient "tangled.sh/tangled.sh/core/appview/xrpcclient"
34
"tangled.sh/tangled.sh/core/eventconsumer"
35
"tangled.sh/tangled.sh/core/idresolver"
36
"tangled.sh/tangled.sh/core/patchutil"
37
"tangled.sh/tangled.sh/core/rbac"
38
"tangled.sh/tangled.sh/core/tid"
···
93
return
94
}
95
96
+
scheme := "http"
97
+
if !rp.config.Core.Dev {
98
+
scheme = "https"
99
+
}
100
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
101
+
xrpcc := &indigoxrpc.Client{
102
+
Host: host,
103
+
}
104
+
105
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
106
+
archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", refParam, repo)
107
+
if err != nil {
108
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
109
+
log.Println("failed to call XRPC repo.archive", xrpcerr)
110
+
rp.pages.Error503(w)
111
+
return
112
+
}
113
+
rp.pages.Error404(w)
114
+
return
115
}
116
117
+
// Set headers for file download
118
+
filename := fmt.Sprintf("%s-%s.tar.gz", f.Name, refParam)
119
+
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
120
+
w.Header().Set("Content-Type", "application/gzip")
121
+
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(archiveBytes)))
122
+
123
+
// Write the archive data directly
124
+
w.Write(archiveBytes)
125
}
126
127
func (rp *Repo) RepoLog(w http.ResponseWriter, r *http.Request) {
···
141
142
ref := chi.URLParam(r, "ref")
143
144
+
scheme := "http"
145
+
if !rp.config.Core.Dev {
146
+
scheme = "https"
147
+
}
148
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
149
+
xrpcc := &indigoxrpc.Client{
150
+
Host: host,
151
+
}
152
+
153
+
limit := int64(60)
154
+
cursor := ""
155
+
if page > 1 {
156
+
// Convert page number to cursor (offset)
157
+
offset := (page - 1) * int(limit)
158
+
cursor = strconv.Itoa(offset)
159
+
}
160
+
161
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
162
+
xrpcBytes, err := tangled.RepoLog(r.Context(), xrpcc, cursor, limit, "", ref, repo)
163
if err != nil {
164
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
165
+
log.Println("failed to call XRPC repo.log", xrpcerr)
166
+
rp.pages.Error503(w)
167
+
return
168
+
}
169
+
rp.pages.Error404(w)
170
return
171
}
172
173
+
var xrpcResp types.RepoLogResponse
174
+
if err := json.Unmarshal(xrpcBytes, &xrpcResp); err != nil {
175
+
log.Println("failed to decode XRPC response", err)
176
rp.pages.Error503(w)
177
return
178
}
179
180
+
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
181
if err != nil {
182
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
183
+
log.Println("failed to call XRPC repo.tags", xrpcerr)
184
+
rp.pages.Error503(w)
185
+
return
186
+
}
187
}
188
189
tagMap := make(map[string][]string)
190
+
if tagBytes != nil {
191
+
var tagResp types.RepoTagsResponse
192
+
if err := json.Unmarshal(tagBytes, &tagResp); err == nil {
193
+
for _, tag := range tagResp.Tags {
194
+
tagMap[tag.Hash] = append(tagMap[tag.Hash], tag.Name)
195
+
}
196
}
197
}
198
199
+
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
200
if err != nil {
201
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
202
+
log.Println("failed to call XRPC repo.branches", xrpcerr)
203
+
rp.pages.Error503(w)
204
+
return
205
+
}
206
}
207
208
+
if branchBytes != nil {
209
+
var branchResp types.RepoBranchesResponse
210
+
if err := json.Unmarshal(branchBytes, &branchResp); err == nil {
211
+
for _, branch := range branchResp.Branches {
212
+
tagMap[branch.Hash] = append(tagMap[branch.Hash], branch.Name)
213
+
}
214
+
}
215
}
216
217
user := rp.oauth.GetUser(r)
218
219
+
emailToDidMap, err := db.GetEmailToDid(rp.db, uniqueEmails(xrpcResp.Commits), true)
220
if err != nil {
221
log.Println("failed to fetch email to did mapping", err)
222
}
223
224
+
vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, xrpcResp.Commits)
225
if err != nil {
226
log.Println(err)
227
}
···
229
repoInfo := f.RepoInfo(user)
230
231
var shas []string
232
+
for _, c := range xrpcResp.Commits {
233
shas = append(shas, c.Hash.String())
234
}
235
pipelines, err := getPipelineStatuses(rp.db, repoInfo, shas)
···
242
LoggedInUser: user,
243
TagMap: tagMap,
244
RepoInfo: repoInfo,
245
+
RepoLogResponse: xrpcResp,
246
EmailToDidOrHandle: emailToDidOrHandle(rp, emailToDidMap),
247
VerifiedCommits: vc,
248
Pipelines: pipelines,
···
354
return
355
}
356
ref := chi.URLParam(r, "ref")
357
358
var diffOpts types.DiffOpts
359
if d := r.URL.Query().Get("diff"); d == "split" {
···
365
return
366
}
367
368
+
scheme := "http"
369
+
if !rp.config.Core.Dev {
370
+
scheme = "https"
371
+
}
372
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
373
+
xrpcc := &indigoxrpc.Client{
374
+
Host: host,
375
}
376
377
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
378
+
xrpcBytes, err := tangled.RepoDiff(r.Context(), xrpcc, ref, repo)
379
if err != nil {
380
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
381
+
log.Println("failed to call XRPC repo.diff", xrpcerr)
382
+
rp.pages.Error503(w)
383
+
return
384
+
}
385
+
rp.pages.Error404(w)
386
return
387
}
388
389
var result types.RepoCommitResponse
390
+
if err := json.Unmarshal(xrpcBytes, &result); err != nil {
391
+
log.Println("failed to decode XRPC response", err)
392
+
rp.pages.Error503(w)
393
return
394
}
395
···
435
436
ref := chi.URLParam(r, "ref")
437
treePath := chi.URLParam(r, "*")
438
439
// if the tree path has a trailing slash, let's strip it
440
// so we don't 404
441
treePath = strings.TrimSuffix(treePath, "/")
442
443
+
scheme := "http"
444
+
if !rp.config.Core.Dev {
445
+
scheme = "https"
446
+
}
447
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
448
+
xrpcc := &indigoxrpc.Client{
449
+
Host: host,
450
}
451
452
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
453
+
xrpcResp, err := tangled.RepoTree(r.Context(), xrpcc, treePath, ref, repo)
454
+
if err != nil {
455
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
456
+
log.Println("failed to call XRPC repo.tree", xrpcerr)
457
+
rp.pages.Error503(w)
458
+
return
459
+
}
460
rp.pages.Error404(w)
461
return
462
}
463
464
+
// Convert XRPC response to internal types.RepoTreeResponse
465
+
files := make([]types.NiceTree, len(xrpcResp.Files))
466
+
for i, xrpcFile := range xrpcResp.Files {
467
+
file := types.NiceTree{
468
+
Name: xrpcFile.Name,
469
+
Mode: xrpcFile.Mode,
470
+
Size: int64(xrpcFile.Size),
471
+
IsFile: xrpcFile.Is_file,
472
+
IsSubtree: xrpcFile.Is_subtree,
473
+
}
474
+
475
+
// Convert last commit info if present
476
+
if xrpcFile.Last_commit != nil {
477
+
commitWhen, _ := time.Parse(time.RFC3339, xrpcFile.Last_commit.When)
478
+
file.LastCommit = &types.LastCommitInfo{
479
+
Hash: plumbing.NewHash(xrpcFile.Last_commit.Hash),
480
+
Message: xrpcFile.Last_commit.Message,
481
+
When: commitWhen,
482
+
}
483
+
}
484
+
485
+
files[i] = file
486
}
487
488
+
result := types.RepoTreeResponse{
489
+
Ref: xrpcResp.Ref,
490
+
Files: files,
491
+
}
492
+
493
+
if xrpcResp.Parent != nil {
494
+
result.Parent = *xrpcResp.Parent
495
+
}
496
+
if xrpcResp.Dotdot != nil {
497
+
result.DotDot = *xrpcResp.Dotdot
498
}
499
500
// redirects tree paths trying to access a blob; in this case the result.Files is unpopulated,
···
533
return
534
}
535
536
+
scheme := "http"
537
+
if !rp.config.Core.Dev {
538
+
scheme = "https"
539
+
}
540
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
541
+
xrpcc := &indigoxrpc.Client{
542
+
Host: host,
543
+
}
544
+
545
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
546
+
xrpcBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
547
if err != nil {
548
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
549
+
log.Println("failed to call XRPC repo.tags", xrpcerr)
550
+
rp.pages.Error503(w)
551
+
return
552
+
}
553
+
rp.pages.Error404(w)
554
return
555
}
556
557
+
var result types.RepoTagsResponse
558
+
if err := json.Unmarshal(xrpcBytes, &result); err != nil {
559
+
log.Println("failed to decode XRPC response", err)
560
rp.pages.Error503(w)
561
return
562
}
563
···
593
rp.pages.RepoTags(w, pages.RepoTagsParams{
594
LoggedInUser: user,
595
RepoInfo: f.RepoInfo(user),
596
+
RepoTagsResponse: result,
597
ArtifactMap: artifactMap,
598
DanglingArtifacts: danglingArtifacts,
599
})
···
606
return
607
}
608
609
+
scheme := "http"
610
+
if !rp.config.Core.Dev {
611
+
scheme = "https"
612
+
}
613
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
614
+
xrpcc := &indigoxrpc.Client{
615
+
Host: host,
616
+
}
617
+
618
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
619
+
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
620
if err != nil {
621
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
622
+
log.Println("failed to call XRPC repo.branches", xrpcerr)
623
+
rp.pages.Error503(w)
624
+
return
625
+
}
626
+
rp.pages.Error404(w)
627
return
628
}
629
630
+
var result types.RepoBranchesResponse
631
+
if err := json.Unmarshal(xrpcBytes, &result); err != nil {
632
+
log.Println("failed to decode XRPC response", err)
633
rp.pages.Error503(w)
634
return
635
}
636
···
640
rp.pages.RepoBranches(w, pages.RepoBranchesParams{
641
LoggedInUser: user,
642
RepoInfo: f.RepoInfo(user),
643
+
RepoBranchesResponse: result,
644
})
645
}
646
···
653
654
ref := chi.URLParam(r, "ref")
655
filePath := chi.URLParam(r, "*")
656
+
657
+
scheme := "http"
658
if !rp.config.Core.Dev {
659
+
scheme = "https"
660
}
661
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
662
+
xrpcc := &indigoxrpc.Client{
663
+
Host: host,
664
}
665
666
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name)
667
+
resp, err := tangled.RepoBlob(r.Context(), xrpcc, filePath, false, ref, repo)
668
if err != nil {
669
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
670
+
log.Println("failed to call XRPC repo.blob", xrpcerr)
671
+
rp.pages.Error503(w)
672
+
return
673
+
}
674
+
rp.pages.Error404(w)
675
return
676
}
677
678
+
// Use XRPC response directly instead of converting to internal types
679
680
var breadcrumbs [][]string
681
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)})
···
688
showRendered := false
689
renderToggle := false
690
691
+
if markup.GetFormat(resp.Path) == markup.FormatMarkdown {
692
renderToggle = true
693
showRendered = r.URL.Query().Get("code") != "true"
694
}
···
698
var isVideo bool
699
var contentSrc string
700
701
+
if resp.IsBinary != nil && *resp.IsBinary {
702
+
ext := strings.ToLower(filepath.Ext(resp.Path))
703
switch ext {
704
case ".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp":
705
isImage = true
···
709
unsupported = true
710
}
711
712
+
// fetch the raw binary content using sh.tangled.repo.blob xrpc
713
+
repoName := path.Join("%s/%s", f.OwnerDid(), f.Name)
714
+
blobURL := fmt.Sprintf("%s://%s/xrpc/sh.tangled.repo.blob?repo=%s&ref=%s&path=%s&raw=true",
715
+
scheme, f.Knot, url.QueryEscape(repoName), url.QueryEscape(ref), url.QueryEscape(filePath))
716
717
contentSrc = blobURL
718
if !rp.config.Core.Dev {
719
contentSrc = markup.GenerateCamoURL(rp.config.Camo.Host, rp.config.Camo.SharedSecret, blobURL)
720
}
721
}
722
723
+
lines := 0
724
+
if resp.IsBinary == nil || !*resp.IsBinary {
725
+
lines = strings.Count(resp.Content, "\n") + 1
726
+
}
727
+
728
+
var sizeHint uint64
729
+
if resp.Size != nil {
730
+
sizeHint = uint64(*resp.Size)
731
+
} else {
732
+
sizeHint = uint64(len(resp.Content))
733
+
}
734
+
735
user := rp.oauth.GetUser(r)
736
+
737
+
// Determine if content is binary (dereference pointer)
738
+
isBinary := false
739
+
if resp.IsBinary != nil {
740
+
isBinary = *resp.IsBinary
741
+
}
742
+
743
rp.pages.RepoBlob(w, pages.RepoBlobParams{
744
+
LoggedInUser: user,
745
+
RepoInfo: f.RepoInfo(user),
746
+
BreadCrumbs: breadcrumbs,
747
+
ShowRendered: showRendered,
748
+
RenderToggle: renderToggle,
749
+
Unsupported: unsupported,
750
+
IsImage: isImage,
751
+
IsVideo: isVideo,
752
+
ContentSrc: contentSrc,
753
+
RepoBlob_Output: resp,
754
+
Contents: resp.Content,
755
+
Lines: lines,
756
+
SizeHint: sizeHint,
757
+
IsBinary: isBinary,
758
})
759
}
760
···
769
ref := chi.URLParam(r, "ref")
770
filePath := chi.URLParam(r, "*")
771
772
+
scheme := "http"
773
if !rp.config.Core.Dev {
774
+
scheme = "https"
775
}
776
777
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name)
778
+
blobURL := fmt.Sprintf("%s://%s/xrpc/sh.tangled.repo.blob?repo=%s&ref=%s&path=%s&raw=true",
779
+
scheme, f.Knot, url.QueryEscape(repo), url.QueryEscape(ref), url.QueryEscape(filePath))
780
781
req, err := http.NewRequest("GET", blobURL, nil)
782
if err != nil {
···
819
return
820
}
821
822
if strings.HasPrefix(contentType, "text/") || isTextualMimeType(contentType) {
823
+
// serve all textual content as text/plain
824
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
825
w.Write(body)
826
} else if strings.HasPrefix(contentType, "image/") || strings.HasPrefix(contentType, "video/") {
827
+
// serve images and videos with their original content type
828
w.Header().Set("Content-Type", contentType)
829
w.Write(body)
830
} else {
831
w.WriteHeader(http.StatusUnsupportedMediaType)
832
w.Write([]byte("unsupported content type"))
833
return
···
848
"message/",
849
}
850
851
+
return slices.Contains(textualTypes, mimeType)
852
}
853
854
// modify the spindle configured for this repo
···
1354
f, err := rp.repoResolver.Resolve(r)
1355
user := rp.oauth.GetUser(r)
1356
1357
+
scheme := "http"
1358
+
if !rp.config.Core.Dev {
1359
+
scheme = "https"
1360
+
}
1361
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
1362
+
xrpcc := &indigoxrpc.Client{
1363
+
Host: host,
1364
+
}
1365
+
1366
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
1367
+
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
1368
if err != nil {
1369
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
1370
+
log.Println("failed to call XRPC repo.branches", xrpcerr)
1371
+
rp.pages.Error503(w)
1372
+
return
1373
+
}
1374
+
rp.pages.Error503(w)
1375
return
1376
}
1377
1378
+
var result types.RepoBranchesResponse
1379
+
if err := json.Unmarshal(xrpcBytes, &result); err != nil {
1380
+
log.Println("failed to decode XRPC response", err)
1381
rp.pages.Error503(w)
1382
return
1383
}
1384
···
1749
return
1750
}
1751
1752
+
scheme := "http"
1753
+
if !rp.config.Core.Dev {
1754
+
scheme = "https"
1755
+
}
1756
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
1757
+
xrpcc := &indigoxrpc.Client{
1758
+
Host: host,
1759
+
}
1760
+
1761
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
1762
+
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
1763
if err != nil {
1764
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
1765
+
log.Println("failed to call XRPC repo.branches", xrpcerr)
1766
+
rp.pages.Error503(w)
1767
+
return
1768
+
}
1769
+
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
1770
return
1771
}
1772
1773
+
var branchResult types.RepoBranchesResponse
1774
+
if err := json.Unmarshal(branchBytes, &branchResult); err != nil {
1775
+
log.Println("failed to decode XRPC branches response", err)
1776
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
1777
return
1778
}
1779
+
branches := branchResult.Branches
1780
1781
sortBranches(branches)
1782
···
1800
head = queryHead
1801
}
1802
1803
+
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
1804
if err != nil {
1805
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
1806
+
log.Println("failed to call XRPC repo.tags", xrpcerr)
1807
+
rp.pages.Error503(w)
1808
+
return
1809
+
}
1810
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
1811
+
return
1812
+
}
1813
+
1814
+
var tags types.RepoTagsResponse
1815
+
if err := json.Unmarshal(tagBytes, &tags); err != nil {
1816
+
log.Println("failed to decode XRPC tags response", err)
1817
+
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
1818
return
1819
}
1820
···
1866
return
1867
}
1868
1869
+
scheme := "http"
1870
+
if !rp.config.Core.Dev {
1871
+
scheme = "https"
1872
+
}
1873
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
1874
+
xrpcc := &indigoxrpc.Client{
1875
+
Host: host,
1876
+
}
1877
+
1878
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
1879
+
1880
+
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
1881
if err != nil {
1882
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
1883
+
log.Println("failed to call XRPC repo.branches", xrpcerr)
1884
+
rp.pages.Error503(w)
1885
+
return
1886
+
}
1887
+
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
1888
+
return
1889
+
}
1890
+
1891
+
var branches types.RepoBranchesResponse
1892
+
if err := json.Unmarshal(branchBytes, &branches); err != nil {
1893
+
log.Println("failed to decode XRPC branches response", err)
1894
+
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
1895
return
1896
}
1897
1898
+
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
1899
if err != nil {
1900
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
1901
+
log.Println("failed to call XRPC repo.tags", xrpcerr)
1902
+
rp.pages.Error503(w)
1903
+
return
1904
+
}
1905
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
1906
+
return
1907
+
}
1908
+
1909
+
var tags types.RepoTagsResponse
1910
+
if err := json.Unmarshal(tagBytes, &tags); err != nil {
1911
+
log.Println("failed to decode XRPC tags response", err)
1912
+
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
1913
return
1914
}
1915
1916
+
compareBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo, base, head)
1917
if err != nil {
1918
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
1919
+
log.Println("failed to call XRPC repo.compare", xrpcerr)
1920
+
rp.pages.Error503(w)
1921
+
return
1922
+
}
1923
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
1924
return
1925
}
1926
1927
+
var formatPatch types.RepoFormatPatchResponse
1928
+
if err := json.Unmarshal(compareBytes, &formatPatch); err != nil {
1929
+
log.Println("failed to decode XRPC compare response", err)
1930
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
1931
return
1932
}
1933
+
1934
diff := patchutil.AsNiceDiff(formatPatch.Patch, base)
1935
1936
repoinfo := f.RepoInfo(user)
+1
-1
appview/xrpcclient/xrpc.go
+1
-1
appview/xrpcclient/xrpc.go