+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
+9
-7
appview/repo/index.go
+9
-7
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 {
···
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
···
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 {
···
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
···
294
defer wg.Done()
295
tagsBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repo)
296
if err != nil {
297
+
errs = errors.Join(errs, fmt.Errorf("failed to call repoTags: %w", err))
298
return
299
}
300
301
if err := json.Unmarshal(tagsBytes, &tagsResp); err != nil {
302
+
errs = errors.Join(errs, fmt.Errorf("failed to unmarshal repoTags: %w", err))
303
}
304
}()
305
···
309
defer wg.Done()
310
resp, err := tangled.RepoTree(ctx, xrpcc, "", ref, repo)
311
if err != nil {
312
+
errs = errors.Join(errs, fmt.Errorf("failed to call repoTree: %w", err))
313
return
314
}
315
treeResp = resp
···
321
defer wg.Done()
322
logBytes, err := tangled.RepoLog(ctx, xrpcc, "", 50, "", ref, repo)
323
if err != nil {
324
+
errs = errors.Join(errs, fmt.Errorf("failed to call repoLog: %w", err))
325
return
326
}
327
328
if err := json.Unmarshal(logBytes, &logResp); err != nil {
329
+
errs = errors.Join(errs, fmt.Errorf("failed to unmarshal repoLog: %w", err))
330
}
331
}()
332
+97
-118
appview/repo/repo.go
+97
-118
appview/repo/repo.go
···
85
}
86
87
func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) {
88
-
refParam := chi.URLParam(r, "ref")
89
f, err := rp.repoResolver.Resolve(r)
90
if err != nil {
91
log.Println("failed to get repo and knot", err)
···
102
}
103
104
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
105
-
archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", refParam, repo)
106
-
if err != nil {
107
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
108
-
log.Println("failed to call XRPC repo.archive", xrpcerr)
109
-
rp.pages.Error503(w)
110
-
return
111
-
}
112
-
rp.pages.Error404(w)
113
return
114
}
115
116
-
// Set headers for file download
117
-
filename := fmt.Sprintf("%s-%s.tar.gz", f.Name, refParam)
118
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
119
w.Header().Set("Content-Type", "application/gzip")
120
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(archiveBytes)))
···
139
}
140
141
ref := chi.URLParam(r, "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 err != nil {
163
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
164
-
log.Println("failed to call XRPC repo.log", xrpcerr)
165
-
rp.pages.Error503(w)
166
-
return
167
-
}
168
-
rp.pages.Error404(w)
169
return
170
}
171
···
177
}
178
179
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
180
-
if err != nil {
181
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
182
-
log.Println("failed to call XRPC repo.tags", xrpcerr)
183
-
rp.pages.Error503(w)
184
-
return
185
-
}
186
}
187
188
tagMap := make(map[string][]string)
···
196
}
197
198
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
199
-
if err != nil {
200
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
201
-
log.Println("failed to call XRPC repo.branches", xrpcerr)
202
-
rp.pages.Error503(w)
203
-
return
204
-
}
205
}
206
207
if branchBytes != nil {
···
353
return
354
}
355
ref := chi.URLParam(r, "ref")
356
357
var diffOpts types.DiffOpts
358
if d := r.URL.Query().Get("diff"); d == "split" {
···
375
376
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
377
xrpcBytes, err := tangled.RepoDiff(r.Context(), xrpcc, ref, repo)
378
-
if err != nil {
379
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
380
-
log.Println("failed to call XRPC repo.diff", xrpcerr)
381
-
rp.pages.Error503(w)
382
-
return
383
-
}
384
-
rp.pages.Error404(w)
385
return
386
}
387
···
433
}
434
435
ref := chi.URLParam(r, "ref")
436
-
treePath := chi.URLParam(r, "*")
437
438
// if the tree path has a trailing slash, let's strip it
439
// so we don't 404
440
treePath = strings.TrimSuffix(treePath, "/")
441
442
scheme := "http"
···
450
451
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
452
xrpcResp, err := tangled.RepoTree(r.Context(), xrpcc, treePath, ref, repo)
453
-
if err != nil {
454
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
455
-
log.Println("failed to call XRPC repo.tree", xrpcerr)
456
-
rp.pages.Error503(w)
457
-
return
458
-
}
459
-
rp.pages.Error404(w)
460
return
461
}
462
···
498
499
// redirects tree paths trying to access a blob; in this case the result.Files is unpopulated,
500
// so we can safely redirect to the "parent" (which is the same file).
501
-
unescapedTreePath, _ := url.PathUnescape(treePath)
502
-
if len(result.Files) == 0 && result.Parent == unescapedTreePath {
503
-
http.Redirect(w, r, fmt.Sprintf("/%s/blob/%s/%s", f.OwnerSlashRepo(), ref, result.Parent), http.StatusFound)
504
return
505
}
506
507
user := rp.oauth.GetUser(r)
508
509
var breadcrumbs [][]string
510
-
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)})
511
if treePath != "" {
512
for idx, elem := range strings.Split(treePath, "/") {
513
-
breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)})
514
}
515
}
516
···
543
544
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
545
xrpcBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
546
-
if err != nil {
547
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
548
-
log.Println("failed to call XRPC repo.tags", xrpcerr)
549
-
rp.pages.Error503(w)
550
-
return
551
-
}
552
-
rp.pages.Error404(w)
553
return
554
}
555
···
616
617
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
618
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
619
-
if err != nil {
620
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
621
-
log.Println("failed to call XRPC repo.branches", xrpcerr)
622
-
rp.pages.Error503(w)
623
-
return
624
-
}
625
-
rp.pages.Error404(w)
626
return
627
}
628
···
651
}
652
653
ref := chi.URLParam(r, "ref")
654
filePath := chi.URLParam(r, "*")
655
656
scheme := "http"
657
if !rp.config.Core.Dev {
···
664
665
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name)
666
resp, err := tangled.RepoBlob(r.Context(), xrpcc, filePath, false, ref, repo)
667
-
if err != nil {
668
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
669
-
log.Println("failed to call XRPC repo.blob", xrpcerr)
670
-
rp.pages.Error503(w)
671
-
return
672
-
}
673
-
rp.pages.Error404(w)
674
return
675
}
676
677
// Use XRPC response directly instead of converting to internal types
678
679
var breadcrumbs [][]string
680
-
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)})
681
if filePath != "" {
682
for idx, elem := range strings.Split(filePath, "/") {
683
-
breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)})
684
}
685
}
686
···
710
711
// fetch the raw binary content using sh.tangled.repo.blob xrpc
712
repoName := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
713
-
blobURL := fmt.Sprintf("%s://%s/xrpc/sh.tangled.repo.blob?repo=%s&ref=%s&path=%s&raw=true",
714
-
scheme, f.Knot, url.QueryEscape(repoName), url.QueryEscape(ref), url.QueryEscape(filePath))
715
716
contentSrc = blobURL
717
if !rp.config.Core.Dev {
···
766
}
767
768
ref := chi.URLParam(r, "ref")
769
filePath := chi.URLParam(r, "*")
770
771
scheme := "http"
772
if !rp.config.Core.Dev {
···
774
}
775
776
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name)
777
-
blobURL := fmt.Sprintf("%s://%s/xrpc/sh.tangled.repo.blob?repo=%s&ref=%s&path=%s&raw=true",
778
-
scheme, f.Knot, url.QueryEscape(repo), url.QueryEscape(ref), url.QueryEscape(filePath))
779
780
req, err := http.NewRequest("GET", blobURL, nil)
781
if err != nil {
···
1364
1365
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
1366
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
1367
-
if err != nil {
1368
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
1369
-
log.Println("failed to call XRPC repo.branches", xrpcerr)
1370
-
rp.pages.Error503(w)
1371
-
return
1372
-
}
1373
rp.pages.Error503(w)
1374
return
1375
}
···
1471
1472
func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) {
1473
ref := chi.URLParam(r, "ref")
1474
1475
user := rp.oauth.GetUser(r)
1476
f, err := rp.repoResolver.Resolve(r)
···
1759
1760
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
1761
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
1762
-
if err != nil {
1763
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
1764
-
log.Println("failed to call XRPC repo.branches", xrpcerr)
1765
-
rp.pages.Error503(w)
1766
-
return
1767
-
}
1768
-
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
1769
return
1770
}
1771
···
1800
}
1801
1802
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
1803
-
if err != nil {
1804
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
1805
-
log.Println("failed to call XRPC repo.tags", xrpcerr)
1806
-
rp.pages.Error503(w)
1807
-
return
1808
-
}
1809
-
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
1810
return
1811
}
1812
···
1877
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
1878
1879
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
1880
-
if err != nil {
1881
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
1882
-
log.Println("failed to call XRPC repo.branches", xrpcerr)
1883
-
rp.pages.Error503(w)
1884
-
return
1885
-
}
1886
-
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
1887
return
1888
}
1889
···
1895
}
1896
1897
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
1898
-
if err != nil {
1899
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
1900
-
log.Println("failed to call XRPC repo.tags", xrpcerr)
1901
-
rp.pages.Error503(w)
1902
-
return
1903
-
}
1904
-
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
1905
return
1906
}
1907
···
1913
}
1914
1915
compareBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo, base, head)
1916
-
if err != nil {
1917
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
1918
-
log.Println("failed to call XRPC repo.compare", xrpcerr)
1919
-
rp.pages.Error503(w)
1920
-
return
1921
-
}
1922
-
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
1923
return
1924
}
1925
···
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
···
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
+7
-3
knotserver/xrpc/repo_archive.go
+7
-3
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 == "" {
24
format = "tar.gz" // default
···
34
return
35
}
36
37
-
gr, err := git.Open(repoPath, unescapedRef)
38
if err != nil {
39
writeError(w, xrpcerr.NewXrpcError(
40
xrpcerr.WithTag("RefNotFound"),
···
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 == "" {
28
format = "tar.gz" // default
···
38
return
39
}
40
41
+
gr, err := git.Open(repoPath, ref)
42
if err != nil {
43
writeError(w, xrpcerr.NewXrpcError(
44
xrpcerr.WithTag("RefNotFound"),
···
50
repoParts := strings.Split(repo, "/")
51
repoName := repoParts[len(repoParts)-1]
52
53
+
safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-")
54
55
var archivePrefix string
56
if prefix != "" {
+5
-1
knotserver/xrpc/repo_blob.go
+5
-1
knotserver/xrpc/repo_blob.go
···
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 == "" {
···
16
)
17
18
func (x *Xrpc) RepoBlob(w http.ResponseWriter, r *http.Request) {
19
+
repo := r.URL.Query().Get("repo")
20
+
repoPath, err := x.parseRepoParam(repo)
21
if err != nil {
22
writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest)
23
return
24
}
25
+
26
+
ref := r.URL.Query().Get("ref")
27
+
// ref can be empty (git.Open handles this)
28
29
treePath := r.URL.Query().Get("path")
30
if treePath == "" {
+3
-2
knotserver/xrpc/repo_branch.go
+3
-2
knotserver/xrpc/repo_branch.go
···
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"
···
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")
···
4
"encoding/json"
5
"net/http"
6
"net/url"
7
+
"time"
8
9
"tangled.sh/tangled.sh/core/api/tangled"
10
"tangled.sh/tangled.sh/core/knotserver/git"
···
71
Name: ref.Name().Short(),
72
Hash: ref.Hash().String(),
73
ShortHash: &[]string{ref.Hash().String()[:7]}[0],
74
+
When: commit.Author.When.Format(time.RFC3339),
75
IsDefault: &isDefault,
76
}
77
···
82
response.Author = &tangled.RepoBranch_Signature{
83
Name: commit.Author.Name,
84
Email: commit.Author.Email,
85
+
When: commit.Author.When.Format(time.RFC3339),
86
}
87
88
w.Header().Set("Content-Type", "application/json")
+1
-4
knotserver/xrpc/repo_branches.go
+1
-4
knotserver/xrpc/repo_branches.go
+4
-8
knotserver/xrpc/repo_compare.go
+4
-8
knotserver/xrpc/repo_compare.go
···
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"),
36
), http.StatusBadRequest)
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 {
···
4
"encoding/json"
5
"fmt"
6
"net/http"
7
8
"tangled.sh/tangled.sh/core/knotserver/git"
9
"tangled.sh/tangled.sh/core/types"
···
18
return
19
}
20
21
+
rev1 := r.URL.Query().Get("rev1")
22
+
if rev1 == "" {
23
writeError(w, xrpcerr.NewXrpcError(
24
xrpcerr.WithTag("InvalidRequest"),
25
xrpcerr.WithMessage("missing rev1 parameter"),
···
27
return
28
}
29
30
+
rev2 := r.URL.Query().Get("rev2")
31
+
if rev2 == "" {
32
writeError(w, xrpcerr.NewXrpcError(
33
xrpcerr.WithTag("InvalidRequest"),
34
xrpcerr.WithMessage("missing rev2 parameter"),
35
), http.StatusBadRequest)
36
return
37
}
38
39
gr, err := git.PlainOpen(repoPath)
40
if err != nil {
+2
-11
knotserver/xrpc/repo_diff.go
+2
-11
knotserver/xrpc/repo_diff.go
···
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 {
+3
-9
knotserver/xrpc/repo_get_default_branch.go
+3
-9
knotserver/xrpc/repo_get_default_branch.go
···
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")
···
3
import (
4
"encoding/json"
5
"net/http"
6
+
"time"
7
8
"tangled.sh/tangled.sh/core/api/tangled"
9
"tangled.sh/tangled.sh/core/knotserver/git"
···
18
return
19
}
20
21
+
gr, err := git.PlainOpen(repoPath)
22
23
branch, err := gr.FindMainBranch()
24
if err != nil {
···
33
response := tangled.RepoGetDefaultBranch_Output{
34
Name: branch,
35
Hash: "",
36
+
When: time.UnixMicro(0).Format(time.RFC3339),
37
}
38
39
w.Header().Set("Content-Type", "application/json")
+2
-7
knotserver/xrpc/repo_languages.go
+2
-7
knotserver/xrpc/repo_languages.go
···
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 {
26
writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest)
27
return
28
}
29
30
gr, err := git.Open(repoPath, ref)
31
if err != nil {
···
5
"encoding/json"
6
"math"
7
"net/http"
8
"time"
9
10
"tangled.sh/tangled.sh/core/api/tangled"
···
13
)
14
15
func (x *Xrpc) RepoLanguages(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
25
gr, err := git.Open(repoPath, ref)
26
if err != nil {
+1
-18
knotserver/xrpc/repo_log.go
+1
-18
knotserver/xrpc/repo_log.go
···
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")
···
36
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
37
limit = l
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)
···
3
import (
4
"encoding/json"
5
"net/http"
6
"strconv"
7
8
"tangled.sh/tangled.sh/core/knotserver/git"
···
18
return
19
}
20
21
+
ref := r.URL.Query().Get("ref")
22
23
path := r.URL.Query().Get("path")
24
cursor := r.URL.Query().Get("cursor")
···
28
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
29
limit = l
30
}
31
}
32
33
gr, err := git.Open(repoPath, ref)
+4
-19
knotserver/xrpc/repo_tree.go
+4
-19
knotserver/xrpc/repo_tree.go
···
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)
···
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
···
3
import (
4
"encoding/json"
5
"net/http"
6
"path/filepath"
7
+
"time"
8
9
"tangled.sh/tangled.sh/core/api/tangled"
10
"tangled.sh/tangled.sh/core/knotserver/git"
···
21
return
22
}
23
24
+
ref := r.URL.Query().Get("ref")
25
+
// ref can be empty (git.Open handles this)
26
27
path := r.URL.Query().Get("path")
28
// path can be empty (defaults to root)
29
30
gr, err := git.Open(repoPath, ref)
31
if err != nil {
32
x.Logger.Error("failed to open git repository", "error", err, "path", repoPath, "ref", ref)
···
62
entry.Last_commit = &tangled.RepoTree_LastCommit{
63
Hash: file.LastCommit.Hash.String(),
64
Message: file.LastCommit.Message,
65
+
When: file.LastCommit.When.Format(time.RFC3339),
66
}
67
}
68
+4
-27
knotserver/xrpc/xrpc.go
+4
-27
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)
···
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) {
···
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)
···
116
}
117
118
return repoPath, nil
119
}
120
121
func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) {