+58
-1
api/tangled/cbor_gen.go
+58
-1
api/tangled/cbor_gen.go
···
1753
1753
}
1754
1754
1755
1755
cw := cbg.NewCborWriter(w)
1756
-
fieldCount := 6
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
+
fieldCount--
1764
+
}
1765
+
1766
+
if t.Source == nil {
1763
1767
fieldCount--
1764
1768
}
1765
1769
···
1855
1859
return err
1856
1860
}
1857
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
+
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
+
}
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
+
}
2008
2065
}
2009
2066
// t.AddedAt (string) (string)
2010
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
3
import (
4
4
"database/sql"
5
5
"time"
6
+
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
+
22
+
// optional
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
-
`select did, name, knot, rkey, description, created
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
-
rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Description, &repo.Created,
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
-
count(s.id) as star_count
71
+
count(s.id) as star_count,
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
-
`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,
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,
166
172
)
167
173
return err
168
174
}
···
172
178
return err
173
179
}
174
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
+
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
-
func scanRepo(rows *sql.Rows, did, name, knot, rkey, description *string, created *time.Time) error {
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
-
if err := rows.Scan(did, name, knot, rkey, &nullableDescription, &createdAt); err != nil {
270
+
var nullableSource sql.NullString
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
+
}
287
+
288
+
if nullableSource.Valid {
289
+
*source = nullableSource.String
290
+
} else {
291
+
*source = ""
270
292
}
271
293
272
294
return nil
+17
appview/db/timeline.go
+17
appview/db/timeline.go
···
9
9
*Repo
10
10
*Follow
11
11
*Star
12
+
12
13
EventAt time.Time
14
+
15
+
// optional: populate only if Repo is a fork
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
+
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
+
37
54
events = append(events, TimelineEvent{
38
55
Repo: &repo,
39
56
EventAt: repo.Created,
+21
-9
appview/pages/pages.go
+21
-9
appview/pages/pages.go
···
158
158
return p.execute("repo/new", w, params)
159
159
}
160
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
+
161
171
type ProfilePageParams struct {
162
172
LoggedInUser *auth.User
163
173
UserDid string
···
212
222
}
213
223
214
224
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
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
224
236
}
225
237
226
238
type RolesInRepo struct {
+10
appview/pages/templates/layouts/repobase.html
+10
appview/pages/templates/layouts/repobase.html
···
2
2
3
3
{{ define "content" }}
4
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 }}
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
+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
1
{{ define "title" }}settings · {{ .RepoInfo.FullName }}{{ end }}
2
2
{{ define "repoContent" }}
3
-
<header class="font-bold text-sm mb-4 uppercase dark:text-white">Collaborators</header>
3
+
<header class="font-bold text-sm mb-4 uppercase dark:text-white">
4
+
Collaborators
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
-
<h3 class="dark:text-white">add collaborator</h3>
25
26
<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>
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>
29
44
</form>
30
45
{{ end }}
31
46
+7
appview/pages/templates/timeline.html
+7
appview/pages/templates/timeline.html
···
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
+
{{ 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 }}
47
53
created
48
54
<a href="/{{ $userHandle }}/{{ .Repo.Name }}" class="no-underline hover:underline">{{ .Repo.Name }}</a>
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
+191
-2
appview/state/repo.go
···
2
2
3
3
import (
4
4
"context"
5
+
"database/sql"
5
6
"encoding/json"
7
+
"errors"
6
8
"fmt"
7
9
"io"
8
10
"log"
9
-
"math/rand/v2"
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
+
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
+
}
804
820
805
821
knot := f.Knot
806
822
if knot == "knot1.tangled.sh" {
807
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
+
}
808
832
}
809
833
810
834
return pages.RepoInfo{
···
821
845
IssueCount: issueCount,
822
846
PullCount: pullCount,
823
847
},
848
+
Source: sourceRepo,
849
+
SourceHandle: sourceHandle.Handle.String(),
824
850
}
825
851
}
826
852
···
1022
1048
return
1023
1049
}
1024
1050
1025
-
commentId := rand.IntN(1000000)
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
+
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
2
3
3
import (
4
4
"context"
5
+
"crypto/rand"
5
6
"fmt"
6
7
"log"
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
+
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
···
85
85
})
86
86
})
87
87
88
+
r.Route("/fork", func(r chi.Router) {
89
+
r.Use(AuthMiddleware(s))
90
+
r.Get("/", s.ForkRepo)
91
+
r.Post("/", s.ForkRepo)
92
+
})
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
+20
appview/state/signer.go
···
103
103
return s.client.Do(req)
104
104
}
105
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
+
106
126
func (s *SignedClient) RemoveRepo(did, repoName string) (*http.Response, error) {
107
127
const (
108
128
Method = "DELETE"
+3
appview/state/state.go
+3
appview/state/state.go
···
190
190
for _, ev := range timeline {
191
191
if ev.Repo != nil {
192
192
didsToResolve = append(didsToResolve, ev.Repo.Did)
193
+
if ev.Source != nil {
194
+
didsToResolve = append(didsToResolve, ev.Source.Did)
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
+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
577
w.WriteHeader(http.StatusNoContent)
578
578
}
579
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
+
580
623
func (h *Handle) RemoveRepo(w http.ResponseWriter, r *http.Request) {
581
624
l := h.l.With("handler", "RemoveRepo")
582
625