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