-30
api/tangled/repodeleteBranch.go
-30
api/tangled/repodeleteBranch.go
···
1
-
// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
2
-
3
-
package tangled
4
-
5
-
// schema: sh.tangled.repo.deleteBranch
6
-
7
-
import (
8
-
"context"
9
-
10
-
"github.com/bluesky-social/indigo/lex/util"
11
-
)
12
-
13
-
const (
14
-
RepoDeleteBranchNSID = "sh.tangled.repo.deleteBranch"
15
-
)
16
-
17
-
// RepoDeleteBranch_Input is the input argument to a sh.tangled.repo.deleteBranch call.
18
-
type RepoDeleteBranch_Input struct {
19
-
Branch string `json:"branch" cborgen:"branch"`
20
-
Repo string `json:"repo" cborgen:"repo"`
21
-
}
22
-
23
-
// RepoDeleteBranch calls the XRPC method "sh.tangled.repo.deleteBranch".
24
-
func RepoDeleteBranch(ctx context.Context, c util.LexClient, input *RepoDeleteBranch_Input) error {
25
-
if err := c.LexDo(ctx, util.Procedure, "application/json", "sh.tangled.repo.deleteBranch", nil, input, nil); err != nil {
26
-
return err
27
-
}
28
-
29
-
return nil
30
-
}
···
+10
-38
appview/db/timeline.go
+10
-38
appview/db/timeline.go
···
9
10
// TODO: this gathers heterogenous events from different sources and aggregates
11
// them in code; if we did this entirely in sql, we could order and limit and paginate easily
12
-
func MakeTimeline(e Execer, limit int, loggedInUserDid string, limitToUsersIsFollowing bool) ([]models.TimelineEvent, error) {
13
var events []models.TimelineEvent
14
15
-
var userIsFollowing []string
16
-
if limitToUsersIsFollowing {
17
-
following, err := GetFollowing(e, loggedInUserDid)
18
-
if err != nil {
19
-
return nil, err
20
-
}
21
-
22
-
userIsFollowing = make([]string, 0, len(following))
23
-
for _, follow := range following {
24
-
userIsFollowing = append(userIsFollowing, follow.SubjectDid)
25
-
}
26
-
}
27
-
28
-
repos, err := getTimelineRepos(e, limit, loggedInUserDid, userIsFollowing)
29
if err != nil {
30
return nil, err
31
}
32
33
-
stars, err := getTimelineStars(e, limit, loggedInUserDid, userIsFollowing)
34
if err != nil {
35
return nil, err
36
}
37
38
-
follows, err := getTimelineFollows(e, limit, loggedInUserDid, userIsFollowing)
39
if err != nil {
40
return nil, err
41
}
···
83
return isStarred, starCount
84
}
85
86
-
func getTimelineRepos(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
87
-
filters := make([]filter, 0)
88
-
if userIsFollowing != nil {
89
-
filters = append(filters, FilterIn("did", userIsFollowing))
90
-
}
91
-
92
-
repos, err := GetRepos(e, limit, filters...)
93
if err != nil {
94
return nil, err
95
}
···
143
return events, nil
144
}
145
146
-
func getTimelineStars(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
147
-
filters := make([]filter, 0)
148
-
if userIsFollowing != nil {
149
-
filters = append(filters, FilterIn("starred_by_did", userIsFollowing))
150
-
}
151
-
152
-
stars, err := GetStars(e, limit, filters...)
153
if err != nil {
154
return nil, err
155
}
···
189
return events, nil
190
}
191
192
-
func getTimelineFollows(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
193
-
filters := make([]filter, 0)
194
-
if userIsFollowing != nil {
195
-
filters = append(filters, FilterIn("user_did", userIsFollowing))
196
-
}
197
-
198
-
follows, err := GetFollows(e, limit, filters...)
199
if err != nil {
200
return nil, err
201
}
···
9
10
// TODO: this gathers heterogenous events from different sources and aggregates
11
// them in code; if we did this entirely in sql, we could order and limit and paginate easily
12
+
func MakeTimeline(e Execer, limit int, loggedInUserDid string) ([]models.TimelineEvent, error) {
13
var events []models.TimelineEvent
14
15
+
repos, err := getTimelineRepos(e, limit, loggedInUserDid)
16
if err != nil {
17
return nil, err
18
}
19
20
+
stars, err := getTimelineStars(e, limit, loggedInUserDid)
21
if err != nil {
22
return nil, err
23
}
24
25
+
follows, err := getTimelineFollows(e, limit, loggedInUserDid)
26
if err != nil {
27
return nil, err
28
}
···
70
return isStarred, starCount
71
}
72
73
+
func getTimelineRepos(e Execer, limit int, loggedInUserDid string) ([]models.TimelineEvent, error) {
74
+
repos, err := GetRepos(e, limit)
75
if err != nil {
76
return nil, err
77
}
···
125
return events, nil
126
}
127
128
+
func getTimelineStars(e Execer, limit int, loggedInUserDid string) ([]models.TimelineEvent, error) {
129
+
stars, err := GetStars(e, limit)
130
if err != nil {
131
return nil, err
132
}
···
166
return events, nil
167
}
168
169
+
func getTimelineFollows(e Execer, limit int, loggedInUserDid string) ([]models.TimelineEvent, error) {
170
+
follows, err := GetFollows(e, limit)
171
if err != nil {
172
return nil, err
173
}
-5
appview/models/pull.go
-5
appview/models/pull.go
+4
-4
appview/pages/funcmap.go
+4
-4
appview/pages/funcmap.go
···
265
return nil
266
},
267
"i": func(name string, classes ...string) template.HTML {
268
-
data, err := p.icon(name, classes)
269
if err != nil {
270
log.Printf("icon %s does not exist", name)
271
-
data, _ = p.icon("airplay", classes)
272
}
273
return template.HTML(data)
274
},
275
-
"cssContentHash": p.CssContentHash,
276
"fileTree": filetree.FileTree,
277
"pathEscape": func(s string) string {
278
return url.PathEscape(s)
···
325
return fmt.Sprintf("%s/%s/%s?%s", p.avatar.Host, signature, handle, sizeArg)
326
}
327
328
-
func (p *Pages) icon(name string, classes []string) (template.HTML, error) {
329
iconPath := filepath.Join("static", "icons", name)
330
331
if filepath.Ext(name) == "" {
···
265
return nil
266
},
267
"i": func(name string, classes ...string) template.HTML {
268
+
data, err := icon(name, classes)
269
if err != nil {
270
log.Printf("icon %s does not exist", name)
271
+
data, _ = icon("airplay", classes)
272
}
273
return template.HTML(data)
274
},
275
+
"cssContentHash": CssContentHash,
276
"fileTree": filetree.FileTree,
277
"pathEscape": func(s string) string {
278
return url.PathEscape(s)
···
325
return fmt.Sprintf("%s/%s/%s?%s", p.avatar.Host, signature, handle, sizeArg)
326
}
327
328
+
func icon(name string, classes []string) (template.HTML, error) {
329
iconPath := filepath.Join("static", "icons", name)
330
331
if filepath.Ext(name) == "" {
+1
-6
appview/pages/markup/markdown.go
+1
-6
appview/pages/markup/markdown.go
···
5
"bytes"
6
"fmt"
7
"io"
8
-
"io/fs"
9
"net/url"
10
"path"
11
"strings"
···
21
"github.com/yuin/goldmark/renderer/html"
22
"github.com/yuin/goldmark/text"
23
"github.com/yuin/goldmark/util"
24
-
callout "gitlab.com/staticnoise/goldmark-callout"
25
htmlparse "golang.org/x/net/html"
26
27
"tangled.org/core/api/tangled"
···
47
IsDev bool
48
RendererType RendererType
49
Sanitizer Sanitizer
50
-
Files fs.FS
51
}
52
53
func (rctx *RenderContext) RenderMarkdown(source string) string {
···
65
extension.WithFootnoteIDPrefix([]byte("footnote")),
66
),
67
treeblood.MathML(),
68
-
callout.CalloutExtention,
69
),
70
goldmark.WithParserOptions(
71
parser.WithAutoHeadingID(),
···
144
func visitNode(ctx *RenderContext, node *htmlparse.Node) {
145
switch node.Type {
146
case htmlparse.ElementNode:
147
-
switch node.Data {
148
-
case "img", "source":
149
for i, attr := range node.Attr {
150
if attr.Key != "src" {
151
continue
···
5
"bytes"
6
"fmt"
7
"io"
8
"net/url"
9
"path"
10
"strings"
···
20
"github.com/yuin/goldmark/renderer/html"
21
"github.com/yuin/goldmark/text"
22
"github.com/yuin/goldmark/util"
23
htmlparse "golang.org/x/net/html"
24
25
"tangled.org/core/api/tangled"
···
45
IsDev bool
46
RendererType RendererType
47
Sanitizer Sanitizer
48
}
49
50
func (rctx *RenderContext) RenderMarkdown(source string) string {
···
62
extension.WithFootnoteIDPrefix([]byte("footnote")),
63
),
64
treeblood.MathML(),
65
),
66
goldmark.WithParserOptions(
67
parser.WithAutoHeadingID(),
···
140
func visitNode(ctx *RenderContext, node *htmlparse.Node) {
141
switch node.Type {
142
case htmlparse.ElementNode:
143
+
if node.Data == "img" || node.Data == "source" {
144
for i, attr := range node.Attr {
145
if attr.Key != "src" {
146
continue
-3
appview/pages/markup/sanitizer.go
-3
appview/pages/markup/sanitizer.go
+19
-22
appview/pages/pages.go
+19
-22
appview/pages/pages.go
···
61
CamoUrl: config.Camo.Host,
62
CamoSecret: config.Camo.SharedSecret,
63
Sanitizer: markup.NewSanitizer(),
64
-
Files: Files,
65
}
66
67
p := &Pages{
···
1129
}
1130
1131
type RepoSinglePullParams struct {
1132
-
LoggedInUser *oauth.User
1133
-
RepoInfo repoinfo.RepoInfo
1134
-
Active string
1135
-
Pull *models.Pull
1136
-
Stack models.Stack
1137
-
AbandonedPulls []*models.Pull
1138
-
BranchDeleteStatus *models.BranchDeleteStatus
1139
-
MergeCheck types.MergeCheckResponse
1140
-
ResubmitCheck ResubmitResult
1141
-
Pipelines map[string]models.Pipeline
1142
1143
OrderedReactionKinds []models.ReactionKind
1144
Reactions map[models.ReactionKind]models.ReactionDisplayData
···
1234
}
1235
1236
type PullActionsParams struct {
1237
-
LoggedInUser *oauth.User
1238
-
RepoInfo repoinfo.RepoInfo
1239
-
Pull *models.Pull
1240
-
RoundNumber int
1241
-
MergeCheck types.MergeCheckResponse
1242
-
ResubmitCheck ResubmitResult
1243
-
BranchDeleteStatus *models.BranchDeleteStatus
1244
-
Stack models.Stack
1245
}
1246
1247
func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error {
···
1478
return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static")))
1479
}
1480
1481
-
sub, err := fs.Sub(p.embedFS, "static")
1482
if err != nil {
1483
p.logger.Error("no static dir found? that's crazy", "err", err)
1484
panic(err)
···
1501
})
1502
}
1503
1504
-
func (p *Pages) CssContentHash() string {
1505
-
cssFile, err := p.embedFS.Open("static/tw.css")
1506
if err != nil {
1507
slog.Debug("Error opening CSS file", "err", err)
1508
return ""
···
61
CamoUrl: config.Camo.Host,
62
CamoSecret: config.Camo.SharedSecret,
63
Sanitizer: markup.NewSanitizer(),
64
}
65
66
p := &Pages{
···
1128
}
1129
1130
type RepoSinglePullParams struct {
1131
+
LoggedInUser *oauth.User
1132
+
RepoInfo repoinfo.RepoInfo
1133
+
Active string
1134
+
Pull *models.Pull
1135
+
Stack models.Stack
1136
+
AbandonedPulls []*models.Pull
1137
+
MergeCheck types.MergeCheckResponse
1138
+
ResubmitCheck ResubmitResult
1139
+
Pipelines map[string]models.Pipeline
1140
1141
OrderedReactionKinds []models.ReactionKind
1142
Reactions map[models.ReactionKind]models.ReactionDisplayData
···
1232
}
1233
1234
type PullActionsParams struct {
1235
+
LoggedInUser *oauth.User
1236
+
RepoInfo repoinfo.RepoInfo
1237
+
Pull *models.Pull
1238
+
RoundNumber int
1239
+
MergeCheck types.MergeCheckResponse
1240
+
ResubmitCheck ResubmitResult
1241
+
Stack models.Stack
1242
}
1243
1244
func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error {
···
1475
return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static")))
1476
}
1477
1478
+
sub, err := fs.Sub(Files, "static")
1479
if err != nil {
1480
p.logger.Error("no static dir found? that's crazy", "err", err)
1481
panic(err)
···
1498
})
1499
}
1500
1501
+
func CssContentHash() string {
1502
+
cssFile, err := Files.Open("static/tw.css")
1503
if err != nil {
1504
slog.Debug("Error opening CSS file", "err", err)
1505
return ""
+2
-2
appview/pages/templates/layouts/base.html
+2
-2
appview/pages/templates/layouts/base.html
···
26
</head>
27
<body class="min-h-screen flex flex-col gap-4 bg-slate-100 dark:bg-gray-900 dark:text-white transition-colors duration-200">
28
{{ block "topbarLayout" . }}
29
-
<header class="w-full col-span-full md:col-span-1 md:col-start-2" style="z-index: 20;">
30
31
{{ if .LoggedInUser }}
32
<div id="upgrade-banner"
···
58
{{ end }}
59
60
{{ block "footerLayout" . }}
61
-
<footer class="mt-12">
62
{{ template "layouts/fragments/footer" . }}
63
</footer>
64
{{ end }}
···
26
</head>
27
<body class="min-h-screen flex flex-col gap-4 bg-slate-100 dark:bg-gray-900 dark:text-white transition-colors duration-200">
28
{{ block "topbarLayout" . }}
29
+
<header class="w-full bg-white dark:bg-gray-800 col-span-full md:col-span-1 md:col-start-2" style="z-index: 20;">
30
31
{{ if .LoggedInUser }}
32
<div id="upgrade-banner"
···
58
{{ end }}
59
60
{{ block "footerLayout" . }}
61
+
<footer class="bg-white dark:bg-gray-800 mt-12">
62
{{ template "layouts/fragments/footer" . }}
63
</footer>
64
{{ end }}
+1
-1
appview/pages/templates/layouts/fragments/topbar.html
+1
-1
appview/pages/templates/layouts/fragments/topbar.html
···
1
{{ define "layouts/fragments/topbar" }}
2
-
<nav class="mx-auto space-x-4 px-6 py-2 rounded-b dark:text-white drop-shadow-sm bg-white dark:bg-gray-800">
3
<div class="flex justify-between p-0 items-center">
4
<div id="left-items">
5
<a href="/" hx-boost="true" class="text-2xl no-underline hover:no-underline flex items-center gap-2">
···
1
{{ define "layouts/fragments/topbar" }}
2
+
<nav class="mx-auto space-x-4 px-6 py-2 rounded-b dark:text-white drop-shadow-sm">
3
<div class="flex justify-between p-0 items-center">
4
<div id="left-items">
5
<a href="/" hx-boost="true" class="text-2xl no-underline hover:no-underline flex items-center gap-2">
+3
-3
appview/pages/templates/repo/commit.html
+3
-3
appview/pages/templates/repo/commit.html
···
80
{{end}}
81
82
{{ define "topbarLayout" }}
83
-
<header class="col-span-full" style="z-index: 20;">
84
{{ template "layouts/fragments/topbar" . }}
85
</header>
86
{{ end }}
87
88
{{ define "mainLayout" }}
89
-
<div class="px-1 flex-grow col-span-full flex flex-col gap-4">
90
{{ block "contentLayout" . }}
91
{{ block "content" . }}{{ end }}
92
{{ end }}
···
105
{{ end }}
106
107
{{ define "footerLayout" }}
108
-
<footer class="col-span-full mt-12">
109
{{ template "layouts/fragments/footer" . }}
110
</footer>
111
{{ end }}
···
80
{{end}}
81
82
{{ define "topbarLayout" }}
83
+
<header class="px-1 col-span-full" style="z-index: 20;">
84
{{ template "layouts/fragments/topbar" . }}
85
</header>
86
{{ end }}
87
88
{{ define "mainLayout" }}
89
+
<div class="px-1 col-span-full flex flex-col gap-4">
90
{{ block "contentLayout" . }}
91
{{ block "content" . }}{{ end }}
92
{{ end }}
···
105
{{ end }}
106
107
{{ define "footerLayout" }}
108
+
<footer class="px-1 col-span-full mt-12">
109
{{ template "layouts/fragments/footer" . }}
110
</footer>
111
{{ end }}
-11
appview/pages/templates/repo/pulls/fragments/pullActions.html
-11
appview/pages/templates/repo/pulls/fragments/pullActions.html
···
33
<span>comment</span>
34
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
35
</button>
36
-
{{ if .BranchDeleteStatus }}
37
-
<button
38
-
hx-delete="/{{ .BranchDeleteStatus.Repo.Did }}/{{ .BranchDeleteStatus.Repo.Name }}/branches"
39
-
hx-vals='{"branch": "{{ .BranchDeleteStatus.Branch }}" }'
40
-
hx-swap="none"
41
-
class="btn p-2 flex items-center gap-2 no-underline hover:no-underline group text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300">
42
-
{{ i "git-branch" "w-4 h-4" }}
43
-
<span>delete branch</span>
44
-
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
45
-
</button>
46
-
{{ end }}
47
{{ if and $isPushAllowed $isOpen $isLastRound }}
48
{{ $disabled := "" }}
49
{{ if $isConflicted }}
+14
-1
appview/pages/templates/repo/pulls/interdiff.html
+14
-1
appview/pages/templates/repo/pulls/interdiff.html
···
28
29
{{ end }}
30
31
{{ define "mainLayout" }}
32
-
<div class="px-1 col-span-full flex-grow flex flex-col gap-4">
33
{{ block "contentLayout" . }}
34
{{ block "content" . }}{{ end }}
35
{{ end }}
···
46
{{ end }}
47
</div>
48
{{ end }}
49
50
{{ define "contentAfter" }}
51
{{ template "repo/fragments/interdiff" (list .RepoInfo.FullName .Interdiff .DiffOpts) }}
···
28
29
{{ end }}
30
31
+
{{ define "topbarLayout" }}
32
+
<header class="px-1 col-span-full" style="z-index: 20;">
33
+
{{ template "layouts/fragments/topbar" . }}
34
+
</header>
35
+
{{ end }}
36
+
37
{{ define "mainLayout" }}
38
+
<div class="px-1 col-span-full flex flex-col gap-4">
39
{{ block "contentLayout" . }}
40
{{ block "content" . }}{{ end }}
41
{{ end }}
···
52
{{ end }}
53
</div>
54
{{ end }}
55
+
56
+
{{ define "footerLayout" }}
57
+
<footer class="px-1 col-span-full mt-12">
58
+
{{ template "layouts/fragments/footer" . }}
59
+
</footer>
60
+
{{ end }}
61
+
62
63
{{ define "contentAfter" }}
64
{{ template "repo/fragments/interdiff" (list .RepoInfo.FullName .Interdiff .DiffOpts) }}
+13
-1
appview/pages/templates/repo/pulls/patch.html
+13
-1
appview/pages/templates/repo/pulls/patch.html
···
34
</section>
35
{{ end }}
36
37
+
{{ define "topbarLayout" }}
38
+
<header class="px-1 col-span-full" style="z-index: 20;">
39
+
{{ template "layouts/fragments/topbar" . }}
40
+
</header>
41
+
{{ end }}
42
+
43
{{ define "mainLayout" }}
44
+
<div class="px-1 col-span-full flex flex-col gap-4">
45
{{ block "contentLayout" . }}
46
{{ block "content" . }}{{ end }}
47
{{ end }}
···
57
</div>
58
{{ end }}
59
</div>
60
+
{{ end }}
61
+
62
+
{{ define "footerLayout" }}
63
+
<footer class="px-1 col-span-full mt-12">
64
+
{{ template "layouts/fragments/footer" . }}
65
+
</footer>
66
{{ end }}
67
68
{{ define "contentAfter" }}
+1
-10
appview/pages/templates/repo/pulls/pull.html
+1
-10
appview/pages/templates/repo/pulls/pull.html
···
187
{{ end }}
188
189
{{ if $.LoggedInUser }}
190
-
{{ template "repo/pulls/fragments/pullActions"
191
-
(dict
192
-
"LoggedInUser" $.LoggedInUser
193
-
"Pull" $.Pull
194
-
"RepoInfo" $.RepoInfo
195
-
"RoundNumber" .RoundNumber
196
-
"MergeCheck" $.MergeCheck
197
-
"ResubmitCheck" $.ResubmitCheck
198
-
"BranchDeleteStatus" $.BranchDeleteStatus
199
-
"Stack" $.Stack) }}
200
{{ else }}
201
<div class="bg-amber-50 dark:bg-amber-900 border border-amber-500 rounded drop-shadow-sm p-2 relative flex gap-2 items-center w-fit">
202
<a href="/signup" class="btn-create py-0 hover:no-underline hover:text-white flex items-center gap-2">
···
187
{{ end }}
188
189
{{ if $.LoggedInUser }}
190
+
{{ template "repo/pulls/fragments/pullActions" (dict "LoggedInUser" $.LoggedInUser "Pull" $.Pull "RepoInfo" $.RepoInfo "RoundNumber" .RoundNumber "MergeCheck" $.MergeCheck "ResubmitCheck" $.ResubmitCheck "Stack" $.Stack) }}
191
{{ else }}
192
<div class="bg-amber-50 dark:bg-amber-900 border border-amber-500 rounded drop-shadow-sm p-2 relative flex gap-2 items-center w-fit">
193
<a href="/signup" class="btn-create py-0 hover:no-underline hover:text-white flex items-center gap-2">
+19
-69
appview/pulls/pulls.go
+19
-69
appview/pulls/pulls.go
···
98
}
99
100
mergeCheckResponse := s.mergeCheck(r, f, pull, stack)
101
-
branchDeleteStatus := s.branchDeleteStatus(r, f, pull)
102
resubmitResult := pages.Unknown
103
if user.Did == pull.OwnerDid {
104
resubmitResult = s.resubmitCheck(r, f, pull, stack)
105
}
106
107
s.pages.PullActionsFragment(w, pages.PullActionsParams{
108
-
LoggedInUser: user,
109
-
RepoInfo: f.RepoInfo(user),
110
-
Pull: pull,
111
-
RoundNumber: roundNumber,
112
-
MergeCheck: mergeCheckResponse,
113
-
ResubmitCheck: resubmitResult,
114
-
BranchDeleteStatus: branchDeleteStatus,
115
-
Stack: stack,
116
})
117
return
118
}
···
155
}
156
157
mergeCheckResponse := s.mergeCheck(r, f, pull, stack)
158
-
branchDeleteStatus := s.branchDeleteStatus(r, f, pull)
159
resubmitResult := pages.Unknown
160
if user != nil && user.Did == pull.OwnerDid {
161
resubmitResult = s.resubmitCheck(r, f, pull, stack)
···
220
}
221
222
s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{
223
-
LoggedInUser: user,
224
-
RepoInfo: repoInfo,
225
-
Pull: pull,
226
-
Stack: stack,
227
-
AbandonedPulls: abandonedPulls,
228
-
BranchDeleteStatus: branchDeleteStatus,
229
-
MergeCheck: mergeCheckResponse,
230
-
ResubmitCheck: resubmitResult,
231
-
Pipelines: m,
232
233
OrderedReactionKinds: models.OrderedReactionKinds,
234
Reactions: reactionMap,
···
305
return result
306
}
307
308
-
func (s *Pulls) branchDeleteStatus(r *http.Request, f *reporesolver.ResolvedRepo, pull *models.Pull) *models.BranchDeleteStatus {
309
-
if pull.State != models.PullMerged {
310
-
return nil
311
-
}
312
-
313
-
user := s.oauth.GetUser(r)
314
-
if user == nil {
315
-
return nil
316
-
}
317
-
318
-
var branch string
319
-
var repo *models.Repo
320
-
// check if the branch exists
321
-
// NOTE: appview could cache branches/tags etc. for every repo by listening for gitRefUpdates
322
-
if pull.IsBranchBased() {
323
-
branch = pull.PullSource.Branch
324
-
repo = &f.Repo
325
-
} else if pull.IsForkBased() {
326
-
branch = pull.PullSource.Branch
327
-
repo = pull.PullSource.Repo
328
-
} else {
329
-
return nil
330
-
}
331
-
332
-
scheme := "http"
333
-
if !s.config.Core.Dev {
334
-
scheme = "https"
335
-
}
336
-
host := fmt.Sprintf("%s://%s", scheme, repo.Knot)
337
-
xrpcc := &indigoxrpc.Client{
338
-
Host: host,
339
-
}
340
-
341
-
resp, err := tangled.RepoBranch(r.Context(), xrpcc, branch, fmt.Sprintf("%s/%s", repo.Did, repo.Name))
342
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
343
-
return nil
344
-
}
345
-
346
-
return &models.BranchDeleteStatus{
347
-
Repo: repo,
348
-
Branch: resp.Name,
349
-
}
350
-
}
351
-
352
func (s *Pulls) resubmitCheck(r *http.Request, f *reporesolver.ResolvedRepo, pull *models.Pull, stack models.Stack) pages.ResubmitResult {
353
if pull.State == models.PullMerged || pull.State == models.PullDeleted || pull.PullSource == nil {
354
return pages.Unknown
···
1201
Repo: string(f.RepoAt()),
1202
Branch: targetBranch,
1203
},
1204
-
Patch: patch,
1205
-
Source: recordPullSource,
1206
-
CreatedAt: time.Now().Format(time.RFC3339),
1207
},
1208
},
1209
})
···
1854
Repo: string(f.RepoAt()),
1855
Branch: pull.TargetBranch,
1856
},
1857
-
Patch: patch, // new patch
1858
-
Source: recordPullSource,
1859
-
CreatedAt: time.Now().Format(time.RFC3339),
1860
},
1861
},
1862
})
···
98
}
99
100
mergeCheckResponse := s.mergeCheck(r, f, pull, stack)
101
resubmitResult := pages.Unknown
102
if user.Did == pull.OwnerDid {
103
resubmitResult = s.resubmitCheck(r, f, pull, stack)
104
}
105
106
s.pages.PullActionsFragment(w, pages.PullActionsParams{
107
+
LoggedInUser: user,
108
+
RepoInfo: f.RepoInfo(user),
109
+
Pull: pull,
110
+
RoundNumber: roundNumber,
111
+
MergeCheck: mergeCheckResponse,
112
+
ResubmitCheck: resubmitResult,
113
+
Stack: stack,
114
})
115
return
116
}
···
153
}
154
155
mergeCheckResponse := s.mergeCheck(r, f, pull, stack)
156
resubmitResult := pages.Unknown
157
if user != nil && user.Did == pull.OwnerDid {
158
resubmitResult = s.resubmitCheck(r, f, pull, stack)
···
217
}
218
219
s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{
220
+
LoggedInUser: user,
221
+
RepoInfo: repoInfo,
222
+
Pull: pull,
223
+
Stack: stack,
224
+
AbandonedPulls: abandonedPulls,
225
+
MergeCheck: mergeCheckResponse,
226
+
ResubmitCheck: resubmitResult,
227
+
Pipelines: m,
228
229
OrderedReactionKinds: models.OrderedReactionKinds,
230
Reactions: reactionMap,
···
301
return result
302
}
303
304
func (s *Pulls) resubmitCheck(r *http.Request, f *reporesolver.ResolvedRepo, pull *models.Pull, stack models.Stack) pages.ResubmitResult {
305
if pull.State == models.PullMerged || pull.State == models.PullDeleted || pull.PullSource == nil {
306
return pages.Unknown
···
1153
Repo: string(f.RepoAt()),
1154
Branch: targetBranch,
1155
},
1156
+
Patch: patch,
1157
+
Source: recordPullSource,
1158
},
1159
},
1160
})
···
1805
Repo: string(f.RepoAt()),
1806
Branch: pull.TargetBranch,
1807
},
1808
+
Patch: patch, // new patch
1809
+
Source: recordPullSource,
1810
},
1811
},
1812
})
+3
-3
appview/repo/opengraph.go
+3
-3
appview/repo/opengraph.go
···
30
contentCard, bottomArea := mainCard.Split(false, 75)
31
32
// Add padding to content
33
-
contentCard.SetMargin(50)
34
35
// Split content horizontally: main content (80%) and avatar area (20%)
36
mainContent, avatarArea := contentCard.Split(true, 80)
···
82
// Draw description (DrawText handles multi-line wrapping automatically)
83
descriptionCard.SetMargin(10)
84
description := repo.Description
85
-
if len(description) > 70 {
86
-
description = description[:70] + "…"
87
}
88
89
_, err = descriptionCard.DrawText(description, color.RGBA{88, 96, 105, 255}, 36, ogcard.Top, ogcard.Left)
···
30
contentCard, bottomArea := mainCard.Split(false, 75)
31
32
// Add padding to content
33
+
contentCard.SetMargin(30)
34
35
// Split content horizontally: main content (80%) and avatar area (20%)
36
mainContent, avatarArea := contentCard.Split(true, 80)
···
82
// Draw description (DrawText handles multi-line wrapping automatically)
83
descriptionCard.SetMargin(10)
84
description := repo.Description
85
+
if len(description) > 80 {
86
+
description = description[:100] + "…"
87
}
88
89
_, err = descriptionCard.DrawText(description, color.RGBA{88, 96, 105, 255}, 36, ogcard.Top, ogcard.Left)
-47
appview/repo/repo.go
-47
appview/repo/repo.go
···
628
})
629
}
630
631
-
func (rp *Repo) DeleteBranch(w http.ResponseWriter, r *http.Request) {
632
-
f, err := rp.repoResolver.Resolve(r)
633
-
if err != nil {
634
-
log.Println("failed to get repo and knot", err)
635
-
return
636
-
}
637
-
638
-
noticeId := "delete-branch-error"
639
-
fail := func(msg string, err error) {
640
-
log.Println(msg, "err", err)
641
-
rp.pages.Notice(w, noticeId, msg)
642
-
}
643
-
644
-
branch := r.FormValue("branch")
645
-
if branch == "" {
646
-
fail("No branch provided.", nil)
647
-
return
648
-
}
649
-
650
-
client, err := rp.oauth.ServiceClient(
651
-
r,
652
-
oauth.WithService(f.Knot),
653
-
oauth.WithLxm(tangled.RepoDeleteBranchNSID),
654
-
oauth.WithDev(rp.config.Core.Dev),
655
-
)
656
-
if err != nil {
657
-
fail("Failed to connect to knotserver", nil)
658
-
return
659
-
}
660
-
661
-
err = tangled.RepoDeleteBranch(
662
-
r.Context(),
663
-
client,
664
-
&tangled.RepoDeleteBranch_Input{
665
-
Branch: branch,
666
-
Repo: f.RepoAt().String(),
667
-
},
668
-
)
669
-
if err := xrpcclient.HandleXrpcErr(err); err != nil {
670
-
fail(fmt.Sprintf("Failed to delete branch: %s", err), err)
671
-
return
672
-
}
673
-
log.Println("deleted branch from knot", "branch", branch, "repo", f.RepoAt())
674
-
675
-
rp.pages.HxRefresh(w)
676
-
}
677
-
678
func (rp *Repo) RepoBlob(w http.ResponseWriter, r *http.Request) {
679
f, err := rp.repoResolver.Resolve(r)
680
if err != nil {
-1
appview/repo/router.go
-1
appview/repo/router.go
+2
-8
appview/state/state.go
+2
-8
appview/state/state.go
···
268
func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {
269
user := s.oauth.GetUser(r)
270
271
-
// TODO: set this flag based on the UI
272
-
filtered := false
273
-
274
var userDid string
275
if user != nil {
276
userDid = user.Did
277
}
278
-
timeline, err := db.MakeTimeline(s.db, 50, userDid, filtered)
279
if err != nil {
280
log.Println(err)
281
s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.")
···
339
}
340
341
func (s *State) Home(w http.ResponseWriter, r *http.Request) {
342
-
// TODO: set this flag based on the UI
343
-
filtered := false
344
-
345
-
timeline, err := db.MakeTimeline(s.db, 5, "", filtered)
346
if err != nil {
347
log.Println(err)
348
s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.")
···
268
func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {
269
user := s.oauth.GetUser(r)
270
271
var userDid string
272
if user != nil {
273
userDid = user.Did
274
}
275
+
timeline, err := db.MakeTimeline(s.db, 50, userDid)
276
if err != nil {
277
log.Println(err)
278
s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.")
···
336
}
337
338
func (s *State) Home(w http.ResponseWriter, r *http.Request) {
339
+
timeline, err := db.MakeTimeline(s.db, 5, "")
340
if err != nil {
341
log.Println(err)
342
s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.")
-1
go.mod
-1
go.mod
···
160
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
161
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
162
github.com/wyatt915/treeblood v0.1.15 // indirect
163
-
gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab // indirect
164
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect
165
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
166
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
···
160
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
161
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
162
github.com/wyatt915/treeblood v0.1.15 // indirect
163
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect
164
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
165
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
-2
go.sum
-2
go.sum
···
444
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
445
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
446
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
447
-
gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab h1:gK9tS6QJw5F0SIhYJnGG2P83kuabOdmWBbSmZhJkz2A=
448
-
gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab/go.mod h1:SPu13/NPe1kMrbGoJldQwqtpNhXsmIuHCfm/aaGjU0c=
449
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA=
450
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8=
451
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q=
···
444
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
445
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
446
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
447
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA=
448
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8=
449
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q=
+12
-71
input.css
+12
-71
input.css
···
134
}
135
136
.prose hr {
137
-
@apply my-2;
138
}
139
140
.prose li:has(input) {
141
-
@apply list-none;
142
}
143
144
.prose ul:has(input) {
145
-
@apply pl-2;
146
}
147
148
.prose .heading .anchor {
149
-
@apply no-underline mx-2 opacity-0;
150
}
151
152
.prose .heading:hover .anchor {
153
-
@apply opacity-70;
154
}
155
156
.prose .heading .anchor:hover {
157
-
@apply opacity-70;
158
}
159
160
.prose a.footnote-backref {
161
-
@apply no-underline;
162
}
163
164
.prose li {
165
-
@apply my-0 py-0;
166
}
167
168
-
.prose ul,
169
-
.prose ol {
170
-
@apply my-1 py-0;
171
}
172
173
.prose img {
···
177
}
178
179
.prose input {
180
-
@apply inline-block my-0 mb-1 mx-1;
181
}
182
183
.prose input[type="checkbox"] {
184
@apply disabled:accent-blue-500 checked:accent-blue-500 disabled:checked:accent-blue-500;
185
}
186
-
187
-
/* Base callout */
188
-
details[data-callout] {
189
-
@apply border-l-4 pl-3 py-2 text-gray-800 dark:text-gray-200 my-4;
190
-
}
191
-
192
-
details[data-callout] > summary {
193
-
@apply font-bold cursor-pointer mb-1;
194
-
}
195
-
196
-
details[data-callout] > .callout-content {
197
-
@apply text-sm leading-snug;
198
-
}
199
-
200
-
/* Note (blue) */
201
-
details[data-callout="note" i] {
202
-
@apply border-blue-400 dark:border-blue-500;
203
-
}
204
-
details[data-callout="note" i] > summary {
205
-
@apply text-blue-700 dark:text-blue-400;
206
-
}
207
-
208
-
/* Important (purple) */
209
-
details[data-callout="important" i] {
210
-
@apply border-purple-400 dark:border-purple-500;
211
-
}
212
-
details[data-callout="important" i] > summary {
213
-
@apply text-purple-700 dark:text-purple-400;
214
-
}
215
-
216
-
/* Warning (yellow) */
217
-
details[data-callout="warning" i] {
218
-
@apply border-yellow-400 dark:border-yellow-500;
219
-
}
220
-
details[data-callout="warning" i] > summary {
221
-
@apply text-yellow-700 dark:text-yellow-400;
222
-
}
223
-
224
-
/* Caution (red) */
225
-
details[data-callout="caution" i] {
226
-
@apply border-red-400 dark:border-red-500;
227
-
}
228
-
details[data-callout="caution" i] > summary {
229
-
@apply text-red-700 dark:text-red-400;
230
-
}
231
-
232
-
/* Tip (green) */
233
-
details[data-callout="tip" i] {
234
-
@apply border-green-400 dark:border-green-500;
235
-
}
236
-
details[data-callout="tip" i] > summary {
237
-
@apply text-green-700 dark:text-green-400;
238
-
}
239
-
240
-
/* Optional: hide the disclosure arrow like GitHub */
241
-
details[data-callout] > summary::-webkit-details-marker {
242
-
display: none;
243
-
}
244
}
245
@layer utilities {
246
.error {
···
287
}
288
/* LineHighlight */
289
.chroma .hl {
290
-
@apply bg-amber-400/30 dark:bg-amber-500/20;
291
}
292
293
/* LineNumbersTable */
···
134
}
135
136
.prose hr {
137
+
@apply my-2;
138
}
139
140
.prose li:has(input) {
141
+
@apply list-none;
142
}
143
144
.prose ul:has(input) {
145
+
@apply pl-2;
146
}
147
148
.prose .heading .anchor {
149
+
@apply no-underline mx-2 opacity-0;
150
}
151
152
.prose .heading:hover .anchor {
153
+
@apply opacity-70;
154
}
155
156
.prose .heading .anchor:hover {
157
+
@apply opacity-70;
158
}
159
160
.prose a.footnote-backref {
161
+
@apply no-underline;
162
}
163
164
.prose li {
165
+
@apply my-0 py-0;
166
}
167
168
+
.prose ul, .prose ol {
169
+
@apply my-1 py-0;
170
}
171
172
.prose img {
···
176
}
177
178
.prose input {
179
+
@apply inline-block my-0 mb-1 mx-1;
180
}
181
182
.prose input[type="checkbox"] {
183
@apply disabled:accent-blue-500 checked:accent-blue-500 disabled:checked:accent-blue-500;
184
}
185
}
186
@layer utilities {
187
.error {
···
228
}
229
/* LineHighlight */
230
.chroma .hl {
231
+
@apply bg-amber-400/30 dark:bg-amber-500/20;
232
}
233
234
/* LineNumbersTable */
-5
knotserver/git/branch.go
-5
knotserver/git/branch.go
-11
knotserver/git/git.go
-11
knotserver/git/git.go
···
71
return &g, nil
72
}
73
74
-
// re-open a repository and update references
75
-
func (g *GitRepo) Refresh() error {
76
-
refreshed, err := PlainOpen(g.path)
77
-
if err != nil {
78
-
return err
79
-
}
80
-
81
-
*g = *refreshed
82
-
return nil
83
-
}
84
-
85
func (g *GitRepo) Commits(offset, limit int) ([]*object.Commit, error) {
86
commits := []*object.Commit{}
87
+37
-150
knotserver/git/merge.go
+37
-150
knotserver/git/merge.go
···
4
"bytes"
5
"crypto/sha256"
6
"fmt"
7
-
"log"
8
"os"
9
"os/exec"
10
"regexp"
···
13
"github.com/dgraph-io/ristretto"
14
"github.com/go-git/go-git/v5"
15
"github.com/go-git/go-git/v5/plumbing"
16
-
"tangled.org/core/patchutil"
17
-
"tangled.org/core/types"
18
)
19
20
type MergeCheckCache struct {
···
35
mergeCheckCache = MergeCheckCache{cache}
36
}
37
38
-
func (m *MergeCheckCache) cacheKey(g *GitRepo, patch string, targetBranch string) string {
39
sep := byte(':')
40
hash := sha256.Sum256(fmt.Append([]byte{}, g.path, sep, g.h.String(), sep, patch, sep, targetBranch))
41
return fmt.Sprintf("%x", hash)
···
52
}
53
}
54
55
-
func (m *MergeCheckCache) Set(g *GitRepo, patch string, targetBranch string, mergeCheck error) {
56
key := m.cacheKey(g, patch, targetBranch)
57
val := m.cacheVal(mergeCheck)
58
m.cache.Set(key, val, 0)
59
}
60
61
-
func (m *MergeCheckCache) Get(g *GitRepo, patch string, targetBranch string) (error, bool) {
62
key := m.cacheKey(g, patch, targetBranch)
63
if val, ok := m.cache.Get(key); ok {
64
if val == struct{}{} {
···
107
return fmt.Sprintf("merge failed: %s", e.Message)
108
}
109
110
-
func (g *GitRepo) createTempFileWithPatch(patchData string) (string, error) {
111
tmpFile, err := os.CreateTemp("", "git-patch-*.patch")
112
if err != nil {
113
return "", fmt.Errorf("failed to create temporary patch file: %w", err)
114
}
115
116
-
if _, err := tmpFile.Write([]byte(patchData)); err != nil {
117
tmpFile.Close()
118
os.Remove(tmpFile.Name())
119
return "", fmt.Errorf("failed to write patch data to temporary file: %w", err)
···
165
return nil
166
}
167
168
-
func (g *GitRepo) applyPatch(patchData, patchFile string, opts MergeOptions) error {
169
var stderr bytes.Buffer
170
var cmd *exec.Cmd
171
172
// configure default git user before merge
173
-
exec.Command("git", "-C", g.path, "config", "user.name", opts.CommitterName).Run()
174
-
exec.Command("git", "-C", g.path, "config", "user.email", opts.CommitterEmail).Run()
175
-
exec.Command("git", "-C", g.path, "config", "advice.mergeConflict", "false").Run()
176
177
// if patch is a format-patch, apply using 'git am'
178
if opts.FormatPatch {
179
-
return g.applyMailbox(patchData)
180
-
}
181
182
-
// else, apply using 'git apply' and commit it manually
183
-
applyCmd := exec.Command("git", "-C", g.path, "apply", patchFile)
184
-
applyCmd.Stderr = &stderr
185
-
if err := applyCmd.Run(); err != nil {
186
-
return fmt.Errorf("patch application failed: %s", stderr.String())
187
-
}
188
189
-
stageCmd := exec.Command("git", "-C", g.path, "add", ".")
190
-
if err := stageCmd.Run(); err != nil {
191
-
return fmt.Errorf("failed to stage changes: %w", err)
192
-
}
193
194
-
commitArgs := []string{"-C", g.path, "commit"}
195
196
-
// Set author if provided
197
-
authorName := opts.AuthorName
198
-
authorEmail := opts.AuthorEmail
199
200
-
if authorName != "" && authorEmail != "" {
201
-
commitArgs = append(commitArgs, "--author", fmt.Sprintf("%s <%s>", authorName, authorEmail))
202
-
}
203
-
// else, will default to knot's global user.name & user.email configured via `KNOT_GIT_USER_*` env variables
204
205
-
commitArgs = append(commitArgs, "-m", opts.CommitMessage)
206
207
-
if opts.CommitBody != "" {
208
-
commitArgs = append(commitArgs, "-m", opts.CommitBody)
209
}
210
-
211
-
cmd = exec.Command("git", commitArgs...)
212
213
cmd.Stderr = &stderr
214
···
219
return nil
220
}
221
222
-
func (g *GitRepo) applyMailbox(patchData string) error {
223
-
fps, err := patchutil.ExtractPatches(patchData)
224
-
if err != nil {
225
-
return fmt.Errorf("failed to extract patches: %w", err)
226
-
}
227
-
228
-
// apply each patch one by one
229
-
// update the newly created commit object to add the change-id header
230
-
total := len(fps)
231
-
for i, p := range fps {
232
-
newCommit, err := g.applySingleMailbox(p)
233
-
if err != nil {
234
-
return err
235
-
}
236
-
237
-
log.Printf("applying mailbox patch %d/%d: committed %s\n", i+1, total, newCommit.String())
238
-
}
239
-
240
-
return nil
241
-
}
242
-
243
-
func (g *GitRepo) applySingleMailbox(singlePatch types.FormatPatch) (plumbing.Hash, error) {
244
-
tmpPatch, err := g.createTempFileWithPatch(singlePatch.Raw)
245
-
if err != nil {
246
-
return plumbing.ZeroHash, fmt.Errorf("failed to create temporary patch file for singluar mailbox patch: %w", err)
247
-
}
248
-
249
-
var stderr bytes.Buffer
250
-
cmd := exec.Command("git", "-C", g.path, "am", tmpPatch)
251
-
cmd.Stderr = &stderr
252
-
253
-
head, err := g.r.Head()
254
-
if err != nil {
255
-
return plumbing.ZeroHash, err
256
-
}
257
-
log.Println("head before apply", head.Hash().String())
258
-
259
-
if err := cmd.Run(); err != nil {
260
-
return plumbing.ZeroHash, fmt.Errorf("patch application failed: %s", stderr.String())
261
-
}
262
-
263
-
if err := g.Refresh(); err != nil {
264
-
return plumbing.ZeroHash, fmt.Errorf("failed to refresh repository state: %w", err)
265
-
}
266
-
267
-
head, err = g.r.Head()
268
-
if err != nil {
269
-
return plumbing.ZeroHash, err
270
-
}
271
-
log.Println("head after apply", head.Hash().String())
272
-
273
-
newHash := head.Hash()
274
-
if changeId, err := singlePatch.ChangeId(); err != nil {
275
-
// no change ID
276
-
} else if updatedHash, err := g.setChangeId(head.Hash(), changeId); err != nil {
277
-
return plumbing.ZeroHash, err
278
-
} else {
279
-
newHash = updatedHash
280
-
}
281
-
282
-
return newHash, nil
283
-
}
284
-
285
-
func (g *GitRepo) setChangeId(hash plumbing.Hash, changeId string) (plumbing.Hash, error) {
286
-
log.Printf("updating change ID of %s to %s\n", hash.String(), changeId)
287
-
obj, err := g.r.CommitObject(hash)
288
-
if err != nil {
289
-
return plumbing.ZeroHash, fmt.Errorf("failed to get commit object for hash %s: %w", hash.String(), err)
290
-
}
291
-
292
-
// write the change-id header
293
-
obj.ExtraHeaders["change-id"] = []byte(changeId)
294
-
295
-
// create a new object
296
-
dest := g.r.Storer.NewEncodedObject()
297
-
if err := obj.Encode(dest); err != nil {
298
-
return plumbing.ZeroHash, fmt.Errorf("failed to create new object: %w", err)
299
-
}
300
-
301
-
// store the new object
302
-
newHash, err := g.r.Storer.SetEncodedObject(dest)
303
-
if err != nil {
304
-
return plumbing.ZeroHash, fmt.Errorf("failed to store new object: %w", err)
305
-
}
306
-
307
-
log.Printf("hash changed from %s to %s\n", obj.Hash.String(), newHash.String())
308
-
309
-
// find the branch that HEAD is pointing to
310
-
ref, err := g.r.Head()
311
-
if err != nil {
312
-
return plumbing.ZeroHash, fmt.Errorf("failed to fetch HEAD: %w", err)
313
-
}
314
-
315
-
// and update that branch to point to new commit
316
-
if ref.Name().IsBranch() {
317
-
err = g.r.Storer.SetReference(plumbing.NewHashReference(ref.Name(), newHash))
318
-
if err != nil {
319
-
return plumbing.ZeroHash, fmt.Errorf("failed to update HEAD: %w", err)
320
-
}
321
-
}
322
-
323
-
// new hash of commit
324
-
return newHash, nil
325
-
}
326
-
327
-
func (g *GitRepo) MergeCheck(patchData string, targetBranch string) error {
328
if val, ok := mergeCheckCache.Get(g, patchData, targetBranch); ok {
329
return val
330
}
···
352
return result
353
}
354
355
-
func (g *GitRepo) MergeWithOptions(patchData string, targetBranch string, opts MergeOptions) error {
356
patchFile, err := g.createTempFileWithPatch(patchData)
357
if err != nil {
358
return &ErrMerge{
···
371
}
372
defer os.RemoveAll(tmpDir)
373
374
-
tmpRepo, err := PlainOpen(tmpDir)
375
-
if err != nil {
376
-
return err
377
-
}
378
-
379
-
if err := tmpRepo.applyPatch(patchData, patchFile, opts); err != nil {
380
return err
381
}
382
···
4
"bytes"
5
"crypto/sha256"
6
"fmt"
7
"os"
8
"os/exec"
9
"regexp"
···
12
"github.com/dgraph-io/ristretto"
13
"github.com/go-git/go-git/v5"
14
"github.com/go-git/go-git/v5/plumbing"
15
)
16
17
type MergeCheckCache struct {
···
32
mergeCheckCache = MergeCheckCache{cache}
33
}
34
35
+
func (m *MergeCheckCache) cacheKey(g *GitRepo, patch []byte, targetBranch string) string {
36
sep := byte(':')
37
hash := sha256.Sum256(fmt.Append([]byte{}, g.path, sep, g.h.String(), sep, patch, sep, targetBranch))
38
return fmt.Sprintf("%x", hash)
···
49
}
50
}
51
52
+
func (m *MergeCheckCache) Set(g *GitRepo, patch []byte, targetBranch string, mergeCheck error) {
53
key := m.cacheKey(g, patch, targetBranch)
54
val := m.cacheVal(mergeCheck)
55
m.cache.Set(key, val, 0)
56
}
57
58
+
func (m *MergeCheckCache) Get(g *GitRepo, patch []byte, targetBranch string) (error, bool) {
59
key := m.cacheKey(g, patch, targetBranch)
60
if val, ok := m.cache.Get(key); ok {
61
if val == struct{}{} {
···
104
return fmt.Sprintf("merge failed: %s", e.Message)
105
}
106
107
+
func (g *GitRepo) createTempFileWithPatch(patchData []byte) (string, error) {
108
tmpFile, err := os.CreateTemp("", "git-patch-*.patch")
109
if err != nil {
110
return "", fmt.Errorf("failed to create temporary patch file: %w", err)
111
}
112
113
+
if _, err := tmpFile.Write(patchData); err != nil {
114
tmpFile.Close()
115
os.Remove(tmpFile.Name())
116
return "", fmt.Errorf("failed to write patch data to temporary file: %w", err)
···
162
return nil
163
}
164
165
+
func (g *GitRepo) applyPatch(tmpDir, patchFile string, opts MergeOptions) error {
166
var stderr bytes.Buffer
167
var cmd *exec.Cmd
168
169
// configure default git user before merge
170
+
exec.Command("git", "-C", tmpDir, "config", "user.name", opts.CommitterName).Run()
171
+
exec.Command("git", "-C", tmpDir, "config", "user.email", opts.CommitterEmail).Run()
172
+
exec.Command("git", "-C", tmpDir, "config", "advice.mergeConflict", "false").Run()
173
174
// if patch is a format-patch, apply using 'git am'
175
if opts.FormatPatch {
176
+
cmd = exec.Command("git", "-C", tmpDir, "am", patchFile)
177
+
} else {
178
+
// else, apply using 'git apply' and commit it manually
179
+
applyCmd := exec.Command("git", "-C", tmpDir, "apply", patchFile)
180
+
applyCmd.Stderr = &stderr
181
+
if err := applyCmd.Run(); err != nil {
182
+
return fmt.Errorf("patch application failed: %s", stderr.String())
183
+
}
184
185
+
stageCmd := exec.Command("git", "-C", tmpDir, "add", ".")
186
+
if err := stageCmd.Run(); err != nil {
187
+
return fmt.Errorf("failed to stage changes: %w", err)
188
+
}
189
190
+
commitArgs := []string{"-C", tmpDir, "commit"}
191
192
+
// Set author if provided
193
+
authorName := opts.AuthorName
194
+
authorEmail := opts.AuthorEmail
195
196
+
if authorName != "" && authorEmail != "" {
197
+
commitArgs = append(commitArgs, "--author", fmt.Sprintf("%s <%s>", authorName, authorEmail))
198
+
}
199
+
// else, will default to knot's global user.name & user.email configured via `KNOT_GIT_USER_*` env variables
200
201
+
commitArgs = append(commitArgs, "-m", opts.CommitMessage)
202
203
+
if opts.CommitBody != "" {
204
+
commitArgs = append(commitArgs, "-m", opts.CommitBody)
205
+
}
206
207
+
cmd = exec.Command("git", commitArgs...)
208
}
209
210
cmd.Stderr = &stderr
211
···
216
return nil
217
}
218
219
+
func (g *GitRepo) MergeCheck(patchData []byte, targetBranch string) error {
220
if val, ok := mergeCheckCache.Get(g, patchData, targetBranch); ok {
221
return val
222
}
···
244
return result
245
}
246
247
+
func (g *GitRepo) MergeWithOptions(patchData []byte, targetBranch string, opts MergeOptions) error {
248
patchFile, err := g.createTempFileWithPatch(patchData)
249
if err != nil {
250
return &ErrMerge{
···
263
}
264
defer os.RemoveAll(tmpDir)
265
266
+
if err := g.applyPatch(tmpDir, patchFile, opts); err != nil {
267
return err
268
}
269
-46
knotserver/internal.go
-46
knotserver/internal.go
···
13
securejoin "github.com/cyphar/filepath-securejoin"
14
"github.com/go-chi/chi/v5"
15
"github.com/go-chi/chi/v5/middleware"
16
-
"github.com/go-git/go-git/v5/plumbing"
17
"tangled.org/core/api/tangled"
18
"tangled.org/core/hook"
19
-
"tangled.org/core/idresolver"
20
"tangled.org/core/knotserver/config"
21
"tangled.org/core/knotserver/db"
22
"tangled.org/core/knotserver/git"
···
120
// non-fatal
121
}
122
123
-
if (line.NewSha.String() != line.OldSha.String()) && line.OldSha.IsZero() {
124
-
msg, err := h.replyCompare(line, gitUserDid, gitRelativeDir, repoName, r.Context())
125
-
if err != nil {
126
-
l.Error("failed to reply with compare link", "err", err, "line", line, "did", gitUserDid, "repo", gitRelativeDir)
127
-
// non-fatal
128
-
} else {
129
-
for msgLine := range msg {
130
-
resp.Messages = append(resp.Messages, msg[msgLine])
131
-
}
132
-
}
133
-
}
134
-
135
err = h.triggerPipeline(&resp.Messages, line, gitUserDid, repoDid, repoName, pushOptions)
136
if err != nil {
137
l.Error("failed to trigger pipeline", "err", err, "line", line, "did", gitUserDid, "repo", gitRelativeDir)
···
140
}
141
142
writeJSON(w, resp)
143
-
}
144
-
145
-
func (h *InternalHandle) replyCompare(line git.PostReceiveLine, gitUserDid string, gitRelativeDir string, repoName string, ctx context.Context) ([]string, error) {
146
-
l := h.l.With("handler", "replyCompare")
147
-
userIdent, err := idresolver.DefaultResolver().ResolveIdent(ctx, gitUserDid)
148
-
user := gitUserDid
149
-
if err != nil {
150
-
l.Error("Failed to fetch user identity", "err", err)
151
-
// non-fatal
152
-
} else {
153
-
user = userIdent.Handle.String()
154
-
}
155
-
gr, err := git.PlainOpen(gitRelativeDir)
156
-
if err != nil {
157
-
l.Error("Failed to open git repository", "err", err)
158
-
return []string{}, err
159
-
}
160
-
defaultBranch, err := gr.FindMainBranch()
161
-
if err != nil {
162
-
l.Error("Failed to fetch default branch", "err", err)
163
-
return []string{}, err
164
-
}
165
-
if line.Ref == plumbing.NewBranchReferenceName(defaultBranch).String() {
166
-
return []string{}, nil
167
-
}
168
-
ZWS := "\u200B"
169
-
var msg []string
170
-
msg = append(msg, ZWS)
171
-
msg = append(msg, fmt.Sprintf("Create a PR pointing to %s", defaultBranch))
172
-
msg = append(msg, fmt.Sprintf("\t%s/%s/%s/compare/%s...%s", h.c.AppViewEndpoint, user, repoName, defaultBranch, strings.TrimPrefix(line.Ref, "refs/heads/")))
173
-
msg = append(msg, ZWS)
174
-
return msg, nil
175
}
176
177
func (h *InternalHandle) insertRefUpdate(line git.PostReceiveLine, gitUserDid, repoDid, repoName string) error {
···
13
securejoin "github.com/cyphar/filepath-securejoin"
14
"github.com/go-chi/chi/v5"
15
"github.com/go-chi/chi/v5/middleware"
16
"tangled.org/core/api/tangled"
17
"tangled.org/core/hook"
18
"tangled.org/core/knotserver/config"
19
"tangled.org/core/knotserver/db"
20
"tangled.org/core/knotserver/git"
···
118
// non-fatal
119
}
120
121
err = h.triggerPipeline(&resp.Messages, line, gitUserDid, repoDid, repoName, pushOptions)
122
if err != nil {
123
l.Error("failed to trigger pipeline", "err", err, "line", line, "did", gitUserDid, "repo", gitRelativeDir)
···
126
}
127
128
writeJSON(w, resp)
129
}
130
131
func (h *InternalHandle) insertRefUpdate(line git.PostReceiveLine, gitUserDid, repoDid, repoName string) error {
-87
knotserver/xrpc/delete_branch.go
-87
knotserver/xrpc/delete_branch.go
···
1
-
package xrpc
2
-
3
-
import (
4
-
"encoding/json"
5
-
"fmt"
6
-
"net/http"
7
-
8
-
comatproto "github.com/bluesky-social/indigo/api/atproto"
9
-
"github.com/bluesky-social/indigo/atproto/syntax"
10
-
"github.com/bluesky-social/indigo/xrpc"
11
-
securejoin "github.com/cyphar/filepath-securejoin"
12
-
"tangled.org/core/api/tangled"
13
-
"tangled.org/core/knotserver/git"
14
-
"tangled.org/core/rbac"
15
-
16
-
xrpcerr "tangled.org/core/xrpc/errors"
17
-
)
18
-
19
-
func (x *Xrpc) DeleteBranch(w http.ResponseWriter, r *http.Request) {
20
-
l := x.Logger
21
-
fail := func(e xrpcerr.XrpcError) {
22
-
l.Error("failed", "kind", e.Tag, "error", e.Message)
23
-
writeError(w, e, http.StatusBadRequest)
24
-
}
25
-
26
-
actorDid, ok := r.Context().Value(ActorDid).(syntax.DID)
27
-
if !ok {
28
-
fail(xrpcerr.MissingActorDidError)
29
-
return
30
-
}
31
-
32
-
var data tangled.RepoDeleteBranch_Input
33
-
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
34
-
fail(xrpcerr.GenericError(err))
35
-
return
36
-
}
37
-
38
-
// unfortunately we have to resolve repo-at here
39
-
repoAt, err := syntax.ParseATURI(data.Repo)
40
-
if err != nil {
41
-
fail(xrpcerr.InvalidRepoError(data.Repo))
42
-
return
43
-
}
44
-
45
-
// resolve this aturi to extract the repo record
46
-
ident, err := x.Resolver.ResolveIdent(r.Context(), repoAt.Authority().String())
47
-
if err != nil || ident.Handle.IsInvalidHandle() {
48
-
fail(xrpcerr.GenericError(fmt.Errorf("failed to resolve handle: %w", err)))
49
-
return
50
-
}
51
-
52
-
xrpcc := xrpc.Client{Host: ident.PDSEndpoint()}
53
-
resp, err := comatproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String())
54
-
if err != nil {
55
-
fail(xrpcerr.GenericError(err))
56
-
return
57
-
}
58
-
59
-
repo := resp.Value.Val.(*tangled.Repo)
60
-
didPath, err := securejoin.SecureJoin(actorDid.String(), repo.Name)
61
-
if err != nil {
62
-
fail(xrpcerr.GenericError(err))
63
-
return
64
-
}
65
-
66
-
if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil {
67
-
l.Error("insufficent permissions", "did", actorDid.String())
68
-
writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)
69
-
return
70
-
}
71
-
72
-
path, _ := securejoin.SecureJoin(x.Config.Repo.ScanPath, didPath)
73
-
gr, err := git.PlainOpen(path)
74
-
if err != nil {
75
-
fail(xrpcerr.GenericError(err))
76
-
return
77
-
}
78
-
79
-
err = gr.DeleteBranch(data.Branch)
80
-
if err != nil {
81
-
l.Error("deleting branch", "error", err.Error(), "branch", data.Branch)
82
-
writeError(w, xrpcerr.GitError(err), http.StatusInternalServerError)
83
-
return
84
-
}
85
-
86
-
w.WriteHeader(http.StatusOK)
87
-
}
···
+1
-1
knotserver/xrpc/merge.go
+1
-1
knotserver/xrpc/merge.go
+1
-1
knotserver/xrpc/merge_check.go
+1
-1
knotserver/xrpc/merge_check.go
-1
knotserver/xrpc/xrpc.go
-1
knotserver/xrpc/xrpc.go
···
38
r.Use(x.ServiceAuth.VerifyServiceAuth)
39
40
r.Post("/"+tangled.RepoSetDefaultBranchNSID, x.SetDefaultBranch)
41
-
r.Post("/"+tangled.RepoDeleteBranchNSID, x.DeleteBranch)
42
r.Post("/"+tangled.RepoCreateNSID, x.CreateRepo)
43
r.Post("/"+tangled.RepoDeleteNSID, x.DeleteRepo)
44
r.Post("/"+tangled.RepoForkStatusNSID, x.ForkStatus)
-30
lexicons/repo/deleteBranch.json
-30
lexicons/repo/deleteBranch.json
···
1
-
{
2
-
"lexicon": 1,
3
-
"id": "sh.tangled.repo.deleteBranch",
4
-
"defs": {
5
-
"main": {
6
-
"type": "procedure",
7
-
"description": "Delete a branch on this repository",
8
-
"input": {
9
-
"encoding": "application/json",
10
-
"schema": {
11
-
"type": "object",
12
-
"required": [
13
-
"repo",
14
-
"branch"
15
-
],
16
-
"properties": {
17
-
"repo": {
18
-
"type": "string",
19
-
"format": "at-uri"
20
-
},
21
-
"branch": {
22
-
"type": "string"
23
-
}
24
-
}
25
-
}
26
-
}
27
-
}
28
-
}
29
-
}
30
-
···
+11
-23
nix/gomod2nix.toml
+11
-23
nix/gomod2nix.toml
···
40
hash = "sha256-GWm5i1ukuBukV0GMF1rffpbOSSXZdfg6/0pABMiGzLQ="
41
replaced = "tangled.sh/oppi.li/go-gitdiff"
42
[mod."github.com/bluesky-social/indigo"]
43
-
version = "v0.0.0-20251003000214-3259b215110e"
44
-
hash = "sha256-qi/GrquJznbLnnHVpd7IqoryCESbi6xE4X1SiEM2qlo="
45
[mod."github.com/bluesky-social/jetstream"]
46
version = "v0.0.0-20241210005130-ea96859b93d1"
47
hash = "sha256-AiapbrkjXboIKc5QNiWH0KyNs0zKnn6UlGwWFlkUfm0="
···
163
[mod."github.com/gogo/protobuf"]
164
version = "v1.3.2"
165
hash = "sha256-pogILFrrk+cAtb0ulqn9+gRZJ7sGnnLLdtqITvxvG6c="
166
-
[mod."github.com/goki/freetype"]
167
-
version = "v1.0.5"
168
-
hash = "sha256-8ILVMx5w1/nV88RZPoG45QJ0jH1YEPJGLpZQdBJFqIs="
169
[mod."github.com/golang-jwt/jwt/v5"]
170
version = "v5.2.3"
171
hash = "sha256-dY2avNPPS3xokn5E+VCLxXcQk7DsM7err2QGrG0nXKo="
···
410
[mod."github.com/spaolacci/murmur3"]
411
version = "v1.1.0"
412
hash = "sha256-RWD4PPrlAsZZ8Xy356MBxpj+/NZI7w2XOU14Ob7/Y9M="
413
-
[mod."github.com/srwiley/oksvg"]
414
-
version = "v0.0.0-20221011165216-be6e8873101c"
415
-
hash = "sha256-lZb6Y8HkrDpx9pxS+QQTcXI2MDSSv9pUyVTat59OrSk="
416
-
[mod."github.com/srwiley/rasterx"]
417
-
version = "v0.0.0-20220730225603-2ab79fcdd4ef"
418
-
hash = "sha256-/XmSE/J+f6FLWXGvljh6uBK71uoCAK3h82XQEQ1Ki68="
419
[mod."github.com/stretchr/testify"]
420
version = "v1.10.0"
421
hash = "sha256-fJ4gnPr0vnrOhjQYQwJ3ARDKPsOtA7d4olQmQWR+wpI="
···
441
version = "v0.1.15"
442
hash = "sha256-hb99exdkoY2Qv8WdDxhwgPXGbEYimUr6wFtPXEvcO9g="
443
[mod."github.com/yuin/goldmark"]
444
-
version = "v1.7.13"
445
-
hash = "sha256-vBCxZrPYPc8x/nvAAv3Au59dCCyfS80Vw3/a9EXK7TE="
446
[mod."github.com/yuin/goldmark-highlighting/v2"]
447
version = "v2.0.0-20230729083705-37449abec8cc"
448
hash = "sha256-HpiwU7jIeDUAg2zOpTIiviQir8dpRPuXYh2nqFFccpg="
449
-
[mod."gitlab.com/staticnoise/goldmark-callout"]
450
-
version = "v0.0.0-20240609120641-6366b799e4ab"
451
-
hash = "sha256-CgqBIYAuSmL2hcFu5OW18nWWaSy3pp3CNp5jlWzBX44="
452
[mod."gitlab.com/yawning/secp256k1-voi"]
453
version = "v0.0.0-20230925100816-f2616030848b"
454
hash = "sha256-X8INg01LTg13iOuwPI3uOhPN7r01sPZtmtwJ2sudjCA="
···
491
[mod."golang.org/x/exp"]
492
version = "v0.0.0-20250620022241-b7579e27df2b"
493
hash = "sha256-IsDTeuWLj4UkPO4NhWTvFeZ22WNtlxjoWiyAJh6zdig="
494
-
[mod."golang.org/x/image"]
495
-
version = "v0.31.0"
496
-
hash = "sha256-ZFTlu9+4QToPPLA8C5UcG2eq/lQylq81RoG/WtYo9rg="
497
[mod."golang.org/x/net"]
498
version = "v0.42.0"
499
hash = "sha256-YxileisIIez+kcAI+21kY5yk0iRuEqti2YdmS8jvP2s="
500
[mod."golang.org/x/sync"]
501
-
version = "v0.17.0"
502
-
hash = "sha256-M85lz4hK3/fzmcUViAp/CowHSxnr3BHSO7pjHp1O6i0="
503
[mod."golang.org/x/sys"]
504
version = "v0.34.0"
505
hash = "sha256-5rZ7p8IaGli5X1sJbfIKOcOEwY4c0yQhinJPh2EtK50="
506
[mod."golang.org/x/text"]
507
-
version = "v0.29.0"
508
-
hash = "sha256-2cWBtJje+Yc+AnSgCANqBlIwnOMZEGkpQ2cFI45VfLI="
509
[mod."golang.org/x/time"]
510
version = "v0.12.0"
511
hash = "sha256-Cp3oxrCMH2wyxjzr5SHVmyhgaoUuSl56Uy00Q7DYEpw="
···
542
[mod."lukechampine.com/blake3"]
543
version = "v1.4.1"
544
hash = "sha256-HaZGo9L44ptPsgxIhvKy3+0KZZm1+xt+cZC1rDQA9Yc="
···
40
hash = "sha256-GWm5i1ukuBukV0GMF1rffpbOSSXZdfg6/0pABMiGzLQ="
41
replaced = "tangled.sh/oppi.li/go-gitdiff"
42
[mod."github.com/bluesky-social/indigo"]
43
+
version = "v0.0.0-20250724221105-5827c8fb61bb"
44
+
hash = "sha256-uDYmzP4/mT7xP62LIL4QIOlkaKWS/IT1uho+udyVOAI="
45
[mod."github.com/bluesky-social/jetstream"]
46
version = "v0.0.0-20241210005130-ea96859b93d1"
47
hash = "sha256-AiapbrkjXboIKc5QNiWH0KyNs0zKnn6UlGwWFlkUfm0="
···
163
[mod."github.com/gogo/protobuf"]
164
version = "v1.3.2"
165
hash = "sha256-pogILFrrk+cAtb0ulqn9+gRZJ7sGnnLLdtqITvxvG6c="
166
[mod."github.com/golang-jwt/jwt/v5"]
167
version = "v5.2.3"
168
hash = "sha256-dY2avNPPS3xokn5E+VCLxXcQk7DsM7err2QGrG0nXKo="
···
407
[mod."github.com/spaolacci/murmur3"]
408
version = "v1.1.0"
409
hash = "sha256-RWD4PPrlAsZZ8Xy356MBxpj+/NZI7w2XOU14Ob7/Y9M="
410
[mod."github.com/stretchr/testify"]
411
version = "v1.10.0"
412
hash = "sha256-fJ4gnPr0vnrOhjQYQwJ3ARDKPsOtA7d4olQmQWR+wpI="
···
432
version = "v0.1.15"
433
hash = "sha256-hb99exdkoY2Qv8WdDxhwgPXGbEYimUr6wFtPXEvcO9g="
434
[mod."github.com/yuin/goldmark"]
435
+
version = "v1.7.12"
436
+
hash = "sha256-thLYBS4woL2X5qRdo7vP+xCvjlGRDU0jXtDCUt6vvWM="
437
[mod."github.com/yuin/goldmark-highlighting/v2"]
438
version = "v2.0.0-20230729083705-37449abec8cc"
439
hash = "sha256-HpiwU7jIeDUAg2zOpTIiviQir8dpRPuXYh2nqFFccpg="
440
[mod."gitlab.com/yawning/secp256k1-voi"]
441
version = "v0.0.0-20230925100816-f2616030848b"
442
hash = "sha256-X8INg01LTg13iOuwPI3uOhPN7r01sPZtmtwJ2sudjCA="
···
479
[mod."golang.org/x/exp"]
480
version = "v0.0.0-20250620022241-b7579e27df2b"
481
hash = "sha256-IsDTeuWLj4UkPO4NhWTvFeZ22WNtlxjoWiyAJh6zdig="
482
[mod."golang.org/x/net"]
483
version = "v0.42.0"
484
hash = "sha256-YxileisIIez+kcAI+21kY5yk0iRuEqti2YdmS8jvP2s="
485
[mod."golang.org/x/sync"]
486
+
version = "v0.16.0"
487
+
hash = "sha256-sqKDRESeMzLe0jWGWltLZL/JIgrn0XaIeBWCzVN3Bks="
488
[mod."golang.org/x/sys"]
489
version = "v0.34.0"
490
hash = "sha256-5rZ7p8IaGli5X1sJbfIKOcOEwY4c0yQhinJPh2EtK50="
491
[mod."golang.org/x/text"]
492
+
version = "v0.27.0"
493
+
hash = "sha256-VX0rOh6L3qIvquKSGjfZQFU8URNtGvkNvxE7OZtboW8="
494
[mod."golang.org/x/time"]
495
version = "v0.12.0"
496
hash = "sha256-Cp3oxrCMH2wyxjzr5SHVmyhgaoUuSl56Uy00Q7DYEpw="
···
527
[mod."lukechampine.com/blake3"]
528
version = "v1.4.1"
529
hash = "sha256-HaZGo9L44ptPsgxIhvKy3+0KZZm1+xt+cZC1rDQA9Yc="
530
+
[mod."tangled.org/anirudh.fi/atproto-oauth"]
531
+
version = "v0.0.0-20250724194903-28e660378cb1"
532
+
hash = "sha256-z7huwCTTHqLb1hxQW62lz9GQ3Orqt4URfeOVhQVd1f8="