+8
-4
appview/db/issues.go
+8
-4
appview/db/issues.go
···
359
repoMap[string(repos[i].RepoAt())] = &repos[i]
360
}
361
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
+
}
370
}
371
372
// collect comments
+2
-2
appview/db/profile.go
+2
-2
appview/db/profile.go
···
553
query = `select count(id) from pulls where owner_did = ? and state = ?`
554
args = append(args, did, PullOpen)
555
case VanityStatOpenIssueCount:
556
-
query = `select count(id) from issues where owner_did = ? and open = 1`
557
args = append(args, did)
558
case VanityStatClosedIssueCount:
559
-
query = `select count(id) from issues where owner_did = ? and open = 0`
560
args = append(args, did)
561
case VanityStatRepositoryCount:
562
query = `select count(id) from repos where did = ?`
···
553
query = `select count(id) from pulls where owner_did = ? and state = ?`
554
args = append(args, did, PullOpen)
555
case VanityStatOpenIssueCount:
556
+
query = `select count(id) from issues where did = ? and open = 1`
557
args = append(args, did)
558
case VanityStatClosedIssueCount:
559
+
query = `select count(id) from issues where did = ? and open = 0`
560
args = append(args, did)
561
case VanityStatRepositoryCount:
562
query = `select count(id) from repos where did = ?`
+1
-1
appview/pages/markup/markdown.go
+1
-1
appview/pages/markup/markdown.go
+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
<div class="flex flex-col md:flex-row md:justify-between gap-2">
26
<div id="breadcrumbs" class="overflow-x-auto whitespace-nowrap text-gray-400 dark:text-gray-500">
27
{{ range .BreadCrumbs }}
28
-
<a href="{{ index . 1}}" class="text-bold text-gray-500 dark:text-gray-400 {{ $linkstyle }}">{{ pathUnescape (index . 0) }}</a> /
29
{{ end }}
30
</div>
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
{{ $stats := .TreeStats }}
33
34
-
<span>at <a href="/{{ $.RepoInfo.FullName }}/tree/{{ $.Ref }}">{{ $.Ref }}</a></span>
35
{{ if eq $stats.NumFolders 1 }}
36
<span class="select-none px-1 md:px-2 [&:before]:content-['ยท']"></span>
37
<span>{{ $stats.NumFolders }} folder</span>
···
55
{{ range .Files }}
56
<div class="grid grid-cols-12 gap-4 items-center py-1">
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 }}
59
{{ $icon := "folder" }}
60
{{ $iconStyle := "size-4 fill-current" }}
61
···
25
<div class="flex flex-col md:flex-row md:justify-between gap-2">
26
<div id="breadcrumbs" class="overflow-x-auto whitespace-nowrap text-gray-400 dark:text-gray-500">
27
{{ range .BreadCrumbs }}
28
+
<a href="{{ index . 1 }}" class="text-bold text-gray-500 dark:text-gray-400 {{ $linkstyle }}">{{ pathUnescape (index . 0) }}</a> /
29
{{ end }}
30
</div>
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
{{ $stats := .TreeStats }}
33
34
+
<span>at <a href="/{{ $.RepoInfo.FullName }}/tree/{{ pathEscape $.Ref }}">{{ $.Ref }}</a></span>
35
{{ if eq $stats.NumFolders 1 }}
36
<span class="select-none px-1 md:px-2 [&:before]:content-['ยท']"></span>
37
<span>{{ $stats.NumFolders }} folder</span>
···
55
{{ range .Files }}
56
<div class="grid grid-cols-12 gap-4 items-center py-1">
57
<div class="col-span-8 md:col-span-4">
58
+
{{ $link := printf "/%s/%s/%s/%s/%s" $.RepoInfo.FullName "tree" (pathEscape $.Ref) $.TreePath .Name }}
59
{{ $icon := "folder" }}
60
{{ $iconStyle := "size-4 fill-current" }}
61
+3
-2
appview/pages/templates/strings/string.html
+3
-2
appview/pages/templates/strings/string.html
···
23
hx-boost="true"
24
href="/strings/{{ .String.Did }}/{{ .String.Rkey }}/edit">
25
{{ i "pencil" "size-4" }}
26
-
<span class="hidden md:inline">edit</span>
27
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
28
</a>
29
<button
···
34
hx-confirm="Are you sure you want to delete the string `{{ .String.Filename }}`?"
35
>
36
{{ i "trash-2" "size-4" }}
37
-
<span class="hidden md:inline">delete</span>
38
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
39
</button>
40
</div>
···
80
<div id="blob-contents" class="whitespace-pre peer-target:bg-yellow-200 dark:peer-target:bg-yellow-900">{{ .String.Contents | escapeHtml }}</div>
81
{{ end }}
82
</div>
83
</section>
84
{{ end }}
···
23
hx-boost="true"
24
href="/strings/{{ .String.Did }}/{{ .String.Rkey }}/edit">
25
{{ i "pencil" "size-4" }}
26
+
<span class="hidden md:inline">edit</span>
27
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
28
</a>
29
<button
···
34
hx-confirm="Are you sure you want to delete the string `{{ .String.Filename }}`?"
35
>
36
{{ i "trash-2" "size-4" }}
37
+
<span class="hidden md:inline">delete</span>
38
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
39
</button>
40
</div>
···
80
<div id="blob-contents" class="whitespace-pre peer-target:bg-yellow-200 dark:peer-target:bg-yellow-900">{{ .String.Contents | escapeHtml }}</div>
81
{{ end }}
82
</div>
83
+
{{ template "fragments/multiline-select" }}
84
</section>
85
{{ end }}
+16
-17
appview/repo/index.go
+16
-17
appview/repo/index.go
···
5
"fmt"
6
"log"
7
"net/http"
8
"slices"
9
"sort"
10
"strings"
···
31
32
func (rp *Repo) RepoIndex(w http.ResponseWriter, r *http.Request) {
33
ref := chi.URLParam(r, "ref")
34
35
f, err := rp.repoResolver.Resolve(r)
36
if err != nil {
···
61
RepoInfo: repoInfo,
62
})
63
return
64
-
} else {
65
-
rp.pages.Error503(w)
66
-
log.Println("failed to build index response", err)
67
-
return
68
}
69
}
70
71
tagMap := make(map[string][]string)
···
245
// first get branches to determine the ref if not specified
246
branchesBytes, err := tangled.RepoBranches(ctx, xrpcc, "", 0, repo)
247
if err != nil {
248
-
return nil, err
249
}
250
251
var branchesResp types.RepoBranchesResponse
252
if err := json.Unmarshal(branchesBytes, &branchesResp); err != nil {
253
-
return nil, err
254
}
255
256
// if no ref specified, use default branch or first available
257
-
if ref == "" && len(branchesResp.Branches) > 0 {
258
for _, branch := range branchesResp.Branches {
259
if branch.IsDefault {
260
ref = branch.Name
261
break
262
}
263
}
264
-
if ref == "" {
265
-
ref = branchesResp.Branches[0].Name
266
-
}
267
}
268
269
-
// check if repo is empty
270
-
if len(branchesResp.Branches) == 0 {
271
return &types.RepoIndexResponse{
272
IsEmpty: true,
273
Branches: branchesResp.Branches,
···
292
defer wg.Done()
293
tagsBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repo)
294
if err != nil {
295
-
errs = errors.Join(errs, err)
296
return
297
}
298
299
if err := json.Unmarshal(tagsBytes, &tagsResp); err != nil {
300
-
errs = errors.Join(errs, err)
301
}
302
}()
303
···
307
defer wg.Done()
308
resp, err := tangled.RepoTree(ctx, xrpcc, "", ref, repo)
309
if err != nil {
310
-
errs = errors.Join(errs, err)
311
return
312
}
313
treeResp = resp
···
319
defer wg.Done()
320
logBytes, err := tangled.RepoLog(ctx, xrpcc, "", 50, "", ref, repo)
321
if err != nil {
322
-
errs = errors.Join(errs, err)
323
return
324
}
325
326
if err := json.Unmarshal(logBytes, &logResp); err != nil {
327
-
errs = errors.Join(errs, err)
328
}
329
}()
330
···
5
"fmt"
6
"log"
7
"net/http"
8
+
"net/url"
9
"slices"
10
"sort"
11
"strings"
···
32
33
func (rp *Repo) RepoIndex(w http.ResponseWriter, r *http.Request) {
34
ref := chi.URLParam(r, "ref")
35
+
ref, _ = url.PathUnescape(ref)
36
37
f, err := rp.repoResolver.Resolve(r)
38
if err != nil {
···
63
RepoInfo: repoInfo,
64
})
65
return
66
}
67
+
68
+
rp.pages.Error503(w)
69
+
log.Println("failed to build index response", err)
70
+
return
71
}
72
73
tagMap := make(map[string][]string)
···
247
// first get branches to determine the ref if not specified
248
branchesBytes, err := tangled.RepoBranches(ctx, xrpcc, "", 0, repo)
249
if err != nil {
250
+
return nil, fmt.Errorf("failed to call repoBranches: %w", err)
251
}
252
253
var branchesResp types.RepoBranchesResponse
254
if err := json.Unmarshal(branchesBytes, &branchesResp); err != nil {
255
+
return nil, fmt.Errorf("failed to unmarshal branches response: %w", err)
256
}
257
258
// if no ref specified, use default branch or first available
259
+
if ref == "" {
260
for _, branch := range branchesResp.Branches {
261
if branch.IsDefault {
262
ref = branch.Name
263
break
264
}
265
}
266
}
267
268
+
// if ref is still empty, this means the default branch is not set
269
+
if ref == "" {
270
return &types.RepoIndexResponse{
271
IsEmpty: true,
272
Branches: branchesResp.Branches,
···
291
defer wg.Done()
292
tagsBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repo)
293
if err != nil {
294
+
errs = errors.Join(errs, fmt.Errorf("failed to call repoTags: %w", err))
295
return
296
}
297
298
if err := json.Unmarshal(tagsBytes, &tagsResp); err != nil {
299
+
errs = errors.Join(errs, fmt.Errorf("failed to unmarshal repoTags: %w", err))
300
}
301
}()
302
···
306
defer wg.Done()
307
resp, err := tangled.RepoTree(ctx, xrpcc, "", ref, repo)
308
if err != nil {
309
+
errs = errors.Join(errs, fmt.Errorf("failed to call repoTree: %w", err))
310
return
311
}
312
treeResp = resp
···
318
defer wg.Done()
319
logBytes, err := tangled.RepoLog(ctx, xrpcc, "", 50, "", ref, repo)
320
if err != nil {
321
+
errs = errors.Join(errs, fmt.Errorf("failed to call repoLog: %w", err))
322
return
323
}
324
325
if err := json.Unmarshal(logBytes, &logResp); err != nil {
326
+
errs = errors.Join(errs, fmt.Errorf("failed to unmarshal repoLog: %w", err))
327
}
328
}()
329
+98
-120
appview/repo/repo.go
+98
-120
appview/repo/repo.go
···
11
"log/slog"
12
"net/http"
13
"net/url"
14
-
"path"
15
"path/filepath"
16
"slices"
17
"strconv"
···
86
}
87
88
func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) {
89
-
refParam := chi.URLParam(r, "ref")
90
f, err := rp.repoResolver.Resolve(r)
91
if err != nil {
92
log.Println("failed to get repo and knot", err)
···
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)))
···
140
}
141
142
ref := chi.URLParam(r, "ref")
143
144
scheme := "http"
145
if !rp.config.Core.Dev {
···
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
···
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)
···
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 {
···
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" {
···
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
···
434
}
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"
···
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
···
499
500
// redirects tree paths trying to access a blob; in this case the result.Files is unpopulated,
501
// 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)
505
return
506
}
507
508
user := rp.oauth.GetUser(r)
509
510
var breadcrumbs [][]string
511
-
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)})
512
if treePath != "" {
513
for idx, elem := range strings.Split(treePath, "/") {
514
-
breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)})
515
}
516
}
517
···
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
···
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
···
652
}
653
654
ref := chi.URLParam(r, "ref")
655
filePath := chi.URLParam(r, "*")
656
657
scheme := "http"
658
if !rp.config.Core.Dev {
···
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)})
682
if filePath != "" {
683
for idx, elem := range strings.Split(filePath, "/") {
684
-
breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)})
685
}
686
}
687
···
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 {
···
767
}
768
769
ref := chi.URLParam(r, "ref")
770
filePath := chi.URLParam(r, "*")
771
772
scheme := "http"
773
if !rp.config.Core.Dev {
···
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 {
···
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
}
···
1472
1473
func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) {
1474
ref := chi.URLParam(r, "ref")
1475
1476
user := rp.oauth.GetUser(r)
1477
f, err := rp.repoResolver.Resolve(r)
···
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
···
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
···
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
···
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
···
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
···
11
"log/slog"
12
"net/http"
13
"net/url"
14
"path/filepath"
15
"slices"
16
"strconv"
···
85
}
86
87
func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) {
88
+
ref := chi.URLParam(r, "ref")
89
+
ref, _ = url.PathUnescape(ref)
90
+
91
f, err := rp.repoResolver.Resolve(r)
92
if err != nil {
93
log.Println("failed to get repo and knot", err)
···
104
}
105
106
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
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)
111
return
112
}
113
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)
117
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
118
w.Header().Set("Content-Type", "application/gzip")
119
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(archiveBytes)))
···
138
}
139
140
ref := chi.URLParam(r, "ref")
141
+
ref, _ = url.PathUnescape(ref)
142
143
scheme := "http"
144
if !rp.config.Core.Dev {
···
159
160
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
161
xrpcBytes, err := tangled.RepoLog(r.Context(), xrpcc, cursor, limit, "", ref, repo)
162
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
163
+
log.Println("failed to call XRPC repo.log", xrpcerr)
164
+
rp.pages.Error503(w)
165
return
166
}
167
···
173
}
174
175
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
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
180
}
181
182
tagMap := make(map[string][]string)
···
190
}
191
192
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
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
197
}
198
199
if branchBytes != nil {
···
345
return
346
}
347
ref := chi.URLParam(r, "ref")
348
+
ref, _ = url.PathUnescape(ref)
349
350
var diffOpts types.DiffOpts
351
if d := r.URL.Query().Get("diff"); d == "split" {
···
368
369
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
370
xrpcBytes, err := tangled.RepoDiff(r.Context(), xrpcc, ref, repo)
371
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
372
+
log.Println("failed to call XRPC repo.diff", xrpcerr)
373
+
rp.pages.Error503(w)
374
return
375
}
376
···
422
}
423
424
ref := chi.URLParam(r, "ref")
425
+
ref, _ = url.PathUnescape(ref)
426
427
// if the tree path has a trailing slash, let's strip it
428
// so we don't 404
429
+
treePath := chi.URLParam(r, "*")
430
+
treePath, _ = url.PathUnescape(treePath)
431
treePath = strings.TrimSuffix(treePath, "/")
432
433
scheme := "http"
···
441
442
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
443
xrpcResp, err := tangled.RepoTree(r.Context(), xrpcc, treePath, ref, repo)
444
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
445
+
log.Println("failed to call XRPC repo.tree", xrpcerr)
446
+
rp.pages.Error503(w)
447
return
448
}
449
···
485
486
// redirects tree paths trying to access a blob; in this case the result.Files is unpopulated,
487
// so we can safely redirect to the "parent" (which is the same file).
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)
491
return
492
}
493
494
user := rp.oauth.GetUser(r)
495
496
var breadcrumbs [][]string
497
+
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), url.PathEscape(ref))})
498
if treePath != "" {
499
for idx, elem := range strings.Split(treePath, "/") {
500
+
breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))})
501
}
502
}
503
···
530
531
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
532
xrpcBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
533
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
534
+
log.Println("failed to call XRPC repo.tags", xrpcerr)
535
+
rp.pages.Error503(w)
536
return
537
}
538
···
599
600
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
601
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
602
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
603
+
log.Println("failed to call XRPC repo.branches", xrpcerr)
604
+
rp.pages.Error503(w)
605
return
606
}
607
···
630
}
631
632
ref := chi.URLParam(r, "ref")
633
+
ref, _ = url.PathUnescape(ref)
634
+
635
filePath := chi.URLParam(r, "*")
636
+
filePath, _ = url.PathUnescape(filePath)
637
638
scheme := "http"
639
if !rp.config.Core.Dev {
···
646
647
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name)
648
resp, err := tangled.RepoBlob(r.Context(), xrpcc, filePath, false, ref, repo)
649
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
650
+
log.Println("failed to call XRPC repo.blob", xrpcerr)
651
+
rp.pages.Error503(w)
652
return
653
}
654
655
// Use XRPC response directly instead of converting to internal types
656
657
var breadcrumbs [][]string
658
+
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), url.PathEscape(ref))})
659
if filePath != "" {
660
for idx, elem := range strings.Split(filePath, "/") {
661
+
breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))})
662
}
663
}
664
···
687
}
688
689
// fetch the raw binary content using sh.tangled.repo.blob xrpc
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()
704
705
contentSrc = blobURL
706
if !rp.config.Core.Dev {
···
755
}
756
757
ref := chi.URLParam(r, "ref")
758
+
ref, _ = url.PathUnescape(ref)
759
+
760
filePath := chi.URLParam(r, "*")
761
+
filePath, _ = url.PathUnescape(filePath)
762
763
scheme := "http"
764
if !rp.config.Core.Dev {
···
766
}
767
768
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name)
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()
781
782
req, err := http.NewRequest("GET", blobURL, nil)
783
if err != nil {
···
1366
1367
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
1368
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
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
}
···
1469
1470
func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) {
1471
ref := chi.URLParam(r, "ref")
1472
+
ref, _ = url.PathUnescape(ref)
1473
1474
user := rp.oauth.GetUser(r)
1475
f, err := rp.repoResolver.Resolve(r)
···
1758
1759
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
1760
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
1761
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
1762
+
log.Println("failed to call XRPC repo.branches", xrpcerr)
1763
+
rp.pages.Error503(w)
1764
return
1765
}
1766
···
1795
}
1796
1797
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
1798
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
1799
+
log.Println("failed to call XRPC repo.tags", xrpcerr)
1800
+
rp.pages.Error503(w)
1801
return
1802
}
1803
···
1868
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
1869
1870
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
1871
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
1872
+
log.Println("failed to call XRPC repo.branches", xrpcerr)
1873
+
rp.pages.Error503(w)
1874
return
1875
}
1876
···
1882
}
1883
1884
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
1885
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
1886
+
log.Println("failed to call XRPC repo.tags", xrpcerr)
1887
+
rp.pages.Error503(w)
1888
return
1889
}
1890
···
1896
}
1897
1898
compareBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo, base, head)
1899
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
1900
+
log.Println("failed to call XRPC repo.compare", xrpcerr)
1901
+
rp.pages.Error503(w)
1902
return
1903
}
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
{
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
+
},
17
"flake-utils": {
18
"inputs": {
19
"systems": "systems"
···
150
},
151
"root": {
152
"inputs": {
153
+
"flake-compat": "flake-compat",
154
"gomod2nix": "gomod2nix",
155
"htmx-src": "htmx-src",
156
"htmx-ws-src": "htmx-ws-src",
+5
flake.nix
+5
flake.nix
···
7
url = "github:nix-community/gomod2nix";
8
inputs.nixpkgs.follows = "nixpkgs";
9
};
10
indigo = {
11
url = "github:oppiliappan/indigo";
12
flake = false;
···
50
inter-fonts-src,
51
sqlite-lib-src,
52
ibm-plex-mono-src,
53
}: let
54
supportedSystems = ["x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin"];
55
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
···
7
url = "github:nix-community/gomod2nix";
8
inputs.nixpkgs.follows = "nixpkgs";
9
};
10
+
flake-compat = {
11
+
url = "https://git.lix.systems/lix-project/flake-compat/archive/main.tar.gz";
12
+
flake = false;
13
+
};
14
indigo = {
15
url = "github:oppiliappan/indigo";
16
flake = false;
···
54
inter-fonts-src,
55
sqlite-lib-src,
56
ibm-plex-mono-src,
57
+
...
58
}: let
59
supportedSystems = ["x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin"];
60
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
+2
-5
input.css
+2
-5
input.css
···
228
}
229
/* LineHighlight */
230
.chroma .hl {
231
-
background-color: #bcc0cc;
232
}
233
/* LineNumbersTable */
234
.chroma .lnt {
235
white-space: pre;
···
864
text-decoration: underline;
865
}
866
}
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
package xrpc
2
3
import (
4
-
"encoding/json"
5
"net/http"
6
"strconv"
7
···
46
response.Cursor = &nextCursor
47
}
48
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
-
}
58
}
+1
-10
knotserver/xrpc/owner.go
+1
-10
knotserver/xrpc/owner.go
···
1
package xrpc
2
3
import (
4
-
"encoding/json"
5
"net/http"
6
7
"tangled.sh/tangled.sh/core/api/tangled"
···
19
Owner: owner,
20
}
21
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
-
}
31
}
+8
-7
knotserver/xrpc/repo_archive.go
+8
-7
knotserver/xrpc/repo_archive.go
···
13
)
14
15
func (x *Xrpc) RepoArchive(w http.ResponseWriter, r *http.Request) {
16
-
repo, repoPath, unescapedRef, err := x.parseStandardParams(r)
17
if err != nil {
18
writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest)
19
return
20
}
21
22
format := r.URL.Query().Get("format")
23
if format == "" {
···
34
return
35
}
36
37
-
gr, err := git.Open(repoPath, unescapedRef)
38
if err != nil {
39
-
writeError(w, xrpcerr.NewXrpcError(
40
-
xrpcerr.WithTag("RefNotFound"),
41
-
xrpcerr.WithMessage("repository or ref not found"),
42
-
), http.StatusNotFound)
43
return
44
}
45
46
repoParts := strings.Split(repo, "/")
47
repoName := repoParts[len(repoParts)-1]
48
49
-
safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(unescapedRef).Short(), "/", "-")
50
51
var archivePrefix string
52
if prefix != "" {
···
13
)
14
15
func (x *Xrpc) RepoArchive(w http.ResponseWriter, r *http.Request) {
16
+
repo := r.URL.Query().Get("repo")
17
+
repoPath, err := x.parseRepoParam(repo)
18
if err != nil {
19
writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest)
20
return
21
}
22
+
23
+
ref := r.URL.Query().Get("ref")
24
+
// ref can be empty (git.Open handles this)
25
26
format := r.URL.Query().Get("format")
27
if format == "" {
···
38
return
39
}
40
41
+
gr, err := git.Open(repoPath, ref)
42
if err != nil {
43
+
writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound)
44
return
45
}
46
47
repoParts := strings.Split(repo, "/")
48
repoName := repoParts[len(repoParts)-1]
49
50
+
safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-")
51
52
var archivePrefix string
53
if prefix != "" {
+8
-15
knotserver/xrpc/repo_blob.go
+8
-15
knotserver/xrpc/repo_blob.go
···
3
import (
4
"crypto/sha256"
5
"encoding/base64"
6
-
"encoding/json"
7
"fmt"
8
"net/http"
9
"path/filepath"
···
16
)
17
18
func (x *Xrpc) RepoBlob(w http.ResponseWriter, r *http.Request) {
19
-
_, repoPath, ref, err := x.parseStandardParams(r)
20
if err != nil {
21
writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest)
22
return
23
}
24
25
treePath := r.URL.Query().Get("path")
26
if treePath == "" {
27
writeError(w, xrpcerr.NewXrpcError(
···
35
36
gr, err := git.Open(repoPath, ref)
37
if err != nil {
38
-
writeError(w, xrpcerr.NewXrpcError(
39
-
xrpcerr.WithTag("RefNotFound"),
40
-
xrpcerr.WithMessage("repository or ref not found"),
41
-
), http.StatusNotFound)
42
return
43
}
44
···
69
return
70
}
71
w.Header().Set("ETag", eTag)
72
73
case strings.HasPrefix(mimeType, "text/"):
74
w.Header().Set("Cache-Control", "public, no-cache")
···
122
response.MimeType = &mimeType
123
}
124
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
-
}
134
}
135
136
// isTextualMimeType returns true if the MIME type represents textual content
···
3
import (
4
"crypto/sha256"
5
"encoding/base64"
6
"fmt"
7
"net/http"
8
"path/filepath"
···
15
)
16
17
func (x *Xrpc) RepoBlob(w http.ResponseWriter, r *http.Request) {
18
+
repo := r.URL.Query().Get("repo")
19
+
repoPath, err := x.parseRepoParam(repo)
20
if err != nil {
21
writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest)
22
return
23
}
24
25
+
ref := r.URL.Query().Get("ref")
26
+
// ref can be empty (git.Open handles this)
27
+
28
treePath := r.URL.Query().Get("path")
29
if treePath == "" {
30
writeError(w, xrpcerr.NewXrpcError(
···
38
39
gr, err := git.Open(repoPath, ref)
40
if err != nil {
41
+
writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound)
42
return
43
}
44
···
69
return
70
}
71
w.Header().Set("ETag", eTag)
72
+
w.Header().Set("Content-Type", mimeType)
73
74
case strings.HasPrefix(mimeType, "text/"):
75
w.Header().Set("Cache-Control", "public, no-cache")
···
123
response.MimeType = &mimeType
124
}
125
126
+
writeJson(w, response)
127
}
128
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
package xrpc
2
3
import (
4
-
"encoding/json"
5
"net/http"
6
"net/url"
7
8
"tangled.sh/tangled.sh/core/api/tangled"
9
"tangled.sh/tangled.sh/core/knotserver/git"
···
31
32
gr, err := git.PlainOpen(repoPath)
33
if err != nil {
34
-
writeError(w, xrpcerr.NewXrpcError(
35
-
xrpcerr.WithTag("RepoNotFound"),
36
-
xrpcerr.WithMessage("repository not found"),
37
-
), http.StatusNotFound)
38
return
39
}
40
···
70
Name: ref.Name().Short(),
71
Hash: ref.Hash().String(),
72
ShortHash: &[]string{ref.Hash().String()[:7]}[0],
73
-
When: commit.Author.When.Format("2006-01-02T15:04:05.000Z"),
74
IsDefault: &isDefault,
75
}
76
···
81
response.Author = &tangled.RepoBranch_Signature{
82
Name: commit.Author.Name,
83
Email: commit.Author.Email,
84
-
When: commit.Author.When.Format("2006-01-02T15:04:05.000Z"),
85
}
86
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
-
}
96
}
···
1
package xrpc
2
3
import (
4
"net/http"
5
"net/url"
6
+
"time"
7
8
"tangled.sh/tangled.sh/core/api/tangled"
9
"tangled.sh/tangled.sh/core/knotserver/git"
···
31
32
gr, err := git.PlainOpen(repoPath)
33
if err != nil {
34
+
writeError(w, xrpcerr.RepoNotFoundError, http.StatusNoContent)
35
return
36
}
37
···
67
Name: ref.Name().Short(),
68
Hash: ref.Hash().String(),
69
ShortHash: &[]string{ref.Hash().String()[:7]}[0],
70
+
When: commit.Author.When.Format(time.RFC3339),
71
IsDefault: &isDefault,
72
}
73
···
78
response.Author = &tangled.RepoBranch_Signature{
79
Name: commit.Author.Name,
80
Email: commit.Author.Email,
81
+
When: commit.Author.When.Format(time.RFC3339),
82
}
83
84
+
writeJson(w, response)
85
}
+11
-25
knotserver/xrpc/repo_branches.go
+11
-25
knotserver/xrpc/repo_branches.go
···
1
package xrpc
2
3
import (
4
-
"encoding/json"
5
"net/http"
6
"strconv"
7
···
20
21
cursor := r.URL.Query().Get("cursor")
22
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
-
}
29
30
gr, err := git.PlainOpen(repoPath)
31
if err != nil {
32
-
writeError(w, xrpcerr.NewXrpcError(
33
-
xrpcerr.WithTag("RepoNotFound"),
34
-
xrpcerr.WithMessage("repository not found"),
35
-
), http.StatusNotFound)
36
return
37
}
38
···
45
}
46
}
47
48
-
end := offset + limit
49
-
if end > len(branches) {
50
-
end = len(branches)
51
-
}
52
53
paginatedBranches := branches[offset:end]
54
···
57
Branches: paginatedBranches,
58
}
59
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
-
}
70
}
···
1
package xrpc
2
3
import (
4
"net/http"
5
"strconv"
6
···
19
20
cursor := r.URL.Query().Get("cursor")
21
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
30
31
gr, err := git.PlainOpen(repoPath)
32
if err != nil {
33
+
writeError(w, xrpcerr.RepoNotFoundError, http.StatusNoContent)
34
return
35
}
36
···
43
}
44
}
45
46
+
end := min(offset+limit, len(branches))
47
48
paginatedBranches := branches[offset:end]
49
···
52
Branches: paginatedBranches,
53
}
54
55
+
writeJson(w, response)
56
}
+7
-23
knotserver/xrpc/repo_compare.go
+7
-23
knotserver/xrpc/repo_compare.go
···
1
package xrpc
2
3
import (
4
-
"encoding/json"
5
"fmt"
6
"net/http"
7
-
"net/url"
8
9
"tangled.sh/tangled.sh/core/knotserver/git"
10
"tangled.sh/tangled.sh/core/types"
···
19
return
20
}
21
22
-
rev1Param := r.URL.Query().Get("rev1")
23
-
if rev1Param == "" {
24
writeError(w, xrpcerr.NewXrpcError(
25
xrpcerr.WithTag("InvalidRequest"),
26
xrpcerr.WithMessage("missing rev1 parameter"),
···
28
return
29
}
30
31
-
rev2Param := r.URL.Query().Get("rev2")
32
-
if rev2Param == "" {
33
writeError(w, xrpcerr.NewXrpcError(
34
xrpcerr.WithTag("InvalidRequest"),
35
xrpcerr.WithMessage("missing rev2 parameter"),
···
37
return
38
}
39
40
-
rev1, _ := url.PathUnescape(rev1Param)
41
-
rev2, _ := url.PathUnescape(rev2Param)
42
-
43
gr, err := git.PlainOpen(repoPath)
44
if err != nil {
45
-
writeError(w, xrpcerr.NewXrpcError(
46
-
xrpcerr.WithTag("RepoNotFound"),
47
-
xrpcerr.WithMessage("repository not found"),
48
-
), http.StatusNotFound)
49
return
50
}
51
···
79
return
80
}
81
82
-
resp := types.RepoFormatPatchResponse{
83
Rev1: commit1.Hash.String(),
84
Rev2: commit2.Hash.String(),
85
FormatPatch: formatPatch,
86
Patch: rawPatch,
87
}
88
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
-
}
98
}
···
1
package xrpc
2
3
import (
4
"fmt"
5
"net/http"
6
7
"tangled.sh/tangled.sh/core/knotserver/git"
8
"tangled.sh/tangled.sh/core/types"
···
17
return
18
}
19
20
+
rev1 := r.URL.Query().Get("rev1")
21
+
if rev1 == "" {
22
writeError(w, xrpcerr.NewXrpcError(
23
xrpcerr.WithTag("InvalidRequest"),
24
xrpcerr.WithMessage("missing rev1 parameter"),
···
26
return
27
}
28
29
+
rev2 := r.URL.Query().Get("rev2")
30
+
if rev2 == "" {
31
writeError(w, xrpcerr.NewXrpcError(
32
xrpcerr.WithTag("InvalidRequest"),
33
xrpcerr.WithMessage("missing rev2 parameter"),
···
35
return
36
}
37
38
gr, err := git.PlainOpen(repoPath)
39
if err != nil {
40
+
writeError(w, xrpcerr.RepoNotFoundError, http.StatusNoContent)
41
return
42
}
43
···
71
return
72
}
73
74
+
response := types.RepoFormatPatchResponse{
75
Rev1: commit1.Hash.String(),
76
Rev2: commit2.Hash.String(),
77
FormatPatch: formatPatch,
78
Patch: rawPatch,
79
}
80
81
+
writeJson(w, response)
82
}
+6
-30
knotserver/xrpc/repo_diff.go
+6
-30
knotserver/xrpc/repo_diff.go
···
1
package xrpc
2
3
import (
4
-
"encoding/json"
5
"net/http"
6
-
"net/url"
7
8
"tangled.sh/tangled.sh/core/knotserver/git"
9
"tangled.sh/tangled.sh/core/types"
···
18
return
19
}
20
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)
31
32
gr, err := git.Open(repoPath, ref)
33
if err != nil {
34
-
writeError(w, xrpcerr.NewXrpcError(
35
-
xrpcerr.WithTag("RefNotFound"),
36
-
xrpcerr.WithMessage("repository or ref not found"),
37
-
), http.StatusNotFound)
38
return
39
}
40
41
diff, err := gr.Diff()
42
if err != nil {
43
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)
48
return
49
}
50
51
-
resp := types.RepoCommitResponse{
52
Ref: ref,
53
Diff: diff,
54
}
55
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
-
}
65
}
···
1
package xrpc
2
3
import (
4
"net/http"
5
6
"tangled.sh/tangled.sh/core/knotserver/git"
7
"tangled.sh/tangled.sh/core/types"
···
16
return
17
}
18
19
+
ref := r.URL.Query().Get("ref")
20
+
// ref can be empty (git.Open handles this)
21
22
gr, err := git.Open(repoPath, ref)
23
if err != nil {
24
+
writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound)
25
return
26
}
27
28
diff, err := gr.Diff()
29
if err != nil {
30
x.Logger.Error("getting diff", "error", err.Error())
31
+
writeError(w, xrpcerr.RefNotFoundError, http.StatusInternalServerError)
32
return
33
}
34
35
+
response := types.RepoCommitResponse{
36
Ref: ref,
37
Diff: diff,
38
}
39
40
+
writeJson(w, response)
41
}
+4
-19
knotserver/xrpc/repo_get_default_branch.go
+4
-19
knotserver/xrpc/repo_get_default_branch.go
···
1
package xrpc
2
3
import (
4
-
"encoding/json"
5
"net/http"
6
7
"tangled.sh/tangled.sh/core/api/tangled"
8
"tangled.sh/tangled.sh/core/knotserver/git"
···
17
return
18
}
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
-
}
28
29
branch, err := gr.FindMainBranch()
30
if err != nil {
···
39
response := tangled.RepoGetDefaultBranch_Output{
40
Name: branch,
41
Hash: "",
42
-
When: "1970-01-01T00:00:00.000Z",
43
}
44
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
-
}
54
}
···
1
package xrpc
2
3
import (
4
"net/http"
5
+
"time"
6
7
"tangled.sh/tangled.sh/core/api/tangled"
8
"tangled.sh/tangled.sh/core/knotserver/git"
···
17
return
18
}
19
20
+
gr, err := git.PlainOpen(repoPath)
21
22
branch, err := gr.FindMainBranch()
23
if err != nil {
···
32
response := tangled.RepoGetDefaultBranch_Output{
33
Name: branch,
34
Hash: "",
35
+
When: time.UnixMicro(0).Format(time.RFC3339),
36
}
37
38
+
writeJson(w, response)
39
}
+4
-21
knotserver/xrpc/repo_languages.go
+4
-21
knotserver/xrpc/repo_languages.go
···
2
3
import (
4
"context"
5
-
"encoding/json"
6
"math"
7
"net/http"
8
-
"net/url"
9
"time"
10
11
"tangled.sh/tangled.sh/core/api/tangled"
···
14
)
15
16
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
repo := r.URL.Query().Get("repo")
24
repoPath, err := x.parseRepoParam(repo)
25
if err != nil {
···
27
return
28
}
29
30
gr, err := git.Open(repoPath, ref)
31
if err != nil {
32
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)
37
return
38
}
39
···
81
response.TotalFiles = &totalFiles
82
}
83
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
-
}
93
}
···
2
3
import (
4
"context"
5
"math"
6
"net/http"
7
"time"
8
9
"tangled.sh/tangled.sh/core/api/tangled"
···
12
)
13
14
func (x *Xrpc) RepoLanguages(w http.ResponseWriter, r *http.Request) {
15
repo := r.URL.Query().Get("repo")
16
repoPath, err := x.parseRepoParam(repo)
17
if err != nil {
···
19
return
20
}
21
22
+
ref := r.URL.Query().Get("ref")
23
+
24
gr, err := git.Open(repoPath, ref)
25
if err != nil {
26
x.Logger.Error("opening repo", "error", err.Error())
27
+
writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound)
28
return
29
}
30
···
72
response.TotalFiles = &totalFiles
73
}
74
75
+
writeJson(w, response)
76
}
+3
-33
knotserver/xrpc/repo_log.go
+3
-33
knotserver/xrpc/repo_log.go
···
1
package xrpc
2
3
import (
4
-
"encoding/json"
5
"net/http"
6
-
"net/url"
7
"strconv"
8
9
"tangled.sh/tangled.sh/core/knotserver/git"
···
19
return
20
}
21
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
-
}
30
31
path := r.URL.Query().Get("path")
32
cursor := r.URL.Query().Get("cursor")
···
38
}
39
}
40
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
gr, err := git.Open(repoPath, ref)
51
if err != nil {
52
-
writeError(w, xrpcerr.NewXrpcError(
53
-
xrpcerr.WithTag("RefNotFound"),
54
-
xrpcerr.WithMessage("repository or ref not found"),
55
-
), http.StatusNotFound)
56
return
57
}
58
···
98
99
response.Log = true
100
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
-
}
111
}
···
1
package xrpc
2
3
import (
4
"net/http"
5
"strconv"
6
7
"tangled.sh/tangled.sh/core/knotserver/git"
···
17
return
18
}
19
20
+
ref := r.URL.Query().Get("ref")
21
22
path := r.URL.Query().Get("path")
23
cursor := r.URL.Query().Get("cursor")
···
29
}
30
}
31
32
gr, err := git.Open(repoPath, ref)
33
if err != nil {
34
+
writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound)
35
return
36
}
37
···
77
78
response.Log = true
79
80
+
writeJson(w, response)
81
}
+6
-33
knotserver/xrpc/repo_tree.go
+6
-33
knotserver/xrpc/repo_tree.go
···
1
package xrpc
2
3
import (
4
-
"encoding/json"
5
"net/http"
6
-
"net/url"
7
"path/filepath"
8
9
"tangled.sh/tangled.sh/core/api/tangled"
10
"tangled.sh/tangled.sh/core/knotserver/git"
···
21
return
22
}
23
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
-
}
32
33
path := r.URL.Query().Get("path")
34
// path can be empty (defaults to root)
35
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
gr, err := git.Open(repoPath, ref)
46
if err != nil {
47
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)
52
return
53
}
54
···
77
entry.Last_commit = &tangled.RepoTree_LastCommit{
78
Hash: file.LastCommit.Hash.String(),
79
Message: file.LastCommit.Message,
80
-
When: file.LastCommit.When.Format("2006-01-02T15:04:05.000Z"),
81
}
82
}
83
···
104
Files: treeEntries,
105
}
106
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
-
}
116
}
···
1
package xrpc
2
3
import (
4
"net/http"
5
"path/filepath"
6
+
"time"
7
8
"tangled.sh/tangled.sh/core/api/tangled"
9
"tangled.sh/tangled.sh/core/knotserver/git"
···
20
return
21
}
22
23
+
ref := r.URL.Query().Get("ref")
24
+
// ref can be empty (git.Open handles this)
25
26
path := r.URL.Query().Get("path")
27
// path can be empty (defaults to root)
28
29
gr, err := git.Open(repoPath, ref)
30
if err != nil {
31
x.Logger.Error("failed to open git repository", "error", err, "path", repoPath, "ref", ref)
32
+
writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound)
33
return
34
}
35
···
58
entry.Last_commit = &tangled.RepoTree_LastCommit{
59
Hash: file.LastCommit.Hash.String(),
60
Message: file.LastCommit.Message,
61
+
When: file.LastCommit.When.Format(time.RFC3339),
62
}
63
}
64
···
85
Files: treeEntries,
86
}
87
88
+
writeJson(w, response)
89
}
+1
-11
knotserver/xrpc/version.go
+1
-11
knotserver/xrpc/version.go
···
1
package xrpc
2
3
import (
4
-
"encoding/json"
5
"fmt"
6
"net/http"
7
"runtime/debug"
8
9
"tangled.sh/tangled.sh/core/api/tangled"
10
-
xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
11
)
12
13
// version is set during build time.
···
58
Version: version,
59
}
60
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
-
}
70
}
+14
-35
knotserver/xrpc/xrpc.go
+14
-35
knotserver/xrpc/xrpc.go
···
4
"encoding/json"
5
"log/slog"
6
"net/http"
7
-
"net/url"
8
"strings"
9
10
securejoin "github.com/cyphar/filepath-securejoin"
···
88
}
89
90
// Parse repo string (did/repoName format)
91
-
parts := strings.Split(repo, "/")
92
-
if len(parts) < 2 {
93
return "", xrpcerr.NewXrpcError(
94
xrpcerr.WithTag("InvalidRequest"),
95
xrpcerr.WithMessage("invalid repo format, expected 'did/repoName'"),
96
)
97
}
98
99
-
did := strings.Join(parts[:len(parts)-1], "/")
100
-
repoName := parts[len(parts)-1]
101
102
// Construct repository path using the same logic as didPath
103
didRepoPath, err := securejoin.SecureJoin(did, repoName)
104
if err != nil {
105
-
return "", xrpcerr.NewXrpcError(
106
-
xrpcerr.WithTag("RepoNotFound"),
107
-
xrpcerr.WithMessage("failed to access repository"),
108
-
)
109
}
110
111
repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, didRepoPath)
112
if err != nil {
113
-
return "", xrpcerr.NewXrpcError(
114
-
xrpcerr.WithTag("RepoNotFound"),
115
-
xrpcerr.WithMessage("failed to access repository"),
116
-
)
117
}
118
119
return repoPath, nil
120
}
121
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
func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) {
145
w.Header().Set("Content-Type", "application/json")
146
w.WriteHeader(status)
147
json.NewEncoder(w).Encode(e)
148
}
···
4
"encoding/json"
5
"log/slog"
6
"net/http"
7
"strings"
8
9
securejoin "github.com/cyphar/filepath-securejoin"
···
87
}
88
89
// Parse repo string (did/repoName format)
90
+
parts := strings.SplitN(repo, "/", 2)
91
+
if len(parts) != 2 {
92
return "", xrpcerr.NewXrpcError(
93
xrpcerr.WithTag("InvalidRequest"),
94
xrpcerr.WithMessage("invalid repo format, expected 'did/repoName'"),
95
)
96
}
97
98
+
did := parts[0]
99
+
repoName := parts[1]
100
101
// Construct repository path using the same logic as didPath
102
didRepoPath, err := securejoin.SecureJoin(did, repoName)
103
if err != nil {
104
+
return "", xrpcerr.RepoNotFoundError
105
}
106
107
repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, didRepoPath)
108
if err != nil {
109
+
return "", xrpcerr.RepoNotFoundError
110
}
111
112
return repoPath, nil
113
}
114
115
func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) {
116
w.Header().Set("Content-Type", "application/json")
117
w.WriteHeader(status)
118
json.NewEncoder(w).Encode(e)
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
WithMessage("owner not set for this service"),
57
)
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
+
69
var AuthError = func(err error) XrpcError {
70
return NewXrpcError(
71
WithTag("Auth"),