+58
-1
api/tangled/cbor_gen.go
+58
-1
api/tangled/cbor_gen.go
···
1753
}
1754
1755
cw := cbg.NewCborWriter(w)
1756
-
fieldCount := 6
1757
1758
if t.AddedAt == nil {
1759
fieldCount--
1760
}
1761
1762
if t.Description == nil {
1763
fieldCount--
1764
}
1765
···
1855
return err
1856
}
1857
1858
// t.AddedAt (string) (string)
1859
if t.AddedAt != nil {
1860
···
2005
}
2006
2007
t.Owner = string(sval)
2008
}
2009
// t.AddedAt (string) (string)
2010
case "addedAt":
···
1753
}
1754
1755
cw := cbg.NewCborWriter(w)
1756
+
fieldCount := 7
1757
1758
if t.AddedAt == nil {
1759
fieldCount--
1760
}
1761
1762
if t.Description == nil {
1763
+
fieldCount--
1764
+
}
1765
+
1766
+
if t.Source == nil {
1767
fieldCount--
1768
}
1769
···
1859
return err
1860
}
1861
1862
+
// t.Source (string) (string)
1863
+
if t.Source != nil {
1864
+
1865
+
if len("source") > 1000000 {
1866
+
return xerrors.Errorf("Value in field \"source\" was too long")
1867
+
}
1868
+
1869
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("source"))); err != nil {
1870
+
return err
1871
+
}
1872
+
if _, err := cw.WriteString(string("source")); err != nil {
1873
+
return err
1874
+
}
1875
+
1876
+
if t.Source == nil {
1877
+
if _, err := cw.Write(cbg.CborNull); err != nil {
1878
+
return err
1879
+
}
1880
+
} else {
1881
+
if len(*t.Source) > 1000000 {
1882
+
return xerrors.Errorf("Value in field t.Source was too long")
1883
+
}
1884
+
1885
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Source))); err != nil {
1886
+
return err
1887
+
}
1888
+
if _, err := cw.WriteString(string(*t.Source)); err != nil {
1889
+
return err
1890
+
}
1891
+
}
1892
+
}
1893
+
1894
// t.AddedAt (string) (string)
1895
if t.AddedAt != nil {
1896
···
2041
}
2042
2043
t.Owner = string(sval)
2044
+
}
2045
+
// t.Source (string) (string)
2046
+
case "source":
2047
+
2048
+
{
2049
+
b, err := cr.ReadByte()
2050
+
if err != nil {
2051
+
return err
2052
+
}
2053
+
if b != cbg.CborNull[0] {
2054
+
if err := cr.UnreadByte(); err != nil {
2055
+
return err
2056
+
}
2057
+
2058
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
2059
+
if err != nil {
2060
+
return err
2061
+
}
2062
+
2063
+
t.Source = (*string)(&sval)
2064
+
}
2065
}
2066
// t.AddedAt (string) (string)
2067
case "addedAt":
+2
api/tangled/tangledrepo.go
+2
api/tangled/tangledrepo.go
+7
appview/db/db.go
+7
appview/db/db.go
+31
-9
appview/db/repos.go
+31
-9
appview/db/repos.go
···
3
import (
4
"database/sql"
5
"time"
6
)
7
8
type Repo struct {
···
16
17
// optionally, populate this when querying for reverse mappings
18
RepoStats *RepoStats
19
}
20
21
func GetAllRepos(e Execer, limit int) ([]Repo, error) {
22
var repos []Repo
23
24
rows, err := e.Query(
25
-
`select did, name, knot, rkey, description, created
26
from repos
27
order by created desc
28
limit ?
···
37
for rows.Next() {
38
var repo Repo
39
err := scanRepo(
40
-
rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Description, &repo.Created,
41
)
42
if err != nil {
43
return nil, err
···
63
r.rkey,
64
r.description,
65
r.created,
66
-
count(s.id) as star_count
67
from
68
repos r
69
left join
···
159
160
func AddRepo(e Execer, repo *Repo) error {
161
_, err := e.Exec(
162
-
`insert into repos
163
-
(did, name, knot, rkey, at_uri, description)
164
-
values (?, ?, ?, ?, ?, ?)`,
165
-
repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.AtUri, repo.Description,
166
)
167
return err
168
}
···
172
return err
173
}
174
175
func AddCollaborator(e Execer, collaborator, repoOwnerDid, repoName, repoKnot string) error {
176
_, err := e.Exec(
177
`insert into collaborators (did, repo)
···
249
PullCount PullCount
250
}
251
252
-
func scanRepo(rows *sql.Rows, did, name, knot, rkey, description *string, created *time.Time) error {
253
var createdAt string
254
var nullableDescription sql.NullString
255
-
if err := rows.Scan(did, name, knot, rkey, &nullableDescription, &createdAt); err != nil {
256
return err
257
}
258
···
267
*created = time.Now()
268
} else {
269
*created = createdAtTime
270
}
271
272
return nil
···
3
import (
4
"database/sql"
5
"time"
6
+
7
+
"github.com/bluesky-social/indigo/atproto/syntax"
8
)
9
10
type Repo struct {
···
18
19
// optionally, populate this when querying for reverse mappings
20
RepoStats *RepoStats
21
+
22
+
// optional
23
+
Source string
24
}
25
26
func GetAllRepos(e Execer, limit int) ([]Repo, error) {
27
var repos []Repo
28
29
rows, err := e.Query(
30
+
`select did, name, knot, rkey, description, created, source
31
from repos
32
order by created desc
33
limit ?
···
42
for rows.Next() {
43
var repo Repo
44
err := scanRepo(
45
+
rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Description, &repo.Created, &repo.Source,
46
)
47
if err != nil {
48
return nil, err
···
68
r.rkey,
69
r.description,
70
r.created,
71
+
count(s.id) as star_count,
72
+
r.source
73
from
74
repos r
75
left join
···
165
166
func AddRepo(e Execer, repo *Repo) error {
167
_, err := e.Exec(
168
+
`insert into repos
169
+
(did, name, knot, rkey, at_uri, description, source)
170
+
values (?, ?, ?, ?, ?, ?, ?)`,
171
+
repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.AtUri, repo.Description, repo.Source,
172
)
173
return err
174
}
···
178
return err
179
}
180
181
+
func GetRepoSource(e Execer, repoAt syntax.ATURI) (string, error) {
182
+
var source string
183
+
err := e.QueryRow(`select source from repos where at_uri = ?`, repoAt).Scan(&source)
184
+
if err != nil {
185
+
return "", err
186
+
}
187
+
return source, nil
188
+
}
189
+
190
func AddCollaborator(e Execer, collaborator, repoOwnerDid, repoName, repoKnot string) error {
191
_, err := e.Exec(
192
`insert into collaborators (did, repo)
···
264
PullCount PullCount
265
}
266
267
+
func scanRepo(rows *sql.Rows, did, name, knot, rkey, description *string, created *time.Time, source *string) error {
268
var createdAt string
269
var nullableDescription sql.NullString
270
+
var nullableSource sql.NullString
271
+
if err := rows.Scan(did, name, knot, rkey, &nullableDescription, &createdAt, &nullableSource); err != nil {
272
return err
273
}
274
···
283
*created = time.Now()
284
} else {
285
*created = createdAtTime
286
+
}
287
+
288
+
if nullableSource.Valid {
289
+
*source = nullableSource.String
290
+
} else {
291
+
*source = ""
292
}
293
294
return nil
+17
appview/db/timeline.go
+17
appview/db/timeline.go
···
9
*Repo
10
*Follow
11
*Star
12
+
13
EventAt time.Time
14
+
15
+
// optional: populate only if Repo is a fork
16
+
Source *Repo
17
}
18
19
// TODO: this gathers heterogenous events from different sources and aggregates
···
38
}
39
40
for _, repo := range repos {
41
+
if repo.Source != "" {
42
+
sourceRepo, err := GetRepoByAtUri(e, repo.Source)
43
+
if err != nil {
44
+
return nil, err
45
+
}
46
+
47
+
events = append(events, TimelineEvent{
48
+
Repo: &repo,
49
+
EventAt: repo.Created,
50
+
Source: sourceRepo,
51
+
})
52
+
}
53
+
54
events = append(events, TimelineEvent{
55
Repo: &repo,
56
EventAt: repo.Created,
+21
-9
appview/pages/pages.go
+21
-9
appview/pages/pages.go
···
158
return p.execute("repo/new", w, params)
159
}
160
161
type ProfilePageParams struct {
162
LoggedInUser *auth.User
163
UserDid string
···
212
}
213
214
type RepoInfo struct {
215
-
Name string
216
-
OwnerDid string
217
-
OwnerHandle string
218
-
Description string
219
-
Knot string
220
-
RepoAt syntax.ATURI
221
-
IsStarred bool
222
-
Stats db.RepoStats
223
-
Roles RolesInRepo
224
}
225
226
type RolesInRepo struct {
···
158
return p.execute("repo/new", w, params)
159
}
160
161
+
type ForkRepoParams struct {
162
+
LoggedInUser *auth.User
163
+
Knots []string
164
+
RepoInfo RepoInfo
165
+
}
166
+
167
+
func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error {
168
+
return p.execute("repo/fork", w, params)
169
+
}
170
+
171
type ProfilePageParams struct {
172
LoggedInUser *auth.User
173
UserDid string
···
222
}
223
224
type RepoInfo struct {
225
+
Name string
226
+
OwnerDid string
227
+
OwnerHandle string
228
+
Description string
229
+
Knot string
230
+
RepoAt syntax.ATURI
231
+
IsStarred bool
232
+
Stats db.RepoStats
233
+
Roles RolesInRepo
234
+
Source *db.Repo
235
+
SourceHandle string
236
}
237
238
type RolesInRepo struct {
+10
appview/pages/templates/layouts/repobase.html
+10
appview/pages/templates/layouts/repobase.html
···
2
3
{{ define "content" }}
4
<section id="repo-header" class="mb-4 py-2 px-6 dark:text-white">
5
+
{{ if .RepoInfo.Source }}
6
+
<p class="text-sm">
7
+
<div class="flex items-center">
8
+
{{ i "git-fork" "w-3 h-3 mr-1"}}
9
+
forked from
10
+
{{ $sourceOwner := didOrHandle .RepoInfo.Source.Did .RepoInfo.SourceHandle }}
11
+
<a class="ml-1 underline" href="/{{ $sourceOwner }}/{{ .RepoInfo.Source.Name }}">{{ $sourceOwner }}/{{ .RepoInfo.Source.Name }}</a>
12
+
</div>
13
+
</p>
14
+
{{ end }}
15
<p class="text-lg">
16
<a href="/{{ .RepoInfo.OwnerWithAt }}">{{ .RepoInfo.OwnerWithAt }}</a>
17
<span class="select-none">/</span>
+38
appview/pages/templates/repo/fork.html
+38
appview/pages/templates/repo/fork.html
···
···
1
+
{{ define "title" }}fork · {{ .RepoInfo.FullName }}{{ end }}
2
+
3
+
{{ define "content" }}
4
+
<div class="p-6">
5
+
<p class="text-xl font-bold dark:text-white">Fork {{ .RepoInfo.FullName }}</p>
6
+
</div>
7
+
<div class="p-6 bg-white dark:bg-gray-800 drop-shadow-sm rounded">
8
+
<form hx-post="/{{ .RepoInfo.FullName }}/fork" class="space-y-12" hx-swap="none">
9
+
<fieldset class="space-y-3">
10
+
<legend class="dark:text-white">Select a knot to fork into</legend>
11
+
<div class="space-y-2">
12
+
<div class="flex flex-col">
13
+
{{ range .Knots }}
14
+
<div class="flex items-center">
15
+
<input
16
+
type="radio"
17
+
name="knot"
18
+
value="{{ . }}"
19
+
class="mr-2"
20
+
id="domain-{{ . }}"
21
+
/>
22
+
<span class="dark:text-white">{{ . }}</span>
23
+
</div>
24
+
{{ else }}
25
+
<p class="dark:text-white">No knots available.</p>
26
+
{{ end }}
27
+
</div>
28
+
</div>
29
+
<p class="text-sm text-gray-500 dark:text-gray-400">A knot hosts repository data. <a href="/knots" class="underline">Learn how to register your own knot.</a></p>
30
+
</fieldset>
31
+
32
+
<div class="space-y-2">
33
+
<button type="submit" class="btn">fork repo</button>
34
+
<div id="repo" class="error"></div>
35
+
</div>
36
+
</form>
37
+
</div>
38
+
{{ end }}
+20
-5
appview/pages/templates/repo/settings.html
+20
-5
appview/pages/templates/repo/settings.html
···
1
{{ define "title" }}settings · {{ .RepoInfo.FullName }}{{ end }}
2
{{ define "repoContent" }}
3
-
<header class="font-bold text-sm mb-4 uppercase dark:text-white">Collaborators</header>
4
5
<div id="collaborator-list" class="flex flex-col gap-2 mb-2">
6
{{ range .Collaborators }}
···
21
</div>
22
23
{{ if .IsCollaboratorInviteAllowed }}
24
-
<h3 class="dark:text-white">add collaborator</h3>
25
<form hx-put="/{{ $.RepoInfo.FullName }}/settings/collaborator">
26
-
<label for="collaborator" class="dark:text-white">did or handle:</label>
27
-
<input type="text" id="collaborator" name="collaborator" required class="dark:bg-gray-700 dark:text-white" />
28
-
<button class="btn my-2 dark:text-white dark:hover:bg-gray-700" type="text">add collaborator</button>
29
</form>
30
{{ end }}
31
···
1
{{ define "title" }}settings · {{ .RepoInfo.FullName }}{{ end }}
2
{{ define "repoContent" }}
3
+
<header class="font-bold text-sm mb-4 uppercase dark:text-white">
4
+
Collaborators
5
+
</header>
6
7
<div id="collaborator-list" class="flex flex-col gap-2 mb-2">
8
{{ range .Collaborators }}
···
23
</div>
24
25
{{ if .IsCollaboratorInviteAllowed }}
26
<form hx-put="/{{ $.RepoInfo.FullName }}/settings/collaborator">
27
+
<label for="collaborator" class="dark:text-white"
28
+
>add collaborator</label
29
+
>
30
+
<input
31
+
type="text"
32
+
id="collaborator"
33
+
name="collaborator"
34
+
required
35
+
class="dark:bg-gray-700 dark:text-white"
36
+
placeholder="enter did or handle"
37
+
/>
38
+
<button
39
+
class="btn my-2 dark:text-white dark:hover:bg-gray-700"
40
+
type="text"
41
+
>
42
+
add
43
+
</button>
44
</form>
45
{{ end }}
46
+7
appview/pages/templates/timeline.html
+7
appview/pages/templates/timeline.html
···
44
<div class="flex items-center">
45
<p class="text-gray-600 dark:text-gray-300">
46
<a href="/{{ $userHandle }}" class="no-underline hover:underline">{{ $userHandle | truncateAt30 }}</a>
47
created
48
<a href="/{{ $userHandle }}/{{ .Repo.Name }}" class="no-underline hover:underline">{{ .Repo.Name }}</a>
49
<time class="text-gray-700 dark:text-gray-400 text-xs">{{ .Repo.Created | timeFmt }}</time>
50
</p>
51
</div>
···
44
<div class="flex items-center">
45
<p class="text-gray-600 dark:text-gray-300">
46
<a href="/{{ $userHandle }}" class="no-underline hover:underline">{{ $userHandle | truncateAt30 }}</a>
47
+
{{ if .Source }}
48
+
forked
49
+
<a href="/{{ $userHandle }}/{{ .Repo.Name }}" class="no-underline hover:underline">{{ .Repo.Name }}</a>
50
+
from
51
+
<a href="/{{ index $.DidHandleMap .Source.Did }}/{{ .Source.Name }}" class="no-underline hover:underline">{{ index $.DidHandleMap .Source.Did }}/{{ .Source.Name }}</a>
52
+
{{ else }}
53
created
54
<a href="/{{ $userHandle }}/{{ .Repo.Name }}" class="no-underline hover:underline">{{ .Repo.Name }}</a>
55
+
{{ end }}
56
<time class="text-gray-700 dark:text-gray-400 text-xs">{{ .Repo.Created | timeFmt }}</time>
57
</p>
58
</div>
+191
-2
appview/state/repo.go
+191
-2
appview/state/repo.go
···
2
3
import (
4
"context"
5
"encoding/json"
6
"fmt"
7
"io"
8
"log"
9
-
"math/rand/v2"
10
"net/http"
11
"path"
12
"slices"
···
801
if err != nil {
802
log.Println("failed to get issue count for ", f.RepoAt)
803
}
804
805
knot := f.Knot
806
if knot == "knot1.tangled.sh" {
807
knot = "tangled.sh"
808
}
809
810
return pages.RepoInfo{
···
821
IssueCount: issueCount,
822
PullCount: pullCount,
823
},
824
}
825
}
826
···
1022
return
1023
}
1024
1025
-
commentId := rand.IntN(1000000)
1026
rkey := s.TID()
1027
1028
err := db.NewIssueComment(s.db, &db.Comment{
···
1481
return
1482
}
1483
}
···
2
3
import (
4
"context"
5
+
"database/sql"
6
"encoding/json"
7
+
"errors"
8
"fmt"
9
"io"
10
"log"
11
+
mathrand "math/rand/v2"
12
"net/http"
13
"path"
14
"slices"
···
803
if err != nil {
804
log.Println("failed to get issue count for ", f.RepoAt)
805
}
806
+
source, err := db.GetRepoSource(s.db, f.RepoAt)
807
+
if errors.Is(err, sql.ErrNoRows) {
808
+
source = ""
809
+
} else if err != nil {
810
+
log.Println("failed to get repo source for ", f.RepoAt)
811
+
}
812
+
813
+
var sourceRepo *db.Repo
814
+
if source != "" {
815
+
sourceRepo, err = db.GetRepoByAtUri(s.db, source)
816
+
if err != nil {
817
+
log.Println("failed to get repo by at uri", err)
818
+
}
819
+
}
820
821
knot := f.Knot
822
if knot == "knot1.tangled.sh" {
823
knot = "tangled.sh"
824
+
}
825
+
826
+
var sourceHandle *identity.Identity
827
+
if sourceRepo != nil {
828
+
sourceHandle, err = s.resolver.ResolveIdent(context.Background(), sourceRepo.Did)
829
+
if err != nil {
830
+
log.Println("failed to resolve source repo", err)
831
+
}
832
}
833
834
return pages.RepoInfo{
···
845
IssueCount: issueCount,
846
PullCount: pullCount,
847
},
848
+
Source: sourceRepo,
849
+
SourceHandle: sourceHandle.Handle.String(),
850
}
851
}
852
···
1048
return
1049
}
1050
1051
+
commentId := mathrand.IntN(1000000)
1052
rkey := s.TID()
1053
1054
err := db.NewIssueComment(s.db, &db.Comment{
···
1507
return
1508
}
1509
}
1510
+
1511
+
func (s *State) ForkRepo(w http.ResponseWriter, r *http.Request) {
1512
+
user := s.auth.GetUser(r)
1513
+
f, err := fullyResolvedRepo(r)
1514
+
if err != nil {
1515
+
log.Printf("failed to resolve source repo: %v", err)
1516
+
return
1517
+
}
1518
+
1519
+
switch r.Method {
1520
+
case http.MethodGet:
1521
+
user := s.auth.GetUser(r)
1522
+
knots, err := s.enforcer.GetDomainsForUser(user.Did)
1523
+
if err != nil {
1524
+
s.pages.Notice(w, "repo", "Invalid user account.")
1525
+
return
1526
+
}
1527
+
1528
+
s.pages.ForkRepo(w, pages.ForkRepoParams{
1529
+
LoggedInUser: user,
1530
+
Knots: knots,
1531
+
RepoInfo: f.RepoInfo(s, user),
1532
+
})
1533
+
1534
+
case http.MethodPost:
1535
+
1536
+
knot := r.FormValue("knot")
1537
+
if knot == "" {
1538
+
s.pages.Notice(w, "repo", "Invalid form submission—missing knot domain.")
1539
+
return
1540
+
}
1541
+
1542
+
ok, err := s.enforcer.E.Enforce(user.Did, knot, knot, "repo:create")
1543
+
if err != nil || !ok {
1544
+
s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.")
1545
+
return
1546
+
}
1547
+
1548
+
forkName := fmt.Sprintf("%s", f.RepoName)
1549
+
1550
+
existingRepo, err := db.GetRepo(s.db, user.Did, f.RepoName)
1551
+
if err == nil && existingRepo != nil {
1552
+
forkName = fmt.Sprintf("%s-%s", forkName, randomString(3))
1553
+
}
1554
+
1555
+
secret, err := db.GetRegistrationKey(s.db, knot)
1556
+
if err != nil {
1557
+
s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", knot))
1558
+
return
1559
+
}
1560
+
1561
+
client, err := NewSignedClient(knot, secret, s.config.Dev)
1562
+
if err != nil {
1563
+
s.pages.Notice(w, "repo", "Failed to connect to knot server.")
1564
+
return
1565
+
}
1566
+
1567
+
var uri string
1568
+
if s.config.Dev {
1569
+
uri = "http"
1570
+
} else {
1571
+
uri = "https"
1572
+
}
1573
+
sourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, knot, f.OwnerDid(), f.RepoName)
1574
+
sourceAt := f.RepoAt.String()
1575
+
1576
+
rkey := s.TID()
1577
+
repo := &db.Repo{
1578
+
Did: user.Did,
1579
+
Name: forkName,
1580
+
Knot: knot,
1581
+
Rkey: rkey,
1582
+
Source: sourceAt,
1583
+
}
1584
+
1585
+
tx, err := s.db.BeginTx(r.Context(), nil)
1586
+
if err != nil {
1587
+
log.Println(err)
1588
+
s.pages.Notice(w, "repo", "Failed to save repository information.")
1589
+
return
1590
+
}
1591
+
defer func() {
1592
+
tx.Rollback()
1593
+
err = s.enforcer.E.LoadPolicy()
1594
+
if err != nil {
1595
+
log.Println("failed to rollback policies")
1596
+
}
1597
+
}()
1598
+
1599
+
resp, err := client.ForkRepo(user.Did, sourceUrl, forkName)
1600
+
if err != nil {
1601
+
s.pages.Notice(w, "repo", "Failed to create repository on knot server.")
1602
+
return
1603
+
}
1604
+
1605
+
switch resp.StatusCode {
1606
+
case http.StatusConflict:
1607
+
s.pages.Notice(w, "repo", "A repository with that name already exists.")
1608
+
return
1609
+
case http.StatusInternalServerError:
1610
+
s.pages.Notice(w, "repo", "Failed to create repository on knot. Try again later.")
1611
+
case http.StatusNoContent:
1612
+
// continue
1613
+
}
1614
+
1615
+
xrpcClient, _ := s.auth.AuthorizedClient(r)
1616
+
1617
+
addedAt := time.Now().Format(time.RFC3339)
1618
+
atresp, err := comatproto.RepoPutRecord(r.Context(), xrpcClient, &comatproto.RepoPutRecord_Input{
1619
+
Collection: tangled.RepoNSID,
1620
+
Repo: user.Did,
1621
+
Rkey: rkey,
1622
+
Record: &lexutil.LexiconTypeDecoder{
1623
+
Val: &tangled.Repo{
1624
+
Knot: repo.Knot,
1625
+
Name: repo.Name,
1626
+
AddedAt: &addedAt,
1627
+
Owner: user.Did,
1628
+
Source: &sourceAt,
1629
+
}},
1630
+
})
1631
+
if err != nil {
1632
+
log.Printf("failed to create record: %s", err)
1633
+
s.pages.Notice(w, "repo", "Failed to announce repository creation.")
1634
+
return
1635
+
}
1636
+
log.Println("created repo record: ", atresp.Uri)
1637
+
1638
+
repo.AtUri = atresp.Uri
1639
+
err = db.AddRepo(tx, repo)
1640
+
if err != nil {
1641
+
log.Println(err)
1642
+
s.pages.Notice(w, "repo", "Failed to save repository information.")
1643
+
return
1644
+
}
1645
+
1646
+
// acls
1647
+
p, _ := securejoin.SecureJoin(user.Did, forkName)
1648
+
err = s.enforcer.AddRepo(user.Did, knot, p)
1649
+
if err != nil {
1650
+
log.Println(err)
1651
+
s.pages.Notice(w, "repo", "Failed to set up repository permissions.")
1652
+
return
1653
+
}
1654
+
1655
+
err = tx.Commit()
1656
+
if err != nil {
1657
+
log.Println("failed to commit changes", err)
1658
+
http.Error(w, err.Error(), http.StatusInternalServerError)
1659
+
return
1660
+
}
1661
+
1662
+
err = s.enforcer.E.SavePolicy()
1663
+
if err != nil {
1664
+
log.Println("failed to update ACLs", err)
1665
+
http.Error(w, err.Error(), http.StatusInternalServerError)
1666
+
return
1667
+
}
1668
+
1669
+
s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, forkName))
1670
+
return
1671
+
}
1672
+
}
+14
appview/state/repo_util.go
+14
appview/state/repo_util.go
···
2
3
import (
4
"context"
5
+
"crypto/rand"
6
"fmt"
7
"log"
8
+
"math/big"
9
"net/http"
10
11
"github.com/bluesky-social/indigo/atproto/identity"
···
114
115
return emailToDidOrHandle
116
}
117
+
118
+
func randomString(n int) string {
119
+
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
120
+
result := make([]byte, n)
121
+
122
+
for i := 0; i < n; i++ {
123
+
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
124
+
result[i] = letters[n.Int64()]
125
+
}
126
+
127
+
return string(result)
128
+
}
+6
appview/state/router.go
+6
appview/state/router.go
+20
appview/state/signer.go
+20
appview/state/signer.go
···
103
return s.client.Do(req)
104
}
105
106
+
func (s *SignedClient) ForkRepo(ownerDid, source, name string) (*http.Response, error) {
107
+
const (
108
+
Method = "POST"
109
+
Endpoint = "/repo/fork"
110
+
)
111
+
112
+
body, _ := json.Marshal(map[string]any{
113
+
"did": ownerDid,
114
+
"source": source,
115
+
"name": name,
116
+
})
117
+
118
+
req, err := s.newRequest(Method, Endpoint, body)
119
+
if err != nil {
120
+
return nil, err
121
+
}
122
+
123
+
return s.client.Do(req)
124
+
}
125
+
126
func (s *SignedClient) RemoveRepo(did, repoName string) (*http.Response, error) {
127
const (
128
Method = "DELETE"
+3
appview/state/state.go
+3
appview/state/state.go
···
190
for _, ev := range timeline {
191
if ev.Repo != nil {
192
didsToResolve = append(didsToResolve, ev.Repo.Did)
193
+
if ev.Source != nil {
194
+
didsToResolve = append(didsToResolve, ev.Source.Did)
195
+
}
196
}
197
if ev.Follow != nil {
198
didsToResolve = append(didsToResolve, ev.Follow.UserDid, ev.Follow.SubjectDid)
+20
knotserver/git/fork.go
+20
knotserver/git/fork.go
···
···
1
+
package git
2
+
3
+
import (
4
+
"fmt"
5
+
6
+
"github.com/go-git/go-git/v5"
7
+
)
8
+
9
+
func Fork(repoPath, source string) error {
10
+
_, err := git.PlainClone(repoPath, true, &git.CloneOptions{
11
+
URL: source,
12
+
Depth: 1,
13
+
SingleBranch: false,
14
+
})
15
+
16
+
if err != nil {
17
+
return fmt.Errorf("failed to bare clone repository: %w", err)
18
+
}
19
+
return nil
20
+
}
+1
knotserver/handler.go
+1
knotserver/handler.go
+43
knotserver/routes.go
+43
knotserver/routes.go
···
577
w.WriteHeader(http.StatusNoContent)
578
}
579
580
+
func (h *Handle) RepoFork(w http.ResponseWriter, r *http.Request) {
581
+
l := h.l.With("handler", "RepoFork")
582
+
583
+
data := struct {
584
+
Did string `json:"did"`
585
+
Source string `json:"source"`
586
+
Name string `json:"name,omitempty"`
587
+
}{}
588
+
589
+
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
590
+
writeError(w, "invalid request body", http.StatusBadRequest)
591
+
return
592
+
}
593
+
594
+
did := data.Did
595
+
source := data.Source
596
+
597
+
if did == "" || source == "" {
598
+
l.Error("invalid request body, empty did or name")
599
+
w.WriteHeader(http.StatusBadRequest)
600
+
return
601
+
}
602
+
603
+
var name string
604
+
if data.Name != "" {
605
+
name = data.Name
606
+
} else {
607
+
name = filepath.Base(source)
608
+
}
609
+
610
+
relativeRepoPath := filepath.Join(did, name)
611
+
repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
612
+
613
+
err := git.Fork(repoPath, source)
614
+
if err != nil {
615
+
l.Error("forking repo", "error", err.Error())
616
+
writeError(w, err.Error(), http.StatusInternalServerError)
617
+
return
618
+
}
619
+
620
+
w.WriteHeader(http.StatusNoContent)
621
+
}
622
+
623
func (h *Handle) RemoveRepo(w http.ResponseWriter, r *http.Request) {
624
l := h.l.With("handler", "RemoveRepo")
625