This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knotserver / xrpc / git_keep_commit.go
5.2 kB 149 lines
1package xrpc 2 3import ( 4 "context" 5 "encoding/hex" 6 "encoding/json" 7 "fmt" 8 "net/http" 9 "os/exec" 10 "strings" 11 12 "github.com/bluesky-social/indigo/atproto/atclient" 13 "github.com/bluesky-social/indigo/atproto/syntax" 14 "tangled.org/core/api/tangled" 15) 16 17func (x *Xrpc) KeepCommit(w http.ResponseWriter, r *http.Request) { 18 var input tangled.GitKeepCommit_Input 19 if err := json.NewDecoder(r.Body).Decode(&input); err != nil { 20 writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "failed to decode json body"}) 21 return 22 } 23 24 if err := gitKeepCommit_Input_Validate(input); err != nil { 25 writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: err.Error()}) 26 return 27 } 28 if syntax.ATURI(input.Record).RecordKey() == "" { 29 writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "record at-uri should have rkey"}) 30 return 31 } 32 33 output, status, apierr := x.keepCommit(r.Context(), input) 34 if apierr != nil { 35 writeJson(w, status, apierr) 36 return 37 } 38 writeJson(w, status, output) 39} 40 41func (x *Xrpc) keepCommit(ctx context.Context, input tangled.GitKeepCommit_Input) (*tangled.GitKeepCommit_Output, int, *atclient.ErrorBody) { 42 repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, input.Repo) 43 if err != nil { 44 return nil, http.StatusNotFound, &atclient.ErrorBody{Name: "RepoNotFound", Message: fmt.Sprintf("unknown repository: %q", input.Repo)} 45 } 46 47 record := syntax.ATURI(input.Record) 48 recordIdent, err := x.Resolver.Directory().Lookup(ctx, record.Authority()) 49 if err != nil { 50 return nil, http.StatusBadRequest, &atclient.ErrorBody{Name: "BadRequest", Message: "Failed to resolve record authority"} 51 } 52 recordDID := recordIdent.DID 53 54 var commitID string 55 56 switch { 57 case input.Source.GitKeepCommit_Commit != nil: 58 source := input.Source.GitKeepCommit_Commit 59 if input.Repo == source.Repo { 60 // no-op. we already have that commit 61 } else { 62 // TODO: target repo should own the source commit 63 return nil, http.StatusBadRequest, &atclient.ErrorBody{Name: "BadRequest", Message: "source repo should match the target repo"} 64 // // fetch commit from source repo 65 // if err := x.fetchCommitFrom(ctx, syntax.DID(source.Repo), source.Oid); err != nil { 66 // return nil, http.StatusInternalServerError, &atclient.ErrorBody{Name: "CommitNotFound", Message: "Failed to fetch commit from source repo"} 67 // } 68 } 69 commitID = source.Oid 70 71 case input.Source.GitKeepCommit_Patches != nil: 72 // TODO: apply patches to target commit and bring tip commit ID 73 return nil, http.StatusBadRequest, &atclient.ErrorBody{Name: "BadRequest", Message: "patches source is not supported"} 74 75 default: 76 return nil, http.StatusBadRequest, &atclient.ErrorBody{Name: "BadRequest", Message: "source should be one of: [commit, patches]"} 77 } 78 79 // create refs/tngl/keep/{did}/{collection}/{rkey}/{oid} 80 refName := EscapeGitRef(fmt.Sprintf("refs/tngl/keep/%s/%s/%s/%s", recordDID.String(), record.Collection(), record.RecordKey(), commitID)) 81 cmd := exec.CommandContext(ctx, "git", "-C", repoPath, "update-ref", refName, commitID) 82 if out, err := cmd.CombinedOutput(); err != nil { 83 x.Logger.Error("failed to keep commit", "err", err, "out", string(out)) 84 return nil, http.StatusInternalServerError, &atclient.ErrorBody{Name: "InternalServerError", Message: "Failed to keep commit"} 85 } 86 87 return &tangled.GitKeepCommit_Output{ 88 Commit: commitID, 89 }, http.StatusOK, nil 90} 91 92// lexgen doesn't give Validate() method... 93func gitKeepCommit_Input_Validate(input tangled.GitKeepCommit_Input) error { 94 if _, err := syntax.ParseDID(input.Repo); err != nil { 95 return fmt.Errorf("repo: invalid repo DID: %w", err) 96 } 97 if _, err := syntax.ParseATURI(input.Record); err != nil { 98 return fmt.Errorf("repo: invalid record at-uri: %w", err) 99 } 100 switch { 101 case input.Source.GitKeepCommit_Commit != nil: 102 if _, err := syntax.ParseDID(input.Source.GitKeepCommit_Commit.Repo); err != nil { 103 return fmt.Errorf("source: commit: invalid repo DID: %w", err) 104 } 105 if ok := IsHash(input.Source.GitKeepCommit_Commit.Oid); !ok { 106 return fmt.Errorf("source: commit: invalid commit OID: %q", input.Source.GitKeepCommit_Commit.Oid) 107 } 108 case input.Source.GitKeepCommit_Patches != nil: 109 return fmt.Errorf("source: patches: patch is not supported yet") 110 // for i, patch := range input.Source.GitKeepCommit_Patches.Patches { 111 // if err := validatePatch(patch); err != nil { 112 // return fmt.Errorf("source: patches: invalid patches at [%d]: %w", i, err) 113 // } 114 // } 115 default: 116 return fmt.Errorf("source should be one of: [commit, patches]") 117 } 118 return nil 119} 120 121func EscapeGitRef(s string) string { 122 var b strings.Builder 123 b.Grow(len(s) * 4 / 3) 124 for i := 0; i < len(s); i++ { 125 c := s[i] 126 if (c >= 'a' && c <= 'z') || 127 (c >= 'A' && c <= 'Z') || 128 (c >= '0' && c <= '9') || 129 c == '/' || c == '-' || c == '_' || c == '.' { 130 b.WriteByte(c) 131 continue 132 } 133 b.WriteByte('%') 134 b.WriteByte("0123456789ABCDEF"[c>>4]) 135 b.WriteByte("0123456789ABCDEF"[c&15]) 136 } 137 return strings.ToLower(b.String()) 138} 139 140func IsHash(s string) bool { 141 switch len(s) { 142 case 40: // SHA1 143 case 64: // SHA2 144 default: 145 return false 146 } 147 _, err := hex.DecodeString(s) 148 return err == nil 149}