This repository has no description
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}