-1
knotserver/handler.go
-1
knotserver/handler.go
-62
knotserver/routes.go
-62
knotserver/routes.go
···
22
22
securejoin "github.com/cyphar/filepath-securejoin"
23
23
"github.com/gliderlabs/ssh"
24
24
"github.com/go-chi/chi/v5"
25
-
gogit "github.com/go-git/go-git/v5"
26
25
"github.com/go-git/go-git/v5/plumbing"
27
26
"github.com/go-git/go-git/v5/plumbing/object"
28
27
"tangled.sh/tangled.sh/core/hook"
···
646
645
w.WriteHeader(http.StatusNoContent)
647
646
return
648
647
}
649
-
}
650
-
651
-
func (h *Handle) NewRepo(w http.ResponseWriter, r *http.Request) {
652
-
l := h.l.With("handler", "NewRepo")
653
-
654
-
data := struct {
655
-
Did string `json:"did"`
656
-
Name string `json:"name"`
657
-
DefaultBranch string `json:"default_branch,omitempty"`
658
-
}{}
659
-
660
-
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
661
-
writeError(w, "invalid request body", http.StatusBadRequest)
662
-
return
663
-
}
664
-
665
-
if data.DefaultBranch == "" {
666
-
data.DefaultBranch = h.c.Repo.MainBranch
667
-
}
668
-
669
-
did := data.Did
670
-
name := data.Name
671
-
defaultBranch := data.DefaultBranch
672
-
673
-
if err := validateRepoName(name); err != nil {
674
-
l.Error("creating repo", "error", err.Error())
675
-
writeError(w, err.Error(), http.StatusBadRequest)
676
-
return
677
-
}
678
-
679
-
relativeRepoPath := filepath.Join(did, name)
680
-
repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
681
-
err := git.InitBare(repoPath, defaultBranch)
682
-
if err != nil {
683
-
l.Error("initializing bare repo", "error", err.Error())
684
-
if errors.Is(err, gogit.ErrRepositoryAlreadyExists) {
685
-
writeError(w, "That repo already exists!", http.StatusConflict)
686
-
return
687
-
} else {
688
-
writeError(w, err.Error(), http.StatusInternalServerError)
689
-
return
690
-
}
691
-
}
692
-
693
-
// add perms for this user to access the repo
694
-
err = h.e.AddRepo(did, rbac.ThisServer, relativeRepoPath)
695
-
if err != nil {
696
-
l.Error("adding repo permissions", "error", err.Error())
697
-
writeError(w, err.Error(), http.StatusInternalServerError)
698
-
return
699
-
}
700
-
701
-
hook.SetupRepo(
702
-
hook.Config(
703
-
hook.WithScanPath(h.c.Repo.ScanPath),
704
-
hook.WithInternalApi(h.c.Server.InternalListenAddr),
705
-
),
706
-
repoPath,
707
-
)
708
-
709
-
w.WriteHeader(http.StatusNoContent)
710
648
}
711
649
712
650
func (h *Handle) RepoForkAheadBehind(w http.ResponseWriter, r *http.Request) {
+127
knotserver/xrpc/create_repo.go
+127
knotserver/xrpc/create_repo.go
···
1
+
package xrpc
2
+
3
+
import (
4
+
"encoding/json"
5
+
"errors"
6
+
"fmt"
7
+
"net/http"
8
+
"path/filepath"
9
+
"strings"
10
+
11
+
"github.com/bluesky-social/indigo/atproto/syntax"
12
+
securejoin "github.com/cyphar/filepath-securejoin"
13
+
gogit "github.com/go-git/go-git/v5"
14
+
"tangled.sh/tangled.sh/core/api/tangled"
15
+
"tangled.sh/tangled.sh/core/hook"
16
+
"tangled.sh/tangled.sh/core/knotserver/git"
17
+
"tangled.sh/tangled.sh/core/rbac"
18
+
xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
19
+
)
20
+
21
+
func (h *Xrpc) CreateRepo(w http.ResponseWriter, r *http.Request) {
22
+
l := h.Logger.With("handler", "NewRepo")
23
+
fail := func(e xrpcerr.XrpcError) {
24
+
l.Error("failed", "kind", e.Tag, "error", e.Message)
25
+
writeError(w, e, http.StatusBadRequest)
26
+
}
27
+
28
+
actorDid, ok := r.Context().Value(ActorDid).(syntax.DID)
29
+
if !ok {
30
+
fail(xrpcerr.MissingActorDidError)
31
+
return
32
+
}
33
+
34
+
isMember, err := h.Enforcer.IsKnotMember(actorDid.String(), rbac.ThisServer)
35
+
if err != nil {
36
+
fail(xrpcerr.GenericError(err))
37
+
return
38
+
}
39
+
if !isMember {
40
+
fail(xrpcerr.AccessControlError(actorDid.String()))
41
+
return
42
+
}
43
+
44
+
var data tangled.RepoCreate_Input
45
+
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
46
+
fail(xrpcerr.GenericError(err))
47
+
return
48
+
}
49
+
50
+
defaultBranch := h.Config.Repo.MainBranch
51
+
if data.Default_branch != nil && *data.Default_branch != "" {
52
+
defaultBranch = *data.Default_branch
53
+
}
54
+
55
+
did := data.Did
56
+
name := data.Name
57
+
58
+
if err := validateRepoName(name); err != nil {
59
+
l.Error("creating repo", "error", err.Error())
60
+
fail(xrpcerr.GenericError(err))
61
+
return
62
+
}
63
+
64
+
relativeRepoPath := filepath.Join(did, name)
65
+
repoPath, _ := securejoin.SecureJoin(h.Config.Repo.ScanPath, relativeRepoPath)
66
+
err = git.InitBare(repoPath, defaultBranch)
67
+
if err != nil {
68
+
l.Error("initializing bare repo", "error", err.Error())
69
+
if errors.Is(err, gogit.ErrRepositoryAlreadyExists) {
70
+
fail(xrpcerr.RepoExistsError("repository already exists"))
71
+
return
72
+
} else {
73
+
writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
74
+
return
75
+
}
76
+
}
77
+
78
+
// add perms for this user to access the repo
79
+
err = h.Enforcer.AddRepo(did, rbac.ThisServer, relativeRepoPath)
80
+
if err != nil {
81
+
l.Error("adding repo permissions", "error", err.Error())
82
+
writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
83
+
return
84
+
}
85
+
86
+
hook.SetupRepo(
87
+
hook.Config(
88
+
hook.WithScanPath(h.Config.Repo.ScanPath),
89
+
hook.WithInternalApi(h.Config.Server.InternalListenAddr),
90
+
),
91
+
repoPath,
92
+
)
93
+
94
+
w.WriteHeader(http.StatusOK)
95
+
}
96
+
97
+
func validateRepoName(name string) error {
98
+
// check for path traversal attempts
99
+
if name == "." || name == ".." ||
100
+
strings.Contains(name, "/") || strings.Contains(name, "\\") {
101
+
return fmt.Errorf("Repository name contains invalid path characters")
102
+
}
103
+
104
+
// check for sequences that could be used for traversal when normalized
105
+
if strings.Contains(name, "./") || strings.Contains(name, "../") ||
106
+
strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") {
107
+
return fmt.Errorf("Repository name contains invalid path sequence")
108
+
}
109
+
110
+
// then continue with character validation
111
+
for _, char := range name {
112
+
if !((char >= 'a' && char <= 'z') ||
113
+
(char >= 'A' && char <= 'Z') ||
114
+
(char >= '0' && char <= '9') ||
115
+
char == '-' || char == '_' || char == '.') {
116
+
return fmt.Errorf("Repository name can only contain alphanumeric characters, periods, hyphens, and underscores")
117
+
}
118
+
}
119
+
120
+
// additional check to prevent multiple sequential dots
121
+
if strings.Contains(name, "..") {
122
+
return fmt.Errorf("Repository name cannot contain sequential dots")
123
+
}
124
+
125
+
// if all checks pass
126
+
return nil
127
+
}
+82
knotserver/xrpc/delete_repo.go
+82
knotserver/xrpc/delete_repo.go
···
1
+
package xrpc
2
+
3
+
import (
4
+
"encoding/json"
5
+
"fmt"
6
+
"net/http"
7
+
"os"
8
+
"path/filepath"
9
+
10
+
"github.com/bluesky-social/indigo/atproto/syntax"
11
+
securejoin "github.com/cyphar/filepath-securejoin"
12
+
"tangled.sh/tangled.sh/core/api/tangled"
13
+
"tangled.sh/tangled.sh/core/rbac"
14
+
xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
15
+
)
16
+
17
+
func (x *Xrpc) DeleteRepo(w http.ResponseWriter, r *http.Request) {
18
+
l := x.Logger.With("handler", "DeleteRepo")
19
+
fail := func(e xrpcerr.XrpcError) {
20
+
l.Error("failed", "kind", e.Tag, "error", e.Message)
21
+
writeError(w, e, http.StatusBadRequest)
22
+
}
23
+
24
+
actorDid, ok := r.Context().Value(ActorDid).(syntax.DID)
25
+
if !ok {
26
+
fail(xrpcerr.MissingActorDidError)
27
+
return
28
+
}
29
+
30
+
isMember, err := x.Enforcer.IsKnotMember(actorDid.String(), rbac.ThisServer)
31
+
if err != nil {
32
+
fail(xrpcerr.GenericError(err))
33
+
return
34
+
}
35
+
if !isMember {
36
+
fail(xrpcerr.AccessControlError(actorDid.String()))
37
+
return
38
+
}
39
+
40
+
var data tangled.RepoDelete_Input
41
+
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
42
+
fail(xrpcerr.GenericError(err))
43
+
return
44
+
}
45
+
46
+
did := data.Did
47
+
name := data.Name
48
+
49
+
if did == "" || name == "" {
50
+
fail(xrpcerr.GenericError(fmt.Errorf("did and name are required")))
51
+
return
52
+
}
53
+
54
+
relativeRepoPath := filepath.Join(did, name)
55
+
if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath); !ok || err != nil {
56
+
l.Error("insufficient permissions", "did", actorDid.String(), "repo", relativeRepoPath)
57
+
writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)
58
+
return
59
+
}
60
+
61
+
repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath)
62
+
if err != nil {
63
+
fail(xrpcerr.GenericError(err))
64
+
return
65
+
}
66
+
67
+
err = os.RemoveAll(repoPath)
68
+
if err != nil {
69
+
l.Error("deleting repo", "error", err.Error())
70
+
writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
71
+
return
72
+
}
73
+
74
+
err = x.Enforcer.RemoveRepo(did, rbac.ThisServer, relativeRepoPath)
75
+
if err != nil {
76
+
l.Error("failed to delete repo from enforcer", "error", err.Error())
77
+
writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
78
+
return
79
+
}
80
+
81
+
w.WriteHeader(http.StatusOK)
82
+
}
+93
knotserver/xrpc/fork_repo.go
+93
knotserver/xrpc/fork_repo.go
···
1
+
package xrpc
2
+
3
+
import (
4
+
"encoding/json"
5
+
"fmt"
6
+
"net/http"
7
+
"path/filepath"
8
+
9
+
"github.com/bluesky-social/indigo/atproto/syntax"
10
+
securejoin "github.com/cyphar/filepath-securejoin"
11
+
"tangled.sh/tangled.sh/core/api/tangled"
12
+
"tangled.sh/tangled.sh/core/hook"
13
+
"tangled.sh/tangled.sh/core/knotserver/git"
14
+
"tangled.sh/tangled.sh/core/rbac"
15
+
xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
16
+
)
17
+
18
+
func (x *Xrpc) ForkRepo(w http.ResponseWriter, r *http.Request) {
19
+
l := x.Logger.With("handler", "ForkRepo")
20
+
fail := func(e xrpcerr.XrpcError) {
21
+
l.Error("failed", "kind", e.Tag, "error", e.Message)
22
+
writeError(w, e, http.StatusBadRequest)
23
+
}
24
+
25
+
actorDid, ok := r.Context().Value(ActorDid).(syntax.DID)
26
+
if !ok {
27
+
fail(xrpcerr.MissingActorDidError)
28
+
return
29
+
}
30
+
31
+
isMember, err := x.Enforcer.IsKnotMember(actorDid.String(), rbac.ThisServer)
32
+
if err != nil {
33
+
fail(xrpcerr.GenericError(err))
34
+
return
35
+
}
36
+
if !isMember {
37
+
fail(xrpcerr.AccessControlError(actorDid.String()))
38
+
return
39
+
}
40
+
41
+
var data tangled.RepoFork_Input
42
+
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
43
+
fail(xrpcerr.GenericError(err))
44
+
return
45
+
}
46
+
47
+
did := data.Did
48
+
source := data.Source
49
+
50
+
if did == "" || source == "" {
51
+
fail(xrpcerr.GenericError(fmt.Errorf("did and source are required")))
52
+
return
53
+
}
54
+
55
+
var name string
56
+
if data.Name != nil && *data.Name != "" {
57
+
name = *data.Name
58
+
} else {
59
+
name = filepath.Base(source)
60
+
}
61
+
62
+
relativeRepoPath := filepath.Join(did, name)
63
+
repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath)
64
+
if err != nil {
65
+
fail(xrpcerr.GenericError(err))
66
+
return
67
+
}
68
+
69
+
err = git.Fork(repoPath, source)
70
+
if err != nil {
71
+
l.Error("forking repo", "error", err.Error())
72
+
writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
73
+
return
74
+
}
75
+
76
+
// add perms for this user to access the repo
77
+
err = x.Enforcer.AddRepo(did, rbac.ThisServer, relativeRepoPath)
78
+
if err != nil {
79
+
l.Error("adding repo permissions", "error", err.Error())
80
+
writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
81
+
return
82
+
}
83
+
84
+
hook.SetupRepo(
85
+
hook.Config(
86
+
hook.WithScanPath(x.Config.Repo.ScanPath),
87
+
hook.WithInternalApi(x.Config.Server.InternalListenAddr),
88
+
),
89
+
repoPath,
90
+
)
91
+
92
+
w.WriteHeader(http.StatusOK)
93
+
}
+111
knotserver/xrpc/fork_status.go
+111
knotserver/xrpc/fork_status.go
···
1
+
package xrpc
2
+
3
+
import (
4
+
"encoding/json"
5
+
"fmt"
6
+
"net/http"
7
+
"path/filepath"
8
+
9
+
"github.com/bluesky-social/indigo/atproto/syntax"
10
+
securejoin "github.com/cyphar/filepath-securejoin"
11
+
"tangled.sh/tangled.sh/core/api/tangled"
12
+
"tangled.sh/tangled.sh/core/knotserver/git"
13
+
"tangled.sh/tangled.sh/core/rbac"
14
+
"tangled.sh/tangled.sh/core/types"
15
+
xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
16
+
)
17
+
18
+
func (x *Xrpc) ForkStatus(w http.ResponseWriter, r *http.Request) {
19
+
l := x.Logger.With("handler", "ForkStatus")
20
+
fail := func(e xrpcerr.XrpcError) {
21
+
l.Error("failed", "kind", e.Tag, "error", e.Message)
22
+
writeError(w, e, http.StatusBadRequest)
23
+
}
24
+
25
+
actorDid, ok := r.Context().Value(ActorDid).(syntax.DID)
26
+
if !ok {
27
+
fail(xrpcerr.MissingActorDidError)
28
+
return
29
+
}
30
+
31
+
var data tangled.RepoForkStatus_Input
32
+
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
33
+
fail(xrpcerr.GenericError(err))
34
+
return
35
+
}
36
+
37
+
did := data.Did
38
+
source := data.Source
39
+
branch := data.Branch
40
+
hiddenRef := data.HiddenRef
41
+
42
+
if did == "" || source == "" || branch == "" || hiddenRef == "" {
43
+
fail(xrpcerr.GenericError(fmt.Errorf("did, source, branch, and hiddenRef are required")))
44
+
return
45
+
}
46
+
47
+
var name string
48
+
if data.Name != "" {
49
+
name = data.Name
50
+
} else {
51
+
name = filepath.Base(source)
52
+
}
53
+
54
+
relativeRepoPath := filepath.Join(did, name)
55
+
56
+
if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath); !ok || err != nil {
57
+
l.Error("insufficient permissions", "did", actorDid.String(), "repo", relativeRepoPath)
58
+
writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)
59
+
return
60
+
}
61
+
62
+
repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath)
63
+
if err != nil {
64
+
fail(xrpcerr.GenericError(err))
65
+
return
66
+
}
67
+
68
+
gr, err := git.PlainOpen(repoPath)
69
+
if err != nil {
70
+
fail(xrpcerr.GenericError(fmt.Errorf("failed to open repository: %w", err)))
71
+
return
72
+
}
73
+
74
+
forkCommit, err := gr.ResolveRevision(branch)
75
+
if err != nil {
76
+
l.Error("error resolving ref revision", "msg", err.Error())
77
+
fail(xrpcerr.GenericError(fmt.Errorf("error resolving revision %s: %w", branch, err)))
78
+
return
79
+
}
80
+
81
+
sourceCommit, err := gr.ResolveRevision(hiddenRef)
82
+
if err != nil {
83
+
l.Error("error resolving hidden ref revision", "msg", err.Error())
84
+
fail(xrpcerr.GenericError(fmt.Errorf("error resolving revision %s: %w", hiddenRef, err)))
85
+
return
86
+
}
87
+
88
+
status := types.UpToDate
89
+
if forkCommit.Hash.String() != sourceCommit.Hash.String() {
90
+
isAncestor, err := forkCommit.IsAncestor(sourceCommit)
91
+
if err != nil {
92
+
l.Error("error checking ancestor relationship", "error", err.Error())
93
+
fail(xrpcerr.GenericError(fmt.Errorf("error resolving whether %s is ancestor of %s: %w", branch, hiddenRef, err)))
94
+
return
95
+
}
96
+
97
+
if isAncestor {
98
+
status = types.FastForwardable
99
+
} else {
100
+
status = types.Conflict
101
+
}
102
+
}
103
+
104
+
response := tangled.RepoForkStatus_Output{
105
+
Status: int64(status),
106
+
}
107
+
108
+
w.Header().Set("Content-Type", "application/json")
109
+
w.WriteHeader(http.StatusOK)
110
+
json.NewEncoder(w).Encode(response)
111
+
}
+73
knotserver/xrpc/fork_sync.go
+73
knotserver/xrpc/fork_sync.go
···
1
+
package xrpc
2
+
3
+
import (
4
+
"encoding/json"
5
+
"fmt"
6
+
"net/http"
7
+
"path/filepath"
8
+
9
+
"github.com/bluesky-social/indigo/atproto/syntax"
10
+
securejoin "github.com/cyphar/filepath-securejoin"
11
+
"tangled.sh/tangled.sh/core/api/tangled"
12
+
"tangled.sh/tangled.sh/core/knotserver/git"
13
+
"tangled.sh/tangled.sh/core/rbac"
14
+
xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
15
+
)
16
+
17
+
func (x *Xrpc) ForkSync(w http.ResponseWriter, r *http.Request) {
18
+
l := x.Logger.With("handler", "ForkSync")
19
+
fail := func(e xrpcerr.XrpcError) {
20
+
l.Error("failed", "kind", e.Tag, "error", e.Message)
21
+
writeError(w, e, http.StatusBadRequest)
22
+
}
23
+
24
+
actorDid, ok := r.Context().Value(ActorDid).(syntax.DID)
25
+
if !ok {
26
+
fail(xrpcerr.MissingActorDidError)
27
+
return
28
+
}
29
+
30
+
var data tangled.RepoForkSync_Input
31
+
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
32
+
fail(xrpcerr.GenericError(err))
33
+
return
34
+
}
35
+
36
+
did := data.Did
37
+
name := data.Name
38
+
branch := data.Branch
39
+
40
+
if did == "" || name == "" || branch == "" {
41
+
fail(xrpcerr.GenericError(fmt.Errorf("did, name, and branch are required")))
42
+
return
43
+
}
44
+
45
+
relativeRepoPath := filepath.Join(did, name)
46
+
47
+
if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath); !ok || err != nil {
48
+
l.Error("insufficient permissions", "did", actorDid.String(), "repo", relativeRepoPath)
49
+
writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)
50
+
return
51
+
}
52
+
53
+
repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath)
54
+
if err != nil {
55
+
fail(xrpcerr.GenericError(err))
56
+
return
57
+
}
58
+
59
+
gr, err := git.PlainOpen(repoPath)
60
+
if err != nil {
61
+
fail(xrpcerr.GenericError(fmt.Errorf("failed to open repository: %w", err)))
62
+
return
63
+
}
64
+
65
+
err = gr.Sync(branch)
66
+
if err != nil {
67
+
l.Error("error syncing repo fork", "error", err.Error())
68
+
writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
69
+
return
70
+
}
71
+
72
+
w.WriteHeader(http.StatusOK)
73
+
}
+112
knotserver/xrpc/merge.go
+112
knotserver/xrpc/merge.go
···
1
+
package xrpc
2
+
3
+
import (
4
+
"encoding/json"
5
+
"errors"
6
+
"fmt"
7
+
"net/http"
8
+
9
+
"github.com/bluesky-social/indigo/atproto/syntax"
10
+
securejoin "github.com/cyphar/filepath-securejoin"
11
+
"tangled.sh/tangled.sh/core/api/tangled"
12
+
"tangled.sh/tangled.sh/core/knotserver/git"
13
+
"tangled.sh/tangled.sh/core/patchutil"
14
+
"tangled.sh/tangled.sh/core/rbac"
15
+
"tangled.sh/tangled.sh/core/types"
16
+
xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
17
+
)
18
+
19
+
func (x *Xrpc) Merge(w http.ResponseWriter, r *http.Request) {
20
+
l := x.Logger.With("handler", "Merge")
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.RepoMerge_Input
33
+
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
34
+
fail(xrpcerr.GenericError(err))
35
+
return
36
+
}
37
+
38
+
did := data.Did
39
+
name := data.Name
40
+
41
+
if did == "" || name == "" {
42
+
fail(xrpcerr.GenericError(fmt.Errorf("did and name are required")))
43
+
return
44
+
}
45
+
46
+
relativeRepoPath, err := securejoin.SecureJoin(did, name)
47
+
if err != nil {
48
+
fail(xrpcerr.GenericError(err))
49
+
return
50
+
}
51
+
52
+
if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath); !ok || err != nil {
53
+
l.Error("insufficient permissions", "did", actorDid.String(), "repo", relativeRepoPath)
54
+
writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)
55
+
return
56
+
}
57
+
58
+
repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath)
59
+
if err != nil {
60
+
fail(xrpcerr.GenericError(err))
61
+
return
62
+
}
63
+
64
+
gr, err := git.Open(repoPath, data.Branch)
65
+
if err != nil {
66
+
fail(xrpcerr.GenericError(fmt.Errorf("failed to open repository: %w", err)))
67
+
return
68
+
}
69
+
70
+
mo := &git.MergeOptions{}
71
+
if data.AuthorName != nil {
72
+
mo.AuthorName = *data.AuthorName
73
+
}
74
+
if data.AuthorEmail != nil {
75
+
mo.AuthorEmail = *data.AuthorEmail
76
+
}
77
+
if data.CommitBody != nil {
78
+
mo.CommitBody = *data.CommitBody
79
+
}
80
+
if data.CommitMessage != nil {
81
+
mo.CommitMessage = *data.CommitMessage
82
+
}
83
+
84
+
mo.FormatPatch = patchutil.IsFormatPatch(data.Patch)
85
+
86
+
err = gr.MergeWithOptions([]byte(data.Patch), data.Branch, mo)
87
+
if err != nil {
88
+
var mergeErr *git.ErrMerge
89
+
if errors.As(err, &mergeErr) {
90
+
conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts))
91
+
for i, conflict := range mergeErr.Conflicts {
92
+
conflicts[i] = types.ConflictInfo{
93
+
Filename: conflict.Filename,
94
+
Reason: conflict.Reason,
95
+
}
96
+
}
97
+
98
+
conflictErr := xrpcerr.NewXrpcError(
99
+
xrpcerr.WithTag("MergeConflict"),
100
+
xrpcerr.WithMessage(fmt.Sprintf("Merge failed due to conflicts: %s", mergeErr.Message)),
101
+
)
102
+
writeError(w, conflictErr, http.StatusConflict)
103
+
return
104
+
} else {
105
+
l.Error("failed to merge", "error", err.Error())
106
+
writeError(w, xrpcerr.GitError(err), http.StatusInternalServerError)
107
+
return
108
+
}
109
+
}
110
+
111
+
w.WriteHeader(http.StatusOK)
112
+
}
+105
knotserver/xrpc/merge_check.go
+105
knotserver/xrpc/merge_check.go
···
1
+
package xrpc
2
+
3
+
import (
4
+
"encoding/json"
5
+
"errors"
6
+
"fmt"
7
+
"net/http"
8
+
9
+
"github.com/bluesky-social/indigo/atproto/syntax"
10
+
securejoin "github.com/cyphar/filepath-securejoin"
11
+
"tangled.sh/tangled.sh/core/api/tangled"
12
+
"tangled.sh/tangled.sh/core/knotserver/git"
13
+
"tangled.sh/tangled.sh/core/rbac"
14
+
xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
15
+
)
16
+
17
+
func (x *Xrpc) MergeCheck(w http.ResponseWriter, r *http.Request) {
18
+
l := x.Logger.With("handler", "MergeCheck")
19
+
fail := func(e xrpcerr.XrpcError) {
20
+
l.Error("failed", "kind", e.Tag, "error", e.Message)
21
+
writeError(w, e, http.StatusBadRequest)
22
+
}
23
+
24
+
actorDid, ok := r.Context().Value(ActorDid).(syntax.DID)
25
+
if !ok {
26
+
fail(xrpcerr.MissingActorDidError)
27
+
return
28
+
}
29
+
30
+
isMember, err := x.Enforcer.IsKnotMember(actorDid.String(), rbac.ThisServer)
31
+
if err != nil {
32
+
fail(xrpcerr.GenericError(err))
33
+
return
34
+
}
35
+
if !isMember {
36
+
fail(xrpcerr.AccessControlError(actorDid.String()))
37
+
return
38
+
}
39
+
40
+
var data tangled.RepoMergeCheck_Input
41
+
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
42
+
fail(xrpcerr.GenericError(err))
43
+
return
44
+
}
45
+
46
+
did := data.Did
47
+
name := data.Name
48
+
49
+
if did == "" || name == "" {
50
+
fail(xrpcerr.GenericError(fmt.Errorf("did and name are required")))
51
+
return
52
+
}
53
+
54
+
relativeRepoPath, err := securejoin.SecureJoin(did, name)
55
+
if err != nil {
56
+
fail(xrpcerr.GenericError(err))
57
+
return
58
+
}
59
+
60
+
repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath)
61
+
if err != nil {
62
+
fail(xrpcerr.GenericError(err))
63
+
return
64
+
}
65
+
66
+
gr, err := git.Open(repoPath, data.Branch)
67
+
if err != nil {
68
+
fail(xrpcerr.GenericError(fmt.Errorf("failed to open repository: %w", err)))
69
+
return
70
+
}
71
+
72
+
err = gr.MergeCheck([]byte(data.Patch), data.Branch)
73
+
74
+
response := tangled.RepoMergeCheck_Output{
75
+
Is_conflicted: false,
76
+
}
77
+
78
+
if err != nil {
79
+
var mergeErr *git.ErrMerge
80
+
if errors.As(err, &mergeErr) {
81
+
response.Is_conflicted = true
82
+
83
+
conflicts := make([]*tangled.RepoMergeCheck_ConflictInfo, len(mergeErr.Conflicts))
84
+
for i, conflict := range mergeErr.Conflicts {
85
+
conflicts[i] = &tangled.RepoMergeCheck_ConflictInfo{
86
+
Filename: conflict.Filename,
87
+
Reason: conflict.Reason,
88
+
}
89
+
}
90
+
response.Conflicts = conflicts
91
+
92
+
if mergeErr.Message != "" {
93
+
response.Message = &mergeErr.Message
94
+
}
95
+
} else {
96
+
response.Is_conflicted = true
97
+
errMsg := err.Error()
98
+
response.Error = &errMsg
99
+
}
100
+
}
101
+
102
+
w.Header().Set("Content-Type", "application/json")
103
+
w.WriteHeader(http.StatusOK)
104
+
json.NewEncoder(w).Encode(response)
105
+
}
+13
-4
knotserver/xrpc/router.go
+13
-4
knotserver/xrpc/router.go
···
31
31
32
32
func (x *Xrpc) Router() http.Handler {
33
33
r := chi.NewRouter()
34
+
r.Group(func(r chi.Router) {
35
+
r.Use(x.ServiceAuth.VerifyServiceAuth)
36
+
37
+
r.Post("/"+tangled.RepoSetDefaultBranchNSID, x.SetDefaultBranch)
38
+
r.Post("/"+tangled.RepoCreateNSID, x.CreateRepo)
39
+
r.Post("/"+tangled.RepoDeleteNSID, x.DeleteRepo)
40
+
r.Post("/"+tangled.RepoForkNSID, x.ForkRepo)
41
+
r.Post("/"+tangled.RepoForkStatusNSID, x.ForkStatus)
42
+
r.Post("/"+tangled.RepoForkSyncNSID, x.ForkSync)
34
43
35
-
r.With(x.ServiceAuth.VerifyServiceAuth).Post("/"+tangled.RepoSetDefaultBranchNSID, x.SetDefaultBranch)
44
+
r.Post("/"+tangled.RepoHiddenRefNSID, x.HiddenRef)
36
45
46
+
r.Post("/"+tangled.RepoMergeNSID, x.Merge)
47
+
r.Post("/"+tangled.RepoMergeCheckNSID, x.MergeCheck)
48
+
})
37
49
return r
38
50
}
39
51
40
-
// this is slightly different from http_util::write_error to follow the spec:
41
-
//
42
-
// the json object returned must include an "error" and a "message"
43
52
func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) {
44
53
w.Header().Set("Content-Type", "application/json")
45
54
w.WriteHeader(status)