Signed-off-by: oppiliappan me@oppi.li
+49
appview/repo/archive.go
+49
appview/repo/archive.go
···
1
+
package repo
2
+
3
+
import (
4
+
"fmt"
5
+
"net/http"
6
+
"net/url"
7
+
"strings"
8
+
9
+
"tangled.org/core/api/tangled"
10
+
xrpcclient "tangled.org/core/appview/xrpcclient"
11
+
12
+
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
13
+
"github.com/go-chi/chi/v5"
14
+
"github.com/go-git/go-git/v5/plumbing"
15
+
)
16
+
17
+
func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) {
18
+
l := rp.logger.With("handler", "DownloadArchive")
19
+
ref := chi.URLParam(r, "ref")
20
+
ref, _ = url.PathUnescape(ref)
21
+
f, err := rp.repoResolver.Resolve(r)
22
+
if err != nil {
23
+
l.Error("failed to get repo and knot", "err", err)
24
+
return
25
+
}
26
+
scheme := "http"
27
+
if !rp.config.Core.Dev {
28
+
scheme = "https"
29
+
}
30
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
31
+
xrpcc := &indigoxrpc.Client{
32
+
Host: host,
33
+
}
34
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
35
+
archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, repo)
36
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
37
+
l.Error("failed to call XRPC repo.archive", "err", xrpcerr)
38
+
rp.pages.Error503(w)
39
+
return
40
+
}
41
+
// Set headers for file download, just pass along whatever the knot specifies
42
+
safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-")
43
+
filename := fmt.Sprintf("%s-%s.tar.gz", f.Name, safeRefFilename)
44
+
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
45
+
w.Header().Set("Content-Type", "application/gzip")
46
+
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(archiveBytes)))
47
+
// Write the archive data directly
48
+
w.Write(archiveBytes)
49
+
}
+219
appview/repo/blob.go
+219
appview/repo/blob.go
···
1
+
package repo
2
+
3
+
import (
4
+
"fmt"
5
+
"io"
6
+
"net/http"
7
+
"net/url"
8
+
"path/filepath"
9
+
"slices"
10
+
"strings"
11
+
12
+
"tangled.org/core/api/tangled"
13
+
"tangled.org/core/appview/pages"
14
+
"tangled.org/core/appview/pages/markup"
15
+
xrpcclient "tangled.org/core/appview/xrpcclient"
16
+
17
+
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
18
+
"github.com/go-chi/chi/v5"
19
+
)
20
+
21
+
func (rp *Repo) Blob(w http.ResponseWriter, r *http.Request) {
22
+
l := rp.logger.With("handler", "RepoBlob")
23
+
f, err := rp.repoResolver.Resolve(r)
24
+
if err != nil {
25
+
l.Error("failed to get repo and knot", "err", err)
26
+
return
27
+
}
28
+
ref := chi.URLParam(r, "ref")
29
+
ref, _ = url.PathUnescape(ref)
30
+
filePath := chi.URLParam(r, "*")
31
+
filePath, _ = url.PathUnescape(filePath)
32
+
scheme := "http"
33
+
if !rp.config.Core.Dev {
34
+
scheme = "https"
35
+
}
36
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
37
+
xrpcc := &indigoxrpc.Client{
38
+
Host: host,
39
+
}
40
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name)
41
+
resp, err := tangled.RepoBlob(r.Context(), xrpcc, filePath, false, ref, repo)
42
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
43
+
l.Error("failed to call XRPC repo.blob", "err", xrpcerr)
44
+
rp.pages.Error503(w)
45
+
return
46
+
}
47
+
// Use XRPC response directly instead of converting to internal types
48
+
var breadcrumbs [][]string
49
+
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), url.PathEscape(ref))})
50
+
if filePath != "" {
51
+
for idx, elem := range strings.Split(filePath, "/") {
52
+
breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))})
53
+
}
54
+
}
55
+
showRendered := false
56
+
renderToggle := false
57
+
if markup.GetFormat(resp.Path) == markup.FormatMarkdown {
58
+
renderToggle = true
59
+
showRendered = r.URL.Query().Get("code") != "true"
60
+
}
61
+
var unsupported bool
62
+
var isImage bool
63
+
var isVideo bool
64
+
var contentSrc string
65
+
if resp.IsBinary != nil && *resp.IsBinary {
66
+
ext := strings.ToLower(filepath.Ext(resp.Path))
67
+
switch ext {
68
+
case ".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp":
69
+
isImage = true
70
+
case ".mp4", ".webm", ".ogg", ".mov", ".avi":
71
+
isVideo = true
72
+
default:
73
+
unsupported = true
74
+
}
75
+
// fetch the raw binary content using sh.tangled.repo.blob xrpc
76
+
repoName := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
77
+
baseURL := &url.URL{
78
+
Scheme: scheme,
79
+
Host: f.Knot,
80
+
Path: "/xrpc/sh.tangled.repo.blob",
81
+
}
82
+
query := baseURL.Query()
83
+
query.Set("repo", repoName)
84
+
query.Set("ref", ref)
85
+
query.Set("path", filePath)
86
+
query.Set("raw", "true")
87
+
baseURL.RawQuery = query.Encode()
88
+
blobURL := baseURL.String()
89
+
contentSrc = blobURL
90
+
if !rp.config.Core.Dev {
91
+
contentSrc = markup.GenerateCamoURL(rp.config.Camo.Host, rp.config.Camo.SharedSecret, blobURL)
92
+
}
93
+
}
94
+
lines := 0
95
+
if resp.IsBinary == nil || !*resp.IsBinary {
96
+
lines = strings.Count(resp.Content, "\n") + 1
97
+
}
98
+
var sizeHint uint64
99
+
if resp.Size != nil {
100
+
sizeHint = uint64(*resp.Size)
101
+
} else {
102
+
sizeHint = uint64(len(resp.Content))
103
+
}
104
+
user := rp.oauth.GetUser(r)
105
+
// Determine if content is binary (dereference pointer)
106
+
isBinary := false
107
+
if resp.IsBinary != nil {
108
+
isBinary = *resp.IsBinary
109
+
}
110
+
rp.pages.RepoBlob(w, pages.RepoBlobParams{
111
+
LoggedInUser: user,
112
+
RepoInfo: f.RepoInfo(user),
113
+
BreadCrumbs: breadcrumbs,
114
+
ShowRendered: showRendered,
115
+
RenderToggle: renderToggle,
116
+
Unsupported: unsupported,
117
+
IsImage: isImage,
118
+
IsVideo: isVideo,
119
+
ContentSrc: contentSrc,
120
+
RepoBlob_Output: resp,
121
+
Contents: resp.Content,
122
+
Lines: lines,
123
+
SizeHint: sizeHint,
124
+
IsBinary: isBinary,
125
+
})
126
+
}
127
+
128
+
func (rp *Repo) RepoBlobRaw(w http.ResponseWriter, r *http.Request) {
129
+
l := rp.logger.With("handler", "RepoBlobRaw")
130
+
f, err := rp.repoResolver.Resolve(r)
131
+
if err != nil {
132
+
l.Error("failed to get repo and knot", "err", err)
133
+
w.WriteHeader(http.StatusBadRequest)
134
+
return
135
+
}
136
+
ref := chi.URLParam(r, "ref")
137
+
ref, _ = url.PathUnescape(ref)
138
+
filePath := chi.URLParam(r, "*")
139
+
filePath, _ = url.PathUnescape(filePath)
140
+
scheme := "http"
141
+
if !rp.config.Core.Dev {
142
+
scheme = "https"
143
+
}
144
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name)
145
+
baseURL := &url.URL{
146
+
Scheme: scheme,
147
+
Host: f.Knot,
148
+
Path: "/xrpc/sh.tangled.repo.blob",
149
+
}
150
+
query := baseURL.Query()
151
+
query.Set("repo", repo)
152
+
query.Set("ref", ref)
153
+
query.Set("path", filePath)
154
+
query.Set("raw", "true")
155
+
baseURL.RawQuery = query.Encode()
156
+
blobURL := baseURL.String()
157
+
req, err := http.NewRequest("GET", blobURL, nil)
158
+
if err != nil {
159
+
l.Error("failed to create request", "err", err)
160
+
return
161
+
}
162
+
// forward the If-None-Match header
163
+
if clientETag := r.Header.Get("If-None-Match"); clientETag != "" {
164
+
req.Header.Set("If-None-Match", clientETag)
165
+
}
166
+
client := &http.Client{}
167
+
resp, err := client.Do(req)
168
+
if err != nil {
169
+
l.Error("failed to reach knotserver", "err", err)
170
+
rp.pages.Error503(w)
171
+
return
172
+
}
173
+
defer resp.Body.Close()
174
+
// forward 304 not modified
175
+
if resp.StatusCode == http.StatusNotModified {
176
+
w.WriteHeader(http.StatusNotModified)
177
+
return
178
+
}
179
+
if resp.StatusCode != http.StatusOK {
180
+
l.Error("knotserver returned non-OK status for raw blob", "url", blobURL, "statuscode", resp.StatusCode)
181
+
w.WriteHeader(resp.StatusCode)
182
+
_, _ = io.Copy(w, resp.Body)
183
+
return
184
+
}
185
+
contentType := resp.Header.Get("Content-Type")
186
+
body, err := io.ReadAll(resp.Body)
187
+
if err != nil {
188
+
l.Error("error reading response body from knotserver", "err", err)
189
+
w.WriteHeader(http.StatusInternalServerError)
190
+
return
191
+
}
192
+
if strings.HasPrefix(contentType, "text/") || isTextualMimeType(contentType) {
193
+
// serve all textual content as text/plain
194
+
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
195
+
w.Write(body)
196
+
} else if strings.HasPrefix(contentType, "image/") || strings.HasPrefix(contentType, "video/") {
197
+
// serve images and videos with their original content type
198
+
w.Header().Set("Content-Type", contentType)
199
+
w.Write(body)
200
+
} else {
201
+
w.WriteHeader(http.StatusUnsupportedMediaType)
202
+
w.Write([]byte("unsupported content type"))
203
+
return
204
+
}
205
+
}
206
+
207
+
func isTextualMimeType(mimeType string) bool {
208
+
textualTypes := []string{
209
+
"application/json",
210
+
"application/xml",
211
+
"application/yaml",
212
+
"application/x-yaml",
213
+
"application/toml",
214
+
"application/javascript",
215
+
"application/ecmascript",
216
+
"message/",
217
+
}
218
+
return slices.Contains(textualTypes, mimeType)
219
+
}
+95
appview/repo/branches.go
+95
appview/repo/branches.go
···
1
+
package repo
2
+
3
+
import (
4
+
"encoding/json"
5
+
"fmt"
6
+
"net/http"
7
+
8
+
"tangled.org/core/api/tangled"
9
+
"tangled.org/core/appview/oauth"
10
+
"tangled.org/core/appview/pages"
11
+
xrpcclient "tangled.org/core/appview/xrpcclient"
12
+
"tangled.org/core/types"
13
+
14
+
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
15
+
)
16
+
17
+
func (rp *Repo) Branches(w http.ResponseWriter, r *http.Request) {
18
+
l := rp.logger.With("handler", "RepoBranches")
19
+
f, err := rp.repoResolver.Resolve(r)
20
+
if err != nil {
21
+
l.Error("failed to get repo and knot", "err", err)
22
+
return
23
+
}
24
+
scheme := "http"
25
+
if !rp.config.Core.Dev {
26
+
scheme = "https"
27
+
}
28
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
29
+
xrpcc := &indigoxrpc.Client{
30
+
Host: host,
31
+
}
32
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
33
+
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
34
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
35
+
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
36
+
rp.pages.Error503(w)
37
+
return
38
+
}
39
+
var result types.RepoBranchesResponse
40
+
if err := json.Unmarshal(xrpcBytes, &result); err != nil {
41
+
l.Error("failed to decode XRPC response", "err", err)
42
+
rp.pages.Error503(w)
43
+
return
44
+
}
45
+
sortBranches(result.Branches)
46
+
user := rp.oauth.GetUser(r)
47
+
rp.pages.RepoBranches(w, pages.RepoBranchesParams{
48
+
LoggedInUser: user,
49
+
RepoInfo: f.RepoInfo(user),
50
+
RepoBranchesResponse: result,
51
+
})
52
+
}
53
+
54
+
func (rp *Repo) DeleteBranch(w http.ResponseWriter, r *http.Request) {
55
+
l := rp.logger.With("handler", "DeleteBranch")
56
+
f, err := rp.repoResolver.Resolve(r)
57
+
if err != nil {
58
+
l.Error("failed to get repo and knot", "err", err)
59
+
return
60
+
}
61
+
noticeId := "delete-branch-error"
62
+
fail := func(msg string, err error) {
63
+
l.Error(msg, "err", err)
64
+
rp.pages.Notice(w, noticeId, msg)
65
+
}
66
+
branch := r.FormValue("branch")
67
+
if branch == "" {
68
+
fail("No branch provided.", nil)
69
+
return
70
+
}
71
+
client, err := rp.oauth.ServiceClient(
72
+
r,
73
+
oauth.WithService(f.Knot),
74
+
oauth.WithLxm(tangled.RepoDeleteBranchNSID),
75
+
oauth.WithDev(rp.config.Core.Dev),
76
+
)
77
+
if err != nil {
78
+
fail("Failed to connect to knotserver", nil)
79
+
return
80
+
}
81
+
err = tangled.RepoDeleteBranch(
82
+
r.Context(),
83
+
client,
84
+
&tangled.RepoDeleteBranch_Input{
85
+
Branch: branch,
86
+
Repo: f.RepoAt().String(),
87
+
},
88
+
)
89
+
if err := xrpcclient.HandleXrpcErr(err); err != nil {
90
+
fail(fmt.Sprintf("Failed to delete branch: %s", err), err)
91
+
return
92
+
}
93
+
l.Error("deleted branch from knot", "branch", branch, "repo", f.RepoAt())
94
+
rp.pages.HxRefresh(w)
95
+
}
+214
appview/repo/compare.go
+214
appview/repo/compare.go
···
1
+
package repo
2
+
3
+
import (
4
+
"encoding/json"
5
+
"fmt"
6
+
"net/http"
7
+
"net/url"
8
+
"strings"
9
+
10
+
"tangled.org/core/api/tangled"
11
+
"tangled.org/core/appview/pages"
12
+
xrpcclient "tangled.org/core/appview/xrpcclient"
13
+
"tangled.org/core/patchutil"
14
+
"tangled.org/core/types"
15
+
16
+
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
17
+
"github.com/go-chi/chi/v5"
18
+
)
19
+
20
+
func (rp *Repo) CompareNew(w http.ResponseWriter, r *http.Request) {
21
+
l := rp.logger.With("handler", "RepoCompareNew")
22
+
23
+
user := rp.oauth.GetUser(r)
24
+
f, err := rp.repoResolver.Resolve(r)
25
+
if err != nil {
26
+
l.Error("failed to get repo and knot", "err", err)
27
+
return
28
+
}
29
+
30
+
scheme := "http"
31
+
if !rp.config.Core.Dev {
32
+
scheme = "https"
33
+
}
34
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
35
+
xrpcc := &indigoxrpc.Client{
36
+
Host: host,
37
+
}
38
+
39
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
40
+
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
41
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
42
+
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
43
+
rp.pages.Error503(w)
44
+
return
45
+
}
46
+
47
+
var branchResult types.RepoBranchesResponse
48
+
if err := json.Unmarshal(branchBytes, &branchResult); err != nil {
49
+
l.Error("failed to decode XRPC branches response", "err", err)
50
+
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
51
+
return
52
+
}
53
+
branches := branchResult.Branches
54
+
55
+
sortBranches(branches)
56
+
57
+
var defaultBranch string
58
+
for _, b := range branches {
59
+
if b.IsDefault {
60
+
defaultBranch = b.Name
61
+
}
62
+
}
63
+
64
+
base := defaultBranch
65
+
head := defaultBranch
66
+
67
+
params := r.URL.Query()
68
+
queryBase := params.Get("base")
69
+
queryHead := params.Get("head")
70
+
if queryBase != "" {
71
+
base = queryBase
72
+
}
73
+
if queryHead != "" {
74
+
head = queryHead
75
+
}
76
+
77
+
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
78
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
79
+
l.Error("failed to call XRPC repo.tags", "err", xrpcerr)
80
+
rp.pages.Error503(w)
81
+
return
82
+
}
83
+
84
+
var tags types.RepoTagsResponse
85
+
if err := json.Unmarshal(tagBytes, &tags); err != nil {
86
+
l.Error("failed to decode XRPC tags response", "err", err)
87
+
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
88
+
return
89
+
}
90
+
91
+
repoinfo := f.RepoInfo(user)
92
+
93
+
rp.pages.RepoCompareNew(w, pages.RepoCompareNewParams{
94
+
LoggedInUser: user,
95
+
RepoInfo: repoinfo,
96
+
Branches: branches,
97
+
Tags: tags.Tags,
98
+
Base: base,
99
+
Head: head,
100
+
})
101
+
}
102
+
103
+
func (rp *Repo) Compare(w http.ResponseWriter, r *http.Request) {
104
+
l := rp.logger.With("handler", "RepoCompare")
105
+
106
+
user := rp.oauth.GetUser(r)
107
+
f, err := rp.repoResolver.Resolve(r)
108
+
if err != nil {
109
+
l.Error("failed to get repo and knot", "err", err)
110
+
return
111
+
}
112
+
113
+
var diffOpts types.DiffOpts
114
+
if d := r.URL.Query().Get("diff"); d == "split" {
115
+
diffOpts.Split = true
116
+
}
117
+
118
+
// if user is navigating to one of
119
+
// /compare/{base}/{head}
120
+
// /compare/{base}...{head}
121
+
base := chi.URLParam(r, "base")
122
+
head := chi.URLParam(r, "head")
123
+
if base == "" && head == "" {
124
+
rest := chi.URLParam(r, "*") // master...feature/xyz
125
+
parts := strings.SplitN(rest, "...", 2)
126
+
if len(parts) == 2 {
127
+
base = parts[0]
128
+
head = parts[1]
129
+
}
130
+
}
131
+
132
+
base, _ = url.PathUnescape(base)
133
+
head, _ = url.PathUnescape(head)
134
+
135
+
if base == "" || head == "" {
136
+
l.Error("invalid comparison")
137
+
rp.pages.Error404(w)
138
+
return
139
+
}
140
+
141
+
scheme := "http"
142
+
if !rp.config.Core.Dev {
143
+
scheme = "https"
144
+
}
145
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
146
+
xrpcc := &indigoxrpc.Client{
147
+
Host: host,
148
+
}
149
+
150
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
151
+
152
+
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
153
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
154
+
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
155
+
rp.pages.Error503(w)
156
+
return
157
+
}
158
+
159
+
var branches types.RepoBranchesResponse
160
+
if err := json.Unmarshal(branchBytes, &branches); err != nil {
161
+
l.Error("failed to decode XRPC branches response", "err", err)
162
+
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
163
+
return
164
+
}
165
+
166
+
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
167
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
168
+
l.Error("failed to call XRPC repo.tags", "err", xrpcerr)
169
+
rp.pages.Error503(w)
170
+
return
171
+
}
172
+
173
+
var tags types.RepoTagsResponse
174
+
if err := json.Unmarshal(tagBytes, &tags); err != nil {
175
+
l.Error("failed to decode XRPC tags response", "err", err)
176
+
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
177
+
return
178
+
}
179
+
180
+
compareBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo, base, head)
181
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
182
+
l.Error("failed to call XRPC repo.compare", "err", xrpcerr)
183
+
rp.pages.Error503(w)
184
+
return
185
+
}
186
+
187
+
var formatPatch types.RepoFormatPatchResponse
188
+
if err := json.Unmarshal(compareBytes, &formatPatch); err != nil {
189
+
l.Error("failed to decode XRPC compare response", "err", err)
190
+
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
191
+
return
192
+
}
193
+
194
+
var diff types.NiceDiff
195
+
if formatPatch.CombinedPatchRaw != "" {
196
+
diff = patchutil.AsNiceDiff(formatPatch.CombinedPatchRaw, base)
197
+
} else {
198
+
diff = patchutil.AsNiceDiff(formatPatch.FormatPatchRaw, base)
199
+
}
200
+
201
+
repoinfo := f.RepoInfo(user)
202
+
203
+
rp.pages.RepoCompare(w, pages.RepoCompareParams{
204
+
LoggedInUser: user,
205
+
RepoInfo: repoinfo,
206
+
Branches: branches.Branches,
207
+
Tags: tags.Tags,
208
+
Base: base,
209
+
Head: head,
210
+
Diff: &diff,
211
+
DiffOpts: diffOpts,
212
+
})
213
+
214
+
}
+1
-1
appview/repo/feed.go
+1
-1
appview/repo/feed.go
···
146
146
return fmt.Sprintf("%s in %s", base, repoName)
147
147
}
148
148
149
-
func (rp *Repo) RepoAtomFeed(w http.ResponseWriter, r *http.Request) {
149
+
func (rp *Repo) AtomFeed(w http.ResponseWriter, r *http.Request) {
150
150
f, err := rp.repoResolver.Resolve(r)
151
151
if err != nil {
152
152
log.Println("failed to fully resolve repo:", err)
+1
-1
appview/repo/index.go
+1
-1
appview/repo/index.go
+223
appview/repo/log.go
+223
appview/repo/log.go
···
1
+
package repo
2
+
3
+
import (
4
+
"encoding/json"
5
+
"fmt"
6
+
"net/http"
7
+
"net/url"
8
+
"strconv"
9
+
10
+
"tangled.org/core/api/tangled"
11
+
"tangled.org/core/appview/commitverify"
12
+
"tangled.org/core/appview/db"
13
+
"tangled.org/core/appview/models"
14
+
"tangled.org/core/appview/pages"
15
+
xrpcclient "tangled.org/core/appview/xrpcclient"
16
+
"tangled.org/core/types"
17
+
18
+
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
19
+
"github.com/go-chi/chi/v5"
20
+
"github.com/go-git/go-git/v5/plumbing"
21
+
)
22
+
23
+
func (rp *Repo) Log(w http.ResponseWriter, r *http.Request) {
24
+
l := rp.logger.With("handler", "RepoLog")
25
+
26
+
f, err := rp.repoResolver.Resolve(r)
27
+
if err != nil {
28
+
l.Error("failed to fully resolve repo", "err", err)
29
+
return
30
+
}
31
+
32
+
page := 1
33
+
if r.URL.Query().Get("page") != "" {
34
+
page, err = strconv.Atoi(r.URL.Query().Get("page"))
35
+
if err != nil {
36
+
page = 1
37
+
}
38
+
}
39
+
40
+
ref := chi.URLParam(r, "ref")
41
+
ref, _ = url.PathUnescape(ref)
42
+
43
+
scheme := "http"
44
+
if !rp.config.Core.Dev {
45
+
scheme = "https"
46
+
}
47
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
48
+
xrpcc := &indigoxrpc.Client{
49
+
Host: host,
50
+
}
51
+
52
+
limit := int64(60)
53
+
cursor := ""
54
+
if page > 1 {
55
+
// Convert page number to cursor (offset)
56
+
offset := (page - 1) * int(limit)
57
+
cursor = strconv.Itoa(offset)
58
+
}
59
+
60
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
61
+
xrpcBytes, err := tangled.RepoLog(r.Context(), xrpcc, cursor, limit, "", ref, repo)
62
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
63
+
l.Error("failed to call XRPC repo.log", "err", xrpcerr)
64
+
rp.pages.Error503(w)
65
+
return
66
+
}
67
+
68
+
var xrpcResp types.RepoLogResponse
69
+
if err := json.Unmarshal(xrpcBytes, &xrpcResp); err != nil {
70
+
l.Error("failed to decode XRPC response", "err", err)
71
+
rp.pages.Error503(w)
72
+
return
73
+
}
74
+
75
+
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
76
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
77
+
l.Error("failed to call XRPC repo.tags", "err", xrpcerr)
78
+
rp.pages.Error503(w)
79
+
return
80
+
}
81
+
82
+
tagMap := make(map[string][]string)
83
+
if tagBytes != nil {
84
+
var tagResp types.RepoTagsResponse
85
+
if err := json.Unmarshal(tagBytes, &tagResp); err == nil {
86
+
for _, tag := range tagResp.Tags {
87
+
hash := tag.Hash
88
+
if tag.Tag != nil {
89
+
hash = tag.Tag.Target.String()
90
+
}
91
+
tagMap[hash] = append(tagMap[hash], tag.Name)
92
+
}
93
+
}
94
+
}
95
+
96
+
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
97
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
98
+
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
99
+
rp.pages.Error503(w)
100
+
return
101
+
}
102
+
103
+
if branchBytes != nil {
104
+
var branchResp types.RepoBranchesResponse
105
+
if err := json.Unmarshal(branchBytes, &branchResp); err == nil {
106
+
for _, branch := range branchResp.Branches {
107
+
tagMap[branch.Hash] = append(tagMap[branch.Hash], branch.Name)
108
+
}
109
+
}
110
+
}
111
+
112
+
user := rp.oauth.GetUser(r)
113
+
114
+
emailToDidMap, err := db.GetEmailToDid(rp.db, uniqueEmails(xrpcResp.Commits), true)
115
+
if err != nil {
116
+
l.Error("failed to fetch email to did mapping", "err", err)
117
+
}
118
+
119
+
vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, xrpcResp.Commits)
120
+
if err != nil {
121
+
l.Error("failed to GetVerifiedObjectCommits", "err", err)
122
+
}
123
+
124
+
repoInfo := f.RepoInfo(user)
125
+
126
+
var shas []string
127
+
for _, c := range xrpcResp.Commits {
128
+
shas = append(shas, c.Hash.String())
129
+
}
130
+
pipelines, err := getPipelineStatuses(rp.db, repoInfo, shas)
131
+
if err != nil {
132
+
l.Error("failed to getPipelineStatuses", "err", err)
133
+
// non-fatal
134
+
}
135
+
136
+
rp.pages.RepoLog(w, pages.RepoLogParams{
137
+
LoggedInUser: user,
138
+
TagMap: tagMap,
139
+
RepoInfo: repoInfo,
140
+
RepoLogResponse: xrpcResp,
141
+
EmailToDid: emailToDidMap,
142
+
VerifiedCommits: vc,
143
+
Pipelines: pipelines,
144
+
})
145
+
}
146
+
147
+
func (rp *Repo) Commit(w http.ResponseWriter, r *http.Request) {
148
+
l := rp.logger.With("handler", "RepoCommit")
149
+
150
+
f, err := rp.repoResolver.Resolve(r)
151
+
if err != nil {
152
+
l.Error("failed to fully resolve repo", "err", err)
153
+
return
154
+
}
155
+
ref := chi.URLParam(r, "ref")
156
+
ref, _ = url.PathUnescape(ref)
157
+
158
+
var diffOpts types.DiffOpts
159
+
if d := r.URL.Query().Get("diff"); d == "split" {
160
+
diffOpts.Split = true
161
+
}
162
+
163
+
if !plumbing.IsHash(ref) {
164
+
rp.pages.Error404(w)
165
+
return
166
+
}
167
+
168
+
scheme := "http"
169
+
if !rp.config.Core.Dev {
170
+
scheme = "https"
171
+
}
172
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
173
+
xrpcc := &indigoxrpc.Client{
174
+
Host: host,
175
+
}
176
+
177
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
178
+
xrpcBytes, err := tangled.RepoDiff(r.Context(), xrpcc, ref, repo)
179
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
180
+
l.Error("failed to call XRPC repo.diff", "err", xrpcerr)
181
+
rp.pages.Error503(w)
182
+
return
183
+
}
184
+
185
+
var result types.RepoCommitResponse
186
+
if err := json.Unmarshal(xrpcBytes, &result); err != nil {
187
+
l.Error("failed to decode XRPC response", "err", err)
188
+
rp.pages.Error503(w)
189
+
return
190
+
}
191
+
192
+
emailToDidMap, err := db.GetEmailToDid(rp.db, []string{result.Diff.Commit.Committer.Email, result.Diff.Commit.Author.Email}, true)
193
+
if err != nil {
194
+
l.Error("failed to get email to did mapping", "err", err)
195
+
}
196
+
197
+
vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, []types.NiceDiff{*result.Diff})
198
+
if err != nil {
199
+
l.Error("failed to GetVerifiedCommits", "err", err)
200
+
}
201
+
202
+
user := rp.oauth.GetUser(r)
203
+
repoInfo := f.RepoInfo(user)
204
+
pipelines, err := getPipelineStatuses(rp.db, repoInfo, []string{result.Diff.Commit.This})
205
+
if err != nil {
206
+
l.Error("failed to getPipelineStatuses", "err", err)
207
+
// non-fatal
208
+
}
209
+
var pipeline *models.Pipeline
210
+
if p, ok := pipelines[result.Diff.Commit.This]; ok {
211
+
pipeline = &p
212
+
}
213
+
214
+
rp.pages.RepoCommit(w, pages.RepoCommitParams{
215
+
LoggedInUser: user,
216
+
RepoInfo: f.RepoInfo(user),
217
+
RepoCommitResponse: result,
218
+
EmailToDid: emailToDidMap,
219
+
VerifiedCommit: vc,
220
+
Pipeline: pipeline,
221
+
DiffOpts: diffOpts,
222
+
})
223
+
}
+1
-1
appview/repo/opengraph.go
+1
-1
appview/repo/opengraph.go
···
327
327
return nil
328
328
}
329
329
330
-
func (rp *Repo) RepoOpenGraphSummary(w http.ResponseWriter, r *http.Request) {
330
+
func (rp *Repo) Opengraph(w http.ResponseWriter, r *http.Request) {
331
331
f, err := rp.repoResolver.Resolve(r)
332
332
if err != nil {
333
333
log.Println("failed to get repo and knot", err)
+71
-1439
appview/repo/repo.go
+71
-1439
appview/repo/repo.go
···
3
3
import (
4
4
"context"
5
5
"database/sql"
6
-
"encoding/json"
7
6
"errors"
8
7
"fmt"
9
-
"io"
10
8
"log/slog"
11
9
"net/http"
12
10
"net/url"
13
-
"path/filepath"
14
11
"slices"
15
-
"strconv"
16
12
"strings"
17
13
"time"
18
14
19
15
"tangled.org/core/api/tangled"
20
-
"tangled.org/core/appview/commitverify"
21
16
"tangled.org/core/appview/config"
22
17
"tangled.org/core/appview/db"
23
18
"tangled.org/core/appview/models"
24
19
"tangled.org/core/appview/notify"
25
20
"tangled.org/core/appview/oauth"
26
21
"tangled.org/core/appview/pages"
27
-
"tangled.org/core/appview/pages/markup"
28
22
"tangled.org/core/appview/reporesolver"
29
23
"tangled.org/core/appview/validator"
30
24
xrpcclient "tangled.org/core/appview/xrpcclient"
31
25
"tangled.org/core/eventconsumer"
32
26
"tangled.org/core/idresolver"
33
-
"tangled.org/core/patchutil"
34
27
"tangled.org/core/rbac"
35
28
"tangled.org/core/tid"
36
-
"tangled.org/core/types"
37
29
"tangled.org/core/xrpc/serviceauth"
38
30
39
31
comatproto "github.com/bluesky-social/indigo/api/atproto"
40
32
atpclient "github.com/bluesky-social/indigo/atproto/client"
41
33
"github.com/bluesky-social/indigo/atproto/syntax"
42
34
lexutil "github.com/bluesky-social/indigo/lex/util"
43
-
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
44
35
securejoin "github.com/cyphar/filepath-securejoin"
45
36
"github.com/go-chi/chi/v5"
46
-
"github.com/go-git/go-git/v5/plumbing"
47
37
)
48
38
49
39
type Repo struct {
···
88
78
}
89
79
}
90
80
91
-
func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) {
92
-
l := rp.logger.With("handler", "DownloadArchive")
93
-
94
-
ref := chi.URLParam(r, "ref")
95
-
ref, _ = url.PathUnescape(ref)
96
-
97
-
f, err := rp.repoResolver.Resolve(r)
98
-
if err != nil {
99
-
l.Error("failed to get repo and knot", "err", err)
100
-
return
101
-
}
102
-
103
-
scheme := "http"
104
-
if !rp.config.Core.Dev {
105
-
scheme = "https"
106
-
}
107
-
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
108
-
xrpcc := &indigoxrpc.Client{
109
-
Host: host,
110
-
}
111
-
112
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
113
-
archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, repo)
114
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
115
-
l.Error("failed to call XRPC repo.archive", "err", xrpcerr)
116
-
rp.pages.Error503(w)
117
-
return
118
-
}
119
-
120
-
// Set headers for file download, just pass along whatever the knot specifies
121
-
safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-")
122
-
filename := fmt.Sprintf("%s-%s.tar.gz", f.Name, safeRefFilename)
123
-
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
124
-
w.Header().Set("Content-Type", "application/gzip")
125
-
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(archiveBytes)))
126
-
127
-
// Write the archive data directly
128
-
w.Write(archiveBytes)
129
-
}
130
-
131
-
func (rp *Repo) RepoLog(w http.ResponseWriter, r *http.Request) {
132
-
l := rp.logger.With("handler", "RepoLog")
133
-
134
-
f, err := rp.repoResolver.Resolve(r)
135
-
if err != nil {
136
-
l.Error("failed to fully resolve repo", "err", err)
137
-
return
138
-
}
139
-
140
-
page := 1
141
-
if r.URL.Query().Get("page") != "" {
142
-
page, err = strconv.Atoi(r.URL.Query().Get("page"))
143
-
if err != nil {
144
-
page = 1
145
-
}
146
-
}
147
-
148
-
ref := chi.URLParam(r, "ref")
149
-
ref, _ = url.PathUnescape(ref)
150
-
151
-
scheme := "http"
152
-
if !rp.config.Core.Dev {
153
-
scheme = "https"
154
-
}
155
-
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
156
-
xrpcc := &indigoxrpc.Client{
157
-
Host: host,
158
-
}
159
-
160
-
limit := int64(60)
161
-
cursor := ""
162
-
if page > 1 {
163
-
// Convert page number to cursor (offset)
164
-
offset := (page - 1) * int(limit)
165
-
cursor = strconv.Itoa(offset)
166
-
}
167
-
168
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
169
-
xrpcBytes, err := tangled.RepoLog(r.Context(), xrpcc, cursor, limit, "", ref, repo)
170
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
171
-
l.Error("failed to call XRPC repo.log", "err", xrpcerr)
172
-
rp.pages.Error503(w)
173
-
return
174
-
}
175
-
176
-
var xrpcResp types.RepoLogResponse
177
-
if err := json.Unmarshal(xrpcBytes, &xrpcResp); err != nil {
178
-
l.Error("failed to decode XRPC response", "err", err)
179
-
rp.pages.Error503(w)
180
-
return
181
-
}
182
-
183
-
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
184
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
185
-
l.Error("failed to call XRPC repo.tags", "err", xrpcerr)
186
-
rp.pages.Error503(w)
187
-
return
188
-
}
189
-
190
-
tagMap := make(map[string][]string)
191
-
if tagBytes != nil {
192
-
var tagResp types.RepoTagsResponse
193
-
if err := json.Unmarshal(tagBytes, &tagResp); err == nil {
194
-
for _, tag := range tagResp.Tags {
195
-
hash := tag.Hash
196
-
if tag.Tag != nil {
197
-
hash = tag.Tag.Target.String()
198
-
}
199
-
tagMap[hash] = append(tagMap[hash], tag.Name)
200
-
}
201
-
}
202
-
}
203
-
204
-
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
205
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
206
-
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
207
-
rp.pages.Error503(w)
208
-
return
209
-
}
210
-
211
-
if branchBytes != nil {
212
-
var branchResp types.RepoBranchesResponse
213
-
if err := json.Unmarshal(branchBytes, &branchResp); err == nil {
214
-
for _, branch := range branchResp.Branches {
215
-
tagMap[branch.Hash] = append(tagMap[branch.Hash], branch.Name)
216
-
}
217
-
}
218
-
}
219
-
220
-
user := rp.oauth.GetUser(r)
221
-
222
-
emailToDidMap, err := db.GetEmailToDid(rp.db, uniqueEmails(xrpcResp.Commits), true)
223
-
if err != nil {
224
-
l.Error("failed to fetch email to did mapping", "err", err)
225
-
}
226
-
227
-
vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, xrpcResp.Commits)
228
-
if err != nil {
229
-
l.Error("failed to GetVerifiedObjectCommits", "err", err)
230
-
}
231
-
232
-
repoInfo := f.RepoInfo(user)
233
-
234
-
var shas []string
235
-
for _, c := range xrpcResp.Commits {
236
-
shas = append(shas, c.Hash.String())
237
-
}
238
-
pipelines, err := getPipelineStatuses(rp.db, repoInfo, shas)
239
-
if err != nil {
240
-
l.Error("failed to getPipelineStatuses", "err", err)
241
-
// non-fatal
242
-
}
243
-
244
-
rp.pages.RepoLog(w, pages.RepoLogParams{
245
-
LoggedInUser: user,
246
-
TagMap: tagMap,
247
-
RepoInfo: repoInfo,
248
-
RepoLogResponse: xrpcResp,
249
-
EmailToDid: emailToDidMap,
250
-
VerifiedCommits: vc,
251
-
Pipelines: pipelines,
252
-
})
253
-
}
254
-
255
-
func (rp *Repo) RepoCommit(w http.ResponseWriter, r *http.Request) {
256
-
l := rp.logger.With("handler", "RepoCommit")
257
-
258
-
f, err := rp.repoResolver.Resolve(r)
259
-
if err != nil {
260
-
l.Error("failed to fully resolve repo", "err", err)
261
-
return
262
-
}
263
-
ref := chi.URLParam(r, "ref")
264
-
ref, _ = url.PathUnescape(ref)
265
-
266
-
var diffOpts types.DiffOpts
267
-
if d := r.URL.Query().Get("diff"); d == "split" {
268
-
diffOpts.Split = true
269
-
}
270
-
271
-
if !plumbing.IsHash(ref) {
272
-
rp.pages.Error404(w)
273
-
return
274
-
}
275
-
276
-
scheme := "http"
277
-
if !rp.config.Core.Dev {
278
-
scheme = "https"
279
-
}
280
-
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
281
-
xrpcc := &indigoxrpc.Client{
282
-
Host: host,
283
-
}
284
-
285
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
286
-
xrpcBytes, err := tangled.RepoDiff(r.Context(), xrpcc, ref, repo)
287
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
288
-
l.Error("failed to call XRPC repo.diff", "err", xrpcerr)
289
-
rp.pages.Error503(w)
290
-
return
291
-
}
292
-
293
-
var result types.RepoCommitResponse
294
-
if err := json.Unmarshal(xrpcBytes, &result); err != nil {
295
-
l.Error("failed to decode XRPC response", "err", err)
296
-
rp.pages.Error503(w)
297
-
return
298
-
}
299
-
300
-
emailToDidMap, err := db.GetEmailToDid(rp.db, []string{result.Diff.Commit.Committer.Email, result.Diff.Commit.Author.Email}, true)
301
-
if err != nil {
302
-
l.Error("failed to get email to did mapping", "err", err)
303
-
}
304
-
305
-
vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, []types.NiceDiff{*result.Diff})
306
-
if err != nil {
307
-
l.Error("failed to GetVerifiedCommits", "err", err)
308
-
}
309
-
310
-
user := rp.oauth.GetUser(r)
311
-
repoInfo := f.RepoInfo(user)
312
-
pipelines, err := getPipelineStatuses(rp.db, repoInfo, []string{result.Diff.Commit.This})
313
-
if err != nil {
314
-
l.Error("failed to getPipelineStatuses", "err", err)
315
-
// non-fatal
316
-
}
317
-
var pipeline *models.Pipeline
318
-
if p, ok := pipelines[result.Diff.Commit.This]; ok {
319
-
pipeline = &p
320
-
}
321
-
322
-
rp.pages.RepoCommit(w, pages.RepoCommitParams{
323
-
LoggedInUser: user,
324
-
RepoInfo: f.RepoInfo(user),
325
-
RepoCommitResponse: result,
326
-
EmailToDid: emailToDidMap,
327
-
VerifiedCommit: vc,
328
-
Pipeline: pipeline,
329
-
DiffOpts: diffOpts,
330
-
})
331
-
}
332
-
333
-
func (rp *Repo) RepoTree(w http.ResponseWriter, r *http.Request) {
334
-
l := rp.logger.With("handler", "RepoTree")
335
-
336
-
f, err := rp.repoResolver.Resolve(r)
337
-
if err != nil {
338
-
l.Error("failed to fully resolve repo", "err", err)
339
-
return
340
-
}
341
-
342
-
ref := chi.URLParam(r, "ref")
343
-
ref, _ = url.PathUnescape(ref)
344
-
345
-
// if the tree path has a trailing slash, let's strip it
346
-
// so we don't 404
347
-
treePath := chi.URLParam(r, "*")
348
-
treePath, _ = url.PathUnescape(treePath)
349
-
treePath = strings.TrimSuffix(treePath, "/")
350
-
351
-
scheme := "http"
352
-
if !rp.config.Core.Dev {
353
-
scheme = "https"
354
-
}
355
-
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
356
-
xrpcc := &indigoxrpc.Client{
357
-
Host: host,
358
-
}
359
-
360
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
361
-
xrpcResp, err := tangled.RepoTree(r.Context(), xrpcc, treePath, ref, repo)
362
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
363
-
l.Error("failed to call XRPC repo.tree", "err", xrpcerr)
364
-
rp.pages.Error503(w)
365
-
return
366
-
}
367
-
368
-
// Convert XRPC response to internal types.RepoTreeResponse
369
-
files := make([]types.NiceTree, len(xrpcResp.Files))
370
-
for i, xrpcFile := range xrpcResp.Files {
371
-
file := types.NiceTree{
372
-
Name: xrpcFile.Name,
373
-
Mode: xrpcFile.Mode,
374
-
Size: int64(xrpcFile.Size),
375
-
IsFile: xrpcFile.Is_file,
376
-
IsSubtree: xrpcFile.Is_subtree,
377
-
}
378
-
379
-
// Convert last commit info if present
380
-
if xrpcFile.Last_commit != nil {
381
-
commitWhen, _ := time.Parse(time.RFC3339, xrpcFile.Last_commit.When)
382
-
file.LastCommit = &types.LastCommitInfo{
383
-
Hash: plumbing.NewHash(xrpcFile.Last_commit.Hash),
384
-
Message: xrpcFile.Last_commit.Message,
385
-
When: commitWhen,
386
-
}
387
-
}
388
-
389
-
files[i] = file
390
-
}
391
-
392
-
result := types.RepoTreeResponse{
393
-
Ref: xrpcResp.Ref,
394
-
Files: files,
395
-
}
396
-
397
-
if xrpcResp.Parent != nil {
398
-
result.Parent = *xrpcResp.Parent
399
-
}
400
-
if xrpcResp.Dotdot != nil {
401
-
result.DotDot = *xrpcResp.Dotdot
402
-
}
403
-
if xrpcResp.Readme != nil {
404
-
result.ReadmeFileName = xrpcResp.Readme.Filename
405
-
result.Readme = xrpcResp.Readme.Contents
406
-
}
407
-
408
-
// redirects tree paths trying to access a blob; in this case the result.Files is unpopulated,
409
-
// so we can safely redirect to the "parent" (which is the same file).
410
-
if len(result.Files) == 0 && result.Parent == treePath {
411
-
redirectTo := fmt.Sprintf("/%s/blob/%s/%s", f.OwnerSlashRepo(), url.PathEscape(ref), result.Parent)
412
-
http.Redirect(w, r, redirectTo, http.StatusFound)
413
-
return
414
-
}
415
-
416
-
user := rp.oauth.GetUser(r)
417
-
418
-
var breadcrumbs [][]string
419
-
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), url.PathEscape(ref))})
420
-
if treePath != "" {
421
-
for idx, elem := range strings.Split(treePath, "/") {
422
-
breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))})
423
-
}
424
-
}
425
-
426
-
sortFiles(result.Files)
427
-
428
-
rp.pages.RepoTree(w, pages.RepoTreeParams{
429
-
LoggedInUser: user,
430
-
BreadCrumbs: breadcrumbs,
431
-
TreePath: treePath,
432
-
RepoInfo: f.RepoInfo(user),
433
-
RepoTreeResponse: result,
434
-
})
435
-
}
436
-
437
-
func (rp *Repo) RepoTags(w http.ResponseWriter, r *http.Request) {
438
-
l := rp.logger.With("handler", "RepoTags")
439
-
440
-
f, err := rp.repoResolver.Resolve(r)
441
-
if err != nil {
442
-
l.Error("failed to get repo and knot", "err", err)
443
-
return
444
-
}
445
-
446
-
scheme := "http"
447
-
if !rp.config.Core.Dev {
448
-
scheme = "https"
449
-
}
450
-
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
451
-
xrpcc := &indigoxrpc.Client{
452
-
Host: host,
453
-
}
454
-
455
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
456
-
xrpcBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
457
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
458
-
l.Error("failed to call XRPC repo.tags", "err", xrpcerr)
459
-
rp.pages.Error503(w)
460
-
return
461
-
}
462
-
463
-
var result types.RepoTagsResponse
464
-
if err := json.Unmarshal(xrpcBytes, &result); err != nil {
465
-
l.Error("failed to decode XRPC response", "err", err)
466
-
rp.pages.Error503(w)
467
-
return
468
-
}
469
-
470
-
artifacts, err := db.GetArtifact(rp.db, db.FilterEq("repo_at", f.RepoAt()))
471
-
if err != nil {
472
-
l.Error("failed grab artifacts", "err", err)
473
-
return
474
-
}
475
-
476
-
// convert artifacts to map for easy UI building
477
-
artifactMap := make(map[plumbing.Hash][]models.Artifact)
478
-
for _, a := range artifacts {
479
-
artifactMap[a.Tag] = append(artifactMap[a.Tag], a)
480
-
}
481
-
482
-
var danglingArtifacts []models.Artifact
483
-
for _, a := range artifacts {
484
-
found := false
485
-
for _, t := range result.Tags {
486
-
if t.Tag != nil {
487
-
if t.Tag.Hash == a.Tag {
488
-
found = true
489
-
}
490
-
}
491
-
}
492
-
493
-
if !found {
494
-
danglingArtifacts = append(danglingArtifacts, a)
495
-
}
496
-
}
497
-
498
-
user := rp.oauth.GetUser(r)
499
-
rp.pages.RepoTags(w, pages.RepoTagsParams{
500
-
LoggedInUser: user,
501
-
RepoInfo: f.RepoInfo(user),
502
-
RepoTagsResponse: result,
503
-
ArtifactMap: artifactMap,
504
-
DanglingArtifacts: danglingArtifacts,
505
-
})
506
-
}
507
-
508
-
func (rp *Repo) RepoBranches(w http.ResponseWriter, r *http.Request) {
509
-
l := rp.logger.With("handler", "RepoBranches")
510
-
511
-
f, err := rp.repoResolver.Resolve(r)
512
-
if err != nil {
513
-
l.Error("failed to get repo and knot", "err", err)
514
-
return
515
-
}
516
-
517
-
scheme := "http"
518
-
if !rp.config.Core.Dev {
519
-
scheme = "https"
520
-
}
521
-
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
522
-
xrpcc := &indigoxrpc.Client{
523
-
Host: host,
524
-
}
525
-
526
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
527
-
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
528
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
529
-
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
530
-
rp.pages.Error503(w)
531
-
return
532
-
}
533
-
534
-
var result types.RepoBranchesResponse
535
-
if err := json.Unmarshal(xrpcBytes, &result); err != nil {
536
-
l.Error("failed to decode XRPC response", "err", err)
537
-
rp.pages.Error503(w)
538
-
return
539
-
}
540
-
541
-
sortBranches(result.Branches)
542
-
543
-
user := rp.oauth.GetUser(r)
544
-
rp.pages.RepoBranches(w, pages.RepoBranchesParams{
545
-
LoggedInUser: user,
546
-
RepoInfo: f.RepoInfo(user),
547
-
RepoBranchesResponse: result,
548
-
})
549
-
}
550
-
551
-
func (rp *Repo) DeleteBranch(w http.ResponseWriter, r *http.Request) {
552
-
l := rp.logger.With("handler", "DeleteBranch")
553
-
554
-
f, err := rp.repoResolver.Resolve(r)
555
-
if err != nil {
556
-
l.Error("failed to get repo and knot", "err", err)
557
-
return
558
-
}
559
-
560
-
noticeId := "delete-branch-error"
561
-
fail := func(msg string, err error) {
562
-
l.Error(msg, "err", err)
563
-
rp.pages.Notice(w, noticeId, msg)
564
-
}
565
-
566
-
branch := r.FormValue("branch")
567
-
if branch == "" {
568
-
fail("No branch provided.", nil)
569
-
return
570
-
}
571
-
572
-
client, err := rp.oauth.ServiceClient(
573
-
r,
574
-
oauth.WithService(f.Knot),
575
-
oauth.WithLxm(tangled.RepoDeleteBranchNSID),
576
-
oauth.WithDev(rp.config.Core.Dev),
577
-
)
578
-
if err != nil {
579
-
fail("Failed to connect to knotserver", nil)
580
-
return
581
-
}
582
-
583
-
err = tangled.RepoDeleteBranch(
584
-
r.Context(),
585
-
client,
586
-
&tangled.RepoDeleteBranch_Input{
587
-
Branch: branch,
588
-
Repo: f.RepoAt().String(),
589
-
},
590
-
)
591
-
if err := xrpcclient.HandleXrpcErr(err); err != nil {
592
-
fail(fmt.Sprintf("Failed to delete branch: %s", err), err)
593
-
return
594
-
}
595
-
l.Error("deleted branch from knot", "branch", branch, "repo", f.RepoAt())
596
-
597
-
rp.pages.HxRefresh(w)
598
-
}
599
-
600
-
func (rp *Repo) RepoBlob(w http.ResponseWriter, r *http.Request) {
601
-
l := rp.logger.With("handler", "RepoBlob")
602
-
603
-
f, err := rp.repoResolver.Resolve(r)
604
-
if err != nil {
605
-
l.Error("failed to get repo and knot", "err", err)
606
-
return
607
-
}
608
-
609
-
ref := chi.URLParam(r, "ref")
610
-
ref, _ = url.PathUnescape(ref)
611
-
612
-
filePath := chi.URLParam(r, "*")
613
-
filePath, _ = url.PathUnescape(filePath)
614
-
615
-
scheme := "http"
616
-
if !rp.config.Core.Dev {
617
-
scheme = "https"
618
-
}
619
-
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
620
-
xrpcc := &indigoxrpc.Client{
621
-
Host: host,
622
-
}
623
-
624
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name)
625
-
resp, err := tangled.RepoBlob(r.Context(), xrpcc, filePath, false, ref, repo)
626
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
627
-
l.Error("failed to call XRPC repo.blob", "err", xrpcerr)
628
-
rp.pages.Error503(w)
629
-
return
630
-
}
631
-
632
-
// Use XRPC response directly instead of converting to internal types
633
-
634
-
var breadcrumbs [][]string
635
-
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), url.PathEscape(ref))})
636
-
if filePath != "" {
637
-
for idx, elem := range strings.Split(filePath, "/") {
638
-
breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))})
639
-
}
640
-
}
641
-
642
-
showRendered := false
643
-
renderToggle := false
644
-
645
-
if markup.GetFormat(resp.Path) == markup.FormatMarkdown {
646
-
renderToggle = true
647
-
showRendered = r.URL.Query().Get("code") != "true"
648
-
}
649
-
650
-
var unsupported bool
651
-
var isImage bool
652
-
var isVideo bool
653
-
var contentSrc string
654
-
655
-
if resp.IsBinary != nil && *resp.IsBinary {
656
-
ext := strings.ToLower(filepath.Ext(resp.Path))
657
-
switch ext {
658
-
case ".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp":
659
-
isImage = true
660
-
case ".mp4", ".webm", ".ogg", ".mov", ".avi":
661
-
isVideo = true
662
-
default:
663
-
unsupported = true
664
-
}
665
-
666
-
// fetch the raw binary content using sh.tangled.repo.blob xrpc
667
-
repoName := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
668
-
669
-
baseURL := &url.URL{
670
-
Scheme: scheme,
671
-
Host: f.Knot,
672
-
Path: "/xrpc/sh.tangled.repo.blob",
673
-
}
674
-
query := baseURL.Query()
675
-
query.Set("repo", repoName)
676
-
query.Set("ref", ref)
677
-
query.Set("path", filePath)
678
-
query.Set("raw", "true")
679
-
baseURL.RawQuery = query.Encode()
680
-
blobURL := baseURL.String()
681
-
682
-
contentSrc = blobURL
683
-
if !rp.config.Core.Dev {
684
-
contentSrc = markup.GenerateCamoURL(rp.config.Camo.Host, rp.config.Camo.SharedSecret, blobURL)
685
-
}
686
-
}
687
-
688
-
lines := 0
689
-
if resp.IsBinary == nil || !*resp.IsBinary {
690
-
lines = strings.Count(resp.Content, "\n") + 1
691
-
}
692
-
693
-
var sizeHint uint64
694
-
if resp.Size != nil {
695
-
sizeHint = uint64(*resp.Size)
696
-
} else {
697
-
sizeHint = uint64(len(resp.Content))
698
-
}
699
-
700
-
user := rp.oauth.GetUser(r)
701
-
702
-
// Determine if content is binary (dereference pointer)
703
-
isBinary := false
704
-
if resp.IsBinary != nil {
705
-
isBinary = *resp.IsBinary
706
-
}
707
-
708
-
rp.pages.RepoBlob(w, pages.RepoBlobParams{
709
-
LoggedInUser: user,
710
-
RepoInfo: f.RepoInfo(user),
711
-
BreadCrumbs: breadcrumbs,
712
-
ShowRendered: showRendered,
713
-
RenderToggle: renderToggle,
714
-
Unsupported: unsupported,
715
-
IsImage: isImage,
716
-
IsVideo: isVideo,
717
-
ContentSrc: contentSrc,
718
-
RepoBlob_Output: resp,
719
-
Contents: resp.Content,
720
-
Lines: lines,
721
-
SizeHint: sizeHint,
722
-
IsBinary: isBinary,
723
-
})
724
-
}
725
-
726
-
func (rp *Repo) RepoBlobRaw(w http.ResponseWriter, r *http.Request) {
727
-
l := rp.logger.With("handler", "RepoBlobRaw")
728
-
729
-
f, err := rp.repoResolver.Resolve(r)
730
-
if err != nil {
731
-
l.Error("failed to get repo and knot", "err", err)
732
-
w.WriteHeader(http.StatusBadRequest)
733
-
return
734
-
}
735
-
736
-
ref := chi.URLParam(r, "ref")
737
-
ref, _ = url.PathUnescape(ref)
738
-
739
-
filePath := chi.URLParam(r, "*")
740
-
filePath, _ = url.PathUnescape(filePath)
741
-
742
-
scheme := "http"
743
-
if !rp.config.Core.Dev {
744
-
scheme = "https"
745
-
}
746
-
747
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name)
748
-
baseURL := &url.URL{
749
-
Scheme: scheme,
750
-
Host: f.Knot,
751
-
Path: "/xrpc/sh.tangled.repo.blob",
752
-
}
753
-
query := baseURL.Query()
754
-
query.Set("repo", repo)
755
-
query.Set("ref", ref)
756
-
query.Set("path", filePath)
757
-
query.Set("raw", "true")
758
-
baseURL.RawQuery = query.Encode()
759
-
blobURL := baseURL.String()
760
-
761
-
req, err := http.NewRequest("GET", blobURL, nil)
762
-
if err != nil {
763
-
l.Error("failed to create request", "err", err)
764
-
return
765
-
}
766
-
767
-
// forward the If-None-Match header
768
-
if clientETag := r.Header.Get("If-None-Match"); clientETag != "" {
769
-
req.Header.Set("If-None-Match", clientETag)
770
-
}
771
-
772
-
client := &http.Client{}
773
-
resp, err := client.Do(req)
774
-
if err != nil {
775
-
l.Error("failed to reach knotserver", "err", err)
776
-
rp.pages.Error503(w)
777
-
return
778
-
}
779
-
defer resp.Body.Close()
780
-
781
-
// forward 304 not modified
782
-
if resp.StatusCode == http.StatusNotModified {
783
-
w.WriteHeader(http.StatusNotModified)
784
-
return
785
-
}
786
-
787
-
if resp.StatusCode != http.StatusOK {
788
-
l.Error("knotserver returned non-OK status for raw blob", "url", blobURL, "statuscode", resp.StatusCode)
789
-
w.WriteHeader(resp.StatusCode)
790
-
_, _ = io.Copy(w, resp.Body)
791
-
return
792
-
}
793
-
794
-
contentType := resp.Header.Get("Content-Type")
795
-
body, err := io.ReadAll(resp.Body)
796
-
if err != nil {
797
-
l.Error("error reading response body from knotserver", "err", err)
798
-
w.WriteHeader(http.StatusInternalServerError)
799
-
return
800
-
}
801
-
802
-
if strings.HasPrefix(contentType, "text/") || isTextualMimeType(contentType) {
803
-
// serve all textual content as text/plain
804
-
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
805
-
w.Write(body)
806
-
} else if strings.HasPrefix(contentType, "image/") || strings.HasPrefix(contentType, "video/") {
807
-
// serve images and videos with their original content type
808
-
w.Header().Set("Content-Type", contentType)
809
-
w.Write(body)
810
-
} else {
811
-
w.WriteHeader(http.StatusUnsupportedMediaType)
812
-
w.Write([]byte("unsupported content type"))
813
-
return
814
-
}
815
-
}
816
-
817
81
// isTextualMimeType returns true if the MIME type represents textual content
818
-
// that should be served as text/plain
819
-
func isTextualMimeType(mimeType string) bool {
820
-
textualTypes := []string{
821
-
"application/json",
822
-
"application/xml",
823
-
"application/yaml",
824
-
"application/x-yaml",
825
-
"application/toml",
826
-
"application/javascript",
827
-
"application/ecmascript",
828
-
"message/",
829
-
}
830
-
831
-
return slices.Contains(textualTypes, mimeType)
832
-
}
833
82
834
83
// modify the spindle configured for this repo
835
84
func (rp *Repo) EditSpindle(w http.ResponseWriter, r *http.Request) {
···
1548
797
Rkey: rkey,
1549
798
SubjectDid: collaboratorIdent.DID,
1550
799
RepoAt: f.RepoAt(),
1551
-
Created: createdAt,
1552
-
})
1553
-
if err != nil {
1554
-
fail("Failed to add collaborator.", err)
1555
-
return
1556
-
}
1557
-
1558
-
err = tx.Commit()
1559
-
if err != nil {
1560
-
fail("Failed to add collaborator.", err)
1561
-
return
1562
-
}
1563
-
1564
-
err = rp.enforcer.E.SavePolicy()
1565
-
if err != nil {
1566
-
fail("Failed to update collaborator permissions.", err)
1567
-
return
1568
-
}
1569
-
1570
-
// clear aturi to when everything is successful
1571
-
aturi = ""
1572
-
1573
-
rp.pages.HxRefresh(w)
1574
-
}
1575
-
1576
-
func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) {
1577
-
user := rp.oauth.GetUser(r)
1578
-
l := rp.logger.With("handler", "DeleteRepo")
1579
-
1580
-
noticeId := "operation-error"
1581
-
f, err := rp.repoResolver.Resolve(r)
1582
-
if err != nil {
1583
-
l.Error("failed to get repo and knot", "err", err)
1584
-
return
1585
-
}
1586
-
1587
-
// remove record from pds
1588
-
atpClient, err := rp.oauth.AuthorizedClient(r)
1589
-
if err != nil {
1590
-
l.Error("failed to get authorized client", "err", err)
1591
-
return
1592
-
}
1593
-
_, err = comatproto.RepoDeleteRecord(r.Context(), atpClient, &comatproto.RepoDeleteRecord_Input{
1594
-
Collection: tangled.RepoNSID,
1595
-
Repo: user.Did,
1596
-
Rkey: f.Rkey,
1597
-
})
1598
-
if err != nil {
1599
-
l.Error("failed to delete record", "err", err)
1600
-
rp.pages.Notice(w, noticeId, "Failed to delete repository from PDS.")
1601
-
return
1602
-
}
1603
-
l.Info("removed repo record", "aturi", f.RepoAt().String())
1604
-
1605
-
client, err := rp.oauth.ServiceClient(
1606
-
r,
1607
-
oauth.WithService(f.Knot),
1608
-
oauth.WithLxm(tangled.RepoDeleteNSID),
1609
-
oauth.WithDev(rp.config.Core.Dev),
1610
-
)
1611
-
if err != nil {
1612
-
l.Error("failed to connect to knot server", "err", err)
1613
-
return
1614
-
}
1615
-
1616
-
err = tangled.RepoDelete(
1617
-
r.Context(),
1618
-
client,
1619
-
&tangled.RepoDelete_Input{
1620
-
Did: f.OwnerDid(),
1621
-
Name: f.Name,
1622
-
Rkey: f.Rkey,
1623
-
},
1624
-
)
1625
-
if err := xrpcclient.HandleXrpcErr(err); err != nil {
1626
-
rp.pages.Notice(w, noticeId, err.Error())
1627
-
return
1628
-
}
1629
-
l.Info("deleted repo from knot")
1630
-
1631
-
tx, err := rp.db.BeginTx(r.Context(), nil)
1632
-
if err != nil {
1633
-
l.Error("failed to start tx")
1634
-
w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
1635
-
return
1636
-
}
1637
-
defer func() {
1638
-
tx.Rollback()
1639
-
err = rp.enforcer.E.LoadPolicy()
1640
-
if err != nil {
1641
-
l.Error("failed to rollback policies")
1642
-
}
1643
-
}()
1644
-
1645
-
// remove collaborator RBAC
1646
-
repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot)
1647
-
if err != nil {
1648
-
rp.pages.Notice(w, noticeId, "Failed to remove collaborators")
1649
-
return
1650
-
}
1651
-
for _, c := range repoCollaborators {
1652
-
did := c[0]
1653
-
rp.enforcer.RemoveCollaborator(did, f.Knot, f.DidSlashRepo())
1654
-
}
1655
-
l.Info("removed collaborators")
1656
-
1657
-
// remove repo RBAC
1658
-
err = rp.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.DidSlashRepo())
1659
-
if err != nil {
1660
-
rp.pages.Notice(w, noticeId, "Failed to update RBAC rules")
1661
-
return
1662
-
}
1663
-
1664
-
// remove repo from db
1665
-
err = db.RemoveRepo(tx, f.OwnerDid(), f.Name)
1666
-
if err != nil {
1667
-
rp.pages.Notice(w, noticeId, "Failed to update appview")
1668
-
return
1669
-
}
1670
-
l.Info("removed repo from db")
1671
-
1672
-
err = tx.Commit()
1673
-
if err != nil {
1674
-
l.Error("failed to commit changes", "err", err)
1675
-
http.Error(w, err.Error(), http.StatusInternalServerError)
1676
-
return
1677
-
}
1678
-
1679
-
err = rp.enforcer.E.SavePolicy()
1680
-
if err != nil {
1681
-
l.Error("failed to update ACLs", "err", err)
1682
-
http.Error(w, err.Error(), http.StatusInternalServerError)
1683
-
return
1684
-
}
1685
-
1686
-
rp.pages.HxRedirect(w, fmt.Sprintf("/%s", f.OwnerDid()))
1687
-
}
1688
-
1689
-
func (rp *Repo) EditBaseSettings(w http.ResponseWriter, r *http.Request) {
1690
-
l := rp.logger.With("handler", "EditBaseSettings")
1691
-
1692
-
noticeId := "repo-base-settings-error"
1693
-
1694
-
f, err := rp.repoResolver.Resolve(r)
1695
-
if err != nil {
1696
-
l.Error("failed to get repo and knot", "err", err)
1697
-
w.WriteHeader(http.StatusBadRequest)
1698
-
return
1699
-
}
1700
-
1701
-
client, err := rp.oauth.AuthorizedClient(r)
1702
-
if err != nil {
1703
-
l.Error("failed to get client")
1704
-
rp.pages.Notice(w, noticeId, "Failed to update repository information, try again later.")
1705
-
return
1706
-
}
1707
-
1708
-
var (
1709
-
description = r.FormValue("description")
1710
-
website = r.FormValue("website")
1711
-
topicStr = r.FormValue("topics")
1712
-
)
1713
-
1714
-
err = rp.validator.ValidateURI(website)
1715
-
if err != nil {
1716
-
l.Error("invalid uri", "err", err)
1717
-
rp.pages.Notice(w, noticeId, err.Error())
1718
-
return
1719
-
}
1720
-
1721
-
topics, err := rp.validator.ValidateRepoTopicStr(topicStr)
1722
-
if err != nil {
1723
-
l.Error("invalid topics", "err", err)
1724
-
rp.pages.Notice(w, noticeId, err.Error())
1725
-
return
1726
-
}
1727
-
l.Debug("got", "topicsStr", topicStr, "topics", topics)
1728
-
1729
-
newRepo := f.Repo
1730
-
newRepo.Description = description
1731
-
newRepo.Website = website
1732
-
newRepo.Topics = topics
1733
-
record := newRepo.AsRecord()
1734
-
1735
-
tx, err := rp.db.BeginTx(r.Context(), nil)
1736
-
if err != nil {
1737
-
l.Error("failed to begin transaction", "err", err)
1738
-
rp.pages.Notice(w, noticeId, "Failed to save repository information.")
1739
-
return
1740
-
}
1741
-
defer tx.Rollback()
1742
-
1743
-
err = db.PutRepo(tx, newRepo)
1744
-
if err != nil {
1745
-
l.Error("failed to update repository", "err", err)
1746
-
rp.pages.Notice(w, noticeId, "Failed to save repository information.")
1747
-
return
1748
-
}
1749
-
1750
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, newRepo.Did, newRepo.Rkey)
1751
-
if err != nil {
1752
-
// failed to get record
1753
-
l.Error("failed to get repo record", "err", err)
1754
-
rp.pages.Notice(w, noticeId, "Failed to save repository information, no record found on PDS.")
1755
-
return
1756
-
}
1757
-
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1758
-
Collection: tangled.RepoNSID,
1759
-
Repo: newRepo.Did,
1760
-
Rkey: newRepo.Rkey,
1761
-
SwapRecord: ex.Cid,
1762
-
Record: &lexutil.LexiconTypeDecoder{
1763
-
Val: &record,
1764
-
},
800
+
Created: createdAt,
1765
801
})
1766
-
1767
802
if err != nil {
1768
-
l.Error("failed to perferom update-repo query", "err", err)
1769
-
// failed to get record
1770
-
rp.pages.Notice(w, noticeId, "Failed to save repository information, unable to save to PDS.")
803
+
fail("Failed to add collaborator.", err)
1771
804
return
1772
805
}
1773
806
1774
807
err = tx.Commit()
1775
808
if err != nil {
1776
-
l.Error("failed to commit", "err", err)
809
+
fail("Failed to add collaborator.", err)
810
+
return
811
+
}
812
+
813
+
err = rp.enforcer.E.SavePolicy()
814
+
if err != nil {
815
+
fail("Failed to update collaborator permissions.", err)
816
+
return
1777
817
}
1778
818
819
+
// clear aturi to when everything is successful
820
+
aturi = ""
821
+
1779
822
rp.pages.HxRefresh(w)
1780
823
}
1781
824
1782
-
func (rp *Repo) SetDefaultBranch(w http.ResponseWriter, r *http.Request) {
1783
-
l := rp.logger.With("handler", "SetDefaultBranch")
825
+
func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) {
826
+
user := rp.oauth.GetUser(r)
827
+
l := rp.logger.With("handler", "DeleteRepo")
1784
828
829
+
noticeId := "operation-error"
1785
830
f, err := rp.repoResolver.Resolve(r)
1786
831
if err != nil {
1787
832
l.Error("failed to get repo and knot", "err", err)
1788
833
return
1789
834
}
1790
835
1791
-
noticeId := "operation-error"
1792
-
branch := r.FormValue("branch")
1793
-
if branch == "" {
1794
-
http.Error(w, "malformed form", http.StatusBadRequest)
836
+
// remove record from pds
837
+
atpClient, err := rp.oauth.AuthorizedClient(r)
838
+
if err != nil {
839
+
l.Error("failed to get authorized client", "err", err)
840
+
return
841
+
}
842
+
_, err = comatproto.RepoDeleteRecord(r.Context(), atpClient, &comatproto.RepoDeleteRecord_Input{
843
+
Collection: tangled.RepoNSID,
844
+
Repo: user.Did,
845
+
Rkey: f.Rkey,
846
+
})
847
+
if err != nil {
848
+
l.Error("failed to delete record", "err", err)
849
+
rp.pages.Notice(w, noticeId, "Failed to delete repository from PDS.")
1795
850
return
1796
851
}
852
+
l.Info("removed repo record", "aturi", f.RepoAt().String())
1797
853
1798
854
client, err := rp.oauth.ServiceClient(
1799
855
r,
1800
856
oauth.WithService(f.Knot),
1801
-
oauth.WithLxm(tangled.RepoSetDefaultBranchNSID),
857
+
oauth.WithLxm(tangled.RepoDeleteNSID),
1802
858
oauth.WithDev(rp.config.Core.Dev),
1803
859
)
1804
860
if err != nil {
1805
861
l.Error("failed to connect to knot server", "err", err)
1806
-
rp.pages.Notice(w, noticeId, "Failed to connect to knot server.")
1807
862
return
1808
863
}
1809
864
1810
-
xe := tangled.RepoSetDefaultBranch(
865
+
err = tangled.RepoDelete(
1811
866
r.Context(),
1812
867
client,
1813
-
&tangled.RepoSetDefaultBranch_Input{
1814
-
Repo: f.RepoAt().String(),
1815
-
DefaultBranch: branch,
868
+
&tangled.RepoDelete_Input{
869
+
Did: f.OwnerDid(),
870
+
Name: f.Name,
871
+
Rkey: f.Rkey,
1816
872
},
1817
873
)
1818
-
if err := xrpcclient.HandleXrpcErr(xe); err != nil {
1819
-
l.Error("xrpc failed", "err", xe)
874
+
if err := xrpcclient.HandleXrpcErr(err); err != nil {
1820
875
rp.pages.Notice(w, noticeId, err.Error())
1821
876
return
1822
877
}
878
+
l.Info("deleted repo from knot")
1823
879
1824
-
rp.pages.HxRefresh(w)
1825
-
}
1826
-
1827
-
func (rp *Repo) Secrets(w http.ResponseWriter, r *http.Request) {
1828
-
user := rp.oauth.GetUser(r)
1829
-
l := rp.logger.With("handler", "Secrets")
1830
-
l = l.With("did", user.Did)
1831
-
1832
-
f, err := rp.repoResolver.Resolve(r)
1833
-
if err != nil {
1834
-
l.Error("failed to get repo and knot", "err", err)
1835
-
return
1836
-
}
1837
-
1838
-
if f.Spindle == "" {
1839
-
l.Error("empty spindle cannot add/rm secret", "err", err)
1840
-
return
1841
-
}
1842
-
1843
-
lxm := tangled.RepoAddSecretNSID
1844
-
if r.Method == http.MethodDelete {
1845
-
lxm = tangled.RepoRemoveSecretNSID
1846
-
}
1847
-
1848
-
spindleClient, err := rp.oauth.ServiceClient(
1849
-
r,
1850
-
oauth.WithService(f.Spindle),
1851
-
oauth.WithLxm(lxm),
1852
-
oauth.WithExp(60),
1853
-
oauth.WithDev(rp.config.Core.Dev),
1854
-
)
880
+
tx, err := rp.db.BeginTx(r.Context(), nil)
1855
881
if err != nil {
1856
-
l.Error("failed to create spindle client", "err", err)
1857
-
return
1858
-
}
1859
-
1860
-
key := r.FormValue("key")
1861
-
if key == "" {
1862
-
w.WriteHeader(http.StatusBadRequest)
882
+
l.Error("failed to start tx")
883
+
w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
1863
884
return
1864
885
}
1865
-
1866
-
switch r.Method {
1867
-
case http.MethodPut:
1868
-
errorId := "add-secret-error"
1869
-
1870
-
value := r.FormValue("value")
1871
-
if value == "" {
1872
-
w.WriteHeader(http.StatusBadRequest)
1873
-
return
1874
-
}
1875
-
1876
-
err = tangled.RepoAddSecret(
1877
-
r.Context(),
1878
-
spindleClient,
1879
-
&tangled.RepoAddSecret_Input{
1880
-
Repo: f.RepoAt().String(),
1881
-
Key: key,
1882
-
Value: value,
1883
-
},
1884
-
)
1885
-
if err != nil {
1886
-
l.Error("Failed to add secret.", "err", err)
1887
-
rp.pages.Notice(w, errorId, "Failed to add secret.")
1888
-
return
1889
-
}
1890
-
1891
-
case http.MethodDelete:
1892
-
errorId := "operation-error"
1893
-
1894
-
err = tangled.RepoRemoveSecret(
1895
-
r.Context(),
1896
-
spindleClient,
1897
-
&tangled.RepoRemoveSecret_Input{
1898
-
Repo: f.RepoAt().String(),
1899
-
Key: key,
1900
-
},
1901
-
)
886
+
defer func() {
887
+
tx.Rollback()
888
+
err = rp.enforcer.E.LoadPolicy()
1902
889
if err != nil {
1903
-
l.Error("Failed to delete secret.", "err", err)
1904
-
rp.pages.Notice(w, errorId, "Failed to delete secret.")
1905
-
return
890
+
l.Error("failed to rollback policies")
1906
891
}
1907
-
}
1908
-
1909
-
rp.pages.HxRefresh(w)
1910
-
}
1911
-
1912
-
type tab = map[string]any
1913
-
1914
-
var (
1915
-
// would be great to have ordered maps right about now
1916
-
settingsTabs []tab = []tab{
1917
-
{"Name": "general", "Icon": "sliders-horizontal"},
1918
-
{"Name": "access", "Icon": "users"},
1919
-
{"Name": "pipelines", "Icon": "layers-2"},
1920
-
}
1921
-
)
1922
-
1923
-
func (rp *Repo) RepoSettings(w http.ResponseWriter, r *http.Request) {
1924
-
tabVal := r.URL.Query().Get("tab")
1925
-
if tabVal == "" {
1926
-
tabVal = "general"
1927
-
}
1928
-
1929
-
switch tabVal {
1930
-
case "general":
1931
-
rp.generalSettings(w, r)
1932
-
1933
-
case "access":
1934
-
rp.accessSettings(w, r)
1935
-
1936
-
case "pipelines":
1937
-
rp.pipelineSettings(w, r)
1938
-
}
1939
-
}
1940
-
1941
-
func (rp *Repo) generalSettings(w http.ResponseWriter, r *http.Request) {
1942
-
l := rp.logger.With("handler", "generalSettings")
1943
-
1944
-
f, err := rp.repoResolver.Resolve(r)
1945
-
user := rp.oauth.GetUser(r)
1946
-
1947
-
scheme := "http"
1948
-
if !rp.config.Core.Dev {
1949
-
scheme = "https"
1950
-
}
1951
-
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
1952
-
xrpcc := &indigoxrpc.Client{
1953
-
Host: host,
1954
-
}
892
+
}()
1955
893
1956
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
1957
-
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
1958
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
1959
-
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
1960
-
rp.pages.Error503(w)
894
+
// remove collaborator RBAC
895
+
repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot)
896
+
if err != nil {
897
+
rp.pages.Notice(w, noticeId, "Failed to remove collaborators")
1961
898
return
1962
899
}
1963
-
1964
-
var result types.RepoBranchesResponse
1965
-
if err := json.Unmarshal(xrpcBytes, &result); err != nil {
1966
-
l.Error("failed to decode XRPC response", "err", err)
1967
-
rp.pages.Error503(w)
1968
-
return
900
+
for _, c := range repoCollaborators {
901
+
did := c[0]
902
+
rp.enforcer.RemoveCollaborator(did, f.Knot, f.DidSlashRepo())
1969
903
}
904
+
l.Info("removed collaborators")
1970
905
1971
-
defaultLabels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", rp.config.Label.DefaultLabelDefs))
906
+
// remove repo RBAC
907
+
err = rp.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.DidSlashRepo())
1972
908
if err != nil {
1973
-
l.Error("failed to fetch labels", "err", err)
1974
-
rp.pages.Error503(w)
909
+
rp.pages.Notice(w, noticeId, "Failed to update RBAC rules")
1975
910
return
1976
911
}
1977
912
1978
-
labels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", f.Repo.Labels))
913
+
// remove repo from db
914
+
err = db.RemoveRepo(tx, f.OwnerDid(), f.Name)
1979
915
if err != nil {
1980
-
l.Error("failed to fetch labels", "err", err)
1981
-
rp.pages.Error503(w)
916
+
rp.pages.Notice(w, noticeId, "Failed to update appview")
1982
917
return
1983
918
}
1984
-
// remove default labels from the labels list, if present
1985
-
defaultLabelMap := make(map[string]bool)
1986
-
for _, dl := range defaultLabels {
1987
-
defaultLabelMap[dl.AtUri().String()] = true
1988
-
}
1989
-
n := 0
1990
-
for _, l := range labels {
1991
-
if !defaultLabelMap[l.AtUri().String()] {
1992
-
labels[n] = l
1993
-
n++
1994
-
}
1995
-
}
1996
-
labels = labels[:n]
1997
-
1998
-
subscribedLabels := make(map[string]struct{})
1999
-
for _, l := range f.Repo.Labels {
2000
-
subscribedLabels[l] = struct{}{}
2001
-
}
2002
-
2003
-
// if there is atleast 1 unsubbed default label, show the "subscribe all" button,
2004
-
// if all default labels are subbed, show the "unsubscribe all" button
2005
-
shouldSubscribeAll := false
2006
-
for _, dl := range defaultLabels {
2007
-
if _, ok := subscribedLabels[dl.AtUri().String()]; !ok {
2008
-
// one of the default labels is not subscribed to
2009
-
shouldSubscribeAll = true
2010
-
break
2011
-
}
2012
-
}
2013
-
2014
-
rp.pages.RepoGeneralSettings(w, pages.RepoGeneralSettingsParams{
2015
-
LoggedInUser: user,
2016
-
RepoInfo: f.RepoInfo(user),
2017
-
Branches: result.Branches,
2018
-
Labels: labels,
2019
-
DefaultLabels: defaultLabels,
2020
-
SubscribedLabels: subscribedLabels,
2021
-
ShouldSubscribeAll: shouldSubscribeAll,
2022
-
Tabs: settingsTabs,
2023
-
Tab: "general",
2024
-
})
2025
-
}
2026
-
2027
-
func (rp *Repo) accessSettings(w http.ResponseWriter, r *http.Request) {
2028
-
l := rp.logger.With("handler", "accessSettings")
2029
-
2030
-
f, err := rp.repoResolver.Resolve(r)
2031
-
user := rp.oauth.GetUser(r)
919
+
l.Info("removed repo from db")
2032
920
2033
-
repoCollaborators, err := f.Collaborators(r.Context())
921
+
err = tx.Commit()
2034
922
if err != nil {
2035
-
l.Error("failed to get collaborators", "err", err)
923
+
l.Error("failed to commit changes", "err", err)
924
+
http.Error(w, err.Error(), http.StatusInternalServerError)
925
+
return
2036
926
}
2037
927
2038
-
rp.pages.RepoAccessSettings(w, pages.RepoAccessSettingsParams{
2039
-
LoggedInUser: user,
2040
-
RepoInfo: f.RepoInfo(user),
2041
-
Tabs: settingsTabs,
2042
-
Tab: "access",
2043
-
Collaborators: repoCollaborators,
2044
-
})
2045
-
}
2046
-
2047
-
func (rp *Repo) pipelineSettings(w http.ResponseWriter, r *http.Request) {
2048
-
l := rp.logger.With("handler", "pipelineSettings")
2049
-
2050
-
f, err := rp.repoResolver.Resolve(r)
2051
-
user := rp.oauth.GetUser(r)
2052
-
2053
-
// all spindles that the repo owner is a member of
2054
-
spindles, err := rp.enforcer.GetSpindlesForUser(f.OwnerDid())
928
+
err = rp.enforcer.E.SavePolicy()
2055
929
if err != nil {
2056
-
l.Error("failed to fetch spindles", "err", err)
930
+
l.Error("failed to update ACLs", "err", err)
931
+
http.Error(w, err.Error(), http.StatusInternalServerError)
2057
932
return
2058
933
}
2059
934
2060
-
var secrets []*tangled.RepoListSecrets_Secret
2061
-
if f.Spindle != "" {
2062
-
if spindleClient, err := rp.oauth.ServiceClient(
2063
-
r,
2064
-
oauth.WithService(f.Spindle),
2065
-
oauth.WithLxm(tangled.RepoListSecretsNSID),
2066
-
oauth.WithExp(60),
2067
-
oauth.WithDev(rp.config.Core.Dev),
2068
-
); err != nil {
2069
-
l.Error("failed to create spindle client", "err", err)
2070
-
} else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt().String()); err != nil {
2071
-
l.Error("failed to fetch secrets", "err", err)
2072
-
} else {
2073
-
secrets = resp.Secrets
2074
-
}
2075
-
}
2076
-
2077
-
slices.SortFunc(secrets, func(a, b *tangled.RepoListSecrets_Secret) int {
2078
-
return strings.Compare(a.Key, b.Key)
2079
-
})
2080
-
2081
-
var dids []string
2082
-
for _, s := range secrets {
2083
-
dids = append(dids, s.CreatedBy)
2084
-
}
2085
-
resolvedIdents := rp.idResolver.ResolveIdents(r.Context(), dids)
2086
-
2087
-
// convert to a more manageable form
2088
-
var niceSecret []map[string]any
2089
-
for id, s := range secrets {
2090
-
when, _ := time.Parse(time.RFC3339, s.CreatedAt)
2091
-
niceSecret = append(niceSecret, map[string]any{
2092
-
"Id": id,
2093
-
"Key": s.Key,
2094
-
"CreatedAt": when,
2095
-
"CreatedBy": resolvedIdents[id].Handle.String(),
2096
-
})
2097
-
}
2098
-
2099
-
rp.pages.RepoPipelineSettings(w, pages.RepoPipelineSettingsParams{
2100
-
LoggedInUser: user,
2101
-
RepoInfo: f.RepoInfo(user),
2102
-
Tabs: settingsTabs,
2103
-
Tab: "pipelines",
2104
-
Spindles: spindles,
2105
-
CurrentSpindle: f.Spindle,
2106
-
Secrets: niceSecret,
2107
-
})
935
+
rp.pages.HxRedirect(w, fmt.Sprintf("/%s", f.OwnerDid()))
2108
936
}
2109
937
2110
938
func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) {
···
2388
1216
})
2389
1217
return err
2390
1218
}
2391
-
2392
-
func (rp *Repo) RepoCompareNew(w http.ResponseWriter, r *http.Request) {
2393
-
l := rp.logger.With("handler", "RepoCompareNew")
2394
-
2395
-
user := rp.oauth.GetUser(r)
2396
-
f, err := rp.repoResolver.Resolve(r)
2397
-
if err != nil {
2398
-
l.Error("failed to get repo and knot", "err", err)
2399
-
return
2400
-
}
2401
-
2402
-
scheme := "http"
2403
-
if !rp.config.Core.Dev {
2404
-
scheme = "https"
2405
-
}
2406
-
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
2407
-
xrpcc := &indigoxrpc.Client{
2408
-
Host: host,
2409
-
}
2410
-
2411
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
2412
-
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
2413
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
2414
-
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
2415
-
rp.pages.Error503(w)
2416
-
return
2417
-
}
2418
-
2419
-
var branchResult types.RepoBranchesResponse
2420
-
if err := json.Unmarshal(branchBytes, &branchResult); err != nil {
2421
-
l.Error("failed to decode XRPC branches response", "err", err)
2422
-
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
2423
-
return
2424
-
}
2425
-
branches := branchResult.Branches
2426
-
2427
-
sortBranches(branches)
2428
-
2429
-
var defaultBranch string
2430
-
for _, b := range branches {
2431
-
if b.IsDefault {
2432
-
defaultBranch = b.Name
2433
-
}
2434
-
}
2435
-
2436
-
base := defaultBranch
2437
-
head := defaultBranch
2438
-
2439
-
params := r.URL.Query()
2440
-
queryBase := params.Get("base")
2441
-
queryHead := params.Get("head")
2442
-
if queryBase != "" {
2443
-
base = queryBase
2444
-
}
2445
-
if queryHead != "" {
2446
-
head = queryHead
2447
-
}
2448
-
2449
-
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
2450
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
2451
-
l.Error("failed to call XRPC repo.tags", "err", xrpcerr)
2452
-
rp.pages.Error503(w)
2453
-
return
2454
-
}
2455
-
2456
-
var tags types.RepoTagsResponse
2457
-
if err := json.Unmarshal(tagBytes, &tags); err != nil {
2458
-
l.Error("failed to decode XRPC tags response", "err", err)
2459
-
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
2460
-
return
2461
-
}
2462
-
2463
-
repoinfo := f.RepoInfo(user)
2464
-
2465
-
rp.pages.RepoCompareNew(w, pages.RepoCompareNewParams{
2466
-
LoggedInUser: user,
2467
-
RepoInfo: repoinfo,
2468
-
Branches: branches,
2469
-
Tags: tags.Tags,
2470
-
Base: base,
2471
-
Head: head,
2472
-
})
2473
-
}
2474
-
2475
-
func (rp *Repo) RepoCompare(w http.ResponseWriter, r *http.Request) {
2476
-
l := rp.logger.With("handler", "RepoCompare")
2477
-
2478
-
user := rp.oauth.GetUser(r)
2479
-
f, err := rp.repoResolver.Resolve(r)
2480
-
if err != nil {
2481
-
l.Error("failed to get repo and knot", "err", err)
2482
-
return
2483
-
}
2484
-
2485
-
var diffOpts types.DiffOpts
2486
-
if d := r.URL.Query().Get("diff"); d == "split" {
2487
-
diffOpts.Split = true
2488
-
}
2489
-
2490
-
// if user is navigating to one of
2491
-
// /compare/{base}/{head}
2492
-
// /compare/{base}...{head}
2493
-
base := chi.URLParam(r, "base")
2494
-
head := chi.URLParam(r, "head")
2495
-
if base == "" && head == "" {
2496
-
rest := chi.URLParam(r, "*") // master...feature/xyz
2497
-
parts := strings.SplitN(rest, "...", 2)
2498
-
if len(parts) == 2 {
2499
-
base = parts[0]
2500
-
head = parts[1]
2501
-
}
2502
-
}
2503
-
2504
-
base, _ = url.PathUnescape(base)
2505
-
head, _ = url.PathUnescape(head)
2506
-
2507
-
if base == "" || head == "" {
2508
-
l.Error("invalid comparison")
2509
-
rp.pages.Error404(w)
2510
-
return
2511
-
}
2512
-
2513
-
scheme := "http"
2514
-
if !rp.config.Core.Dev {
2515
-
scheme = "https"
2516
-
}
2517
-
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
2518
-
xrpcc := &indigoxrpc.Client{
2519
-
Host: host,
2520
-
}
2521
-
2522
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
2523
-
2524
-
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
2525
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
2526
-
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
2527
-
rp.pages.Error503(w)
2528
-
return
2529
-
}
2530
-
2531
-
var branches types.RepoBranchesResponse
2532
-
if err := json.Unmarshal(branchBytes, &branches); err != nil {
2533
-
l.Error("failed to decode XRPC branches response", "err", err)
2534
-
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
2535
-
return
2536
-
}
2537
-
2538
-
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
2539
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
2540
-
l.Error("failed to call XRPC repo.tags", "err", xrpcerr)
2541
-
rp.pages.Error503(w)
2542
-
return
2543
-
}
2544
-
2545
-
var tags types.RepoTagsResponse
2546
-
if err := json.Unmarshal(tagBytes, &tags); err != nil {
2547
-
l.Error("failed to decode XRPC tags response", "err", err)
2548
-
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
2549
-
return
2550
-
}
2551
-
2552
-
compareBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo, base, head)
2553
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
2554
-
l.Error("failed to call XRPC repo.compare", "err", xrpcerr)
2555
-
rp.pages.Error503(w)
2556
-
return
2557
-
}
2558
-
2559
-
var formatPatch types.RepoFormatPatchResponse
2560
-
if err := json.Unmarshal(compareBytes, &formatPatch); err != nil {
2561
-
l.Error("failed to decode XRPC compare response", "err", err)
2562
-
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
2563
-
return
2564
-
}
2565
-
2566
-
var diff types.NiceDiff
2567
-
if formatPatch.CombinedPatchRaw != "" {
2568
-
diff = patchutil.AsNiceDiff(formatPatch.CombinedPatchRaw, base)
2569
-
} else {
2570
-
diff = patchutil.AsNiceDiff(formatPatch.FormatPatchRaw, base)
2571
-
}
2572
-
2573
-
repoinfo := f.RepoInfo(user)
2574
-
2575
-
rp.pages.RepoCompare(w, pages.RepoCompareParams{
2576
-
LoggedInUser: user,
2577
-
RepoInfo: repoinfo,
2578
-
Branches: branches.Branches,
2579
-
Tags: tags.Tags,
2580
-
Base: base,
2581
-
Head: head,
2582
-
Diff: &diff,
2583
-
DiffOpts: diffOpts,
2584
-
})
2585
-
2586
-
}
+14
-14
appview/repo/router.go
+14
-14
appview/repo/router.go
···
9
9
10
10
func (rp *Repo) Router(mw *middleware.Middleware) http.Handler {
11
11
r := chi.NewRouter()
12
-
r.Get("/", rp.RepoIndex)
13
-
r.Get("/opengraph", rp.RepoOpenGraphSummary)
14
-
r.Get("/feed.atom", rp.RepoAtomFeed)
15
-
r.Get("/commits/{ref}", rp.RepoLog)
12
+
r.Get("/", rp.Index)
13
+
r.Get("/opengraph", rp.Opengraph)
14
+
r.Get("/feed.atom", rp.AtomFeed)
15
+
r.Get("/commits/{ref}", rp.Log)
16
16
r.Route("/tree/{ref}", func(r chi.Router) {
17
-
r.Get("/", rp.RepoIndex)
18
-
r.Get("/*", rp.RepoTree)
17
+
r.Get("/", rp.Index)
18
+
r.Get("/*", rp.Tree)
19
19
})
20
-
r.Get("/commit/{ref}", rp.RepoCommit)
21
-
r.Get("/branches", rp.RepoBranches)
20
+
r.Get("/commit/{ref}", rp.Commit)
21
+
r.Get("/branches", rp.Branches)
22
22
r.Delete("/branches", rp.DeleteBranch)
23
23
r.Route("/tags", func(r chi.Router) {
24
-
r.Get("/", rp.RepoTags)
24
+
r.Get("/", rp.Tags)
25
25
r.Route("/{tag}", func(r chi.Router) {
26
26
r.Get("/download/{file}", rp.DownloadArtifact)
27
27
···
37
37
})
38
38
})
39
39
})
40
-
r.Get("/blob/{ref}/*", rp.RepoBlob)
40
+
r.Get("/blob/{ref}/*", rp.Blob)
41
41
r.Get("/raw/{ref}/*", rp.RepoBlobRaw)
42
42
43
43
// intentionally doesn't use /* as this isn't
···
54
54
})
55
55
56
56
r.Route("/compare", func(r chi.Router) {
57
-
r.Get("/", rp.RepoCompareNew) // start an new comparison
57
+
r.Get("/", rp.CompareNew) // start an new comparison
58
58
59
59
// we have to wildcard here since we want to support GitHub's compare syntax
60
60
// /compare/{ref1}...{ref2}
61
61
// for example:
62
62
// /compare/master...some/feature
63
63
// /compare/master...example.com:another/feature <- this is a fork
64
-
r.Get("/{base}/{head}", rp.RepoCompare)
65
-
r.Get("/*", rp.RepoCompare)
64
+
r.Get("/{base}/{head}", rp.Compare)
65
+
r.Get("/*", rp.Compare)
66
66
})
67
67
68
68
// label panel in issues/pulls/discussions/tasks
···
75
75
r.Group(func(r chi.Router) {
76
76
r.Use(middleware.AuthMiddleware(rp.oauth))
77
77
r.With(mw.RepoPermissionMiddleware("repo:settings")).Route("/settings", func(r chi.Router) {
78
-
r.Get("/", rp.RepoSettings)
78
+
r.Get("/", rp.Settings)
79
79
r.With(mw.RepoPermissionMiddleware("repo:owner")).Put("/base", rp.EditBaseSettings)
80
80
r.With(mw.RepoPermissionMiddleware("repo:owner")).Post("/spindle", rp.EditSpindle)
81
81
r.With(mw.RepoPermissionMiddleware("repo:owner")).Put("/label", rp.AddLabelDef)
+442
appview/repo/settings.go
+442
appview/repo/settings.go
···
1
+
package repo
2
+
3
+
import (
4
+
"encoding/json"
5
+
"fmt"
6
+
"net/http"
7
+
"slices"
8
+
"strings"
9
+
"time"
10
+
11
+
"tangled.org/core/api/tangled"
12
+
"tangled.org/core/appview/db"
13
+
"tangled.org/core/appview/oauth"
14
+
"tangled.org/core/appview/pages"
15
+
xrpcclient "tangled.org/core/appview/xrpcclient"
16
+
"tangled.org/core/types"
17
+
18
+
comatproto "github.com/bluesky-social/indigo/api/atproto"
19
+
lexutil "github.com/bluesky-social/indigo/lex/util"
20
+
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
21
+
)
22
+
23
+
type tab = map[string]any
24
+
25
+
var (
26
+
// would be great to have ordered maps right about now
27
+
settingsTabs []tab = []tab{
28
+
{"Name": "general", "Icon": "sliders-horizontal"},
29
+
{"Name": "access", "Icon": "users"},
30
+
{"Name": "pipelines", "Icon": "layers-2"},
31
+
}
32
+
)
33
+
34
+
func (rp *Repo) SetDefaultBranch(w http.ResponseWriter, r *http.Request) {
35
+
l := rp.logger.With("handler", "SetDefaultBranch")
36
+
37
+
f, err := rp.repoResolver.Resolve(r)
38
+
if err != nil {
39
+
l.Error("failed to get repo and knot", "err", err)
40
+
return
41
+
}
42
+
43
+
noticeId := "operation-error"
44
+
branch := r.FormValue("branch")
45
+
if branch == "" {
46
+
http.Error(w, "malformed form", http.StatusBadRequest)
47
+
return
48
+
}
49
+
50
+
client, err := rp.oauth.ServiceClient(
51
+
r,
52
+
oauth.WithService(f.Knot),
53
+
oauth.WithLxm(tangled.RepoSetDefaultBranchNSID),
54
+
oauth.WithDev(rp.config.Core.Dev),
55
+
)
56
+
if err != nil {
57
+
l.Error("failed to connect to knot server", "err", err)
58
+
rp.pages.Notice(w, noticeId, "Failed to connect to knot server.")
59
+
return
60
+
}
61
+
62
+
xe := tangled.RepoSetDefaultBranch(
63
+
r.Context(),
64
+
client,
65
+
&tangled.RepoSetDefaultBranch_Input{
66
+
Repo: f.RepoAt().String(),
67
+
DefaultBranch: branch,
68
+
},
69
+
)
70
+
if err := xrpcclient.HandleXrpcErr(xe); err != nil {
71
+
l.Error("xrpc failed", "err", xe)
72
+
rp.pages.Notice(w, noticeId, err.Error())
73
+
return
74
+
}
75
+
76
+
rp.pages.HxRefresh(w)
77
+
}
78
+
79
+
func (rp *Repo) Secrets(w http.ResponseWriter, r *http.Request) {
80
+
user := rp.oauth.GetUser(r)
81
+
l := rp.logger.With("handler", "Secrets")
82
+
l = l.With("did", user.Did)
83
+
84
+
f, err := rp.repoResolver.Resolve(r)
85
+
if err != nil {
86
+
l.Error("failed to get repo and knot", "err", err)
87
+
return
88
+
}
89
+
90
+
if f.Spindle == "" {
91
+
l.Error("empty spindle cannot add/rm secret", "err", err)
92
+
return
93
+
}
94
+
95
+
lxm := tangled.RepoAddSecretNSID
96
+
if r.Method == http.MethodDelete {
97
+
lxm = tangled.RepoRemoveSecretNSID
98
+
}
99
+
100
+
spindleClient, err := rp.oauth.ServiceClient(
101
+
r,
102
+
oauth.WithService(f.Spindle),
103
+
oauth.WithLxm(lxm),
104
+
oauth.WithExp(60),
105
+
oauth.WithDev(rp.config.Core.Dev),
106
+
)
107
+
if err != nil {
108
+
l.Error("failed to create spindle client", "err", err)
109
+
return
110
+
}
111
+
112
+
key := r.FormValue("key")
113
+
if key == "" {
114
+
w.WriteHeader(http.StatusBadRequest)
115
+
return
116
+
}
117
+
118
+
switch r.Method {
119
+
case http.MethodPut:
120
+
errorId := "add-secret-error"
121
+
122
+
value := r.FormValue("value")
123
+
if value == "" {
124
+
w.WriteHeader(http.StatusBadRequest)
125
+
return
126
+
}
127
+
128
+
err = tangled.RepoAddSecret(
129
+
r.Context(),
130
+
spindleClient,
131
+
&tangled.RepoAddSecret_Input{
132
+
Repo: f.RepoAt().String(),
133
+
Key: key,
134
+
Value: value,
135
+
},
136
+
)
137
+
if err != nil {
138
+
l.Error("Failed to add secret.", "err", err)
139
+
rp.pages.Notice(w, errorId, "Failed to add secret.")
140
+
return
141
+
}
142
+
143
+
case http.MethodDelete:
144
+
errorId := "operation-error"
145
+
146
+
err = tangled.RepoRemoveSecret(
147
+
r.Context(),
148
+
spindleClient,
149
+
&tangled.RepoRemoveSecret_Input{
150
+
Repo: f.RepoAt().String(),
151
+
Key: key,
152
+
},
153
+
)
154
+
if err != nil {
155
+
l.Error("Failed to delete secret.", "err", err)
156
+
rp.pages.Notice(w, errorId, "Failed to delete secret.")
157
+
return
158
+
}
159
+
}
160
+
161
+
rp.pages.HxRefresh(w)
162
+
}
163
+
164
+
func (rp *Repo) Settings(w http.ResponseWriter, r *http.Request) {
165
+
tabVal := r.URL.Query().Get("tab")
166
+
if tabVal == "" {
167
+
tabVal = "general"
168
+
}
169
+
170
+
switch tabVal {
171
+
case "general":
172
+
rp.generalSettings(w, r)
173
+
174
+
case "access":
175
+
rp.accessSettings(w, r)
176
+
177
+
case "pipelines":
178
+
rp.pipelineSettings(w, r)
179
+
}
180
+
}
181
+
182
+
func (rp *Repo) generalSettings(w http.ResponseWriter, r *http.Request) {
183
+
l := rp.logger.With("handler", "generalSettings")
184
+
185
+
f, err := rp.repoResolver.Resolve(r)
186
+
user := rp.oauth.GetUser(r)
187
+
188
+
scheme := "http"
189
+
if !rp.config.Core.Dev {
190
+
scheme = "https"
191
+
}
192
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
193
+
xrpcc := &indigoxrpc.Client{
194
+
Host: host,
195
+
}
196
+
197
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
198
+
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
199
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
200
+
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
201
+
rp.pages.Error503(w)
202
+
return
203
+
}
204
+
205
+
var result types.RepoBranchesResponse
206
+
if err := json.Unmarshal(xrpcBytes, &result); err != nil {
207
+
l.Error("failed to decode XRPC response", "err", err)
208
+
rp.pages.Error503(w)
209
+
return
210
+
}
211
+
212
+
defaultLabels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", rp.config.Label.DefaultLabelDefs))
213
+
if err != nil {
214
+
l.Error("failed to fetch labels", "err", err)
215
+
rp.pages.Error503(w)
216
+
return
217
+
}
218
+
219
+
labels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", f.Repo.Labels))
220
+
if err != nil {
221
+
l.Error("failed to fetch labels", "err", err)
222
+
rp.pages.Error503(w)
223
+
return
224
+
}
225
+
// remove default labels from the labels list, if present
226
+
defaultLabelMap := make(map[string]bool)
227
+
for _, dl := range defaultLabels {
228
+
defaultLabelMap[dl.AtUri().String()] = true
229
+
}
230
+
n := 0
231
+
for _, l := range labels {
232
+
if !defaultLabelMap[l.AtUri().String()] {
233
+
labels[n] = l
234
+
n++
235
+
}
236
+
}
237
+
labels = labels[:n]
238
+
239
+
subscribedLabels := make(map[string]struct{})
240
+
for _, l := range f.Repo.Labels {
241
+
subscribedLabels[l] = struct{}{}
242
+
}
243
+
244
+
// if there is atleast 1 unsubbed default label, show the "subscribe all" button,
245
+
// if all default labels are subbed, show the "unsubscribe all" button
246
+
shouldSubscribeAll := false
247
+
for _, dl := range defaultLabels {
248
+
if _, ok := subscribedLabels[dl.AtUri().String()]; !ok {
249
+
// one of the default labels is not subscribed to
250
+
shouldSubscribeAll = true
251
+
break
252
+
}
253
+
}
254
+
255
+
rp.pages.RepoGeneralSettings(w, pages.RepoGeneralSettingsParams{
256
+
LoggedInUser: user,
257
+
RepoInfo: f.RepoInfo(user),
258
+
Branches: result.Branches,
259
+
Labels: labels,
260
+
DefaultLabels: defaultLabels,
261
+
SubscribedLabels: subscribedLabels,
262
+
ShouldSubscribeAll: shouldSubscribeAll,
263
+
Tabs: settingsTabs,
264
+
Tab: "general",
265
+
})
266
+
}
267
+
268
+
func (rp *Repo) accessSettings(w http.ResponseWriter, r *http.Request) {
269
+
l := rp.logger.With("handler", "accessSettings")
270
+
271
+
f, err := rp.repoResolver.Resolve(r)
272
+
user := rp.oauth.GetUser(r)
273
+
274
+
repoCollaborators, err := f.Collaborators(r.Context())
275
+
if err != nil {
276
+
l.Error("failed to get collaborators", "err", err)
277
+
}
278
+
279
+
rp.pages.RepoAccessSettings(w, pages.RepoAccessSettingsParams{
280
+
LoggedInUser: user,
281
+
RepoInfo: f.RepoInfo(user),
282
+
Tabs: settingsTabs,
283
+
Tab: "access",
284
+
Collaborators: repoCollaborators,
285
+
})
286
+
}
287
+
288
+
func (rp *Repo) pipelineSettings(w http.ResponseWriter, r *http.Request) {
289
+
l := rp.logger.With("handler", "pipelineSettings")
290
+
291
+
f, err := rp.repoResolver.Resolve(r)
292
+
user := rp.oauth.GetUser(r)
293
+
294
+
// all spindles that the repo owner is a member of
295
+
spindles, err := rp.enforcer.GetSpindlesForUser(f.OwnerDid())
296
+
if err != nil {
297
+
l.Error("failed to fetch spindles", "err", err)
298
+
return
299
+
}
300
+
301
+
var secrets []*tangled.RepoListSecrets_Secret
302
+
if f.Spindle != "" {
303
+
if spindleClient, err := rp.oauth.ServiceClient(
304
+
r,
305
+
oauth.WithService(f.Spindle),
306
+
oauth.WithLxm(tangled.RepoListSecretsNSID),
307
+
oauth.WithExp(60),
308
+
oauth.WithDev(rp.config.Core.Dev),
309
+
); err != nil {
310
+
l.Error("failed to create spindle client", "err", err)
311
+
} else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt().String()); err != nil {
312
+
l.Error("failed to fetch secrets", "err", err)
313
+
} else {
314
+
secrets = resp.Secrets
315
+
}
316
+
}
317
+
318
+
slices.SortFunc(secrets, func(a, b *tangled.RepoListSecrets_Secret) int {
319
+
return strings.Compare(a.Key, b.Key)
320
+
})
321
+
322
+
var dids []string
323
+
for _, s := range secrets {
324
+
dids = append(dids, s.CreatedBy)
325
+
}
326
+
resolvedIdents := rp.idResolver.ResolveIdents(r.Context(), dids)
327
+
328
+
// convert to a more manageable form
329
+
var niceSecret []map[string]any
330
+
for id, s := range secrets {
331
+
when, _ := time.Parse(time.RFC3339, s.CreatedAt)
332
+
niceSecret = append(niceSecret, map[string]any{
333
+
"Id": id,
334
+
"Key": s.Key,
335
+
"CreatedAt": when,
336
+
"CreatedBy": resolvedIdents[id].Handle.String(),
337
+
})
338
+
}
339
+
340
+
rp.pages.RepoPipelineSettings(w, pages.RepoPipelineSettingsParams{
341
+
LoggedInUser: user,
342
+
RepoInfo: f.RepoInfo(user),
343
+
Tabs: settingsTabs,
344
+
Tab: "pipelines",
345
+
Spindles: spindles,
346
+
CurrentSpindle: f.Spindle,
347
+
Secrets: niceSecret,
348
+
})
349
+
}
350
+
351
+
func (rp *Repo) EditBaseSettings(w http.ResponseWriter, r *http.Request) {
352
+
l := rp.logger.With("handler", "EditBaseSettings")
353
+
354
+
noticeId := "repo-base-settings-error"
355
+
356
+
f, err := rp.repoResolver.Resolve(r)
357
+
if err != nil {
358
+
l.Error("failed to get repo and knot", "err", err)
359
+
w.WriteHeader(http.StatusBadRequest)
360
+
return
361
+
}
362
+
363
+
client, err := rp.oauth.AuthorizedClient(r)
364
+
if err != nil {
365
+
l.Error("failed to get client")
366
+
rp.pages.Notice(w, noticeId, "Failed to update repository information, try again later.")
367
+
return
368
+
}
369
+
370
+
var (
371
+
description = r.FormValue("description")
372
+
website = r.FormValue("website")
373
+
topicStr = r.FormValue("topics")
374
+
)
375
+
376
+
err = rp.validator.ValidateURI(website)
377
+
if err != nil {
378
+
l.Error("invalid uri", "err", err)
379
+
rp.pages.Notice(w, noticeId, err.Error())
380
+
return
381
+
}
382
+
383
+
topics, err := rp.validator.ValidateRepoTopicStr(topicStr)
384
+
if err != nil {
385
+
l.Error("invalid topics", "err", err)
386
+
rp.pages.Notice(w, noticeId, err.Error())
387
+
return
388
+
}
389
+
l.Debug("got", "topicsStr", topicStr, "topics", topics)
390
+
391
+
newRepo := f.Repo
392
+
newRepo.Description = description
393
+
newRepo.Website = website
394
+
newRepo.Topics = topics
395
+
record := newRepo.AsRecord()
396
+
397
+
tx, err := rp.db.BeginTx(r.Context(), nil)
398
+
if err != nil {
399
+
l.Error("failed to begin transaction", "err", err)
400
+
rp.pages.Notice(w, noticeId, "Failed to save repository information.")
401
+
return
402
+
}
403
+
defer tx.Rollback()
404
+
405
+
err = db.PutRepo(tx, newRepo)
406
+
if err != nil {
407
+
l.Error("failed to update repository", "err", err)
408
+
rp.pages.Notice(w, noticeId, "Failed to save repository information.")
409
+
return
410
+
}
411
+
412
+
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, newRepo.Did, newRepo.Rkey)
413
+
if err != nil {
414
+
// failed to get record
415
+
l.Error("failed to get repo record", "err", err)
416
+
rp.pages.Notice(w, noticeId, "Failed to save repository information, no record found on PDS.")
417
+
return
418
+
}
419
+
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
420
+
Collection: tangled.RepoNSID,
421
+
Repo: newRepo.Did,
422
+
Rkey: newRepo.Rkey,
423
+
SwapRecord: ex.Cid,
424
+
Record: &lexutil.LexiconTypeDecoder{
425
+
Val: &record,
426
+
},
427
+
})
428
+
429
+
if err != nil {
430
+
l.Error("failed to perferom update-repo query", "err", err)
431
+
// failed to get record
432
+
rp.pages.Notice(w, noticeId, "Failed to save repository information, unable to save to PDS.")
433
+
return
434
+
}
435
+
436
+
err = tx.Commit()
437
+
if err != nil {
438
+
l.Error("failed to commit", "err", err)
439
+
}
440
+
441
+
rp.pages.HxRefresh(w)
442
+
}
+107
appview/repo/tree.go
+107
appview/repo/tree.go
···
1
+
package repo
2
+
3
+
import (
4
+
"fmt"
5
+
"net/http"
6
+
"net/url"
7
+
"strings"
8
+
"time"
9
+
10
+
"tangled.org/core/api/tangled"
11
+
"tangled.org/core/appview/pages"
12
+
xrpcclient "tangled.org/core/appview/xrpcclient"
13
+
"tangled.org/core/types"
14
+
15
+
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
16
+
"github.com/go-chi/chi/v5"
17
+
"github.com/go-git/go-git/v5/plumbing"
18
+
)
19
+
20
+
func (rp *Repo) Tree(w http.ResponseWriter, r *http.Request) {
21
+
l := rp.logger.With("handler", "RepoTree")
22
+
f, err := rp.repoResolver.Resolve(r)
23
+
if err != nil {
24
+
l.Error("failed to fully resolve repo", "err", err)
25
+
return
26
+
}
27
+
ref := chi.URLParam(r, "ref")
28
+
ref, _ = url.PathUnescape(ref)
29
+
// if the tree path has a trailing slash, let's strip it
30
+
// so we don't 404
31
+
treePath := chi.URLParam(r, "*")
32
+
treePath, _ = url.PathUnescape(treePath)
33
+
treePath = strings.TrimSuffix(treePath, "/")
34
+
scheme := "http"
35
+
if !rp.config.Core.Dev {
36
+
scheme = "https"
37
+
}
38
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
39
+
xrpcc := &indigoxrpc.Client{
40
+
Host: host,
41
+
}
42
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
43
+
xrpcResp, err := tangled.RepoTree(r.Context(), xrpcc, treePath, ref, repo)
44
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
45
+
l.Error("failed to call XRPC repo.tree", "err", xrpcerr)
46
+
rp.pages.Error503(w)
47
+
return
48
+
}
49
+
// Convert XRPC response to internal types.RepoTreeResponse
50
+
files := make([]types.NiceTree, len(xrpcResp.Files))
51
+
for i, xrpcFile := range xrpcResp.Files {
52
+
file := types.NiceTree{
53
+
Name: xrpcFile.Name,
54
+
Mode: xrpcFile.Mode,
55
+
Size: int64(xrpcFile.Size),
56
+
IsFile: xrpcFile.Is_file,
57
+
IsSubtree: xrpcFile.Is_subtree,
58
+
}
59
+
// Convert last commit info if present
60
+
if xrpcFile.Last_commit != nil {
61
+
commitWhen, _ := time.Parse(time.RFC3339, xrpcFile.Last_commit.When)
62
+
file.LastCommit = &types.LastCommitInfo{
63
+
Hash: plumbing.NewHash(xrpcFile.Last_commit.Hash),
64
+
Message: xrpcFile.Last_commit.Message,
65
+
When: commitWhen,
66
+
}
67
+
}
68
+
files[i] = file
69
+
}
70
+
result := types.RepoTreeResponse{
71
+
Ref: xrpcResp.Ref,
72
+
Files: files,
73
+
}
74
+
if xrpcResp.Parent != nil {
75
+
result.Parent = *xrpcResp.Parent
76
+
}
77
+
if xrpcResp.Dotdot != nil {
78
+
result.DotDot = *xrpcResp.Dotdot
79
+
}
80
+
if xrpcResp.Readme != nil {
81
+
result.ReadmeFileName = xrpcResp.Readme.Filename
82
+
result.Readme = xrpcResp.Readme.Contents
83
+
}
84
+
// redirects tree paths trying to access a blob; in this case the result.Files is unpopulated,
85
+
// so we can safely redirect to the "parent" (which is the same file).
86
+
if len(result.Files) == 0 && result.Parent == treePath {
87
+
redirectTo := fmt.Sprintf("/%s/blob/%s/%s", f.OwnerSlashRepo(), url.PathEscape(ref), result.Parent)
88
+
http.Redirect(w, r, redirectTo, http.StatusFound)
89
+
return
90
+
}
91
+
user := rp.oauth.GetUser(r)
92
+
var breadcrumbs [][]string
93
+
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), url.PathEscape(ref))})
94
+
if treePath != "" {
95
+
for idx, elem := range strings.Split(treePath, "/") {
96
+
breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))})
97
+
}
98
+
}
99
+
sortFiles(result.Files)
100
+
rp.pages.RepoTree(w, pages.RepoTreeParams{
101
+
LoggedInUser: user,
102
+
BreadCrumbs: breadcrumbs,
103
+
TreePath: treePath,
104
+
RepoInfo: f.RepoInfo(user),
105
+
RepoTreeResponse: result,
106
+
})
107
+
}