1package xrpc
2
3import (
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
17func (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}