From c0e7c0bbf5dc7cfebe1e3b7df197780864b6ef79 Mon Sep 17 00:00:00 2001 From: oppiliappan Date: Mon, 6 Oct 2025 22:08:27 +0100 Subject: [PATCH] knotserver/xrpc: add sh.tangled.repo.deleteBranch Change-Id: rvtqynpmozzytrwomqvrmzxspoookkkq Signed-off-by: oppiliappan --- api/tangled/repodeleteBranch.go | 30 +++++++++++ knotserver/git/branch.go | 5 ++ knotserver/xrpc/delete_branch.go | 87 ++++++++++++++++++++++++++++++++ knotserver/xrpc/xrpc.go | 1 + lexicons/repo/deleteBranch.json | 30 +++++++++++ 5 files changed, 153 insertions(+) create mode 100644 api/tangled/repodeleteBranch.go create mode 100644 knotserver/xrpc/delete_branch.go create mode 100644 lexicons/repo/deleteBranch.json diff --git a/api/tangled/repodeleteBranch.go b/api/tangled/repodeleteBranch.go new file mode 100644 index 00000000..d861878b --- /dev/null +++ b/api/tangled/repodeleteBranch.go @@ -0,0 +1,30 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +package tangled + +// schema: sh.tangled.repo.deleteBranch + +import ( + "context" + + "github.com/bluesky-social/indigo/lex/util" +) + +const ( + RepoDeleteBranchNSID = "sh.tangled.repo.deleteBranch" +) + +// RepoDeleteBranch_Input is the input argument to a sh.tangled.repo.deleteBranch call. +type RepoDeleteBranch_Input struct { + Branch string `json:"branch" cborgen:"branch"` + Repo string `json:"repo" cborgen:"repo"` +} + +// RepoDeleteBranch calls the XRPC method "sh.tangled.repo.deleteBranch". +func RepoDeleteBranch(ctx context.Context, c util.LexClient, input *RepoDeleteBranch_Input) error { + if err := c.LexDo(ctx, util.Procedure, "application/json", "sh.tangled.repo.deleteBranch", nil, input, nil); err != nil { + return err + } + + return nil +} diff --git a/knotserver/git/branch.go b/knotserver/git/branch.go index 5e00a847..6aabb5d4 100644 --- a/knotserver/git/branch.go +++ b/knotserver/git/branch.go @@ -110,3 +110,8 @@ func (g *GitRepo) Branches() ([]types.Branch, error) { slices.Reverse(branches) return branches, nil } + +func (g *GitRepo) DeleteBranch(branch string) error { + ref := plumbing.NewBranchReferenceName(branch) + return g.r.Storer.RemoveReference(ref) +} diff --git a/knotserver/xrpc/delete_branch.go b/knotserver/xrpc/delete_branch.go new file mode 100644 index 00000000..95d46946 --- /dev/null +++ b/knotserver/xrpc/delete_branch.go @@ -0,0 +1,87 @@ +package xrpc + +import ( + "encoding/json" + "fmt" + "net/http" + + comatproto "github.com/bluesky-social/indigo/api/atproto" + "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/bluesky-social/indigo/xrpc" + securejoin "github.com/cyphar/filepath-securejoin" + "tangled.org/core/api/tangled" + "tangled.org/core/knotserver/git" + "tangled.org/core/rbac" + + xrpcerr "tangled.org/core/xrpc/errors" +) + +func (x *Xrpc) DeleteBranch(w http.ResponseWriter, r *http.Request) { + l := x.Logger + fail := func(e xrpcerr.XrpcError) { + l.Error("failed", "kind", e.Tag, "error", e.Message) + writeError(w, e, http.StatusBadRequest) + } + + actorDid, ok := r.Context().Value(ActorDid).(syntax.DID) + if !ok { + fail(xrpcerr.MissingActorDidError) + return + } + + var data tangled.RepoDeleteBranch_Input + if err := json.NewDecoder(r.Body).Decode(&data); err != nil { + fail(xrpcerr.GenericError(err)) + return + } + + // unfortunately we have to resolve repo-at here + repoAt, err := syntax.ParseATURI(data.Repo) + if err != nil { + fail(xrpcerr.InvalidRepoError(data.Repo)) + return + } + + // resolve this aturi to extract the repo record + ident, err := x.Resolver.ResolveIdent(r.Context(), repoAt.Authority().String()) + if err != nil || ident.Handle.IsInvalidHandle() { + fail(xrpcerr.GenericError(fmt.Errorf("failed to resolve handle: %w", err))) + return + } + + xrpcc := xrpc.Client{Host: ident.PDSEndpoint()} + resp, err := comatproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) + if err != nil { + fail(xrpcerr.GenericError(err)) + return + } + + repo := resp.Value.Val.(*tangled.Repo) + didPath, err := securejoin.SecureJoin(actorDid.String(), repo.Name) + if err != nil { + fail(xrpcerr.GenericError(err)) + return + } + + if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil { + l.Error("insufficent permissions", "did", actorDid.String()) + writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) + return + } + + path, _ := securejoin.SecureJoin(x.Config.Repo.ScanPath, didPath) + gr, err := git.PlainOpen(path) + if err != nil { + fail(xrpcerr.GenericError(err)) + return + } + + err = gr.DeleteBranch(data.Branch) + if err != nil { + l.Error("deleting branch", "error", err.Error(), "branch", data.Branch) + writeError(w, xrpcerr.GitError(err), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} diff --git a/knotserver/xrpc/xrpc.go b/knotserver/xrpc/xrpc.go index 03ee5301..c110356f 100644 --- a/knotserver/xrpc/xrpc.go +++ b/knotserver/xrpc/xrpc.go @@ -38,6 +38,7 @@ func (x *Xrpc) Router() http.Handler { r.Use(x.ServiceAuth.VerifyServiceAuth) r.Post("/"+tangled.RepoSetDefaultBranchNSID, x.SetDefaultBranch) + r.Post("/"+tangled.RepoDeleteBranchNSID, x.DeleteBranch) r.Post("/"+tangled.RepoCreateNSID, x.CreateRepo) r.Post("/"+tangled.RepoDeleteNSID, x.DeleteRepo) r.Post("/"+tangled.RepoForkStatusNSID, x.ForkStatus) diff --git a/lexicons/repo/deleteBranch.json b/lexicons/repo/deleteBranch.json new file mode 100644 index 00000000..f1687167 --- /dev/null +++ b/lexicons/repo/deleteBranch.json @@ -0,0 +1,30 @@ +{ + "lexicon": 1, + "id": "sh.tangled.repo.deleteBranch", + "defs": { + "main": { + "type": "procedure", + "description": "Delete a branch on this repository", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "repo", + "branch" + ], + "properties": { + "repo": { + "type": "string", + "format": "at-uri" + }, + "branch": { + "type": "string" + } + } + } + } + } + } +} + -- 2.43.0