+8
-4
appview/db/issues.go
+8
-4
appview/db/issues.go
···
359
359
repoMap[string(repos[i].RepoAt())] = &repos[i]
360
360
}
361
361
362
-
for issueAt := range issueMap {
363
-
i := issueMap[issueAt]
364
-
r := repoMap[string(i.RepoAt)]
365
-
i.Repo = r
362
+
for issueAt, i := range issueMap {
363
+
if r, ok := repoMap[string(i.RepoAt)]; ok {
364
+
i.Repo = r
365
+
} else {
366
+
// do not show up the issue if the repo is deleted
367
+
// TODO: foreign key where?
368
+
delete(issueMap, issueAt)
369
+
}
366
370
}
367
371
368
372
// collect comments
+2
-2
appview/db/profile.go
+2
-2
appview/db/profile.go
···
553
553
query = `select count(id) from pulls where owner_did = ? and state = ?`
554
554
args = append(args, did, PullOpen)
555
555
case VanityStatOpenIssueCount:
556
-
query = `select count(id) from issues where owner_did = ? and open = 1`
556
+
query = `select count(id) from issues where did = ? and open = 1`
557
557
args = append(args, did)
558
558
case VanityStatClosedIssueCount:
559
-
query = `select count(id) from issues where owner_did = ? and open = 0`
559
+
query = `select count(id) from issues where did = ? and open = 0`
560
560
args = append(args, did)
561
561
case VanityStatRepositoryCount:
562
562
query = `select count(id) from repos where did = ?`
+1
-1
appview/pages/markup/markdown.go
+1
-1
appview/pages/markup/markdown.go
···
235
235
repoName := fmt.Sprintf("%s/%s", rctx.RepoInfo.OwnerDid, rctx.RepoInfo.Name)
236
236
237
237
query := fmt.Sprintf("repo=%s&ref=%s&path=%s&raw=true",
238
-
repoName, url.PathEscape(rctx.RepoInfo.Ref), actualPath)
238
+
url.PathEscape(repoName), url.PathEscape(rctx.RepoInfo.Ref), actualPath)
239
239
240
240
parsedURL := &url.URL{
241
241
Scheme: scheme,
+90
appview/pages/templates/fragments/multiline-select.html
+90
appview/pages/templates/fragments/multiline-select.html
···
1
+
{{ define "fragments/multiline-select" }}
2
+
<script>
3
+
function highlight(scroll = false) {
4
+
document.querySelectorAll(".hl").forEach(el => {
5
+
el.classList.remove("hl");
6
+
});
7
+
8
+
const hash = window.location.hash;
9
+
if (!hash || !hash.startsWith("#L")) {
10
+
return;
11
+
}
12
+
13
+
const rangeStr = hash.substring(2);
14
+
const parts = rangeStr.split("-");
15
+
let startLine, endLine;
16
+
17
+
if (parts.length === 2) {
18
+
startLine = parseInt(parts[0], 10);
19
+
endLine = parseInt(parts[1], 10);
20
+
} else {
21
+
startLine = parseInt(parts[0], 10);
22
+
endLine = startLine;
23
+
}
24
+
25
+
if (isNaN(startLine) || isNaN(endLine)) {
26
+
console.log("nan");
27
+
console.log(startLine);
28
+
console.log(endLine);
29
+
return;
30
+
}
31
+
32
+
let target = null;
33
+
34
+
for (let i = startLine; i<= endLine; i++) {
35
+
const idEl = document.getElementById(`L${i}`);
36
+
if (idEl) {
37
+
const el = idEl.closest(".line");
38
+
if (el) {
39
+
el.classList.add("hl");
40
+
target = el;
41
+
}
42
+
}
43
+
}
44
+
45
+
if (scroll && target) {
46
+
target.scrollIntoView({
47
+
behavior: "smooth",
48
+
block: "center",
49
+
});
50
+
}
51
+
}
52
+
53
+
document.addEventListener("DOMContentLoaded", () => {
54
+
console.log("DOMContentLoaded");
55
+
highlight(true);
56
+
});
57
+
window.addEventListener("hashchange", () => {
58
+
console.log("hashchange");
59
+
highlight();
60
+
});
61
+
window.addEventListener("popstate", () => {
62
+
console.log("popstate");
63
+
highlight();
64
+
});
65
+
66
+
const lineNumbers = document.querySelectorAll('a[href^="#L"');
67
+
let startLine = null;
68
+
69
+
lineNumbers.forEach(el => {
70
+
el.addEventListener("click", (event) => {
71
+
event.preventDefault();
72
+
const currentLine = parseInt(el.href.split("#L")[1]);
73
+
74
+
if (event.shiftKey && startLine !== null) {
75
+
const endLine = currentLine;
76
+
const min = Math.min(startLine, endLine);
77
+
const max = Math.max(startLine, endLine);
78
+
const newHash = `#L${min}-${max}`;
79
+
history.pushState(null, '', newHash);
80
+
} else {
81
+
const newHash = `#L${currentLine}`;
82
+
history.pushState(null, '', newHash);
83
+
startLine = currentLine;
84
+
}
85
+
86
+
highlight();
87
+
});
88
+
});
89
+
</script>
90
+
{{ end }}
+1
appview/pages/templates/repo/blob.html
+1
appview/pages/templates/repo/blob.html
+3
-3
appview/pages/templates/repo/tree.html
+3
-3
appview/pages/templates/repo/tree.html
···
25
25
<div class="flex flex-col md:flex-row md:justify-between gap-2">
26
26
<div id="breadcrumbs" class="overflow-x-auto whitespace-nowrap text-gray-400 dark:text-gray-500">
27
27
{{ range .BreadCrumbs }}
28
-
<a href="{{ index . 1}}" class="text-bold text-gray-500 dark:text-gray-400 {{ $linkstyle }}">{{ pathUnescape (index . 0) }}</a> /
28
+
<a href="{{ index . 1 }}" class="text-bold text-gray-500 dark:text-gray-400 {{ $linkstyle }}">{{ pathUnescape (index . 0) }}</a> /
29
29
{{ end }}
30
30
</div>
31
31
<div id="dir-info" class="text-gray-500 dark:text-gray-400 text-xs md:text-sm flex flex-wrap items-center gap-1 md:gap-0">
32
32
{{ $stats := .TreeStats }}
33
33
34
-
<span>at <a href="/{{ $.RepoInfo.FullName }}/tree/{{ $.Ref }}">{{ $.Ref }}</a></span>
34
+
<span>at <a href="/{{ $.RepoInfo.FullName }}/tree/{{ pathEscape $.Ref }}">{{ $.Ref }}</a></span>
35
35
{{ if eq $stats.NumFolders 1 }}
36
36
<span class="select-none px-1 md:px-2 [&:before]:content-['ยท']"></span>
37
37
<span>{{ $stats.NumFolders }} folder</span>
···
55
55
{{ range .Files }}
56
56
<div class="grid grid-cols-12 gap-4 items-center py-1">
57
57
<div class="col-span-8 md:col-span-4">
58
-
{{ $link := printf "/%s/%s/%s/%s/%s" $.RepoInfo.FullName "tree" (urlquery $.Ref) $.TreePath .Name }}
58
+
{{ $link := printf "/%s/%s/%s/%s/%s" $.RepoInfo.FullName "tree" (pathEscape $.Ref) $.TreePath .Name }}
59
59
{{ $icon := "folder" }}
60
60
{{ $iconStyle := "size-4 fill-current" }}
61
61
+3
-2
appview/pages/templates/strings/string.html
+3
-2
appview/pages/templates/strings/string.html
···
23
23
hx-boost="true"
24
24
href="/strings/{{ .String.Did }}/{{ .String.Rkey }}/edit">
25
25
{{ i "pencil" "size-4" }}
26
-
<span class="hidden md:inline">edit</span>
26
+
<span class="hidden md:inline">edit</span>
27
27
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
28
28
</a>
29
29
<button
···
34
34
hx-confirm="Are you sure you want to delete the string `{{ .String.Filename }}`?"
35
35
>
36
36
{{ i "trash-2" "size-4" }}
37
-
<span class="hidden md:inline">delete</span>
37
+
<span class="hidden md:inline">delete</span>
38
38
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
39
39
</button>
40
40
</div>
···
80
80
<div id="blob-contents" class="whitespace-pre peer-target:bg-yellow-200 dark:peer-target:bg-yellow-900">{{ .String.Contents | escapeHtml }}</div>
81
81
{{ end }}
82
82
</div>
83
+
{{ template "fragments/multiline-select" }}
83
84
</section>
84
85
{{ end }}
+16
-17
appview/repo/index.go
+16
-17
appview/repo/index.go
···
5
5
"fmt"
6
6
"log"
7
7
"net/http"
8
+
"net/url"
8
9
"slices"
9
10
"sort"
10
11
"strings"
···
31
32
32
33
func (rp *Repo) RepoIndex(w http.ResponseWriter, r *http.Request) {
33
34
ref := chi.URLParam(r, "ref")
35
+
ref, _ = url.PathUnescape(ref)
34
36
35
37
f, err := rp.repoResolver.Resolve(r)
36
38
if err != nil {
···
61
63
RepoInfo: repoInfo,
62
64
})
63
65
return
64
-
} else {
65
-
rp.pages.Error503(w)
66
-
log.Println("failed to build index response", err)
67
-
return
68
66
}
67
+
68
+
rp.pages.Error503(w)
69
+
log.Println("failed to build index response", err)
70
+
return
69
71
}
70
72
71
73
tagMap := make(map[string][]string)
···
245
247
// first get branches to determine the ref if not specified
246
248
branchesBytes, err := tangled.RepoBranches(ctx, xrpcc, "", 0, repo)
247
249
if err != nil {
248
-
return nil, err
250
+
return nil, fmt.Errorf("failed to call repoBranches: %w", err)
249
251
}
250
252
251
253
var branchesResp types.RepoBranchesResponse
252
254
if err := json.Unmarshal(branchesBytes, &branchesResp); err != nil {
253
-
return nil, err
255
+
return nil, fmt.Errorf("failed to unmarshal branches response: %w", err)
254
256
}
255
257
256
258
// if no ref specified, use default branch or first available
257
-
if ref == "" && len(branchesResp.Branches) > 0 {
259
+
if ref == "" {
258
260
for _, branch := range branchesResp.Branches {
259
261
if branch.IsDefault {
260
262
ref = branch.Name
261
263
break
262
264
}
263
265
}
264
-
if ref == "" {
265
-
ref = branchesResp.Branches[0].Name
266
-
}
267
266
}
268
267
269
-
// check if repo is empty
270
-
if len(branchesResp.Branches) == 0 {
268
+
// if ref is still empty, this means the default branch is not set
269
+
if ref == "" {
271
270
return &types.RepoIndexResponse{
272
271
IsEmpty: true,
273
272
Branches: branchesResp.Branches,
···
292
291
defer wg.Done()
293
292
tagsBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repo)
294
293
if err != nil {
295
-
errs = errors.Join(errs, err)
294
+
errs = errors.Join(errs, fmt.Errorf("failed to call repoTags: %w", err))
296
295
return
297
296
}
298
297
299
298
if err := json.Unmarshal(tagsBytes, &tagsResp); err != nil {
300
-
errs = errors.Join(errs, err)
299
+
errs = errors.Join(errs, fmt.Errorf("failed to unmarshal repoTags: %w", err))
301
300
}
302
301
}()
303
302
···
307
306
defer wg.Done()
308
307
resp, err := tangled.RepoTree(ctx, xrpcc, "", ref, repo)
309
308
if err != nil {
310
-
errs = errors.Join(errs, err)
309
+
errs = errors.Join(errs, fmt.Errorf("failed to call repoTree: %w", err))
311
310
return
312
311
}
313
312
treeResp = resp
···
319
318
defer wg.Done()
320
319
logBytes, err := tangled.RepoLog(ctx, xrpcc, "", 50, "", ref, repo)
321
320
if err != nil {
322
-
errs = errors.Join(errs, err)
321
+
errs = errors.Join(errs, fmt.Errorf("failed to call repoLog: %w", err))
323
322
return
324
323
}
325
324
326
325
if err := json.Unmarshal(logBytes, &logResp); err != nil {
327
-
errs = errors.Join(errs, err)
326
+
errs = errors.Join(errs, fmt.Errorf("failed to unmarshal repoLog: %w", err))
328
327
}
329
328
}()
330
329
+98
-120
appview/repo/repo.go
+98
-120
appview/repo/repo.go
···
11
11
"log/slog"
12
12
"net/http"
13
13
"net/url"
14
-
"path"
15
14
"path/filepath"
16
15
"slices"
17
16
"strconv"
···
86
85
}
87
86
88
87
func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) {
89
-
refParam := chi.URLParam(r, "ref")
88
+
ref := chi.URLParam(r, "ref")
89
+
ref, _ = url.PathUnescape(ref)
90
+
90
91
f, err := rp.repoResolver.Resolve(r)
91
92
if err != nil {
92
93
log.Println("failed to get repo and knot", err)
···
103
104
}
104
105
105
106
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)
107
+
archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, repo)
108
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
109
+
log.Println("failed to call XRPC repo.archive", xrpcerr)
110
+
rp.pages.Error503(w)
114
111
return
115
112
}
116
113
117
-
// Set headers for file download
118
-
filename := fmt.Sprintf("%s-%s.tar.gz", f.Name, refParam)
114
+
// Set headers for file download, just pass along whatever the knot specifies
115
+
safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-")
116
+
filename := fmt.Sprintf("%s-%s.tar.gz", f.Name, safeRefFilename)
119
117
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
120
118
w.Header().Set("Content-Type", "application/gzip")
121
119
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(archiveBytes)))
···
140
138
}
141
139
142
140
ref := chi.URLParam(r, "ref")
141
+
ref, _ = url.PathUnescape(ref)
143
142
144
143
scheme := "http"
145
144
if !rp.config.Core.Dev {
···
160
159
161
160
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
162
161
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)
162
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
163
+
log.Println("failed to call XRPC repo.log", xrpcerr)
164
+
rp.pages.Error503(w)
170
165
return
171
166
}
172
167
···
178
173
}
179
174
180
175
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
-
}
176
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
177
+
log.Println("failed to call XRPC repo.tags", xrpcerr)
178
+
rp.pages.Error503(w)
179
+
return
187
180
}
188
181
189
182
tagMap := make(map[string][]string)
···
197
190
}
198
191
199
192
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
-
}
193
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
194
+
log.Println("failed to call XRPC repo.branches", xrpcerr)
195
+
rp.pages.Error503(w)
196
+
return
206
197
}
207
198
208
199
if branchBytes != nil {
···
354
345
return
355
346
}
356
347
ref := chi.URLParam(r, "ref")
348
+
ref, _ = url.PathUnescape(ref)
357
349
358
350
var diffOpts types.DiffOpts
359
351
if d := r.URL.Query().Get("diff"); d == "split" {
···
376
368
377
369
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
378
370
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)
371
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
372
+
log.Println("failed to call XRPC repo.diff", xrpcerr)
373
+
rp.pages.Error503(w)
386
374
return
387
375
}
388
376
···
434
422
}
435
423
436
424
ref := chi.URLParam(r, "ref")
437
-
treePath := chi.URLParam(r, "*")
425
+
ref, _ = url.PathUnescape(ref)
438
426
439
427
// if the tree path has a trailing slash, let's strip it
440
428
// so we don't 404
429
+
treePath := chi.URLParam(r, "*")
430
+
treePath, _ = url.PathUnescape(treePath)
441
431
treePath = strings.TrimSuffix(treePath, "/")
442
432
443
433
scheme := "http"
···
451
441
452
442
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
453
443
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)
444
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
445
+
log.Println("failed to call XRPC repo.tree", xrpcerr)
446
+
rp.pages.Error503(w)
461
447
return
462
448
}
463
449
···
499
485
500
486
// redirects tree paths trying to access a blob; in this case the result.Files is unpopulated,
501
487
// so we can safely redirect to the "parent" (which is the same file).
502
-
unescapedTreePath, _ := url.PathUnescape(treePath)
503
-
if len(result.Files) == 0 && result.Parent == unescapedTreePath {
504
-
http.Redirect(w, r, fmt.Sprintf("/%s/blob/%s/%s", f.OwnerSlashRepo(), ref, result.Parent), http.StatusFound)
488
+
if len(result.Files) == 0 && result.Parent == treePath {
489
+
redirectTo := fmt.Sprintf("/%s/blob/%s/%s", f.OwnerSlashRepo(), url.PathEscape(ref), result.Parent)
490
+
http.Redirect(w, r, redirectTo, http.StatusFound)
505
491
return
506
492
}
507
493
508
494
user := rp.oauth.GetUser(r)
509
495
510
496
var breadcrumbs [][]string
511
-
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)})
497
+
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), url.PathEscape(ref))})
512
498
if treePath != "" {
513
499
for idx, elem := range strings.Split(treePath, "/") {
514
-
breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)})
500
+
breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))})
515
501
}
516
502
}
517
503
···
544
530
545
531
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
546
532
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)
533
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
534
+
log.Println("failed to call XRPC repo.tags", xrpcerr)
535
+
rp.pages.Error503(w)
554
536
return
555
537
}
556
538
···
617
599
618
600
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
619
601
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)
602
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
603
+
log.Println("failed to call XRPC repo.branches", xrpcerr)
604
+
rp.pages.Error503(w)
627
605
return
628
606
}
629
607
···
652
630
}
653
631
654
632
ref := chi.URLParam(r, "ref")
633
+
ref, _ = url.PathUnescape(ref)
634
+
655
635
filePath := chi.URLParam(r, "*")
636
+
filePath, _ = url.PathUnescape(filePath)
656
637
657
638
scheme := "http"
658
639
if !rp.config.Core.Dev {
···
665
646
666
647
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name)
667
648
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)
649
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
650
+
log.Println("failed to call XRPC repo.blob", xrpcerr)
651
+
rp.pages.Error503(w)
675
652
return
676
653
}
677
654
678
655
// Use XRPC response directly instead of converting to internal types
679
656
680
657
var breadcrumbs [][]string
681
-
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)})
658
+
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), url.PathEscape(ref))})
682
659
if filePath != "" {
683
660
for idx, elem := range strings.Split(filePath, "/") {
684
-
breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)})
661
+
breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))})
685
662
}
686
663
}
687
664
···
710
687
}
711
688
712
689
// 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))
690
+
repoName := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
691
+
692
+
baseURL := &url.URL{
693
+
Scheme: scheme,
694
+
Host: f.Knot,
695
+
Path: "/xrpc/sh.tangled.repo.blob",
696
+
}
697
+
query := baseURL.Query()
698
+
query.Set("repo", repoName)
699
+
query.Set("ref", ref)
700
+
query.Set("path", filePath)
701
+
query.Set("raw", "true")
702
+
baseURL.RawQuery = query.Encode()
703
+
blobURL := baseURL.String()
716
704
717
705
contentSrc = blobURL
718
706
if !rp.config.Core.Dev {
···
767
755
}
768
756
769
757
ref := chi.URLParam(r, "ref")
758
+
ref, _ = url.PathUnescape(ref)
759
+
770
760
filePath := chi.URLParam(r, "*")
761
+
filePath, _ = url.PathUnescape(filePath)
771
762
772
763
scheme := "http"
773
764
if !rp.config.Core.Dev {
···
775
766
}
776
767
777
768
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))
769
+
baseURL := &url.URL{
770
+
Scheme: scheme,
771
+
Host: f.Knot,
772
+
Path: "/xrpc/sh.tangled.repo.blob",
773
+
}
774
+
query := baseURL.Query()
775
+
query.Set("repo", repo)
776
+
query.Set("ref", ref)
777
+
query.Set("path", filePath)
778
+
query.Set("raw", "true")
779
+
baseURL.RawQuery = query.Encode()
780
+
blobURL := baseURL.String()
780
781
781
782
req, err := http.NewRequest("GET", blobURL, nil)
782
783
if err != nil {
···
1365
1366
1366
1367
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
1367
1368
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
-
}
1369
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
1370
+
log.Println("failed to call XRPC repo.branches", xrpcerr)
1374
1371
rp.pages.Error503(w)
1375
1372
return
1376
1373
}
···
1472
1469
1473
1470
func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) {
1474
1471
ref := chi.URLParam(r, "ref")
1472
+
ref, _ = url.PathUnescape(ref)
1475
1473
1476
1474
user := rp.oauth.GetUser(r)
1477
1475
f, err := rp.repoResolver.Resolve(r)
···
1760
1758
1761
1759
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
1762
1760
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.")
1761
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
1762
+
log.Println("failed to call XRPC repo.branches", xrpcerr)
1763
+
rp.pages.Error503(w)
1770
1764
return
1771
1765
}
1772
1766
···
1801
1795
}
1802
1796
1803
1797
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.")
1798
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
1799
+
log.Println("failed to call XRPC repo.tags", xrpcerr)
1800
+
rp.pages.Error503(w)
1811
1801
return
1812
1802
}
1813
1803
···
1878
1868
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
1879
1869
1880
1870
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.")
1871
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
1872
+
log.Println("failed to call XRPC repo.branches", xrpcerr)
1873
+
rp.pages.Error503(w)
1888
1874
return
1889
1875
}
1890
1876
···
1896
1882
}
1897
1883
1898
1884
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.")
1885
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
1886
+
log.Println("failed to call XRPC repo.tags", xrpcerr)
1887
+
rp.pages.Error503(w)
1906
1888
return
1907
1889
}
1908
1890
···
1914
1896
}
1915
1897
1916
1898
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.")
1899
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
1900
+
log.Println("failed to call XRPC repo.compare", xrpcerr)
1901
+
rp.pages.Error503(w)
1924
1902
return
1925
1903
}
1926
1904
-1
appview/state/profile.go
-1
appview/state/profile.go
+16
default.nix
+16
default.nix
···
1
+
# Default setup from https://git.lix.systems/lix-project/flake-compat
2
+
let
3
+
lockFile = builtins.fromJSON (builtins.readFile ./flake.lock);
4
+
flake-compat-node = lockFile.nodes.${lockFile.nodes.root.inputs.flake-compat};
5
+
flake-compat = builtins.fetchTarball {
6
+
inherit (flake-compat-node.locked) url;
7
+
sha256 = flake-compat-node.locked.narHash;
8
+
};
9
+
10
+
flake = (
11
+
import flake-compat {
12
+
src = ./.;
13
+
}
14
+
);
15
+
in
16
+
flake.defaultNix
+15
flake.lock
+15
flake.lock
···
1
1
{
2
2
"nodes": {
3
+
"flake-compat": {
4
+
"flake": false,
5
+
"locked": {
6
+
"lastModified": 1751685974,
7
+
"narHash": "sha256-NKw96t+BgHIYzHUjkTK95FqYRVKB8DHpVhefWSz/kTw=",
8
+
"rev": "549f2762aebeff29a2e5ece7a7dc0f955281a1d1",
9
+
"type": "tarball",
10
+
"url": "https://git.lix.systems/api/v1/repos/lix-project/flake-compat/archive/549f2762aebeff29a2e5ece7a7dc0f955281a1d1.tar.gz?rev=549f2762aebeff29a2e5ece7a7dc0f955281a1d1"
11
+
},
12
+
"original": {
13
+
"type": "tarball",
14
+
"url": "https://git.lix.systems/lix-project/flake-compat/archive/main.tar.gz"
15
+
}
16
+
},
3
17
"flake-utils": {
4
18
"inputs": {
5
19
"systems": "systems"
···
136
150
},
137
151
"root": {
138
152
"inputs": {
153
+
"flake-compat": "flake-compat",
139
154
"gomod2nix": "gomod2nix",
140
155
"htmx-src": "htmx-src",
141
156
"htmx-ws-src": "htmx-ws-src",
+5
flake.nix
+5
flake.nix
···
7
7
url = "github:nix-community/gomod2nix";
8
8
inputs.nixpkgs.follows = "nixpkgs";
9
9
};
10
+
flake-compat = {
11
+
url = "https://git.lix.systems/lix-project/flake-compat/archive/main.tar.gz";
12
+
flake = false;
13
+
};
10
14
indigo = {
11
15
url = "github:oppiliappan/indigo";
12
16
flake = false;
···
50
54
inter-fonts-src,
51
55
sqlite-lib-src,
52
56
ibm-plex-mono-src,
57
+
...
53
58
}: let
54
59
supportedSystems = ["x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin"];
55
60
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
+2
-5
input.css
+2
-5
input.css
···
228
228
}
229
229
/* LineHighlight */
230
230
.chroma .hl {
231
-
background-color: #bcc0cc;
231
+
@apply bg-amber-400/30 dark:bg-amber-500/20;
232
232
}
233
+
233
234
/* LineNumbersTable */
234
235
.chroma .lnt {
235
236
white-space: pre;
···
864
865
text-decoration: underline;
865
866
}
866
867
}
867
-
868
-
.chroma .line:has(.ln:target) {
869
-
@apply bg-amber-400/30 dark:bg-amber-500/20;
870
-
}
+1
-10
knotserver/xrpc/list_keys.go
+1
-10
knotserver/xrpc/list_keys.go
···
1
1
package xrpc
2
2
3
3
import (
4
-
"encoding/json"
5
4
"net/http"
6
5
"strconv"
7
6
···
46
45
response.Cursor = &nextCursor
47
46
}
48
47
49
-
w.Header().Set("Content-Type", "application/json")
50
-
if err := json.NewEncoder(w).Encode(response); err != nil {
51
-
x.Logger.Error("failed to encode response", "error", err)
52
-
writeError(w, xrpcerr.NewXrpcError(
53
-
xrpcerr.WithTag("InternalServerError"),
54
-
xrpcerr.WithMessage("failed to encode response"),
55
-
), http.StatusInternalServerError)
56
-
return
57
-
}
48
+
writeJson(w, response)
58
49
}
+1
-10
knotserver/xrpc/owner.go
+1
-10
knotserver/xrpc/owner.go
···
1
1
package xrpc
2
2
3
3
import (
4
-
"encoding/json"
5
4
"net/http"
6
5
7
6
"tangled.sh/tangled.sh/core/api/tangled"
···
19
18
Owner: owner,
20
19
}
21
20
22
-
w.Header().Set("Content-Type", "application/json")
23
-
if err := json.NewEncoder(w).Encode(response); err != nil {
24
-
x.Logger.Error("failed to encode response", "error", err)
25
-
writeError(w, xrpcerr.NewXrpcError(
26
-
xrpcerr.WithTag("InternalServerError"),
27
-
xrpcerr.WithMessage("failed to encode response"),
28
-
), http.StatusInternalServerError)
29
-
return
30
-
}
21
+
writeJson(w, response)
31
22
}
+8
-7
knotserver/xrpc/repo_archive.go
+8
-7
knotserver/xrpc/repo_archive.go
···
13
13
)
14
14
15
15
func (x *Xrpc) RepoArchive(w http.ResponseWriter, r *http.Request) {
16
-
repo, repoPath, unescapedRef, err := x.parseStandardParams(r)
16
+
repo := r.URL.Query().Get("repo")
17
+
repoPath, err := x.parseRepoParam(repo)
17
18
if err != nil {
18
19
writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest)
19
20
return
20
21
}
22
+
23
+
ref := r.URL.Query().Get("ref")
24
+
// ref can be empty (git.Open handles this)
21
25
22
26
format := r.URL.Query().Get("format")
23
27
if format == "" {
···
34
38
return
35
39
}
36
40
37
-
gr, err := git.Open(repoPath, unescapedRef)
41
+
gr, err := git.Open(repoPath, ref)
38
42
if err != nil {
39
-
writeError(w, xrpcerr.NewXrpcError(
40
-
xrpcerr.WithTag("RefNotFound"),
41
-
xrpcerr.WithMessage("repository or ref not found"),
42
-
), http.StatusNotFound)
43
+
writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound)
43
44
return
44
45
}
45
46
46
47
repoParts := strings.Split(repo, "/")
47
48
repoName := repoParts[len(repoParts)-1]
48
49
49
-
safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(unescapedRef).Short(), "/", "-")
50
+
safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-")
50
51
51
52
var archivePrefix string
52
53
if prefix != "" {
+8
-15
knotserver/xrpc/repo_blob.go
+8
-15
knotserver/xrpc/repo_blob.go
···
3
3
import (
4
4
"crypto/sha256"
5
5
"encoding/base64"
6
-
"encoding/json"
7
6
"fmt"
8
7
"net/http"
9
8
"path/filepath"
···
16
15
)
17
16
18
17
func (x *Xrpc) RepoBlob(w http.ResponseWriter, r *http.Request) {
19
-
_, repoPath, ref, err := x.parseStandardParams(r)
18
+
repo := r.URL.Query().Get("repo")
19
+
repoPath, err := x.parseRepoParam(repo)
20
20
if err != nil {
21
21
writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest)
22
22
return
23
23
}
24
24
25
+
ref := r.URL.Query().Get("ref")
26
+
// ref can be empty (git.Open handles this)
27
+
25
28
treePath := r.URL.Query().Get("path")
26
29
if treePath == "" {
27
30
writeError(w, xrpcerr.NewXrpcError(
···
35
38
36
39
gr, err := git.Open(repoPath, ref)
37
40
if err != nil {
38
-
writeError(w, xrpcerr.NewXrpcError(
39
-
xrpcerr.WithTag("RefNotFound"),
40
-
xrpcerr.WithMessage("repository or ref not found"),
41
-
), http.StatusNotFound)
41
+
writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound)
42
42
return
43
43
}
44
44
···
69
69
return
70
70
}
71
71
w.Header().Set("ETag", eTag)
72
+
w.Header().Set("Content-Type", mimeType)
72
73
73
74
case strings.HasPrefix(mimeType, "text/"):
74
75
w.Header().Set("Cache-Control", "public, no-cache")
···
122
123
response.MimeType = &mimeType
123
124
}
124
125
125
-
w.Header().Set("Content-Type", "application/json")
126
-
if err := json.NewEncoder(w).Encode(response); err != nil {
127
-
x.Logger.Error("failed to encode response", "error", err)
128
-
writeError(w, xrpcerr.NewXrpcError(
129
-
xrpcerr.WithTag("InternalServerError"),
130
-
xrpcerr.WithMessage("failed to encode response"),
131
-
), http.StatusInternalServerError)
132
-
return
133
-
}
126
+
writeJson(w, response)
134
127
}
135
128
136
129
// isTextualMimeType returns true if the MIME type represents textual content
+5
-16
knotserver/xrpc/repo_branch.go
+5
-16
knotserver/xrpc/repo_branch.go
···
1
1
package xrpc
2
2
3
3
import (
4
-
"encoding/json"
5
4
"net/http"
6
5
"net/url"
6
+
"time"
7
7
8
8
"tangled.sh/tangled.sh/core/api/tangled"
9
9
"tangled.sh/tangled.sh/core/knotserver/git"
···
31
31
32
32
gr, err := git.PlainOpen(repoPath)
33
33
if err != nil {
34
-
writeError(w, xrpcerr.NewXrpcError(
35
-
xrpcerr.WithTag("RepoNotFound"),
36
-
xrpcerr.WithMessage("repository not found"),
37
-
), http.StatusNotFound)
34
+
writeError(w, xrpcerr.RepoNotFoundError, http.StatusNoContent)
38
35
return
39
36
}
40
37
···
70
67
Name: ref.Name().Short(),
71
68
Hash: ref.Hash().String(),
72
69
ShortHash: &[]string{ref.Hash().String()[:7]}[0],
73
-
When: commit.Author.When.Format("2006-01-02T15:04:05.000Z"),
70
+
When: commit.Author.When.Format(time.RFC3339),
74
71
IsDefault: &isDefault,
75
72
}
76
73
···
81
78
response.Author = &tangled.RepoBranch_Signature{
82
79
Name: commit.Author.Name,
83
80
Email: commit.Author.Email,
84
-
When: commit.Author.When.Format("2006-01-02T15:04:05.000Z"),
81
+
When: commit.Author.When.Format(time.RFC3339),
85
82
}
86
83
87
-
w.Header().Set("Content-Type", "application/json")
88
-
if err := json.NewEncoder(w).Encode(response); err != nil {
89
-
x.Logger.Error("failed to encode response", "error", err)
90
-
writeError(w, xrpcerr.NewXrpcError(
91
-
xrpcerr.WithTag("InternalServerError"),
92
-
xrpcerr.WithMessage("failed to encode response"),
93
-
), http.StatusInternalServerError)
94
-
return
95
-
}
84
+
writeJson(w, response)
96
85
}
+11
-25
knotserver/xrpc/repo_branches.go
+11
-25
knotserver/xrpc/repo_branches.go
···
1
1
package xrpc
2
2
3
3
import (
4
-
"encoding/json"
5
4
"net/http"
6
5
"strconv"
7
6
···
20
19
21
20
cursor := r.URL.Query().Get("cursor")
22
21
23
-
limit := 50 // default
24
-
if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
25
-
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
26
-
limit = l
27
-
}
28
-
}
22
+
// limit := 50 // default
23
+
// if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
24
+
// if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
25
+
// limit = l
26
+
// }
27
+
// }
28
+
29
+
limit := 500
29
30
30
31
gr, err := git.PlainOpen(repoPath)
31
32
if err != nil {
32
-
writeError(w, xrpcerr.NewXrpcError(
33
-
xrpcerr.WithTag("RepoNotFound"),
34
-
xrpcerr.WithMessage("repository not found"),
35
-
), http.StatusNotFound)
33
+
writeError(w, xrpcerr.RepoNotFoundError, http.StatusNoContent)
36
34
return
37
35
}
38
36
···
45
43
}
46
44
}
47
45
48
-
end := offset + limit
49
-
if end > len(branches) {
50
-
end = len(branches)
51
-
}
46
+
end := min(offset+limit, len(branches))
52
47
53
48
paginatedBranches := branches[offset:end]
54
49
···
57
52
Branches: paginatedBranches,
58
53
}
59
54
60
-
// Write JSON response directly
61
-
w.Header().Set("Content-Type", "application/json")
62
-
if err := json.NewEncoder(w).Encode(response); err != nil {
63
-
x.Logger.Error("failed to encode response", "error", err)
64
-
writeError(w, xrpcerr.NewXrpcError(
65
-
xrpcerr.WithTag("InternalServerError"),
66
-
xrpcerr.WithMessage("failed to encode response"),
67
-
), http.StatusInternalServerError)
68
-
return
69
-
}
55
+
writeJson(w, response)
70
56
}
+7
-23
knotserver/xrpc/repo_compare.go
+7
-23
knotserver/xrpc/repo_compare.go
···
1
1
package xrpc
2
2
3
3
import (
4
-
"encoding/json"
5
4
"fmt"
6
5
"net/http"
7
-
"net/url"
8
6
9
7
"tangled.sh/tangled.sh/core/knotserver/git"
10
8
"tangled.sh/tangled.sh/core/types"
···
19
17
return
20
18
}
21
19
22
-
rev1Param := r.URL.Query().Get("rev1")
23
-
if rev1Param == "" {
20
+
rev1 := r.URL.Query().Get("rev1")
21
+
if rev1 == "" {
24
22
writeError(w, xrpcerr.NewXrpcError(
25
23
xrpcerr.WithTag("InvalidRequest"),
26
24
xrpcerr.WithMessage("missing rev1 parameter"),
···
28
26
return
29
27
}
30
28
31
-
rev2Param := r.URL.Query().Get("rev2")
32
-
if rev2Param == "" {
29
+
rev2 := r.URL.Query().Get("rev2")
30
+
if rev2 == "" {
33
31
writeError(w, xrpcerr.NewXrpcError(
34
32
xrpcerr.WithTag("InvalidRequest"),
35
33
xrpcerr.WithMessage("missing rev2 parameter"),
···
37
35
return
38
36
}
39
37
40
-
rev1, _ := url.PathUnescape(rev1Param)
41
-
rev2, _ := url.PathUnescape(rev2Param)
42
-
43
38
gr, err := git.PlainOpen(repoPath)
44
39
if err != nil {
45
-
writeError(w, xrpcerr.NewXrpcError(
46
-
xrpcerr.WithTag("RepoNotFound"),
47
-
xrpcerr.WithMessage("repository not found"),
48
-
), http.StatusNotFound)
40
+
writeError(w, xrpcerr.RepoNotFoundError, http.StatusNoContent)
49
41
return
50
42
}
51
43
···
79
71
return
80
72
}
81
73
82
-
resp := types.RepoFormatPatchResponse{
74
+
response := types.RepoFormatPatchResponse{
83
75
Rev1: commit1.Hash.String(),
84
76
Rev2: commit2.Hash.String(),
85
77
FormatPatch: formatPatch,
86
78
Patch: rawPatch,
87
79
}
88
80
89
-
w.Header().Set("Content-Type", "application/json")
90
-
if err := json.NewEncoder(w).Encode(resp); err != nil {
91
-
x.Logger.Error("failed to encode response", "error", err)
92
-
writeError(w, xrpcerr.NewXrpcError(
93
-
xrpcerr.WithTag("InternalServerError"),
94
-
xrpcerr.WithMessage("failed to encode response"),
95
-
), http.StatusInternalServerError)
96
-
return
97
-
}
81
+
writeJson(w, response)
98
82
}
+6
-30
knotserver/xrpc/repo_diff.go
+6
-30
knotserver/xrpc/repo_diff.go
···
1
1
package xrpc
2
2
3
3
import (
4
-
"encoding/json"
5
4
"net/http"
6
-
"net/url"
7
5
8
6
"tangled.sh/tangled.sh/core/knotserver/git"
9
7
"tangled.sh/tangled.sh/core/types"
···
18
16
return
19
17
}
20
18
21
-
refParam := r.URL.Query().Get("ref")
22
-
if refParam == "" {
23
-
writeError(w, xrpcerr.NewXrpcError(
24
-
xrpcerr.WithTag("InvalidRequest"),
25
-
xrpcerr.WithMessage("missing ref parameter"),
26
-
), http.StatusBadRequest)
27
-
return
28
-
}
29
-
30
-
ref, _ := url.QueryUnescape(refParam)
19
+
ref := r.URL.Query().Get("ref")
20
+
// ref can be empty (git.Open handles this)
31
21
32
22
gr, err := git.Open(repoPath, ref)
33
23
if err != nil {
34
-
writeError(w, xrpcerr.NewXrpcError(
35
-
xrpcerr.WithTag("RefNotFound"),
36
-
xrpcerr.WithMessage("repository or ref not found"),
37
-
), http.StatusNotFound)
24
+
writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound)
38
25
return
39
26
}
40
27
41
28
diff, err := gr.Diff()
42
29
if err != nil {
43
30
x.Logger.Error("getting diff", "error", err.Error())
44
-
writeError(w, xrpcerr.NewXrpcError(
45
-
xrpcerr.WithTag("RefNotFound"),
46
-
xrpcerr.WithMessage("failed to generate diff"),
47
-
), http.StatusInternalServerError)
31
+
writeError(w, xrpcerr.RefNotFoundError, http.StatusInternalServerError)
48
32
return
49
33
}
50
34
51
-
resp := types.RepoCommitResponse{
35
+
response := types.RepoCommitResponse{
52
36
Ref: ref,
53
37
Diff: diff,
54
38
}
55
39
56
-
w.Header().Set("Content-Type", "application/json")
57
-
if err := json.NewEncoder(w).Encode(resp); err != nil {
58
-
x.Logger.Error("failed to encode response", "error", err)
59
-
writeError(w, xrpcerr.NewXrpcError(
60
-
xrpcerr.WithTag("InternalServerError"),
61
-
xrpcerr.WithMessage("failed to encode response"),
62
-
), http.StatusInternalServerError)
63
-
return
64
-
}
40
+
writeJson(w, response)
65
41
}
+4
-19
knotserver/xrpc/repo_get_default_branch.go
+4
-19
knotserver/xrpc/repo_get_default_branch.go
···
1
1
package xrpc
2
2
3
3
import (
4
-
"encoding/json"
5
4
"net/http"
5
+
"time"
6
6
7
7
"tangled.sh/tangled.sh/core/api/tangled"
8
8
"tangled.sh/tangled.sh/core/knotserver/git"
···
17
17
return
18
18
}
19
19
20
-
gr, err := git.Open(repoPath, "")
21
-
if err != nil {
22
-
writeError(w, xrpcerr.NewXrpcError(
23
-
xrpcerr.WithTag("RepoNotFound"),
24
-
xrpcerr.WithMessage("repository not found"),
25
-
), http.StatusNotFound)
26
-
return
27
-
}
20
+
gr, err := git.PlainOpen(repoPath)
28
21
29
22
branch, err := gr.FindMainBranch()
30
23
if err != nil {
···
39
32
response := tangled.RepoGetDefaultBranch_Output{
40
33
Name: branch,
41
34
Hash: "",
42
-
When: "1970-01-01T00:00:00.000Z",
35
+
When: time.UnixMicro(0).Format(time.RFC3339),
43
36
}
44
37
45
-
w.Header().Set("Content-Type", "application/json")
46
-
if err := json.NewEncoder(w).Encode(response); err != nil {
47
-
x.Logger.Error("failed to encode response", "error", err)
48
-
writeError(w, xrpcerr.NewXrpcError(
49
-
xrpcerr.WithTag("InternalServerError"),
50
-
xrpcerr.WithMessage("failed to encode response"),
51
-
), http.StatusInternalServerError)
52
-
return
53
-
}
38
+
writeJson(w, response)
54
39
}
+4
-21
knotserver/xrpc/repo_languages.go
+4
-21
knotserver/xrpc/repo_languages.go
···
2
2
3
3
import (
4
4
"context"
5
-
"encoding/json"
6
5
"math"
7
6
"net/http"
8
-
"net/url"
9
7
"time"
10
8
11
9
"tangled.sh/tangled.sh/core/api/tangled"
···
14
12
)
15
13
16
14
func (x *Xrpc) RepoLanguages(w http.ResponseWriter, r *http.Request) {
17
-
refParam := r.URL.Query().Get("ref")
18
-
if refParam == "" {
19
-
refParam = "HEAD" // default
20
-
}
21
-
ref, _ := url.PathUnescape(refParam)
22
-
23
15
repo := r.URL.Query().Get("repo")
24
16
repoPath, err := x.parseRepoParam(repo)
25
17
if err != nil {
···
27
19
return
28
20
}
29
21
22
+
ref := r.URL.Query().Get("ref")
23
+
30
24
gr, err := git.Open(repoPath, ref)
31
25
if err != nil {
32
26
x.Logger.Error("opening repo", "error", err.Error())
33
-
writeError(w, xrpcerr.NewXrpcError(
34
-
xrpcerr.WithTag("RefNotFound"),
35
-
xrpcerr.WithMessage("repository or ref not found"),
36
-
), http.StatusNotFound)
27
+
writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound)
37
28
return
38
29
}
39
30
···
81
72
response.TotalFiles = &totalFiles
82
73
}
83
74
84
-
w.Header().Set("Content-Type", "application/json")
85
-
if err := json.NewEncoder(w).Encode(response); err != nil {
86
-
x.Logger.Error("failed to encode response", "error", err)
87
-
writeError(w, xrpcerr.NewXrpcError(
88
-
xrpcerr.WithTag("InternalServerError"),
89
-
xrpcerr.WithMessage("failed to encode response"),
90
-
), http.StatusInternalServerError)
91
-
return
92
-
}
75
+
writeJson(w, response)
93
76
}
+3
-33
knotserver/xrpc/repo_log.go
+3
-33
knotserver/xrpc/repo_log.go
···
1
1
package xrpc
2
2
3
3
import (
4
-
"encoding/json"
5
4
"net/http"
6
-
"net/url"
7
5
"strconv"
8
6
9
7
"tangled.sh/tangled.sh/core/knotserver/git"
···
19
17
return
20
18
}
21
19
22
-
refParam := r.URL.Query().Get("ref")
23
-
if refParam == "" {
24
-
writeError(w, xrpcerr.NewXrpcError(
25
-
xrpcerr.WithTag("InvalidRequest"),
26
-
xrpcerr.WithMessage("missing ref parameter"),
27
-
), http.StatusBadRequest)
28
-
return
29
-
}
20
+
ref := r.URL.Query().Get("ref")
30
21
31
22
path := r.URL.Query().Get("path")
32
23
cursor := r.URL.Query().Get("cursor")
···
38
29
}
39
30
}
40
31
41
-
ref, err := url.QueryUnescape(refParam)
42
-
if err != nil {
43
-
writeError(w, xrpcerr.NewXrpcError(
44
-
xrpcerr.WithTag("InvalidRequest"),
45
-
xrpcerr.WithMessage("invalid ref parameter"),
46
-
), http.StatusBadRequest)
47
-
return
48
-
}
49
-
50
32
gr, err := git.Open(repoPath, ref)
51
33
if err != nil {
52
-
writeError(w, xrpcerr.NewXrpcError(
53
-
xrpcerr.WithTag("RefNotFound"),
54
-
xrpcerr.WithMessage("repository or ref not found"),
55
-
), http.StatusNotFound)
34
+
writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound)
56
35
return
57
36
}
58
37
···
98
77
99
78
response.Log = true
100
79
101
-
// Write JSON response directly
102
-
w.Header().Set("Content-Type", "application/json")
103
-
if err := json.NewEncoder(w).Encode(response); err != nil {
104
-
x.Logger.Error("failed to encode response", "error", err)
105
-
writeError(w, xrpcerr.NewXrpcError(
106
-
xrpcerr.WithTag("InternalServerError"),
107
-
xrpcerr.WithMessage("failed to encode response"),
108
-
), http.StatusInternalServerError)
109
-
return
110
-
}
80
+
writeJson(w, response)
111
81
}
+6
-33
knotserver/xrpc/repo_tree.go
+6
-33
knotserver/xrpc/repo_tree.go
···
1
1
package xrpc
2
2
3
3
import (
4
-
"encoding/json"
5
4
"net/http"
6
-
"net/url"
7
5
"path/filepath"
6
+
"time"
8
7
9
8
"tangled.sh/tangled.sh/core/api/tangled"
10
9
"tangled.sh/tangled.sh/core/knotserver/git"
···
21
20
return
22
21
}
23
22
24
-
refParam := r.URL.Query().Get("ref")
25
-
if refParam == "" {
26
-
writeError(w, xrpcerr.NewXrpcError(
27
-
xrpcerr.WithTag("InvalidRequest"),
28
-
xrpcerr.WithMessage("missing ref parameter"),
29
-
), http.StatusBadRequest)
30
-
return
31
-
}
23
+
ref := r.URL.Query().Get("ref")
24
+
// ref can be empty (git.Open handles this)
32
25
33
26
path := r.URL.Query().Get("path")
34
27
// path can be empty (defaults to root)
35
28
36
-
ref, err := url.QueryUnescape(refParam)
37
-
if err != nil {
38
-
writeError(w, xrpcerr.NewXrpcError(
39
-
xrpcerr.WithTag("InvalidRequest"),
40
-
xrpcerr.WithMessage("invalid ref parameter"),
41
-
), http.StatusBadRequest)
42
-
return
43
-
}
44
-
45
29
gr, err := git.Open(repoPath, ref)
46
30
if err != nil {
47
31
x.Logger.Error("failed to open git repository", "error", err, "path", repoPath, "ref", ref)
48
-
writeError(w, xrpcerr.NewXrpcError(
49
-
xrpcerr.WithTag("RefNotFound"),
50
-
xrpcerr.WithMessage("repository or ref not found"),
51
-
), http.StatusNotFound)
32
+
writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound)
52
33
return
53
34
}
54
35
···
77
58
entry.Last_commit = &tangled.RepoTree_LastCommit{
78
59
Hash: file.LastCommit.Hash.String(),
79
60
Message: file.LastCommit.Message,
80
-
When: file.LastCommit.When.Format("2006-01-02T15:04:05.000Z"),
61
+
When: file.LastCommit.When.Format(time.RFC3339),
81
62
}
82
63
}
83
64
···
104
85
Files: treeEntries,
105
86
}
106
87
107
-
w.Header().Set("Content-Type", "application/json")
108
-
if err := json.NewEncoder(w).Encode(response); err != nil {
109
-
x.Logger.Error("failed to encode response", "error", err)
110
-
writeError(w, xrpcerr.NewXrpcError(
111
-
xrpcerr.WithTag("InternalServerError"),
112
-
xrpcerr.WithMessage("failed to encode response"),
113
-
), http.StatusInternalServerError)
114
-
return
115
-
}
88
+
writeJson(w, response)
116
89
}
+1
-11
knotserver/xrpc/version.go
+1
-11
knotserver/xrpc/version.go
···
1
1
package xrpc
2
2
3
3
import (
4
-
"encoding/json"
5
4
"fmt"
6
5
"net/http"
7
6
"runtime/debug"
8
7
9
8
"tangled.sh/tangled.sh/core/api/tangled"
10
-
xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
11
9
)
12
10
13
11
// version is set during build time.
···
58
56
Version: version,
59
57
}
60
58
61
-
w.Header().Set("Content-Type", "application/json")
62
-
if err := json.NewEncoder(w).Encode(response); err != nil {
63
-
x.Logger.Error("failed to encode response", "error", err)
64
-
writeError(w, xrpcerr.NewXrpcError(
65
-
xrpcerr.WithTag("InternalServerError"),
66
-
xrpcerr.WithMessage("failed to encode response"),
67
-
), http.StatusInternalServerError)
68
-
return
69
-
}
59
+
writeJson(w, response)
70
60
}
+14
-35
knotserver/xrpc/xrpc.go
+14
-35
knotserver/xrpc/xrpc.go
···
4
4
"encoding/json"
5
5
"log/slog"
6
6
"net/http"
7
-
"net/url"
8
7
"strings"
9
8
10
9
securejoin "github.com/cyphar/filepath-securejoin"
···
88
87
}
89
88
90
89
// Parse repo string (did/repoName format)
91
-
parts := strings.Split(repo, "/")
92
-
if len(parts) < 2 {
90
+
parts := strings.SplitN(repo, "/", 2)
91
+
if len(parts) != 2 {
93
92
return "", xrpcerr.NewXrpcError(
94
93
xrpcerr.WithTag("InvalidRequest"),
95
94
xrpcerr.WithMessage("invalid repo format, expected 'did/repoName'"),
96
95
)
97
96
}
98
97
99
-
did := strings.Join(parts[:len(parts)-1], "/")
100
-
repoName := parts[len(parts)-1]
98
+
did := parts[0]
99
+
repoName := parts[1]
101
100
102
101
// Construct repository path using the same logic as didPath
103
102
didRepoPath, err := securejoin.SecureJoin(did, repoName)
104
103
if err != nil {
105
-
return "", xrpcerr.NewXrpcError(
106
-
xrpcerr.WithTag("RepoNotFound"),
107
-
xrpcerr.WithMessage("failed to access repository"),
108
-
)
104
+
return "", xrpcerr.RepoNotFoundError
109
105
}
110
106
111
107
repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, didRepoPath)
112
108
if err != nil {
113
-
return "", xrpcerr.NewXrpcError(
114
-
xrpcerr.WithTag("RepoNotFound"),
115
-
xrpcerr.WithMessage("failed to access repository"),
116
-
)
109
+
return "", xrpcerr.RepoNotFoundError
117
110
}
118
111
119
112
return repoPath, nil
120
113
}
121
114
122
-
// parseStandardParams parses common query parameters used by most handlers
123
-
func (x *Xrpc) parseStandardParams(r *http.Request) (repo, repoPath, ref string, err error) {
124
-
// Parse repo parameter
125
-
repo = r.URL.Query().Get("repo")
126
-
repoPath, err = x.parseRepoParam(repo)
127
-
if err != nil {
128
-
return "", "", "", err
129
-
}
130
-
131
-
// Parse and unescape ref parameter
132
-
refParam := r.URL.Query().Get("ref")
133
-
if refParam == "" {
134
-
return "", "", "", xrpcerr.NewXrpcError(
135
-
xrpcerr.WithTag("InvalidRequest"),
136
-
xrpcerr.WithMessage("missing ref parameter"),
137
-
)
138
-
}
139
-
140
-
ref, _ = url.QueryUnescape(refParam)
141
-
return repo, repoPath, ref, nil
142
-
}
143
-
144
115
func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) {
145
116
w.Header().Set("Content-Type", "application/json")
146
117
w.WriteHeader(status)
147
118
json.NewEncoder(w).Encode(e)
148
119
}
120
+
121
+
func writeJson(w http.ResponseWriter, response any) {
122
+
w.Header().Set("Content-Type", "application/json")
123
+
if err := json.NewEncoder(w).Encode(response); err != nil {
124
+
writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
125
+
return
126
+
}
127
+
}
+10
xrpc/errors/errors.go
+10
xrpc/errors/errors.go
···
56
56
WithMessage("owner not set for this service"),
57
57
)
58
58
59
+
var RepoNotFoundError = NewXrpcError(
60
+
WithTag("RepoNotFound"),
61
+
WithMessage("failed to access repository"),
62
+
)
63
+
64
+
var RefNotFoundError = NewXrpcError(
65
+
WithTag("RefNotFound"),
66
+
WithMessage("failed to access ref"),
67
+
)
68
+
59
69
var AuthError = func(err error) XrpcError {
60
70
return NewXrpcError(
61
71
WithTag("Auth"),