This repository has no description
0

Configure Feed

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

legacy PR #1

Open opened by alice.pds.demo.boltless.dev targeting master from sl/ref-based-pr

two rounds, each a gzipped format-patch blob

Labels

None yet.

Participants 1
AT URI
at://did:plc:3e3arfbizb2pj4gi3a26iqom/sh.tangled.repo.pull/3lzg7tkxvq222
+7071 -80
Diff #0
+92
api/tangled/gitkeepCommit.go
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + package tangled 4 + 5 + // schema: sh.tangled.git.keepCommit 6 + 7 + import ( 8 + "context" 9 + "encoding/json" 10 + "fmt" 11 + 12 + "github.com/bluesky-social/indigo/lex/util" 13 + ) 14 + 15 + const ( 16 + GitKeepCommitNSID = "sh.tangled.git.keepCommit" 17 + ) 18 + 19 + // GitKeepCommit_Commit is a "commit" in the sh.tangled.git.keepCommit schema. 20 + // 21 + // RECORDTYPE: GitKeepCommit_Commit 22 + type GitKeepCommit_Commit struct { 23 + LexiconTypeID string `json:"$type,const=sh.tangled.git.keepCommit#commit" cborgen:"$type,const=sh.tangled.git.keepCommit#commit"` 24 + Oid string `json:"oid" cborgen:"oid"` 25 + Repo string `json:"repo" cborgen:"repo"` 26 + } 27 + 28 + // GitKeepCommit_Input is the input argument to a sh.tangled.git.keepCommit call. 29 + type GitKeepCommit_Input struct { 30 + // record: at-uri of target record. rkey is required 31 + Record string `json:"record" cborgen:"record"` 32 + Repo string `json:"repo" cborgen:"repo"` 33 + Source *GitKeepCommit_Input_Source `json:"source" cborgen:"source"` 34 + } 35 + 36 + type GitKeepCommit_Input_Source struct { 37 + GitKeepCommit_Commit *GitKeepCommit_Commit 38 + GitKeepCommit_Patches *GitKeepCommit_Patches 39 + } 40 + 41 + func (t *GitKeepCommit_Input_Source) MarshalJSON() ([]byte, error) { 42 + if t.GitKeepCommit_Commit != nil { 43 + t.GitKeepCommit_Commit.LexiconTypeID = "sh.tangled.git.keepCommit#commit" 44 + return json.Marshal(t.GitKeepCommit_Commit) 45 + } 46 + if t.GitKeepCommit_Patches != nil { 47 + t.GitKeepCommit_Patches.LexiconTypeID = "sh.tangled.git.keepCommit#patches" 48 + return json.Marshal(t.GitKeepCommit_Patches) 49 + } 50 + return nil, fmt.Errorf("cannot marshal empty enum") 51 + } 52 + func (t *GitKeepCommit_Input_Source) UnmarshalJSON(b []byte) error { 53 + typ, err := util.TypeExtract(b) 54 + if err != nil { 55 + return err 56 + } 57 + 58 + switch typ { 59 + case "sh.tangled.git.keepCommit#commit": 60 + t.GitKeepCommit_Commit = new(GitKeepCommit_Commit) 61 + return json.Unmarshal(b, t.GitKeepCommit_Commit) 62 + case "sh.tangled.git.keepCommit#patches": 63 + t.GitKeepCommit_Patches = new(GitKeepCommit_Patches) 64 + return json.Unmarshal(b, t.GitKeepCommit_Patches) 65 + 66 + default: 67 + return nil 68 + } 69 + } 70 + 71 + // GitKeepCommit_Output is the output of a sh.tangled.git.keepCommit call. 72 + type GitKeepCommit_Output struct { 73 + Commit string `json:"commit" cborgen:"commit"` 74 + } 75 + 76 + // GitKeepCommit_Patches is a "patches" in the sh.tangled.git.keepCommit schema. 77 + // 78 + // RECORDTYPE: GitKeepCommit_Patches 79 + type GitKeepCommit_Patches struct { 80 + LexiconTypeID string `json:"$type,const=sh.tangled.git.keepCommit#patches" cborgen:"$type,const=sh.tangled.git.keepCommit#patches"` 81 + Patches []string `json:"patches,omitempty" cborgen:"patches,omitempty"` 82 + } 83 + 84 + // GitKeepCommit calls the XRPC method "sh.tangled.git.keepCommit". 85 + func GitKeepCommit(ctx context.Context, c util.LexClient, input *GitKeepCommit_Input) (*GitKeepCommit_Output, error) { 86 + var out GitKeepCommit_Output 87 + if err := c.LexDo(ctx, util.Procedure, "application/json", "sh.tangled.git.keepCommit", nil, input, &out); err != nil { 88 + return nil, err 89 + } 90 + 91 + return &out, nil 92 + }
+89
lexicons/git/keepCommit.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "sh.tangled.git.keepCommit", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Keep commit for an atproto record.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["repo", "source", "record"], 13 + "properties": { 14 + "repo": { 15 + "type": "string", 16 + "format": "did" 17 + }, 18 + "source": { 19 + "type": "union", 20 + "refs": ["#commit", "#patches"] 21 + }, 22 + "record": { 23 + "type": "string", 24 + "format": "at-uri", 25 + "description": "at-uri of target record. rkey is required" 26 + } 27 + } 28 + } 29 + }, 30 + "output": { 31 + "encoding": "application/json", 32 + "schema": { 33 + "type": "object", 34 + "required": ["commit"], 35 + "properties": { 36 + "commit": { 37 + "type": "string", 38 + "minLength": 40, 39 + "maxLength": 128 40 + } 41 + } 42 + } 43 + }, 44 + "errors": [ 45 + { 46 + "name": "InvalidRequest", 47 + "description": "Invalid request parameters" 48 + }, 49 + { 50 + "name": "RepoNotFound", 51 + "description": "Repo is not registered on this knot" 52 + }, 53 + { 54 + "name": "CommitNotFound", 55 + "description": "Commit is not found from source Repo" 56 + }, 57 + { 58 + "name": "PatchApplyFailed", 59 + "description": "Failed to apply patches" 60 + } 61 + ] 62 + }, 63 + "commit": { 64 + "type": "object", 65 + "required": ["repo", "oid"], 66 + "properties": { 67 + "repo": { 68 + "type": "string", 69 + "format": "did" 70 + }, 71 + "oid": { 72 + "type": "string", 73 + "minLength": 40, 74 + "maxLength": 128 75 + } 76 + } 77 + }, 78 + "patches": { 79 + "type": "object", 80 + "required": ["patch"], 81 + "properties": { 82 + "patches": { 83 + "type": "array", 84 + "items": { "type": "string" } 85 + } 86 + } 87 + } 88 + } 89 + }
+1
appview/oauth/scopes.go
··· 33 33 "rpc:sh.tangled.knot.removeMember?aud=*", 34 34 "rpc:sh.tangled.ci.triggerPipeline?aud=*", 35 35 "rpc:sh.tangled.ci.cancelPipeline?aud=*", 36 + "rpc:sh.tangled.git.keepCommit?aud=*", 36 37 "rpc:sh.tangled.repo.addCollaborator?aud=*", 37 38 "rpc:sh.tangled.repo.addSecret?aud=*", 38 39 "rpc:sh.tangled.repo.create?aud=*",
+1
consts/consts.go
··· 11 11 type Capability string 12 12 13 13 const CapKnotACL Capability = "knot-acl" 14 + const CapKeepCommit Capability = "knot-keepcommit"
+149
knotserver/xrpc/git_keep_commit.go
··· 1 + package xrpc 2 + 3 + import ( 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 + 17 + func (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 + 41 + func (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... 93 + func 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 + 121 + func 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 + 140 + func 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 + }
+17
knotserver/xrpc/git_keep_commit_test.go
··· 1 + package xrpc 2 + 3 + import ( 4 + "fmt" 5 + "testing" 6 + 7 + "github.com/alecthomas/assert/v2" 8 + "github.com/bluesky-social/indigo/atproto/syntax" 9 + ) 10 + 11 + func TestEscapeGitRef(t *testing.T) { 12 + recordDID := syntax.DID("did:plc:alice") 13 + record := syntax.ATURI("at://did:plc:alice/sh.tangled.repo.pull/something") 14 + commitID := "what" 15 + refName := EscapeGitRef(fmt.Sprintf("refs/tngl/keep/%s/%s/%s/%s", recordDID.String(), record.Collection(), record.RecordKey(), commitID)) 16 + assert.Equal(t, "refs/tngl/keep/did%3aplc%3aalice/sh.tangled.repo.pull/something/what", refName) 17 + }
+4 -1
knotserver/xrpc/version.go
··· 12 12 // version is set during build time. 13 13 var version string 14 14 15 - var knotCapabilities = []string{string(consts.CapKnotACL)} 15 + var knotCapabilities = []string{ 16 + string(consts.CapKnotACL), 17 + string(consts.CapKeepCommit), 18 + } 16 19 17 20 func (x *Xrpc) Version(w http.ResponseWriter, r *http.Request) { 18 21 if version == "" {
+17 -5
knotserver/xrpc/xrpc.go
··· 49 49 r.Group(func(r chi.Router) { 50 50 r.Use(x.ServiceAuth.VerifyServiceAuth) 51 51 52 + r.Post("/"+tangled.GitKeepCommitNSID, x.KeepCommit) 53 + r.Post("/"+tangled.GitMergeCommitNSID, x.MergeCommit) 52 54 r.Post("/"+tangled.RepoSetDefaultBranchNSID, x.SetDefaultBranch) 53 55 r.Post("/"+tangled.RepoDeleteBranchNSID, x.DeleteBranch) 54 56 r.Post("/"+tangled.RepoCreateNSID, x.CreateRepo) ··· 66 68 67 69 68 70 71 + // - we can calculate on PR submit/resubmit/gitRefUpdate etc. 72 + // - use ETags on clients to keep requests to a minimum 73 + r.Post("/"+tangled.RepoMergeCheckNSID, x.MergeCheck) 74 + r.Post("/"+tangled.GitMergeCheckNSID, x.GitMergeCheck) 69 75 76 + // repo query endpoints (no auth required) 77 + r.Get("/"+tangled.RepoTreeNSID, x.RepoTree) 70 78 71 79 72 80 ··· 187 195 188 196 189 197 190 - 191 - 192 - 193 - 194 - 195 198 w.Header().Set("Content-Type", "application/json") 196 199 w.Write(lw.buf.Bytes()) 200 + } 201 + 202 + func writeJson(w http.ResponseWriter, status int, response any) error { 203 + w.Header().Set("Content-Type", "application/json") 204 + w.WriteHeader(status) 205 + if err := json.NewEncoder(w).Encode(response); err != nil { 206 + return err 207 + } 208 + return nil 197 209 }
+39
api/tangled/tempgetMergeBase.go
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + package tangled 4 + 5 + // schema: sh.tangled.git.temp.getMergeBase 6 + 7 + import ( 8 + "context" 9 + 10 + "github.com/bluesky-social/indigo/lex/util" 11 + ) 12 + 13 + const ( 14 + GitTempGetMergeBaseNSID = "sh.tangled.git.temp.getMergeBase" 15 + ) 16 + 17 + // GitTempGetMergeBase_Output is the output of a sh.tangled.git.temp.getMergeBase call. 18 + type GitTempGetMergeBase_Output struct { 19 + Commit string `json:"commit" cborgen:"commit"` 20 + } 21 + 22 + // GitTempGetMergeBase calls the XRPC method "sh.tangled.git.temp.getMergeBase". 23 + // 24 + // base: Ref used to find the common base. 25 + // head: Ref to list history for. 26 + // repo: DID of the repository 27 + func GitTempGetMergeBase(ctx context.Context, c util.LexClient, base string, head string, repo string) (*GitTempGetMergeBase_Output, error) { 28 + var out GitTempGetMergeBase_Output 29 + 30 + params := map[string]interface{}{} 31 + params["base"] = base 32 + params["head"] = head 33 + params["repo"] = repo 34 + if err := c.LexDo(ctx, util.Query, "", "sh.tangled.git.temp.getMergeBase", params, nil, &out); err != nil { 35 + return nil, err 36 + } 37 + 38 + return &out, nil 39 + }
+54
lexicons/git/temp/getMergeBase.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "sh.tangled.git.temp.getMergeBase", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "parameters": { 8 + "type": "params", 9 + "required": ["repo", "base", "head"], 10 + "properties": { 11 + "repo": { 12 + "type": "string", 13 + "format": "did", 14 + "description": "DID of the repository" 15 + }, 16 + "base": { 17 + "type": "string", 18 + "description": "Ref used to find the common base." 19 + }, 20 + "head": { 21 + "type": "string", 22 + "description": "Ref to list history for." 23 + } 24 + } 25 + }, 26 + "output": { 27 + "encoding": "application/json", 28 + "schema": { 29 + "type": "object", 30 + "required": ["commit"], 31 + "properties": { 32 + "commit": { 33 + "type": "string" 34 + } 35 + } 36 + } 37 + }, 38 + "errors": [ 39 + { 40 + "name": "RepoNotFound", 41 + "description": "Repository not found or access denied" 42 + }, 43 + { 44 + "name": "RefNotFound", 45 + "description": "Git reference not found" 46 + }, 47 + { 48 + "name": "InvalidRequest", 49 + "description": "Invalid request parameters" 50 + } 51 + ] 52 + } 53 + } 54 + }
+57
knotmirror/xrpc/git_get_merge_base.go
··· 1 + package xrpc 2 + 3 + import ( 4 + "fmt" 5 + "net/http" 6 + "os/exec" 7 + "strings" 8 + 9 + "github.com/bluesky-social/indigo/atproto/atclient" 10 + "github.com/bluesky-social/indigo/atproto/syntax" 11 + "tangled.org/core/api/tangled" 12 + ) 13 + 14 + func (x *Xrpc) GetMergeBase(w http.ResponseWriter, r *http.Request) { 15 + var ( 16 + repoQuery = r.URL.Query().Get("repo") 17 + base = r.URL.Query().Get("base") 18 + head = r.URL.Query().Get("head") 19 + ) 20 + 21 + repo, err := syntax.ParseDID(repoQuery) 22 + if err != nil { 23 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) 24 + return 25 + } 26 + 27 + if base == "" { 28 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "missing base parameter"}) 29 + return 30 + } 31 + if head == "" { 32 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "missing head parameter"}) 33 + return 34 + } 35 + 36 + l := x.logger.With("method", "git.getMergeBase", "repo", repo, "base", base, "head", head) 37 + l.Debug("request") 38 + 39 + ctx := r.Context() 40 + 41 + repoPath, err := x.makeRepoPath(ctx, repo) 42 + if err != nil { 43 + writeJson(w, http.StatusNotFound, atclient.ErrorBody{Name: "RepoNotFound", Message: fmt.Sprintf("unknown repository: %q", repo)}) 44 + return 45 + } 46 + 47 + cmd := exec.Command("git", "-C", repoPath, "merge-base", head, base) 48 + out, err := cmd.Output() 49 + if err != nil { 50 + writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalError", Message: "failed to compute merge-base"}) 51 + return 52 + } 53 + 54 + writeJson(w, http.StatusOK, tangled.GitTempGetMergeBase_Output{ 55 + Commit: strings.TrimSpace(string(out)), 56 + }) 57 + }
+3 -1
knotmirror/xrpc/xrpc.go
··· 63 63 64 64 65 65 66 - 66 + // r.Get("/"+tangled.GitTempGetCommitNSID, x.GetCommit) // todo 67 67 // r.Get("/"+tangled.GitTempGetDiffNSID, x.GetDiff) // todo 68 68 r.Get("/"+tangled.GitTempGetEntryNSID, x.GetEntry) // todo 69 + r.Get("/"+tangled.GitTempFormatPatchNSID, x.FormatPatch) 69 70 // r.Get("/"+tangled.GitTempGetHeadNSID, x.GetHead) // todo 71 + r.Get("/"+tangled.GitTempGetMergeBaseNSID, x.GetMergeBase) 70 72 r.Get("/"+tangled.GitTempGetTagNSID, x.GetTag) // using types.Response 71 73 r.Get("/"+tangled.GitTempGetTreeNSID, x.GetTree) 72 74 r.Get("/"+tangled.GitTempListBranchesNSID, x.ListBranches) // wip, unknown output
+17
api/tangled/gitdefs.go
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + package tangled 4 + 5 + // schema: sh.tangled.git.defs 6 + 7 + const () 8 + 9 + // GitDefs_Signature is a "signature" in the sh.tangled.git.defs schema. 10 + type GitDefs_Signature struct { 11 + // email: Person email 12 + Email string `json:"email" cborgen:"email"` 13 + // name: Person name 14 + Name string `json:"name" cborgen:"name"` 15 + // when: Timestamp of the signature 16 + When string `json:"when" cborgen:"when"` 17 + }
+59
api/tangled/gitmergeCheck.go
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + package tangled 4 + 5 + // schema: sh.tangled.git.mergeCheck 6 + 7 + import ( 8 + "context" 9 + 10 + "github.com/bluesky-social/indigo/lex/util" 11 + ) 12 + 13 + const ( 14 + GitMergeCheckNSID = "sh.tangled.git.mergeCheck" 15 + ) 16 + 17 + // GitMergeCheck_ConflictInfo is a "conflictInfo" in the sh.tangled.git.mergeCheck schema. 18 + type GitMergeCheck_ConflictInfo struct { 19 + // filename: Name of the conflicted file 20 + Filename string `json:"filename" cborgen:"filename"` 21 + // reason: Reason for the conflict 22 + Reason string `json:"reason" cborgen:"reason"` 23 + } 24 + 25 + // GitMergeCheck_Input is the input argument to a sh.tangled.git.mergeCheck call. 26 + type GitMergeCheck_Input struct { 27 + // branch: Target branch to merge into 28 + Branch string `json:"branch" cborgen:"branch"` 29 + // repo: DID of Git repository 30 + Repo string `json:"repo" cborgen:"repo"` 31 + Source *GitMergeCheck_Input_Source `json:"source" cborgen:"source"` 32 + } 33 + 34 + type GitMergeCheck_Input_Source struct { 35 + // commit: Head commit ID of PR to merge 36 + Commit string `json:"commit" cborgen:"commit"` 37 + // repo: DID of source git repository 38 + Repo string `json:"repo" cborgen:"repo"` 39 + } 40 + 41 + // GitMergeCheck_Output is the output of a sh.tangled.git.mergeCheck call. 42 + type GitMergeCheck_Output struct { 43 + // conflicts: List of files with merge conflicts 44 + Conflicts []*GitMergeCheck_ConflictInfo `json:"conflicts,omitempty" cborgen:"conflicts,omitempty"` 45 + // isConflicted: Whether the merge has conflicts 46 + IsConflicted bool `json:"isConflicted" cborgen:"isConflicted"` 47 + // message: Additional message about the merge check 48 + Message *string `json:"message,omitempty" cborgen:"message,omitempty"` 49 + } 50 + 51 + // GitMergeCheck calls the XRPC method "sh.tangled.git.mergeCheck". 52 + func GitMergeCheck(ctx context.Context, c util.LexClient, input *GitMergeCheck_Input) (*GitMergeCheck_Output, error) { 53 + var out GitMergeCheck_Output 54 + if err := c.LexDo(ctx, util.Procedure, "application/json", "sh.tangled.git.mergeCheck", nil, input, &out); err != nil { 55 + return nil, err 56 + } 57 + 58 + return &out, nil 59 + }
+53
api/tangled/gitmergeCommit.go
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + package tangled 4 + 5 + // schema: sh.tangled.git.mergeCommit 6 + 7 + import ( 8 + "context" 9 + 10 + "github.com/bluesky-social/indigo/lex/util" 11 + ) 12 + 13 + const ( 14 + GitMergeCommitNSID = "sh.tangled.git.mergeCommit" 15 + ) 16 + 17 + // GitMergeCommit_Input is the input argument to a sh.tangled.git.mergeCommit call. 18 + type GitMergeCommit_Input struct { 19 + // mergeCommit: Merge commit content. Used when merge commit is created. 20 + MergeCommit *GitMergeCommit_Input_MergeCommit `json:"mergeCommit,omitempty" cborgen:"mergeCommit,omitempty"` 21 + Source *GitMergeCommit_Input_Source `json:"source" cborgen:"source"` 22 + Style string `json:"style" cborgen:"style"` 23 + Target *GitMergeCommit_Input_Target `json:"target" cborgen:"target"` 24 + } 25 + 26 + // Merge commit content. Used when merge commit is created. 27 + type GitMergeCommit_Input_MergeCommit struct { 28 + Author *GitDefs_Signature `json:"author,omitempty" cborgen:"author,omitempty"` 29 + Message *string `json:"message,omitempty" cborgen:"message,omitempty"` 30 + } 31 + 32 + type GitMergeCommit_Input_Source struct { 33 + // commit: Head commit ID to merge 34 + Commit string `json:"commit" cborgen:"commit"` 35 + // repo: DID of source git repository 36 + Repo string `json:"repo" cborgen:"repo"` 37 + } 38 + 39 + type GitMergeCommit_Input_Target struct { 40 + // branch: Target branch to merge into 41 + Branch string `json:"branch" cborgen:"branch"` 42 + // repo: DID of target git repository 43 + Repo string `json:"repo" cborgen:"repo"` 44 + } 45 + 46 + // GitMergeCommit calls the XRPC method "sh.tangled.git.mergeCommit". 47 + func GitMergeCommit(ctx context.Context, c util.LexClient, input *GitMergeCommit_Input) error { 48 + if err := c.LexDo(ctx, util.Procedure, "application/json", "sh.tangled.git.mergeCommit", nil, input, nil); err != nil { 49 + return err 50 + } 51 + 52 + return nil 53 + }
+25
lexicons/git/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "sh.tangled.git.defs", 4 + "defs": { 5 + "signature": { 6 + "type": "object", 7 + "required": ["name", "email", "when"], 8 + "properties": { 9 + "name": { 10 + "type": "string", 11 + "description": "Person name" 12 + }, 13 + "email": { 14 + "type": "string", 15 + "description": "Person email" 16 + }, 17 + "when": { 18 + "type": "string", 19 + "format": "datetime", 20 + "description": "Timestamp of the signature" 21 + } 22 + } 23 + } 24 + } 25 + }
+96
lexicons/git/mergeCheck.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "sh.tangled.git.mergeCheck", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Check if a merge is possible between two branches. Performs 3-way merge check", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["repo", "branch", "source"], 13 + "properties": { 14 + "repo": { 15 + "type": "string", 16 + "format": "did", 17 + "description": "DID of Git repository" 18 + }, 19 + "branch": { 20 + "type": "string", 21 + "description": "Target branch to merge into" 22 + }, 23 + "source": { 24 + "type": "object", 25 + "required": ["repo", "commit"], 26 + "properties": { 27 + "repo": { 28 + "type": "string", 29 + "format": "did", 30 + "description": "DID of source git repository" 31 + }, 32 + "commit": { 33 + "type": "string", 34 + "description": "Head commit ID of PR to merge" 35 + } 36 + } 37 + } 38 + } 39 + } 40 + }, 41 + "output": { 42 + "encoding": "application/json", 43 + "schema": { 44 + "type": "object", 45 + "required": ["isConflicted"], 46 + "properties": { 47 + "isConflicted": { 48 + "type": "boolean", 49 + "description": "Whether the merge has conflicts" 50 + }, 51 + "conflicts": { 52 + "type": "array", 53 + "description": "List of files with merge conflicts", 54 + "items": { 55 + "type": "ref", 56 + "ref": "#conflictInfo" 57 + } 58 + }, 59 + "message": { 60 + "type": "string", 61 + "description": "Additional message about the merge check" 62 + } 63 + } 64 + } 65 + }, 66 + "errors": [ 67 + { 68 + "name": "InvalidRequest", 69 + "description": "Invalid request parameters" 70 + }, 71 + { 72 + "name": "RepoNotFound", 73 + "description": "Repo is not registered on this knot" 74 + }, 75 + { 76 + "name": "CommitNotFound", 77 + "description": "Commit is not found from source Repo" 78 + } 79 + ] 80 + }, 81 + "conflictInfo": { 82 + "type": "object", 83 + "required": ["filename", "reason"], 84 + "properties": { 85 + "filename": { 86 + "type": "string", 87 + "description": "Name of the conflicted file" 88 + }, 89 + "reason": { 90 + "type": "string", 91 + "description": "Reason for the conflict" 92 + } 93 + } 94 + } 95 + } 96 + }
+68
lexicons/git/mergeCommit.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "sh.tangled.git.mergeCommit", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Merge a commit into repository's branch", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["target", "source", "style"], 13 + "properties": { 14 + "target": { 15 + "type": "object", 16 + "required": ["repo", "branch"], 17 + "properties": { 18 + "repo": { 19 + "type": "string", 20 + "format": "did", 21 + "description": "DID of target git repository" 22 + }, 23 + "branch": { 24 + "type": "string", 25 + "description": "Target branch to merge into" 26 + } 27 + } 28 + }, 29 + "source": { 30 + "type": "object", 31 + "required": ["repo", "commit"], 32 + "properties": { 33 + "repo": { 34 + "type": "string", 35 + "format": "did", 36 + "description": "DID of source git repository" 37 + }, 38 + "commit": { 39 + "type": "string", 40 + "description": "Head commit ID to merge" 41 + } 42 + } 43 + }, 44 + "style": { 45 + "type": "string", 46 + "enum": [ 47 + "rebase", 48 + "merge", 49 + "rebase-merge", 50 + "squash-rebase", 51 + "squash-rebase-merge", 52 + "fast-forward-only" 53 + ] 54 + }, 55 + "mergeCommit": { 56 + "type": "object", 57 + "description": "Merge commit content. Used when merge commit is created.", 58 + "properties": { 59 + "author": { "type": "ref", "ref": "sh.tangled.git.defs#signature" }, 60 + "message": { "type": "string", "maxLength": 1000000 } 61 + } 62 + } 63 + } 64 + } 65 + } 66 + } 67 + } 68 + }
+1 -1
lexicons/repo/merge.json
··· 4 4 "defs": { 5 5 "main": { 6 6 "type": "procedure", 7 - "description": "Merge a patch into a repository branch", 7 + "description": "DEPRECATED: Merge a patch into a repository branch", 8 8 "input": { 9 9 "encoding": "application/json", 10 10 "schema": {
+225
knotserver/xrpc/git_merge_check.go
··· 1 + package xrpc 2 + 3 + import ( 4 + "context" 5 + "crypto/sha256" 6 + "encoding/json" 7 + "fmt" 8 + "net/http" 9 + "net/url" 10 + "os" 11 + "strings" 12 + 13 + "github.com/bluesky-social/indigo/atproto/atclient" 14 + "github.com/bluesky-social/indigo/atproto/syntax" 15 + "github.com/dgraph-io/ristretto" 16 + "tangled.org/core/api/tangled" 17 + ) 18 + 19 + type MergeInput struct { 20 + TargetRepo syntax.DID 21 + TargetBranch string 22 + SourceRepo syntax.DID 23 + SourceCommit string 24 + } 25 + 26 + type MergeCheckCache struct { 27 + cache *ristretto.Cache 28 + } 29 + 30 + func (m *MergeCheckCache) cacheKey(input MergeInput) string { 31 + raw := strings.Join([]string{ 32 + input.TargetRepo.String(), 33 + input.TargetBranch, 34 + input.SourceRepo.String(), 35 + input.SourceCommit, 36 + }, "\x00") 37 + sum := sha256.Sum256([]byte(raw)) 38 + return fmt.Sprintf("%x", sum) 39 + } 40 + 41 + func (m *MergeCheckCache) cacheVal(out *tangled.GitMergeCheck_Output) any { 42 + return *out 43 + } 44 + 45 + func (m *MergeCheckCache) Set(input MergeInput, mergeCheck *tangled.GitMergeCheck_Output) { 46 + key := m.cacheKey(input) 47 + val := m.cacheVal(mergeCheck) 48 + m.cache.Set(key, val, 0) 49 + } 50 + 51 + func (m *MergeCheckCache) Get(input MergeInput) (tangled.GitMergeCheck_Output, bool) { 52 + key := m.cacheKey(input) 53 + if val, ok := m.cache.Get(key); ok { 54 + if out, ok := val.(tangled.GitMergeCheck_Output); ok { 55 + // cache hit 56 + return out, true 57 + } 58 + } 59 + 60 + // cache miss 61 + return tangled.GitMergeCheck_Output{}, false 62 + } 63 + 64 + var mergeCheckCache MergeCheckCache 65 + 66 + func init() { 67 + cache, _ := ristretto.NewCache(&ristretto.Config{ 68 + NumCounters: 1e7, 69 + MaxCost: 1 << 30, 70 + BufferItems: 64, 71 + TtlTickerDurationInSec: 60 * 60 * 24 * 2, // 2 days 72 + }) 73 + mergeCheckCache = MergeCheckCache{cache} 74 + } 75 + 76 + func (x *Xrpc) GitMergeCheck(w http.ResponseWriter, r *http.Request) { 77 + var input tangled.GitMergeCheck_Input 78 + if err := json.NewDecoder(r.Body).Decode(&input); err != nil { 79 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "failed to decode json body"}) 80 + return 81 + } 82 + l := x.Logger.With("handler", "MergeCheck2", "input", input) 83 + l.Debug("request") 84 + 85 + if err := gitMergeCheck_Input_Validate(input); err != nil { 86 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "InvalidRequest", Message: err.Error()}) 87 + return 88 + } 89 + 90 + mergeInput := MergeInput{ 91 + TargetRepo: syntax.DID(input.Repo), 92 + TargetBranch: input.Branch, 93 + SourceRepo: syntax.DID(input.Source.Repo), 94 + SourceCommit: input.Source.Commit, 95 + } 96 + 97 + // check cache 98 + if cached, ok := mergeCheckCache.Get(mergeInput); ok { 99 + l.Debug("cache hit") 100 + writeJson(w, http.StatusOK, cached) 101 + return 102 + } 103 + 104 + output, status, apierr := x.mergeCheck(r.Context(), input) 105 + if apierr != nil { 106 + l.Error("failed", "kind", apierr.Name, "error", apierr.Message) 107 + writeJson(w, status, apierr) 108 + return 109 + } 110 + 111 + // update cache 112 + mergeCheckCache.Set(mergeInput, &output) 113 + 114 + writeJson(w, status, output) 115 + } 116 + 117 + func (x *Xrpc) mergeCheck(ctx context.Context, input tangled.GitMergeCheck_Input) (tangled.GitMergeCheck_Output, int, *atclient.ErrorBody) { 118 + l := x.Logger.With("handler", "mergeCheck") 119 + 120 + fail := func(status int, name, clientMsg string, detail ...any) (tangled.GitMergeCheck_Output, int, *atclient.ErrorBody) { 121 + l.Error(clientMsg, append([]any{"name", name}, detail...)...) 122 + return tangled.GitMergeCheck_Output{}, status, &atclient.ErrorBody{Name: name, Message: clientMsg} 123 + } 124 + 125 + baseRepoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, input.Repo) 126 + if err != nil { 127 + return fail(http.StatusNotFound, "RepoNotFound", "unknown repository", "repo", input.Repo, "err", err) 128 + } 129 + sourceRepoDid := syntax.DID(input.Source.Repo) 130 + 131 + var sourceRepoUrl string 132 + var sourceRepoPath string // non-empty only when the source is local to this knot 133 + if p, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, input.Source.Repo); err == nil { 134 + sourceRepoPath = p 135 + sourceRepoUrl = "file://" + p 136 + } else { 137 + ident, err := x.Resolver.Directory().LookupDID(ctx, sourceRepoDid) 138 + if err != nil { 139 + return fail(http.StatusNotFound, "RepoNotFound", "unknown repository", "source", sourceRepoDid, "err", err) 140 + } 141 + sourceKnot := ident.GetServiceEndpoint("atproto_pds") 142 + u, err := url.Parse(sourceKnot) 143 + if err != nil { 144 + return fail(http.StatusNotFound, "RepoNotFound", "unknown repository", "source", sourceRepoDid, "knot", sourceKnot, "err", err) 145 + } 146 + sourceRepoUrl = u.JoinPath(sourceRepoDid.String()).String() 147 + } 148 + 149 + env := append(os.Environ(), "GIT_TERMINAL_PROMPT=0") 150 + 151 + // 1. create temp repo with git alternate to the base repo's objects. 152 + tmpRepoPath, cleanup, err := createTemporaryRepoForMerge(ctx, x.Sandbox, baseRepoPath, input.Branch) 153 + if err != nil { 154 + return fail(http.StatusInternalServerError, "InternalError", "failed to prepare merge check", "err", err) 155 + } 156 + defer cleanup() 157 + 158 + runGit := func(args ...string) ([]byte, []byte, error) { 159 + args = append([]string{"-C", tmpRepoPath}, args...) 160 + return gitWithSandbox(ctx, x.Sandbox, env, []string{tmpRepoPath}, args...) 161 + } 162 + 163 + // 2. fetch source commit and pin it to a "tracking" branch. 164 + fetchPaths := []string{tmpRepoPath} 165 + if sourceRepoPath != "" { 166 + fetchPaths = append(fetchPaths, sourceRepoPath) 167 + } 168 + if _, stderr, err := gitWithSandbox(ctx, x.Sandbox, env, fetchPaths, "-C", tmpRepoPath, "fetch", sourceRepoUrl, input.Source.Commit); err != nil { 169 + return fail(http.StatusNotFound, "CommitNotFound", "source commit unavailable", "commit", input.Source.Commit, "err", err, "stderr", strings.TrimSpace(string(stderr))) 170 + } 171 + if _, stderr, err := runGit("branch", "tracking", input.Source.Commit); err != nil { 172 + return fail(http.StatusNotFound, "CommitNotFound", "source commit unavailable", "commit", input.Source.Commit, "err", err, "stderr", strings.TrimSpace(string(stderr))) 173 + } 174 + 175 + // 3. populate the working tree on the base branch. 176 + if _, stderr, err := runGit("checkout", "-f", "base"); err != nil { 177 + return fail(http.StatusInternalServerError, "InternalError", "failed to perform merge check", "step", "checkout base", "err", err, "stderr", strings.TrimSpace(string(stderr))) 178 + } 179 + 180 + // 4. attempt a 3-way merge without committing. 181 + if _, stderr, err := runGit("merge", "--no-commit", "--no-ff", "tracking"); err != nil { 182 + lsOut, _, _ := runGit("ls-files", "--unmerged") 183 + files := parseUnmergedFiles(lsOut) 184 + if len(files) == 0 { 185 + return fail(http.StatusInternalServerError, "InternalError", "failed to perform merge check", "step", "merge", "err", err, "stderr", strings.TrimSpace(string(stderr))) 186 + } 187 + 188 + conflicts := make([]*tangled.GitMergeCheck_ConflictInfo, 0, len(files)) 189 + for _, f := range files { 190 + conflicts = append(conflicts, &tangled.GitMergeCheck_ConflictInfo{ 191 + Filename: f, 192 + Reason: "merge conflict", 193 + }) 194 + } 195 + msg := strings.TrimSpace(string(stderr)) 196 + l.Debug("merge check found conflicts", "files", files) 197 + return tangled.GitMergeCheck_Output{ 198 + IsConflicted: true, 199 + Conflicts: conflicts, 200 + Message: &msg, 201 + }, http.StatusOK, nil 202 + } 203 + 204 + return tangled.GitMergeCheck_Output{IsConflicted: false}, http.StatusOK, nil 205 + } 206 + 207 + // lexgen doesn't give Validate() method... 208 + func gitMergeCheck_Input_Validate(input tangled.GitMergeCheck_Input) error { 209 + if _, err := syntax.ParseDID(input.Repo); err != nil { 210 + return fmt.Errorf("repo: invalid DID: %w", err) 211 + } 212 + if input.Branch == "" { 213 + return fmt.Errorf("branch: required") 214 + } 215 + if input.Source == nil { 216 + return fmt.Errorf("source: required") 217 + } 218 + if _, err := syntax.ParseDID(input.Source.Repo); err != nil { 219 + return fmt.Errorf("source.repo: invalid DID: %w", err) 220 + } 221 + if input.Source.Commit == "" { 222 + return fmt.Errorf("source.commit: required") 223 + } 224 + return nil 225 + }
+374
knotserver/xrpc/git_merge_commit.go
··· 1 + package xrpc 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "encoding/json" 7 + "fmt" 8 + "log/slog" 9 + "net/http" 10 + "net/url" 11 + "os" 12 + "os/exec" 13 + "path/filepath" 14 + "strings" 15 + 16 + "github.com/bluesky-social/indigo/atproto/atclient" 17 + "github.com/bluesky-social/indigo/atproto/syntax" 18 + "tangled.org/core/api/tangled" 19 + "tangled.org/core/knotserver/sandbox" 20 + "tangled.org/core/rbac" 21 + ) 22 + 23 + func (x *Xrpc) MergeCommit(w http.ResponseWriter, r *http.Request) { 24 + var input tangled.GitMergeCommit_Input 25 + if err := json.NewDecoder(r.Body).Decode(&input); err != nil { 26 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "failed to decode json body"}) 27 + return 28 + } 29 + l := x.Logger.With("handler", "MergeCommit", "input", input) 30 + l.Debug("request") 31 + 32 + if err := gitMergeCommit_Input_Validate(input); err != nil { 33 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: err.Error()}) 34 + return 35 + } 36 + 37 + actorDid, ok := r.Context().Value(ActorDid).(syntax.DID) 38 + if !ok { 39 + writeJson(w, http.StatusUnauthorized, &atclient.ErrorBody{Name: "Unauthorized", Message: "missing actor DID"}) 40 + return 41 + } 42 + if allowed, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, input.Target.Repo); err != nil || !allowed { 43 + writeJson(w, http.StatusUnauthorized, &atclient.ErrorBody{Name: "Forbidden", Message: fmt.Sprintf("%s is not allowed to merge into this repository", actorDid.String())}) 44 + return 45 + } 46 + 47 + output, status, apierr := x.mergeCommit(r.Context(), input) 48 + if apierr != nil { 49 + l.Error("failed", "kind", apierr.Name, "error", apierr.Message) 50 + writeJson(w, status, apierr) 51 + return 52 + } 53 + 54 + writeJson(w, status, output) 55 + } 56 + 57 + func (x *Xrpc) mergeCommit(ctx context.Context, input tangled.GitMergeCommit_Input) (any, int, *atclient.ErrorBody) { 58 + l := x.Logger.With("handler", "mergePullRequest") 59 + 60 + fail := func(status int, name, clientMsg string, detail ...any) (any, int, *atclient.ErrorBody) { 61 + l.Error(clientMsg, append([]any{"name", name}, detail...)...) 62 + return nil, status, &atclient.ErrorBody{Name: name, Message: clientMsg} 63 + } 64 + 65 + baseRepoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, input.Target.Repo) 66 + if err != nil { 67 + return fail(http.StatusNotFound, "RepoNotFound", "unknown repository", "repo", input.Target.Repo, "err", err) 68 + } 69 + sourceRepoDid := syntax.DID(input.Source.Repo) 70 + 71 + var sourceRepoUrl string 72 + var sourceRepoPath string // non-empty only when the source is local to this knot 73 + if p, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, input.Source.Repo); err == nil { 74 + sourceRepoPath = p 75 + sourceRepoUrl = "file://" + p 76 + } else { 77 + ident, err := x.Resolver.Directory().LookupDID(ctx, sourceRepoDid) 78 + if err != nil { 79 + return fail(http.StatusNotFound, "RepoNotFound", "unknown repository", "source", sourceRepoDid, "err", err) 80 + } 81 + sourceKnot := ident.GetServiceEndpoint("atproto_pds") 82 + u, err := url.Parse(sourceKnot) 83 + if err != nil { 84 + return fail(http.StatusNotFound, "RepoNotFound", "unknown repository", "source", sourceRepoDid, "knot", sourceKnot, "err", err) 85 + } 86 + sourceRepoUrl = u.JoinPath(sourceRepoDid.String()).String() 87 + } 88 + 89 + var authorName, authorEmail, authorDate string 90 + if input.MergeCommit != nil && input.MergeCommit.Author != nil { 91 + authorName = input.MergeCommit.Author.Name 92 + authorEmail = input.MergeCommit.Author.Email 93 + authorDate = input.MergeCommit.Author.When 94 + } else { 95 + authorName = x.Config.Git.UserName 96 + authorEmail = x.Config.Git.UserEmail 97 + } 98 + 99 + var message string 100 + if input.MergeCommit != nil && input.MergeCommit.Message != nil { 101 + message = *input.MergeCommit.Message 102 + } else { 103 + shortSha := input.Source.Commit 104 + if len(shortSha) > 8 { 105 + shortSha = shortSha[:8] 106 + } 107 + message = fmt.Sprintf("Merge %s into %s", shortSha, input.Target.Branch) 108 + } 109 + 110 + env := append(os.Environ(), 111 + "GIT_TERMINAL_PROMPT=0", 112 + "GIT_AUTHOR_NAME="+authorName, 113 + "GIT_AUTHOR_EMAIL="+authorEmail, 114 + "GIT_COMMITTER_NAME="+x.Config.Git.UserName, 115 + "GIT_COMMITTER_EMAIL="+x.Config.Git.UserEmail, 116 + ) 117 + if authorDate != "" { 118 + env = append(env, "GIT_AUTHOR_DATE="+authorDate) 119 + } 120 + 121 + // 1. create temp repo with git alternate 122 + tmpRepoPath, cleanup, err := createTemporaryRepoForMerge(ctx, x.Sandbox, baseRepoPath, input.Target.Branch) 123 + if err != nil { 124 + return fail(http.StatusInternalServerError, "InternalError", "failed to prepare merge", "err", err) 125 + } 126 + defer cleanup() 127 + 128 + runGit := func(args ...string) ([]byte, []byte, error) { 129 + args = append([]string{"-C", tmpRepoPath}, args...) 130 + return gitWithSandbox(ctx, x.Sandbox, env, []string{tmpRepoPath}, args...) 131 + } 132 + 133 + // 2. fetch source as "tracking" branch 134 + fetchPaths := []string{tmpRepoPath} 135 + if sourceRepoPath != "" { 136 + fetchPaths = append(fetchPaths, sourceRepoPath) 137 + } 138 + if _, stderr, err := gitWithSandbox(ctx, x.Sandbox, env, fetchPaths, "-C", tmpRepoPath, "fetch", sourceRepoUrl, input.Source.Commit); err != nil { 139 + return fail(http.StatusInternalServerError, "InternalError", "failed to fetch source commit", "err", err, "stderr", strings.TrimSpace(string(stderr))) 140 + } 141 + if _, stderr, err := runGit("branch", "tracking", input.Source.Commit); err != nil { 142 + return fail(http.StatusBadRequest, "InvalidCommit", "source commit unavailable", "commit", input.Source.Commit, "err", err, "stderr", strings.TrimSpace(string(stderr))) 143 + } 144 + 145 + // populate the working tree on the base branch. 146 + if _, stderr, err := runGit("checkout", "-f", "base"); err != nil { 147 + return fail(http.StatusInternalServerError, "InternalError", "failed to merge commit", "step", "checkout base", "err", err, "stderr", strings.TrimSpace(string(stderr))) 148 + } 149 + 150 + // 3. merge 151 + switch input.Style { 152 + case "rebase": 153 + if status, apierr := rebaseTrackingOntoBase(runGit, l, tmpRepoPath); apierr != nil { 154 + return nil, status, apierr 155 + } 156 + if _, stderr, err := runGit("checkout", "base"); err != nil { 157 + return fail(http.StatusInternalServerError, "InternalError", "failed to merge commit", "step", "checkout base after rebase", "err", err, "stderr", strings.TrimSpace(string(stderr))) 158 + } 159 + if _, stderr, err := runGit("merge", "--ff-only", "staging"); err != nil { 160 + return mergeConflictError(runGit, l, stderr) 161 + } 162 + 163 + case "merge": 164 + if _, stderr, err := runGit("merge", "--no-ff", "--no-commit", "tracking"); err != nil { 165 + return mergeConflictError(runGit, l, stderr) 166 + } 167 + if _, stderr, err := runGit("commit", "--no-gpg-sign", "--message="+message); err != nil { 168 + return fail(http.StatusInternalServerError, "InternalError", "failed to merge commit", "step", "merge commit", "err", err, "stderr", strings.TrimSpace(string(stderr))) 169 + } 170 + 171 + case "rebase-merge": 172 + if status, apierr := rebaseTrackingOntoBase(runGit, l, tmpRepoPath); apierr != nil { 173 + return nil, status, apierr 174 + } 175 + if _, stderr, err := runGit("checkout", "base"); err != nil { 176 + return fail(http.StatusInternalServerError, "InternalError", "failed to merge commit", "step", "checkout base after rebase", "err", err, "stderr", strings.TrimSpace(string(stderr))) 177 + } 178 + if _, stderr, err := runGit("merge", "--no-ff", "--no-commit", "staging"); err != nil { 179 + return mergeConflictError(runGit, l, stderr) 180 + } 181 + if _, stderr, err := runGit("commit", "--no-gpg-sign", "--message="+message); err != nil { 182 + return fail(http.StatusInternalServerError, "InternalError", "failed to merge commit", "step", "merge commit", "err", err, "stderr", strings.TrimSpace(string(stderr))) 183 + } 184 + 185 + case "squash-rebase": 186 + if _, stderr, err := runGit("merge", "--squash", "tracking"); err != nil { 187 + return mergeConflictError(runGit, l, stderr) 188 + } 189 + if _, stderr, err := runGit("commit", "--no-gpg-sign", "--message="+message); err != nil { 190 + return fail(http.StatusInternalServerError, "InternalError", "failed to merge commit", "step", "squash commit", "err", err, "stderr", strings.TrimSpace(string(stderr))) 191 + } 192 + 193 + case "squash-merge": 194 + // TODO: implement this 195 + return nil, http.StatusBadRequest, &atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("unknown merge style: %q", input.Style)} 196 + 197 + case "squash-rebase-merge": 198 + return fail(http.StatusInternalServerError, "InternalError", "squash-rebase-merge is not yet supported") 199 + 200 + case "fast-forward-only": 201 + if _, stderr, err := runGit("merge", "--ff-only", "tracking"); err != nil { 202 + return fail(http.StatusConflict, "MergeConflict", "cannot fast-forward: source and target have diverged", "commit", input.Source.Commit, "branch", input.Target.Branch, "err", err, "stderr", strings.TrimSpace(string(stderr))) 203 + } 204 + 205 + default: 206 + return nil, http.StatusBadRequest, &atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("unknown merge style: %q", input.Style)} 207 + } 208 + 209 + // 4. push the merged "base" back to the real base repo's branch. this goes through 210 + // receive-pack so the base repo's hooks fire (ref-update notification). 211 + if _, stderr, err := gitWithSandbox(ctx, x.Sandbox, env, []string{tmpRepoPath, baseRepoPath}, "-C", tmpRepoPath, "push", "origin", "base:refs/heads/"+input.Target.Branch); err != nil { 212 + msg := strings.TrimSpace(string(stderr)) 213 + if strings.Contains(msg, "non-fast-forward") || strings.Contains(msg, "rejected") { 214 + return fail(http.StatusConflict, "PushRejected", "target branch changed; retry the merge", "branch", input.Target.Branch, "err", err, "stderr", msg) 215 + } 216 + return fail(http.StatusInternalServerError, "InternalError", "failed to complete merge", "step", "push", "err", err, "stderr", msg) 217 + } 218 + 219 + return nil, http.StatusOK, nil 220 + } 221 + 222 + // rebaseTrackingOntoBase checks out "tracking" as "staging" and rebases it onto "base". 223 + func rebaseTrackingOntoBase(run func(...string) ([]byte, []byte, error), l *slog.Logger, tmpRepoPath string) (int, *atclient.ErrorBody) { 224 + if _, stderr, err := run("checkout", "-b", "staging", "tracking"); err != nil { 225 + l.Error("failed to merge commit", "step", "checkout staging", "err", err, "stderr", strings.TrimSpace(string(stderr))) 226 + return http.StatusInternalServerError, &atclient.ErrorBody{Name: "InternalError", Message: "failed to merge commit"} 227 + } 228 + if _, stderr, err := run("rebase", "base"); err != nil { 229 + if _, statErr := os.Stat(filepath.Join(tmpRepoPath, ".git", "REBASE_HEAD")); statErr == nil { 230 + l.Error("rebase produced conflicts", "err", err, "stderr", strings.TrimSpace(string(stderr))) 231 + return http.StatusConflict, &atclient.ErrorBody{Name: "MergeConflict", Message: "rebase produced conflicts"} 232 + } 233 + l.Error("failed to merge commit", "step", "rebase", "err", err, "stderr", strings.TrimSpace(string(stderr))) 234 + return http.StatusInternalServerError, &atclient.ErrorBody{Name: "InternalError", Message: "failed to merge commit"} 235 + } 236 + return 0, nil 237 + } 238 + 239 + // mergeConflictError inspects the temp repo for unmerged paths after a failed merge and 240 + // returns a 409 listing them, falling back to a 500 when the failure is not a conflict. 241 + func mergeConflictError(runGit func(...string) ([]byte, []byte, error), l *slog.Logger, mergeStderr []byte) (any, int, *atclient.ErrorBody) { 242 + stdout, _, _ := runGit("ls-files", "--unmerged") 243 + files := parseUnmergedFiles(stdout) 244 + if len(files) > 0 { 245 + l.Error("merge produced conflicts", "files", files, "stderr", strings.TrimSpace(string(mergeStderr))) 246 + return nil, http.StatusConflict, &atclient.ErrorBody{Name: "MergeConflict", Message: "merge produced conflicts"} 247 + } 248 + l.Error("failed to merge commit", "step", "merge", "stderr", strings.TrimSpace(string(mergeStderr))) 249 + return nil, http.StatusInternalServerError, &atclient.ErrorBody{Name: "InternalError", Message: "failed to merge commit"} 250 + } 251 + 252 + // parseUnmergedFiles parses `git ls-files --unmerged` output into a deduplicated list of paths. 253 + // Each line looks like: "<mode> <sha> <stage>\t<path>". 254 + func parseUnmergedFiles(out []byte) []string { 255 + seen := make(map[string]struct{}) 256 + var files []string 257 + for line := range strings.SplitSeq(string(out), "\n") { 258 + _, path, ok := strings.Cut(line, "\t") 259 + if !ok { 260 + continue 261 + } 262 + if _, ok := seen[path]; ok { 263 + continue 264 + } 265 + seen[path] = struct{}{} 266 + files = append(files, path) 267 + } 268 + return files 269 + } 270 + 271 + // lexgen doesn't give Validate() method... 272 + func gitMergeCommit_Input_Validate(input tangled.GitMergeCommit_Input) error { 273 + if input.Target == nil { 274 + return fmt.Errorf("target: required") 275 + } 276 + if input.Source == nil { 277 + return fmt.Errorf("source: required") 278 + } 279 + if _, err := syntax.ParseDID(input.Target.Repo); err != nil { 280 + return fmt.Errorf("target.repo: invalid DID: %w", err) 281 + } 282 + if input.Target.Branch == "" { 283 + return fmt.Errorf("target.branch: required") 284 + } 285 + if _, err := syntax.ParseDID(input.Source.Repo); err != nil { 286 + return fmt.Errorf("source.repo: invalid DID: %w", err) 287 + } 288 + if input.Source.Commit == "" { 289 + return fmt.Errorf("source.commit: required") 290 + } 291 + switch input.Style { 292 + case "merge", "rebase", "rebase-merge", "squash", "fast-forward-only": 293 + default: 294 + return fmt.Errorf("style: unknown merge style %q", input.Style) 295 + } 296 + return nil 297 + } 298 + 299 + // createTemporaryRepoForMerge creates a temporary non-bare repo with the base repo's 300 + // "base" branch (and a copy "original_base") checked out via a git alternate to the base 301 + // repo's object store. Returns the temp repo path and a cleanup func that removes it. 302 + func createTemporaryRepoForMerge(ctx context.Context, sb sandbox.Backend, baseRepoPath string, baseBranch string) (path string, cleanup context.CancelFunc, err error) { 303 + tmp, err := os.MkdirTemp("", "merge-*") 304 + if err != nil { 305 + return "", nil, fmt.Errorf("create temp dir: %w", err) 306 + } 307 + cleanup = func() { os.RemoveAll(tmp) } 308 + 309 + env := append(os.Environ(), "GIT_TERMINAL_PROMPT=0") 310 + run := func(paths []string, args ...string) ([]byte, []byte, error) { 311 + return gitWithSandbox(ctx, sb, env, paths, args...) 312 + } 313 + 314 + // git init needs a working dir (non-bare). 315 + if _, stderr, err := run([]string{tmp}, "-C", tmp, "init"); err != nil { 316 + cleanup() 317 + return "", nil, fmt.Errorf("git init: %s", strings.TrimSpace(string(stderr))) 318 + } 319 + 320 + // borrow the base repo's objects via alternates. 321 + if err := func(repoPath, srcRepoPath string) error { 322 + p := filepath.Join(repoPath, ".git", "objects", "info", "alternates") 323 + f, err := os.OpenFile(p, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600) 324 + if err != nil { 325 + return err 326 + } 327 + defer f.Close() 328 + _, err = fmt.Fprintln(f, filepath.Join(srcRepoPath, "objects")) 329 + return err 330 + }(tmp, baseRepoPath); err != nil { 331 + cleanup() 332 + return "", nil, fmt.Errorf("add base objects: %w", err) 333 + } 334 + 335 + if _, stderr, err := run([]string{tmp}, "-C", tmp, "remote", "add", "origin", baseRepoPath); err != nil { 336 + cleanup() 337 + return "", nil, fmt.Errorf("git remote add: %s", strings.TrimSpace(string(stderr))) 338 + } 339 + 340 + if _, stderr, err := run([]string{tmp, baseRepoPath}, "-C", tmp, "fetch", "--no-tags", "origin", baseBranch+":base", baseBranch+":original_base"); err != nil { 341 + cleanup() 342 + return "", nil, fmt.Errorf("git fetch base branch %q: %s", baseBranch, strings.TrimSpace(string(stderr))) 343 + } 344 + 345 + if _, stderr, err := run([]string{tmp}, "-C", tmp, "symbolic-ref", "HEAD", "refs/heads/base"); err != nil { 346 + cleanup() 347 + return "", nil, fmt.Errorf("git symbolic-ref: %s", strings.TrimSpace(string(stderr))) 348 + } 349 + 350 + return tmp, cleanup, nil 351 + } 352 + 353 + func gitWithSandbox(ctx context.Context, sb sandbox.Backend, env []string, paths []string, args ...string) (stdout, stderr []byte, err error) { 354 + cmd := exec.CommandContext(ctx, "git", args...) 355 + var outBuf, errBuf bytes.Buffer 356 + // set stdout/stderr/env before wrapping: the landlock backend copies these into a 357 + // fresh *exec.Cmd, so mutating the original afterwards would be lost. 358 + cmd.Stdout = &outBuf 359 + cmd.Stderr = &errBuf 360 + cmd.Env = env 361 + 362 + if sb != nil { 363 + wrapped, werr := sb.WrapMulti(paths, cmd) 364 + if werr != nil { 365 + return nil, nil, fmt.Errorf("sandbox wrap: %w", werr) 366 + } 367 + cmd = wrapped 368 + } else if len(paths) > 0 { 369 + cmd.Dir = paths[0] 370 + } 371 + 372 + err = cmd.Run() 373 + return outBuf.Bytes(), errBuf.Bytes(), err 374 + }
+33
api/tangled/tempformatPatch.go
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + package tangled 4 + 5 + // schema: sh.tangled.git.temp.formatPatch 6 + 7 + import ( 8 + "bytes" 9 + "context" 10 + 11 + "github.com/bluesky-social/indigo/lex/util" 12 + ) 13 + 14 + const ( 15 + GitTempFormatPatchNSID = "sh.tangled.git.temp.formatPatch" 16 + ) 17 + 18 + // GitTempFormatPatch calls the XRPC method "sh.tangled.git.temp.formatPatch". 19 + // 20 + // repo: DID of the repository 21 + func GitTempFormatPatch(ctx context.Context, c util.LexClient, from string, repo string, to string) ([]byte, error) { 22 + buf := new(bytes.Buffer) 23 + 24 + params := map[string]interface{}{} 25 + params["from"] = from 26 + params["repo"] = repo 27 + params["to"] = to 28 + if err := c.LexDo(ctx, util.Query, "", "sh.tangled.git.temp.formatPatch", params, nil, buf); err != nil { 29 + return nil, err 30 + } 31 + 32 + return buf.Bytes(), nil 33 + }
+43
lexicons/git/temp/formatPatch.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "sh.tangled.git.temp.formatPatch", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "parameters": { 8 + "type": "params", 9 + "required": ["repo", "from", "to"], 10 + "properties": { 11 + "repo": { 12 + "type": "string", 13 + "format": "did", 14 + "description": "DID of the repository" 15 + }, 16 + "from": { 17 + "type": "string" 18 + }, 19 + "to": { 20 + "type": "string" 21 + } 22 + } 23 + }, 24 + "output": { 25 + "encoding": "*/*" 26 + }, 27 + "errors": [ 28 + { 29 + "name": "RepoNotFound", 30 + "description": "Repository not found or access denied" 31 + }, 32 + { 33 + "name": "RevisionNotFound", 34 + "description": "One or both revisions not found" 35 + }, 36 + { 37 + "name": "InvalidRequest", 38 + "description": "Invalid request parameters" 39 + } 40 + ] 41 + } 42 + } 43 + }
+54
knotmirror/xrpc/git_format_patch.go
··· 1 + package xrpc 2 + 3 + import ( 4 + "bytes" 5 + "fmt" 6 + "net/http" 7 + "os/exec" 8 + "strings" 9 + 10 + "github.com/bluesky-social/indigo/atproto/atclient" 11 + "github.com/bluesky-social/indigo/atproto/syntax" 12 + ) 13 + 14 + func (x *Xrpc) FormatPatch(w http.ResponseWriter, r *http.Request) { 15 + var ( 16 + repoQuery = r.URL.Query().Get("repo") 17 + from = r.URL.Query().Get("from") 18 + to = r.URL.Query().Get("to") 19 + ) 20 + 21 + repo, err := syntax.ParseDID(repoQuery) 22 + if err != nil { 23 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) 24 + return 25 + } 26 + 27 + // reject leading dashes so user input can't be parsed as git flags 28 + if from == "" || to == "" || strings.HasPrefix(from, "-") || strings.HasPrefix(to, "-") { 29 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "missing or invalid from/to parameter"}) 30 + return 31 + } 32 + 33 + l := x.logger.With("method", "git.formatPatch", "repo", repo, "from", from, "to", to) 34 + l.Debug("request") 35 + 36 + ctx := r.Context() 37 + 38 + repoPath, err := x.makeRepoPath(ctx, repo) 39 + if err != nil { 40 + writeJson(w, http.StatusNotFound, atclient.ErrorBody{Name: "RepoNotFound", Message: fmt.Sprintf("unknown repository: %q", repo)}) 41 + return 42 + } 43 + 44 + cmd := exec.CommandContext(ctx, "git", "-C", repoPath, "format-patch", "--stdout", fmt.Sprintf("%s..%s", from, to)) 45 + stderr := new(bytes.Buffer) 46 + cmd.Stderr = stderr 47 + 48 + w.Header().Set("Content-Type", "text/plain; charset=utf-8") 49 + cmd.Stdout = w 50 + if err := cmd.Run(); err != nil { 51 + l.Error("running format-patch", "err", err, "stderr", stderr.String()) 52 + w.WriteHeader(http.StatusInternalServerError) 53 + } 54 + }
+268 -1
api/tangled/cbor_gen.go
··· 12416 12416 } 12417 12417 12418 12418 cw := cbg.NewCborWriter(w) 12419 - fieldCount := 10 12419 + fieldCount := 11 12420 12420 12421 12421 if t.Body == nil { 12422 12422 fieldCount-- ··· 12613 12613 } 12614 12614 } 12615 12615 12616 + // t.Versions ([]*tangled.RepoPull_Version) (slice) 12617 + if len("versions") > 1000000 { 12618 + return xerrors.Errorf("Value in field \"versions\" was too long") 12619 + } 12620 + 12621 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("versions"))); err != nil { 12622 + return err 12623 + } 12624 + if _, err := cw.WriteString(string("versions")); err != nil { 12625 + return err 12626 + } 12627 + 12628 + if len(t.Versions) > 8192 { 12629 + return xerrors.Errorf("Slice value in field t.Versions was too long") 12630 + } 12631 + 12632 + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Versions))); err != nil { 12633 + return err 12634 + } 12635 + for _, v := range t.Versions { 12636 + if err := v.MarshalCBOR(cw); err != nil { 12637 + return err 12638 + } 12639 + 12640 + } 12641 + 12616 12642 // t.CreatedAt (string) (string) 12617 12643 if len("createdAt") > 1000000 { 12618 12644 return xerrors.Errorf("Value in field \"createdAt\" was too long") ··· 12919 12945 12920 12946 } 12921 12947 } 12948 + // t.Versions ([]*tangled.RepoPull_Version) (slice) 12949 + case "versions": 12950 + 12951 + maj, extra, err = cr.ReadHeader() 12952 + if err != nil { 12953 + return err 12954 + } 12955 + 12956 + if extra > 8192 { 12957 + return fmt.Errorf("t.Versions: array too large (%d)", extra) 12958 + } 12959 + 12960 + if maj != cbg.MajArray { 12961 + return fmt.Errorf("expected cbor array") 12962 + } 12963 + 12964 + if extra > 0 { 12965 + t.Versions = make([]*RepoPull_Version, extra) 12966 + } 12967 + 12968 + for i := 0; i < int(extra); i++ { 12969 + { 12970 + var maj byte 12971 + var extra uint64 12972 + var err error 12973 + _ = maj 12974 + _ = extra 12975 + _ = err 12976 + 12977 + { 12978 + 12979 + b, err := cr.ReadByte() 12980 + if err != nil { 12981 + return err 12982 + } 12983 + if b != cbg.CborNull[0] { 12984 + if err := cr.UnreadByte(); err != nil { 12985 + return err 12986 + } 12987 + t.Versions[i] = new(RepoPull_Version) 12988 + if err := t.Versions[i].UnmarshalCBOR(cr); err != nil { 12989 + return xerrors.Errorf("unmarshaling t.Versions[i] pointer: %w", err) 12990 + } 12991 + } 12992 + 12993 + } 12994 + 12995 + } 12996 + } 12922 12997 // t.CreatedAt (string) (string) 12923 12998 case "createdAt": 12924 12999 ··· 13987 14062 13988 14063 return nil 13989 14064 } 14065 + func (t *RepoPull_Version) MarshalCBOR(w io.Writer) error { 14066 + if t == nil { 14067 + _, err := w.Write(cbg.CborNull) 14068 + return err 14069 + } 14070 + 14071 + cw := cbg.NewCborWriter(w) 14072 + fieldCount := 3 14073 + 14074 + if t.Base == nil { 14075 + fieldCount-- 14076 + } 14077 + 14078 + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 14079 + return err 14080 + } 14081 + 14082 + // t.Base (string) (string) 14083 + if t.Base != nil { 14084 + 14085 + if len("base") > 1000000 { 14086 + return xerrors.Errorf("Value in field \"base\" was too long") 14087 + } 14088 + 14089 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("base"))); err != nil { 14090 + return err 14091 + } 14092 + if _, err := cw.WriteString(string("base")); err != nil { 14093 + return err 14094 + } 14095 + 14096 + if t.Base == nil { 14097 + if _, err := cw.Write(cbg.CborNull); err != nil { 14098 + return err 14099 + } 14100 + } else { 14101 + if len(*t.Base) > 1000000 { 14102 + return xerrors.Errorf("Value in field t.Base was too long") 14103 + } 14104 + 14105 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Base))); err != nil { 14106 + return err 14107 + } 14108 + if _, err := cw.WriteString(string(*t.Base)); err != nil { 14109 + return err 14110 + } 14111 + } 14112 + } 14113 + 14114 + // t.Head (string) (string) 14115 + if len("head") > 1000000 { 14116 + return xerrors.Errorf("Value in field \"head\" was too long") 14117 + } 14118 + 14119 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("head"))); err != nil { 14120 + return err 14121 + } 14122 + if _, err := cw.WriteString(string("head")); err != nil { 14123 + return err 14124 + } 14125 + 14126 + if len(t.Head) > 1000000 { 14127 + return xerrors.Errorf("Value in field t.Head was too long") 14128 + } 14129 + 14130 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Head))); err != nil { 14131 + return err 14132 + } 14133 + if _, err := cw.WriteString(string(t.Head)); err != nil { 14134 + return err 14135 + } 14136 + 14137 + // t.CreatedAt (string) (string) 14138 + if len("createdAt") > 1000000 { 14139 + return xerrors.Errorf("Value in field \"createdAt\" was too long") 14140 + } 14141 + 14142 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { 14143 + return err 14144 + } 14145 + if _, err := cw.WriteString(string("createdAt")); err != nil { 14146 + return err 14147 + } 14148 + 14149 + if len(t.CreatedAt) > 1000000 { 14150 + return xerrors.Errorf("Value in field t.CreatedAt was too long") 14151 + } 14152 + 14153 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { 14154 + return err 14155 + } 14156 + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 14157 + return err 14158 + } 14159 + return nil 14160 + } 14161 + 14162 + func (t *RepoPull_Version) UnmarshalCBOR(r io.Reader) (err error) { 14163 + *t = RepoPull_Version{} 14164 + 14165 + cr := cbg.NewCborReader(r) 14166 + 14167 + maj, extra, err := cr.ReadHeader() 14168 + if err != nil { 14169 + return err 14170 + } 14171 + defer func() { 14172 + if err == io.EOF { 14173 + err = io.ErrUnexpectedEOF 14174 + } 14175 + }() 14176 + 14177 + if maj != cbg.MajMap { 14178 + return fmt.Errorf("cbor input should be of type map") 14179 + } 14180 + 14181 + if extra > cbg.MaxLength { 14182 + return fmt.Errorf("RepoPull_Version: map struct too large (%d)", extra) 14183 + } 14184 + 14185 + n := extra 14186 + 14187 + nameBuf := make([]byte, 9) 14188 + for i := uint64(0); i < n; i++ { 14189 + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 14190 + if err != nil { 14191 + return err 14192 + } 14193 + 14194 + if !ok { 14195 + // Field doesn't exist on this type, so ignore it 14196 + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { 14197 + return err 14198 + } 14199 + continue 14200 + } 14201 + 14202 + switch string(nameBuf[:nameLen]) { 14203 + // t.Base (string) (string) 14204 + case "base": 14205 + 14206 + { 14207 + b, err := cr.ReadByte() 14208 + if err != nil { 14209 + return err 14210 + } 14211 + if b != cbg.CborNull[0] { 14212 + if err := cr.UnreadByte(); err != nil { 14213 + return err 14214 + } 14215 + 14216 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 14217 + if err != nil { 14218 + return err 14219 + } 14220 + 14221 + t.Base = (*string)(&sval) 14222 + } 14223 + } 14224 + // t.Head (string) (string) 14225 + case "head": 14226 + 14227 + { 14228 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 14229 + if err != nil { 14230 + return err 14231 + } 14232 + 14233 + t.Head = string(sval) 14234 + } 14235 + // t.CreatedAt (string) (string) 14236 + case "createdAt": 14237 + 14238 + { 14239 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 14240 + if err != nil { 14241 + return err 14242 + } 14243 + 14244 + t.CreatedAt = string(sval) 14245 + } 14246 + 14247 + default: 14248 + // Field doesn't exist on this type, so ignore it 14249 + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { 14250 + return err 14251 + } 14252 + } 14253 + } 14254 + 14255 + return nil 14256 + } 13990 14257 func (t *Spindle) MarshalCBOR(w io.Writer) error { 13991 14258 if t == nil { 13992 14259 _, err := w.Write(cbg.CborNull)
+22 -11
api/tangled/repopull.go
··· 17 17 } // 18 18 // RECORDTYPE: RepoPull 19 19 type RepoPull struct { 20 - LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull" cborgen:"$type,const=sh.tangled.repo.pull"` 21 - Body *string `json:"body,omitempty" cborgen:"body,omitempty"` 22 - CreatedAt string `json:"createdAt" cborgen:"createdAt"` 23 - DependentOn *string `json:"dependentOn,omitempty" cborgen:"dependentOn,omitempty"` 24 - Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"` 25 - References []string `json:"references,omitempty" cborgen:"references,omitempty"` 26 - Rounds []*RepoPull_Round `json:"rounds" cborgen:"rounds"` 27 - Source *RepoPull_Source `json:"source,omitempty" cborgen:"source,omitempty"` 28 - Target *RepoPull_Target `json:"target" cborgen:"target"` 29 - Title string `json:"title" cborgen:"title"` 20 + LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull" cborgen:"$type,const=sh.tangled.repo.pull"` 21 + Body *string `json:"body,omitempty" cborgen:"body,omitempty"` 22 + CreatedAt string `json:"createdAt" cborgen:"createdAt"` 23 + DependentOn *string `json:"dependentOn,omitempty" cborgen:"dependentOn,omitempty"` 24 + Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"` 25 + References []string `json:"references,omitempty" cborgen:"references,omitempty"` 26 + // rounds: DEPRECATED: always empty 27 + Rounds []*RepoPull_Round `json:"rounds" cborgen:"rounds"` 28 + Source *RepoPull_Source `json:"source,omitempty" cborgen:"source,omitempty"` 29 + Target *RepoPull_Target `json:"target" cborgen:"target"` 30 + Title string `json:"title" cborgen:"title"` 31 + Versions []*RepoPull_Version `json:"versions" cborgen:"versions"` 30 32 } 31 33 32 34 // RepoPull_Round is a "round" in the sh.tangled.repo.pull schema. 33 35 // 34 - // revisions of this pull request, newer rounds are appended to this array. appviews may reject records do not treat this field as append-only. the blob format is gzipped text-based git-format-patches. 36 + // DEPRECATED: revisions of this pull request, newer rounds are appended to this array. appviews may reject records do not treat this field as append-only. the blob format is gzipped text-based git-format-patches. 35 37 type RepoPull_Round struct { 36 38 CreatedAt string `json:"createdAt" cborgen:"createdAt"` 37 39 PatchBlob *util.LexBlob `json:"patchBlob" cborgen:"patchBlob"` ··· 48 50 Branch string `json:"branch" cborgen:"branch"` 49 51 Repo string `json:"repo" cborgen:"repo"` 50 52 } 53 + 54 + // RepoPull_Version is a "version" in the sh.tangled.repo.pull schema. 55 + type RepoPull_Version struct { 56 + // base: merge-base commit ID for combined interdiff 57 + Base *string `json:"base,omitempty" cborgen:"base,omitempty"` 58 + CreatedAt string `json:"createdAt" cborgen:"createdAt"` 59 + // head: HEAD commit ID 60 + Head string `json:"head" cborgen:"head"` 61 + }
+1
cmd/cborgen/cborgen.go
··· 67 67 tangled.RepoPull_Source{}, 68 68 tangled.RepoPullStatus{}, 69 69 tangled.RepoPull_Target{}, 70 + tangled.RepoPull_Version{}, 70 71 tangled.Spindle{}, 71 72 tangled.SpindleMember{}, 72 73 tangled.String{},
+32 -2
lexicons/pulls/pull.json
··· 13 13 "target", 14 14 "title", 15 15 "createdAt", 16 - "rounds" 16 + "rounds", 17 + "versions" 17 18 ], 18 19 "properties": { 19 20 "title": { ··· 24 25 }, 25 26 "rounds": { 26 27 "type": "array", 28 + "description": "DEPRECATED: always empty", 27 29 "items": { 28 30 "type": "ref", 29 31 "ref": "#round" 30 32 } 31 33 }, 34 + "versions": { 35 + "type": "array", 36 + "items": { 37 + "type": "ref", 38 + "ref": "#version" 39 + } 40 + }, 32 41 "source": { 33 42 "type": "ref", 34 43 "ref": "#source" ··· 99 108 "patchBlob", 100 109 "createdAt" 101 110 ], 102 - "description": "revisions of this pull request, newer rounds are appended to this array. appviews may reject records do not treat this field as append-only. the blob format is gzipped text-based git-format-patches.", 111 + "description": "DEPRECATED: revisions of this pull request, newer rounds are appended to this array. appviews may reject records do not treat this field as append-only. the blob format is gzipped text-based git-format-patches.", 103 112 "properties": { 104 113 "createdAt": { 105 114 "type": "string", ··· 112 121 ] 113 122 } 114 123 } 124 + }, 125 + "version": { 126 + "type": "object", 127 + "required": [ 128 + "head", 129 + "createdAt" 130 + ], 131 + "properties": { 132 + "createdAt": { 133 + "type": "string", 134 + "format": "datetime" 135 + }, 136 + "base": { 137 + "type": "string", 138 + "description": "merge-base commit ID for combined interdiff" 139 + }, 140 + "head": { 141 + "type": "string", 142 + "description": "HEAD commit ID" 143 + } 144 + } 115 145 } 116 146 } 117 147 }
+2
.cargo/config.toml
··· 1 + [target.aarch64-apple-darwin] 2 + linker = "arm64-apple-darwin-cc"
+2
.gitattributes
··· 1 1 api/tangled/** linguist-generated -diff 2 2 api/tangled/*_ext.go -linguist-generated diff 3 3 flake.lock -diff 4 + Cargo.lock -diff 5 + gitmirror/proto/gen/** linguist-generated -diff
+1455 -35
Cargo.lock
··· 148 148 "rustversion", 149 149 ] 150 150 151 + [[package]] 152 + name = "arrayvec" 153 + version = "0.7.7" 154 + source = "registry+https://github.com/rust-lang/crates.io-index" 155 + checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" 156 + 151 157 [[package]] 152 158 name = "assert-json-diff" 153 159 version = "2.0.2" ··· 309 315 source = "registry+https://github.com/rust-lang/crates.io-index" 310 316 checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" 311 317 318 + [[package]] 319 + name = "bitflags" 320 + version = "1.3.2" 321 + source = "registry+https://github.com/rust-lang/crates.io-index" 322 + checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 323 + 312 324 [[package]] 313 325 name = "bitflags" 314 326 version = "2.11.1" ··· 333 345 "generic-array", 334 346 ] 335 347 348 + [[package]] 349 + name = "block-buffer" 350 + version = "0.12.1" 351 + source = "registry+https://github.com/rust-lang/crates.io-index" 352 + checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" 353 + dependencies = [ 354 + "hybrid-array", 355 + ] 356 + 336 357 [[package]] 337 358 name = "bobbin" 338 359 version = "0.0.1" ··· 647 668 "tinyvec", 648 669 ] 649 670 671 + [[package]] 672 + name = "bstr" 673 + version = "1.12.3" 674 + source = "registry+https://github.com/rust-lang/crates.io-index" 675 + checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" 676 + dependencies = [ 677 + "memchr", 678 + "regex-automata", 679 + "serde_core", 680 + ] 681 + 650 682 [[package]] 651 683 name = "bumpalo" 652 684 version = "3.20.3" ··· 668 700 "serde", 669 701 ] 670 702 703 + [[package]] 704 + name = "bytesize" 705 + version = "2.4.0" 706 + source = "registry+https://github.com/rust-lang/crates.io-index" 707 + checksum = "49e78e506b9d7633710dab98996f22f95f3d0f488e8f1aa162830556ed9fc14d" 708 + 671 709 [[package]] 672 710 name = "cast" 673 711 version = "0.3.0" ··· 825 863 source = "registry+https://github.com/rust-lang/crates.io-index" 826 864 checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" 827 865 866 + [[package]] 867 + name = "clru" 868 + version = "0.6.3" 869 + source = "registry+https://github.com/rust-lang/crates.io-index" 870 + checksum = "197fd99cb113a8d5d9b6376f3aa817f32c1078f2343b714fff7d2ca44fdf67d5" 871 + dependencies = [ 872 + "hashbrown 0.16.1", 873 + ] 874 + 828 875 [[package]] 829 876 name = "cobs" 830 877 version = "0.3.0" ··· 937 984 "libc", 938 985 ] 939 986 987 + [[package]] 988 + name = "cpufeatures" 989 + version = "0.3.0" 990 + source = "registry+https://github.com/rust-lang/crates.io-index" 991 + checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" 992 + dependencies = [ 993 + "libc", 994 + ] 995 + 940 996 [[package]] 941 997 name = "crc32fast" 942 998 version = "1.5.0" ··· 1047 1103 "typenum", 1048 1104 ] 1049 1105 1106 + [[package]] 1107 + name = "crypto-common" 1108 + version = "0.2.2" 1109 + source = "registry+https://github.com/rust-lang/crates.io-index" 1110 + checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" 1111 + dependencies = [ 1112 + "hybrid-array", 1113 + ] 1114 + 1050 1115 [[package]] 1051 1116 name = "darling" 1052 1117 version = "0.23.0" ··· 1145 1210 source = "registry+https://github.com/rust-lang/crates.io-index" 1146 1211 checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" 1147 1212 1213 + [[package]] 1214 + name = "defmt" 1215 + version = "1.1.0" 1216 + source = "registry+https://github.com/rust-lang/crates.io-index" 1217 + checksum = "a6e524506490a1953d237cb87b1cfc1e46f88c18f10a22dfe0f507dc6bfc7f7f" 1218 + dependencies = [ 1219 + "bitflags 1.3.2", 1220 + "defmt-macros", 1221 + ] 1222 + 1223 + [[package]] 1224 + name = "defmt-macros" 1225 + version = "1.1.0" 1226 + source = "registry+https://github.com/rust-lang/crates.io-index" 1227 + checksum = "f0a27770e9c8f719a79d8b638281f4d828f77d8fd61e0bd94451b9b85e576a0b" 1228 + dependencies = [ 1229 + "defmt-parser", 1230 + "proc-macro-error2", 1231 + "proc-macro2", 1232 + "quote", 1233 + "syn", 1234 + ] 1235 + 1236 + [[package]] 1237 + name = "defmt-parser" 1238 + version = "1.0.0" 1239 + source = "registry+https://github.com/rust-lang/crates.io-index" 1240 + checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" 1241 + dependencies = [ 1242 + "thiserror 2.0.18", 1243 + ] 1244 + 1148 1245 [[package]] 1149 1246 name = "der" 1150 1247 version = "0.7.10" ··· 1199 1296 source = "registry+https://github.com/rust-lang/crates.io-index" 1200 1297 checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 1201 1298 dependencies = [ 1202 - "block-buffer", 1299 + "block-buffer 0.10.4", 1203 1300 "const-oid", 1204 - "crypto-common", 1301 + "crypto-common 0.1.6", 1205 1302 "subtle", 1206 1303 ] 1207 1304 1305 + [[package]] 1306 + name = "digest" 1307 + version = "0.11.3" 1308 + source = "registry+https://github.com/rust-lang/crates.io-index" 1309 + checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" 1310 + dependencies = [ 1311 + "block-buffer 0.12.1", 1312 + "crypto-common 0.2.2", 1313 + ] 1314 + 1208 1315 [[package]] 1209 1316 name = "displaydoc" 1210 1317 version = "0.2.6" ··· 1222 1329 source = "registry+https://github.com/rust-lang/crates.io-index" 1223 1330 checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" 1224 1331 1332 + [[package]] 1333 + name = "dunce" 1334 + version = "1.0.5" 1335 + source = "registry+https://github.com/rust-lang/crates.io-index" 1336 + checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" 1337 + 1225 1338 [[package]] 1226 1339 name = "ecdsa" 1227 1340 version = "0.16.9" ··· 1229 1342 checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" 1230 1343 dependencies = [ 1231 1344 "der", 1232 - "digest", 1345 + "digest 0.10.7", 1233 1346 "elliptic-curve", 1234 1347 "rfc6979", 1235 1348 "signature", ··· 1250 1363 dependencies = [ 1251 1364 "base16ct", 1252 1365 "crypto-bigint", 1253 - "digest", 1366 + "digest 0.10.7", 1254 1367 "ff", 1255 1368 "generic-array", 1256 1369 "group", ··· 1316 1429 source = "registry+https://github.com/rust-lang/crates.io-index" 1317 1430 checksum = "9afc2bd4d5a73106dd53d10d73d3401c2f32730ba2c0b93ddb888a8983680471" 1318 1431 1432 + [[package]] 1433 + name = "faster-hex" 1434 + version = "0.10.0" 1435 + source = "registry+https://github.com/rust-lang/crates.io-index" 1436 + checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" 1437 + dependencies = [ 1438 + "heapless 0.8.0", 1439 + "serde", 1440 + ] 1441 + 1319 1442 [[package]] 1320 1443 name = "fastrand" 1321 1444 version = "2.4.1" ··· 1332 1455 "subtle", 1333 1456 ] 1334 1457 1458 + [[package]] 1459 + name = "filetime" 1460 + version = "0.2.29" 1461 + source = "registry+https://github.com/rust-lang/crates.io-index" 1462 + checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" 1463 + dependencies = [ 1464 + "cfg-if", 1465 + "libc", 1466 + ] 1467 + 1335 1468 [[package]] 1336 1469 name = "find-msvc-tools" 1337 1470 version = "0.1.9" ··· 1573 1706 name = "getrandom" 1574 1707 version = "0.4.2" 1575 1708 source = "registry+https://github.com/rust-lang/crates.io-index" 1576 - checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" 1709 + checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" 1710 + dependencies = [ 1711 + "cfg-if", 1712 + "libc", 1713 + "r-efi 6.0.0", 1714 + "wasip2", 1715 + "wasip3", 1716 + ] 1717 + 1718 + [[package]] 1719 + name = "gitmirror" 1720 + version = "0.1.0" 1721 + dependencies = [ 1722 + "anyhow", 1723 + "clap", 1724 + "gix", 1725 + "line-numbers", 1726 + "prost", 1727 + "prost-types", 1728 + "rustc-hash", 1729 + "tempfile", 1730 + "tokio", 1731 + "tokio-stream", 1732 + "tonic", 1733 + "tonic-prost", 1734 + "tonic-prost-build", 1735 + "tracing", 1736 + "tracing-subscriber", 1737 + ] 1738 + 1739 + [[package]] 1740 + name = "gix" 1741 + version = "0.84.0" 1742 + source = "registry+https://github.com/rust-lang/crates.io-index" 1743 + checksum = "ae54ae0ebd1a5a3c3f8d95dd3b5ca6e63f4fed9bfd585e13801a97d7bde8f9ce" 1744 + dependencies = [ 1745 + "gix-actor", 1746 + "gix-archive", 1747 + "gix-attributes", 1748 + "gix-blame", 1749 + "gix-command", 1750 + "gix-commitgraph", 1751 + "gix-config", 1752 + "gix-credentials", 1753 + "gix-date", 1754 + "gix-diff", 1755 + "gix-dir", 1756 + "gix-discover", 1757 + "gix-error", 1758 + "gix-features", 1759 + "gix-filter", 1760 + "gix-fs", 1761 + "gix-glob", 1762 + "gix-hash", 1763 + "gix-hashtable", 1764 + "gix-ignore", 1765 + "gix-index", 1766 + "gix-lock", 1767 + "gix-mailmap", 1768 + "gix-merge", 1769 + "gix-negotiate", 1770 + "gix-object", 1771 + "gix-odb", 1772 + "gix-pack", 1773 + "gix-path", 1774 + "gix-pathspec", 1775 + "gix-prompt", 1776 + "gix-protocol", 1777 + "gix-ref", 1778 + "gix-refspec", 1779 + "gix-revision", 1780 + "gix-revwalk", 1781 + "gix-sec", 1782 + "gix-shallow", 1783 + "gix-status", 1784 + "gix-submodule", 1785 + "gix-tempfile", 1786 + "gix-trace", 1787 + "gix-traverse", 1788 + "gix-url", 1789 + "gix-utils", 1790 + "gix-validate", 1791 + "gix-worktree", 1792 + "gix-worktree-state", 1793 + "gix-worktree-stream", 1794 + "nonempty", 1795 + "parking_lot", 1796 + "regex", 1797 + "signal-hook", 1798 + "smallvec", 1799 + "thiserror 2.0.18", 1800 + ] 1801 + 1802 + [[package]] 1803 + name = "gix-actor" 1804 + version = "0.41.1" 1805 + source = "registry+https://github.com/rust-lang/crates.io-index" 1806 + checksum = "8bc998b8f746dda8565450d08a63b792ced9165d8c27a1ed3f02799ec6a7820f" 1807 + dependencies = [ 1808 + "bstr", 1809 + "gix-date", 1810 + "gix-error", 1811 + ] 1812 + 1813 + [[package]] 1814 + name = "gix-archive" 1815 + version = "0.33.0" 1816 + source = "registry+https://github.com/rust-lang/crates.io-index" 1817 + checksum = "16909cacc78936ab96f6c3be08379d0a2e88bfa3a7527972d2ed75c7517ef31e" 1818 + dependencies = [ 1819 + "bstr", 1820 + "gix-date", 1821 + "gix-error", 1822 + "gix-object", 1823 + "gix-worktree-stream", 1824 + ] 1825 + 1826 + [[package]] 1827 + name = "gix-attributes" 1828 + version = "0.33.2" 1829 + source = "registry+https://github.com/rust-lang/crates.io-index" 1830 + checksum = "39b40888d0ed415c0744a6cdc61eebf0304c9d26ab726725b718443c322e5ba4" 1831 + dependencies = [ 1832 + "bstr", 1833 + "gix-glob", 1834 + "gix-path", 1835 + "gix-quote", 1836 + "gix-trace", 1837 + "kstring", 1838 + "smallvec", 1839 + "thiserror 2.0.18", 1840 + "unicode-bom", 1841 + ] 1842 + 1843 + [[package]] 1844 + name = "gix-bitmap" 1845 + version = "0.3.2" 1846 + source = "registry+https://github.com/rust-lang/crates.io-index" 1847 + checksum = "52ebef0c26ad305747649e727bbcd56a7b7910754eb7cea88f6dff6f93c51283" 1848 + dependencies = [ 1849 + "gix-error", 1850 + ] 1851 + 1852 + [[package]] 1853 + name = "gix-blame" 1854 + version = "0.14.0" 1855 + source = "registry+https://github.com/rust-lang/crates.io-index" 1856 + checksum = "4d39a0c14af94c2edaa5eefe06d5ef2cdea55316ae9a9321314288e3f55fa4c0" 1857 + dependencies = [ 1858 + "gix-commitgraph", 1859 + "gix-date", 1860 + "gix-diff", 1861 + "gix-error", 1862 + "gix-hash", 1863 + "gix-object", 1864 + "gix-revwalk", 1865 + "gix-trace", 1866 + "gix-traverse", 1867 + "gix-worktree", 1868 + "smallvec", 1869 + "thiserror 2.0.18", 1870 + ] 1871 + 1872 + [[package]] 1873 + name = "gix-chunk" 1874 + version = "0.7.2" 1875 + source = "registry+https://github.com/rust-lang/crates.io-index" 1876 + checksum = "9faee47943b638e58ddd5e275a4906ad3e4b6c8584f1d41bd18ab9032ec52afb" 1877 + dependencies = [ 1878 + "gix-error", 1879 + ] 1880 + 1881 + [[package]] 1882 + name = "gix-command" 1883 + version = "0.9.1" 1884 + source = "registry+https://github.com/rust-lang/crates.io-index" 1885 + checksum = "00706d4fef135ef4b01680d5218c6ee40cda8baf697b864296cbc887d19118f6" 1886 + dependencies = [ 1887 + "bstr", 1888 + "gix-path", 1889 + "gix-quote", 1890 + "gix-trace", 1891 + "shell-words", 1892 + ] 1893 + 1894 + [[package]] 1895 + name = "gix-commitgraph" 1896 + version = "0.37.1" 1897 + source = "registry+https://github.com/rust-lang/crates.io-index" 1898 + checksum = "7f675d0df484a7f6a47e64bd6f311af489d947c0323b0564f36d14f3d7762abb" 1899 + dependencies = [ 1900 + "bstr", 1901 + "gix-chunk", 1902 + "gix-error", 1903 + "gix-hash", 1904 + "memmap2", 1905 + "nonempty", 1906 + ] 1907 + 1908 + [[package]] 1909 + name = "gix-config" 1910 + version = "0.57.0" 1911 + source = "registry+https://github.com/rust-lang/crates.io-index" 1912 + checksum = "4f2372d4b49ca28431e7d150cab9d25edc1890f0184bd57eb0e917c7799e63de" 1913 + dependencies = [ 1914 + "bstr", 1915 + "gix-config-value", 1916 + "gix-features", 1917 + "gix-glob", 1918 + "gix-path", 1919 + "gix-ref", 1920 + "gix-sec", 1921 + "smallvec", 1922 + "thiserror 2.0.18", 1923 + "unicode-bom", 1924 + ] 1925 + 1926 + [[package]] 1927 + name = "gix-config-value" 1928 + version = "0.18.1" 1929 + source = "registry+https://github.com/rust-lang/crates.io-index" 1930 + checksum = "ed42168329552f6c2e5df09665c104199d45d84bedb53683738a49b57fe1baab" 1931 + dependencies = [ 1932 + "bitflags 2.11.1", 1933 + "bstr", 1934 + "gix-path", 1935 + "libc", 1936 + "thiserror 2.0.18", 1937 + ] 1938 + 1939 + [[package]] 1940 + name = "gix-credentials" 1941 + version = "0.38.1" 1942 + source = "registry+https://github.com/rust-lang/crates.io-index" 1943 + checksum = "f40cd22f0dd71988be12d6e78b1709de2370e1957c5f107ff31e56caeba3745d" 1944 + dependencies = [ 1945 + "bstr", 1946 + "gix-command", 1947 + "gix-config-value", 1948 + "gix-date", 1949 + "gix-path", 1950 + "gix-prompt", 1951 + "gix-sec", 1952 + "gix-trace", 1953 + "gix-url", 1954 + "thiserror 2.0.18", 1955 + ] 1956 + 1957 + [[package]] 1958 + name = "gix-date" 1959 + version = "0.15.5" 1960 + source = "registry+https://github.com/rust-lang/crates.io-index" 1961 + checksum = "3d63f9e28b59ddeb1a1eb9e5cf986a9222b5d484947445edbc20473939cc7fd0" 1962 + dependencies = [ 1963 + "bstr", 1964 + "gix-error", 1965 + "itoa", 1966 + "jiff", 1967 + ] 1968 + 1969 + [[package]] 1970 + name = "gix-diff" 1971 + version = "0.64.0" 1972 + source = "registry+https://github.com/rust-lang/crates.io-index" 1973 + checksum = "3b6d9528f32d94cef2edf39a1ac01fe5a0fc44ddbb18d9e44099936047c3302b" 1974 + dependencies = [ 1975 + "bstr", 1976 + "gix-attributes", 1977 + "gix-command", 1978 + "gix-filter", 1979 + "gix-fs", 1980 + "gix-hash", 1981 + "gix-imara-diff", 1982 + "gix-index", 1983 + "gix-object", 1984 + "gix-path", 1985 + "gix-pathspec", 1986 + "gix-tempfile", 1987 + "gix-trace", 1988 + "gix-traverse", 1989 + "gix-worktree", 1990 + "thiserror 2.0.18", 1991 + ] 1992 + 1993 + [[package]] 1994 + name = "gix-dir" 1995 + version = "0.26.0" 1996 + source = "registry+https://github.com/rust-lang/crates.io-index" 1997 + checksum = "21bb2a53a6fd917ec499ed0bfb5b6887de7a15bd79197dcea7c987938749a9f1" 1998 + dependencies = [ 1999 + "bstr", 2000 + "gix-discover", 2001 + "gix-fs", 2002 + "gix-ignore", 2003 + "gix-index", 2004 + "gix-object", 2005 + "gix-path", 2006 + "gix-pathspec", 2007 + "gix-trace", 2008 + "gix-utils", 2009 + "gix-worktree", 2010 + "thiserror 2.0.18", 2011 + ] 2012 + 2013 + [[package]] 2014 + name = "gix-discover" 2015 + version = "0.52.0" 2016 + source = "registry+https://github.com/rust-lang/crates.io-index" 2017 + checksum = "77bacdd12b7879d2178a80c58c2f319995e4654e1a7a23e3181e5c8a12b824f7" 2018 + dependencies = [ 2019 + "bstr", 2020 + "dunce", 2021 + "gix-fs", 2022 + "gix-path", 2023 + "gix-ref", 2024 + "gix-sec", 2025 + "thiserror 2.0.18", 2026 + ] 2027 + 2028 + [[package]] 2029 + name = "gix-error" 2030 + version = "0.2.4" 2031 + source = "registry+https://github.com/rust-lang/crates.io-index" 2032 + checksum = "e57831e199be480af90dcd7e459abed8a174c09ec9a6e2cc8f7ca6c54598b06b" 2033 + dependencies = [ 2034 + "bstr", 2035 + ] 2036 + 2037 + [[package]] 2038 + name = "gix-features" 2039 + version = "0.48.1" 2040 + source = "registry+https://github.com/rust-lang/crates.io-index" 2041 + checksum = "1849ae154d38bc403185be14fa871e38e3c93ee606875d94e207fdb9fba52dbc" 2042 + dependencies = [ 2043 + "bytes", 2044 + "bytesize", 2045 + "crc32fast", 2046 + "crossbeam-channel", 2047 + "gix-path", 2048 + "gix-trace", 2049 + "gix-utils", 2050 + "libc", 2051 + "once_cell", 2052 + "parking_lot", 2053 + "prodash", 2054 + "thiserror 2.0.18", 2055 + "walkdir", 2056 + "zlib-rs", 2057 + ] 2058 + 2059 + [[package]] 2060 + name = "gix-filter" 2061 + version = "0.31.0" 2062 + source = "registry+https://github.com/rust-lang/crates.io-index" 2063 + checksum = "ecf74b7d16f6694ce4a3049074c41be0c7987105743674f1671807bd6dce09fa" 2064 + dependencies = [ 2065 + "bstr", 2066 + "encoding_rs", 2067 + "gix-attributes", 2068 + "gix-command", 2069 + "gix-hash", 2070 + "gix-object", 2071 + "gix-packetline", 2072 + "gix-path", 2073 + "gix-quote", 2074 + "gix-trace", 2075 + "gix-utils", 2076 + "smallvec", 2077 + "thiserror 2.0.18", 2078 + ] 2079 + 2080 + [[package]] 2081 + name = "gix-fs" 2082 + version = "0.21.2" 2083 + source = "registry+https://github.com/rust-lang/crates.io-index" 2084 + checksum = "6cdff46db8798e47e2f727d84b9379aac5add3dd3d9d0b07bb4d7d5d640771fe" 2085 + dependencies = [ 2086 + "bstr", 2087 + "fastrand", 2088 + "gix-features", 2089 + "gix-path", 2090 + "gix-utils", 2091 + "thiserror 2.0.18", 2092 + ] 2093 + 2094 + [[package]] 2095 + name = "gix-glob" 2096 + version = "0.26.1" 2097 + source = "registry+https://github.com/rust-lang/crates.io-index" 2098 + checksum = "d1fcb8ef5b16bcf874abe9b68d8abb3c0493c876d367ab824151f30a0f3f3756" 2099 + dependencies = [ 2100 + "bitflags 2.11.1", 2101 + "bstr", 2102 + "gix-features", 2103 + "gix-path", 2104 + ] 2105 + 2106 + [[package]] 2107 + name = "gix-hash" 2108 + version = "0.25.1" 2109 + source = "registry+https://github.com/rust-lang/crates.io-index" 2110 + checksum = "cb0926d3819c837750b4e03c7754901e73f68b8c9b690753a6372a1bed4eedce" 2111 + dependencies = [ 2112 + "faster-hex", 2113 + "gix-features", 2114 + "sha1-checked", 2115 + "sha2 0.11.0", 2116 + "thiserror 2.0.18", 2117 + ] 2118 + 2119 + [[package]] 2120 + name = "gix-hashtable" 2121 + version = "0.15.2" 2122 + source = "registry+https://github.com/rust-lang/crates.io-index" 2123 + checksum = "7e261d54091f0d1c729bc83f54548c071bdec60a697de1e58e88bdfd7a99d24e" 2124 + dependencies = [ 2125 + "gix-hash", 2126 + "hashbrown 0.17.1", 2127 + "parking_lot", 2128 + ] 2129 + 2130 + [[package]] 2131 + name = "gix-ignore" 2132 + version = "0.21.1" 2133 + source = "registry+https://github.com/rust-lang/crates.io-index" 2134 + checksum = "d491bab9bf2c9f341dc754f425c31d5d3f63aca615312167b82e1deeaca97d8d" 2135 + dependencies = [ 2136 + "bstr", 2137 + "gix-glob", 2138 + "gix-path", 2139 + "gix-trace", 2140 + "unicode-bom", 2141 + ] 2142 + 2143 + [[package]] 2144 + name = "gix-imara-diff" 2145 + version = "0.2.3" 2146 + source = "registry+https://github.com/rust-lang/crates.io-index" 2147 + checksum = "b305d85504de270ad3525d726a6b69cc59ee7b2269b014387651107ab9f0755b" 2148 + dependencies = [ 2149 + "bstr", 2150 + "hashbrown 0.17.1", 2151 + ] 2152 + 2153 + [[package]] 2154 + name = "gix-index" 2155 + version = "0.52.0" 2156 + source = "registry+https://github.com/rust-lang/crates.io-index" 2157 + checksum = "4e6b28cc592dc753adb58302bb14a64e412ee591a3bec77aa4df87bff74fa80d" 2158 + dependencies = [ 2159 + "bitflags 2.11.1", 2160 + "bstr", 2161 + "filetime", 2162 + "fnv", 2163 + "gix-bitmap", 2164 + "gix-features", 2165 + "gix-fs", 2166 + "gix-hash", 2167 + "gix-lock", 2168 + "gix-object", 2169 + "gix-traverse", 2170 + "gix-utils", 2171 + "gix-validate", 2172 + "hashbrown 0.17.1", 2173 + "itoa", 2174 + "libc", 2175 + "memmap2", 2176 + "rustix", 2177 + "smallvec", 2178 + "thiserror 2.0.18", 2179 + ] 2180 + 2181 + [[package]] 2182 + name = "gix-lock" 2183 + version = "23.0.1" 2184 + source = "registry+https://github.com/rust-lang/crates.io-index" 2185 + checksum = "65c9dedd9e90b0d47624d2ed241d394e09294118364e87b9b7e5f1fe755f3c2c" 2186 + dependencies = [ 2187 + "gix-tempfile", 2188 + "gix-utils", 2189 + "thiserror 2.0.18", 2190 + ] 2191 + 2192 + [[package]] 2193 + name = "gix-mailmap" 2194 + version = "0.33.1" 2195 + source = "registry+https://github.com/rust-lang/crates.io-index" 2196 + checksum = "195fd20808055824531be2fd0d34136d900e5fbca3ffb0a3c07e8beeefb9c828" 2197 + dependencies = [ 2198 + "bstr", 2199 + "gix-actor", 2200 + "gix-date", 2201 + "gix-error", 2202 + ] 2203 + 2204 + [[package]] 2205 + name = "gix-merge" 2206 + version = "0.17.0" 2207 + source = "registry+https://github.com/rust-lang/crates.io-index" 2208 + checksum = "7543e1eceb25fbbd1a29459c794148d8bafc1c77555eb386617a2d99b5371971" 2209 + dependencies = [ 2210 + "bstr", 2211 + "gix-command", 2212 + "gix-diff", 2213 + "gix-filter", 2214 + "gix-fs", 2215 + "gix-hash", 2216 + "gix-imara-diff", 2217 + "gix-index", 2218 + "gix-object", 2219 + "gix-path", 2220 + "gix-quote", 2221 + "gix-revision", 2222 + "gix-revwalk", 2223 + "gix-tempfile", 2224 + "gix-trace", 2225 + "gix-worktree", 2226 + "nonempty", 2227 + "thiserror 2.0.18", 2228 + ] 2229 + 2230 + [[package]] 2231 + name = "gix-negotiate" 2232 + version = "0.32.0" 2233 + source = "registry+https://github.com/rust-lang/crates.io-index" 2234 + checksum = "890c936a215bae25818c076cb881cb2e54d2c66ba947ba58b8dd47cff921bf55" 2235 + dependencies = [ 2236 + "bitflags 2.11.1", 2237 + "gix-commitgraph", 2238 + "gix-date", 2239 + "gix-hash", 2240 + "gix-object", 2241 + "gix-revwalk", 2242 + ] 2243 + 2244 + [[package]] 2245 + name = "gix-object" 2246 + version = "0.61.0" 2247 + source = "registry+https://github.com/rust-lang/crates.io-index" 2248 + checksum = "d5cd857e29429c7213bdef3f5aef83f8cc124774fe8ae0d27b1607d218d6d525" 2249 + dependencies = [ 2250 + "bstr", 2251 + "gix-actor", 2252 + "gix-date", 2253 + "gix-features", 2254 + "gix-hash", 2255 + "gix-hashtable", 2256 + "gix-utils", 2257 + "gix-validate", 2258 + "itoa", 2259 + "smallvec", 2260 + "thiserror 2.0.18", 2261 + ] 2262 + 2263 + [[package]] 2264 + name = "gix-odb" 2265 + version = "0.81.0" 2266 + source = "registry+https://github.com/rust-lang/crates.io-index" 2267 + checksum = "7d004c32858b1556f2d7874405edb3c97dc78fc09beaa87d57bb077ee2858a7d" 2268 + dependencies = [ 2269 + "arc-swap", 2270 + "gix-features", 2271 + "gix-fs", 2272 + "gix-hash", 2273 + "gix-hashtable", 2274 + "gix-object", 2275 + "gix-pack", 2276 + "gix-path", 2277 + "gix-quote", 2278 + "memmap2", 2279 + "parking_lot", 2280 + "tempfile", 2281 + "thiserror 2.0.18", 2282 + ] 2283 + 2284 + [[package]] 2285 + name = "gix-pack" 2286 + version = "0.71.0" 2287 + source = "registry+https://github.com/rust-lang/crates.io-index" 2288 + checksum = "e43626f2a27d1033674ec1a196b845614231e6bbd949d5e21c133045ff56b174" 2289 + dependencies = [ 2290 + "clru", 2291 + "gix-chunk", 2292 + "gix-error", 2293 + "gix-features", 2294 + "gix-hash", 2295 + "gix-hashtable", 2296 + "gix-object", 2297 + "gix-path", 2298 + "memmap2", 2299 + "smallvec", 2300 + "thiserror 2.0.18", 2301 + "uluru", 2302 + ] 2303 + 2304 + [[package]] 2305 + name = "gix-packetline" 2306 + version = "0.21.5" 2307 + source = "registry+https://github.com/rust-lang/crates.io-index" 2308 + checksum = "b217dd0ee0c4021ecf169a4a519b1b4f80d15e3f3765f3dc466223dc0ac891d7" 2309 + dependencies = [ 2310 + "bstr", 2311 + "faster-hex", 2312 + "gix-trace", 2313 + "thiserror 2.0.18", 2314 + ] 2315 + 2316 + [[package]] 2317 + name = "gix-path" 2318 + version = "0.12.1" 2319 + source = "registry+https://github.com/rust-lang/crates.io-index" 2320 + checksum = "afa6ac14cd14939ea94a496ce7460daa6511c09f5b84757e9cfc6f9c8d0f93a6" 2321 + dependencies = [ 2322 + "bstr", 2323 + "gix-trace", 2324 + "gix-validate", 2325 + "thiserror 2.0.18", 2326 + ] 2327 + 2328 + [[package]] 2329 + name = "gix-pathspec" 2330 + version = "0.18.1" 2331 + source = "registry+https://github.com/rust-lang/crates.io-index" 2332 + checksum = "3050783b41ee11511e1e8fb35623df81806194f4030395f14f48ea37c2798c9f" 2333 + dependencies = [ 2334 + "bitflags 2.11.1", 2335 + "bstr", 2336 + "gix-attributes", 2337 + "gix-config-value", 2338 + "gix-glob", 2339 + "gix-path", 2340 + "thiserror 2.0.18", 2341 + ] 2342 + 2343 + [[package]] 2344 + name = "gix-prompt" 2345 + version = "0.15.1" 2346 + source = "registry+https://github.com/rust-lang/crates.io-index" 2347 + checksum = "3ee604d7746080ae7e1023bf47204bcc2c5f307bfbe2306a3c90b1bfd1a2c6d8" 2348 + dependencies = [ 2349 + "gix-command", 2350 + "gix-config-value", 2351 + "parking_lot", 2352 + "rustix", 2353 + "thiserror 2.0.18", 2354 + ] 2355 + 2356 + [[package]] 2357 + name = "gix-protocol" 2358 + version = "0.62.0" 2359 + source = "registry+https://github.com/rust-lang/crates.io-index" 2360 + checksum = "51dea3acb390707ab868f1f9584f18449eb95d869deffae96768e47d303595ee" 2361 + dependencies = [ 2362 + "bstr", 2363 + "gix-date", 2364 + "gix-features", 2365 + "gix-hash", 2366 + "gix-ref", 2367 + "gix-shallow", 2368 + "gix-transport", 2369 + "gix-utils", 2370 + "maybe-async", 2371 + "nonempty", 2372 + "thiserror 2.0.18", 2373 + ] 2374 + 2375 + [[package]] 2376 + name = "gix-quote" 2377 + version = "0.7.2" 2378 + source = "registry+https://github.com/rust-lang/crates.io-index" 2379 + checksum = "a6e541fc33cc2b783b7979040d445a0c86a2eca747c8faea4ca84230d06ae6ef" 2380 + dependencies = [ 2381 + "bstr", 2382 + "gix-error", 2383 + "gix-utils", 2384 + ] 2385 + 2386 + [[package]] 2387 + name = "gix-ref" 2388 + version = "0.64.0" 2389 + source = "registry+https://github.com/rust-lang/crates.io-index" 2390 + checksum = "4c04f64c37eb7e6feb73c7060f8dc6f381cc5de5d53249bfd450bc48a86b2e8b" 2391 + dependencies = [ 2392 + "gix-actor", 2393 + "gix-features", 2394 + "gix-fs", 2395 + "gix-hash", 2396 + "gix-lock", 2397 + "gix-object", 2398 + "gix-path", 2399 + "gix-tempfile", 2400 + "gix-utils", 2401 + "gix-validate", 2402 + "memmap2", 2403 + "thiserror 2.0.18", 2404 + ] 2405 + 2406 + [[package]] 2407 + name = "gix-refspec" 2408 + version = "0.42.0" 2409 + source = "registry+https://github.com/rust-lang/crates.io-index" 2410 + checksum = "b216ae06ec74b5f24ad0142026a997fb0a935b7410eaf9c1616fc3f0e6c5a6d3" 2411 + dependencies = [ 2412 + "bstr", 2413 + "gix-error", 2414 + "gix-glob", 2415 + "gix-hash", 2416 + "gix-revision", 2417 + "gix-validate", 2418 + "smallvec", 2419 + "thiserror 2.0.18", 2420 + ] 2421 + 2422 + [[package]] 2423 + name = "gix-revision" 2424 + version = "0.46.0" 2425 + source = "registry+https://github.com/rust-lang/crates.io-index" 2426 + checksum = "0b47c88884dd3c1a19a39da19d10211fcdea2809aadc86869b6e824a1774340f" 2427 + dependencies = [ 2428 + "bitflags 2.11.1", 2429 + "bstr", 2430 + "gix-commitgraph", 2431 + "gix-date", 2432 + "gix-error", 2433 + "gix-hash", 2434 + "gix-hashtable", 2435 + "gix-object", 2436 + "gix-revwalk", 2437 + "gix-trace", 2438 + "nonempty", 2439 + ] 2440 + 2441 + [[package]] 2442 + name = "gix-revwalk" 2443 + version = "0.32.0" 2444 + source = "registry+https://github.com/rust-lang/crates.io-index" 2445 + checksum = "85f5756abffe0917827aac683b13684ed99875bc398fa1f9b8f479b0681ef9e6" 2446 + dependencies = [ 2447 + "gix-commitgraph", 2448 + "gix-date", 2449 + "gix-error", 2450 + "gix-hash", 2451 + "gix-hashtable", 2452 + "gix-object", 2453 + "smallvec", 2454 + "thiserror 2.0.18", 2455 + ] 2456 + 2457 + [[package]] 2458 + name = "gix-sec" 2459 + version = "0.14.1" 2460 + source = "registry+https://github.com/rust-lang/crates.io-index" 2461 + checksum = "ab8519976e4c7e486270740a5400369f37940779b80bd1377d94cfa1125d01b3" 2462 + dependencies = [ 2463 + "bitflags 2.11.1", 2464 + "gix-path", 2465 + "libc", 2466 + "windows-sys 0.61.2", 2467 + ] 2468 + 2469 + [[package]] 2470 + name = "gix-shallow" 2471 + version = "0.12.1" 2472 + source = "registry+https://github.com/rust-lang/crates.io-index" 2473 + checksum = "a292fc2fe548c5dfa575479d16b445b0ddf1dd2f56f1fec6aed386f82553cd97" 2474 + dependencies = [ 2475 + "bstr", 2476 + "gix-hash", 2477 + "gix-lock", 2478 + "nonempty", 2479 + "thiserror 2.0.18", 2480 + ] 2481 + 2482 + [[package]] 2483 + name = "gix-status" 2484 + version = "0.31.0" 2485 + source = "registry+https://github.com/rust-lang/crates.io-index" 2486 + checksum = "22042e385d28a34275e029d98f4970285045be14b9073658ca897923f2ed8700" 2487 + dependencies = [ 2488 + "bstr", 2489 + "filetime", 2490 + "gix-diff", 2491 + "gix-dir", 2492 + "gix-features", 2493 + "gix-filter", 2494 + "gix-fs", 2495 + "gix-hash", 2496 + "gix-index", 2497 + "gix-object", 2498 + "gix-path", 2499 + "gix-pathspec", 2500 + "gix-worktree", 2501 + "portable-atomic", 2502 + "thiserror 2.0.18", 2503 + ] 2504 + 2505 + [[package]] 2506 + name = "gix-submodule" 2507 + version = "0.31.0" 2508 + source = "registry+https://github.com/rust-lang/crates.io-index" 2509 + checksum = "3059890ef054066c22a94bfc6a3eaba0d806aedcd630a0bc9e5783fd88884781" 2510 + dependencies = [ 2511 + "bstr", 2512 + "gix-config", 2513 + "gix-path", 2514 + "gix-pathspec", 2515 + "gix-refspec", 2516 + "gix-url", 2517 + "thiserror 2.0.18", 2518 + ] 2519 + 2520 + [[package]] 2521 + name = "gix-tempfile" 2522 + version = "23.0.2" 2523 + source = "registry+https://github.com/rust-lang/crates.io-index" 2524 + checksum = "6ef60812443484e67bf84e444cc71b4c78ae62deb822221774a4fa0c57fdb17f" 2525 + dependencies = [ 2526 + "dashmap", 2527 + "gix-fs", 2528 + "libc", 2529 + "parking_lot", 2530 + "signal-hook", 2531 + "signal-hook-registry", 2532 + "tempfile", 2533 + ] 2534 + 2535 + [[package]] 2536 + name = "gix-trace" 2537 + version = "0.1.20" 2538 + source = "registry+https://github.com/rust-lang/crates.io-index" 2539 + checksum = "44dc45eae785c0eb14173e0f152e6e224dcf4d45b6a6999a3aed22af541ad678" 2540 + 2541 + [[package]] 2542 + name = "gix-transport" 2543 + version = "0.57.2" 2544 + source = "registry+https://github.com/rust-lang/crates.io-index" 2545 + checksum = "186874f7ad1fb2f9a2f2aa9c2dabc7f9dd087bef74c1a0eee2b4a9cf0248fcb3" 2546 + dependencies = [ 2547 + "bstr", 2548 + "gix-command", 2549 + "gix-features", 2550 + "gix-packetline", 2551 + "gix-quote", 2552 + "gix-sec", 2553 + "gix-url", 2554 + "thiserror 2.0.18", 2555 + ] 2556 + 2557 + [[package]] 2558 + name = "gix-traverse" 2559 + version = "0.58.0" 2560 + source = "registry+https://github.com/rust-lang/crates.io-index" 2561 + checksum = "e8de590ecc86a3b2870665f2288324fa9f7f8672c7fc2d4e020fdd81cd1f7aed" 2562 + dependencies = [ 2563 + "bitflags 2.11.1", 2564 + "gix-commitgraph", 2565 + "gix-date", 2566 + "gix-hash", 2567 + "gix-hashtable", 2568 + "gix-object", 2569 + "gix-revwalk", 2570 + "smallvec", 2571 + "thiserror 2.0.18", 2572 + ] 2573 + 2574 + [[package]] 2575 + name = "gix-url" 2576 + version = "0.36.1" 2577 + source = "registry+https://github.com/rust-lang/crates.io-index" 2578 + checksum = "65bb01ec69d55e82ccb7a19e264501ead4e6aac38463a8cebfdd81e22bb67ab2" 2579 + dependencies = [ 2580 + "bstr", 2581 + "gix-path", 2582 + "percent-encoding", 2583 + "thiserror 2.0.18", 2584 + ] 2585 + 2586 + [[package]] 2587 + name = "gix-utils" 2588 + version = "0.3.3" 2589 + source = "registry+https://github.com/rust-lang/crates.io-index" 2590 + checksum = "66c50966184123caf580ffa64e28031a878597f1c7fceb8fe19566c38eb1b771" 2591 + dependencies = [ 2592 + "bstr", 2593 + "fastrand", 2594 + "unicode-normalization", 2595 + ] 2596 + 2597 + [[package]] 2598 + name = "gix-validate" 2599 + version = "0.11.2" 2600 + source = "registry+https://github.com/rust-lang/crates.io-index" 2601 + checksum = "7bc6fc771c4063ba7cd2f47b91fb6076251c6a823b64b7fe7b8874b0fe4afae3" 2602 + dependencies = [ 2603 + "bstr", 2604 + ] 2605 + 2606 + [[package]] 2607 + name = "gix-worktree" 2608 + version = "0.53.0" 2609 + source = "registry+https://github.com/rust-lang/crates.io-index" 2610 + checksum = "cef414ed275e8407cd5d53d301e83be19700b0dd3f859d2434417b58f454a2d1" 2611 + dependencies = [ 2612 + "bstr", 2613 + "gix-attributes", 2614 + "gix-fs", 2615 + "gix-glob", 2616 + "gix-hash", 2617 + "gix-ignore", 2618 + "gix-index", 2619 + "gix-object", 2620 + "gix-path", 2621 + "gix-validate", 2622 + ] 2623 + 2624 + [[package]] 2625 + name = "gix-worktree-state" 2626 + version = "0.31.0" 2627 + source = "registry+https://github.com/rust-lang/crates.io-index" 2628 + checksum = "4bffae8b3ca258fdd50370cd51f06deb4c76a3b43db3868bc28dde45ffa77d69" 2629 + dependencies = [ 2630 + "bstr", 2631 + "gix-features", 2632 + "gix-filter", 2633 + "gix-fs", 2634 + "gix-index", 2635 + "gix-object", 2636 + "gix-path", 2637 + "gix-worktree", 2638 + "io-close", 2639 + "thiserror 2.0.18", 2640 + ] 2641 + 2642 + [[package]] 2643 + name = "gix-worktree-stream" 2644 + version = "0.33.0" 2645 + source = "registry+https://github.com/rust-lang/crates.io-index" 2646 + checksum = "d25e9ed30100c63f7590bc581c225e53f731a53e06aa79a245739c07f7dcc557" 1577 2647 dependencies = [ 1578 - "cfg-if", 1579 - "libc", 1580 - "r-efi 6.0.0", 1581 - "wasip2", 1582 - "wasip3", 2648 + "gix-attributes", 2649 + "gix-error", 2650 + "gix-features", 2651 + "gix-filter", 2652 + "gix-fs", 2653 + "gix-hash", 2654 + "gix-object", 2655 + "gix-path", 2656 + "gix-traverse", 2657 + "parking_lot", 1583 2658 ] 1584 2659 1585 2660 [[package]] ··· 1632 2707 "byteorder", 1633 2708 ] 1634 2709 2710 + [[package]] 2711 + name = "hash32" 2712 + version = "0.3.1" 2713 + source = "registry+https://github.com/rust-lang/crates.io-index" 2714 + checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" 2715 + dependencies = [ 2716 + "byteorder", 2717 + ] 2718 + 1635 2719 [[package]] 1636 2720 name = "hashbrown" 1637 2721 version = "0.14.5" ··· 1669 2753 version = "0.17.1" 1670 2754 source = "registry+https://github.com/rust-lang/crates.io-index" 1671 2755 checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" 2756 + dependencies = [ 2757 + "allocator-api2", 2758 + "equivalent", 2759 + "foldhash 0.2.0", 2760 + ] 1672 2761 1673 2762 [[package]] 1674 2763 name = "heapless" ··· 1677 2766 checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" 1678 2767 dependencies = [ 1679 2768 "atomic-polyfill", 1680 - "hash32", 2769 + "hash32 0.2.1", 1681 2770 "rustc_version", 1682 2771 "serde", 1683 2772 "spin 0.9.8", 1684 2773 "stable_deref_trait", 1685 2774 ] 1686 2775 2776 + [[package]] 2777 + name = "heapless" 2778 + version = "0.8.0" 2779 + source = "registry+https://github.com/rust-lang/crates.io-index" 2780 + checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" 2781 + dependencies = [ 2782 + "hash32 0.3.1", 2783 + "stable_deref_trait", 2784 + ] 2785 + 1687 2786 [[package]] 1688 2787 name = "heck" 1689 2788 version = "0.4.1" ··· 1714 2813 source = "registry+https://github.com/rust-lang/crates.io-index" 1715 2814 checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 1716 2815 dependencies = [ 1717 - "digest", 2816 + "digest 0.10.7", 1718 2817 ] 1719 2818 1720 2819 [[package]] ··· 1768 2867 source = "registry+https://github.com/rust-lang/crates.io-index" 1769 2868 checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 1770 2869 2870 + [[package]] 2871 + name = "human_format" 2872 + version = "1.2.1" 2873 + source = "registry+https://github.com/rust-lang/crates.io-index" 2874 + checksum = "eaec953f16e5bcf6b8a3cb3aa959b17e5577dbd2693e94554c462c08be22624b" 2875 + 2876 + [[package]] 2877 + name = "hybrid-array" 2878 + version = "0.4.12" 2879 + source = "registry+https://github.com/rust-lang/crates.io-index" 2880 + checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" 2881 + dependencies = [ 2882 + "typenum", 2883 + ] 2884 + 1771 2885 [[package]] 1772 2886 name = "hyper" 1773 2887 version = "1.10.0" ··· 1806 2920 "webpki-roots 1.0.7", 1807 2921 ] 1808 2922 2923 + [[package]] 2924 + name = "hyper-timeout" 2925 + version = "0.5.2" 2926 + source = "registry+https://github.com/rust-lang/crates.io-index" 2927 + checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" 2928 + dependencies = [ 2929 + "hyper", 2930 + "hyper-util", 2931 + "pin-project-lite", 2932 + "tokio", 2933 + "tower-service", 2934 + ] 2935 + 1809 2936 [[package]] 1810 2937 name = "hyper-util" 1811 2938 version = "0.1.20" ··· 1991 3118 "rustversion", 1992 3119 ] 1993 3120 3121 + [[package]] 3122 + name = "io-close" 3123 + version = "0.3.7" 3124 + source = "registry+https://github.com/rust-lang/crates.io-index" 3125 + checksum = "9cadcf447f06744f8ce713d2d6239bb5bde2c357a452397a9ed90c625da390bc" 3126 + dependencies = [ 3127 + "libc", 3128 + "winapi", 3129 + ] 3130 + 1994 3131 [[package]] 1995 3132 name = "ipld-core" 1996 3133 version = "0.4.3" ··· 2126 3263 "serde_path_to_error", 2127 3264 "serde_repr", 2128 3265 "serde_with", 2129 - "sha2", 3266 + "sha2 0.10.9", 2130 3267 "syn", 2131 3268 "thiserror 2.0.18", 2132 3269 "unicode-segmentation", 2133 3270 ] 2134 3271 3272 + [[package]] 3273 + name = "jiff" 3274 + version = "0.2.29" 3275 + source = "registry+https://github.com/rust-lang/crates.io-index" 3276 + checksum = "34f877a98676d2fb664698d74cc6a51ce6c484ce8c770f05d0108ec9090aeb46" 3277 + dependencies = [ 3278 + "defmt", 3279 + "jiff-static", 3280 + "jiff-tzdb-platform", 3281 + "log", 3282 + "portable-atomic", 3283 + "portable-atomic-util", 3284 + "serde_core", 3285 + "windows-link", 3286 + ] 3287 + 3288 + [[package]] 3289 + name = "jiff-static" 3290 + version = "0.2.29" 3291 + source = "registry+https://github.com/rust-lang/crates.io-index" 3292 + checksum = "0666b5ab5ecaca213fc2a85b8c0083d9004e84ee2d5f9a7e0017aaf50986f25f" 3293 + dependencies = [ 3294 + "proc-macro2", 3295 + "quote", 3296 + "syn", 3297 + ] 3298 + 3299 + [[package]] 3300 + name = "jiff-tzdb" 3301 + version = "0.1.6" 3302 + source = "registry+https://github.com/rust-lang/crates.io-index" 3303 + checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076" 3304 + 3305 + [[package]] 3306 + name = "jiff-tzdb-platform" 3307 + version = "0.1.3" 3308 + source = "registry+https://github.com/rust-lang/crates.io-index" 3309 + checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" 3310 + dependencies = [ 3311 + "jiff-tzdb", 3312 + ] 3313 + 2135 3314 [[package]] 2136 3315 name = "jobserver" 2137 3316 version = "0.1.34" ··· 2163 3342 "cfg-if", 2164 3343 "ecdsa", 2165 3344 "elliptic-curve", 2166 - "sha2", 3345 + "sha2 0.10.9", 3346 + ] 3347 + 3348 + [[package]] 3349 + name = "kstring" 3350 + version = "2.0.2" 3351 + source = "registry+https://github.com/rust-lang/crates.io-index" 3352 + checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" 3353 + dependencies = [ 3354 + "static_assertions", 2167 3355 ] 2168 3356 2169 3357 [[package]] ··· 2200 3388 source = "registry+https://github.com/rust-lang/crates.io-index" 2201 3389 checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" 2202 3390 3391 + [[package]] 3392 + name = "line-numbers" 3393 + version = "0.4.0" 3394 + source = "registry+https://github.com/rust-lang/crates.io-index" 3395 + checksum = "24ab6409a2f9ffae158ae823a012a7d7d68386663a14bedc260ca66eacf65c2c" 3396 + 2203 3397 [[package]] 2204 3398 name = "linux-raw-sys" 2205 3399 version = "0.12.1" ··· 2300 3494 source = "registry+https://github.com/rust-lang/crates.io-index" 2301 3495 checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" 2302 3496 3497 + [[package]] 3498 + name = "maybe-async" 3499 + version = "0.2.11" 3500 + source = "registry+https://github.com/rust-lang/crates.io-index" 3501 + checksum = "746873a384ad60adc5db74471dfaba74bd278afbdcfd81db93fafcdfc8b5ca0c" 3502 + dependencies = [ 3503 + "proc-macro2", 3504 + "quote", 3505 + "syn", 3506 + ] 3507 + 2303 3508 [[package]] 2304 3509 name = "measure_time" 2305 3510 version = "0.9.0" ··· 2464 3669 source = "registry+https://github.com/rust-lang/crates.io-index" 2465 3670 checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" 2466 3671 dependencies = [ 2467 - "bitflags", 3672 + "bitflags 2.11.1", 2468 3673 "cfg-if", 2469 3674 "cfg_aliases", 2470 3675 "libc", ··· 2481 3686 "minimal-lexical", 2482 3687 ] 2483 3688 3689 + [[package]] 3690 + name = "nonempty" 3691 + version = "0.12.0" 3692 + source = "registry+https://github.com/rust-lang/crates.io-index" 3693 + checksum = "9737e026353e5cd0736f98eddae28665118eb6f6600902a7f50db585621fecb6" 3694 + 2484 3695 [[package]] 2485 3696 name = "nu-ansi-term" 2486 3697 version = "0.50.3" ··· 2614 3825 "ecdsa", 2615 3826 "elliptic-curve", 2616 3827 "primeorder", 2617 - "sha2", 3828 + "sha2 0.10.9", 2618 3829 ] 2619 3830 2620 3831 [[package]] ··· 2778 3989 source = "registry+https://github.com/rust-lang/crates.io-index" 2779 3990 checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" 2780 3991 3992 + [[package]] 3993 + name = "portable-atomic-util" 3994 + version = "0.2.7" 3995 + source = "registry+https://github.com/rust-lang/crates.io-index" 3996 + checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" 3997 + dependencies = [ 3998 + "portable-atomic", 3999 + ] 4000 + 2781 4001 [[package]] 2782 4002 name = "postcard" 2783 4003 version = "1.1.3" ··· 2787 4007 "cobs", 2788 4008 "embedded-io 0.4.0", 2789 4009 "embedded-io 0.6.1", 2790 - "heapless", 4010 + "heapless 0.7.17", 2791 4011 "serde", 2792 4012 ] 2793 4013 ··· 2834 4054 "elliptic-curve", 2835 4055 ] 2836 4056 4057 + [[package]] 4058 + name = "proc-macro-error-attr2" 4059 + version = "2.0.0" 4060 + source = "registry+https://github.com/rust-lang/crates.io-index" 4061 + checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" 4062 + dependencies = [ 4063 + "proc-macro2", 4064 + "quote", 4065 + ] 4066 + 4067 + [[package]] 4068 + name = "proc-macro-error2" 4069 + version = "2.0.1" 4070 + source = "registry+https://github.com/rust-lang/crates.io-index" 4071 + checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" 4072 + dependencies = [ 4073 + "proc-macro-error-attr2", 4074 + "proc-macro2", 4075 + "quote", 4076 + "syn", 4077 + ] 4078 + 2837 4079 [[package]] 2838 4080 name = "proc-macro2" 2839 4081 version = "1.0.106" ··· 2856 4098 "yansi", 2857 4099 ] 2858 4100 4101 + [[package]] 4102 + name = "prodash" 4103 + version = "31.0.0" 4104 + source = "registry+https://github.com/rust-lang/crates.io-index" 4105 + checksum = "962200e2d7d551451297d9fdce85138374019ada198e30ea9ede38034e27604c" 4106 + dependencies = [ 4107 + "bytesize", 4108 + "human_format", 4109 + "parking_lot", 4110 + ] 4111 + 2859 4112 [[package]] 2860 4113 name = "prost" 2861 - version = "0.14.3" 4114 + version = "0.14.4" 2862 4115 source = "registry+https://github.com/rust-lang/crates.io-index" 2863 - checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" 4116 + checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" 2864 4117 dependencies = [ 2865 4118 "bytes", 2866 4119 "prost-derive", ··· 2880 4133 "prettyplease", 2881 4134 "prost", 2882 4135 "prost-types", 4136 + "pulldown-cmark", 4137 + "pulldown-cmark-to-cmark", 2883 4138 "regex", 2884 4139 "syn", 2885 4140 "tempfile", ··· 2887 4142 2888 4143 [[package]] 2889 4144 name = "prost-derive" 2890 - version = "0.14.3" 4145 + version = "0.14.4" 2891 4146 source = "registry+https://github.com/rust-lang/crates.io-index" 2892 - checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" 4147 + checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" 2893 4148 dependencies = [ 2894 4149 "anyhow", 2895 4150 "itertools 0.14.0", ··· 2967 4222 2968 4223 [[package]] 2969 4224 name = "prost-types" 2970 - version = "0.14.3" 4225 + version = "0.14.4" 2971 4226 source = "registry+https://github.com/rust-lang/crates.io-index" 2972 - checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" 4227 + checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" 2973 4228 dependencies = [ 2974 4229 "prost", 2975 4230 ] 2976 4231 4232 + [[package]] 4233 + name = "pulldown-cmark" 4234 + version = "0.13.4" 4235 + source = "registry+https://github.com/rust-lang/crates.io-index" 4236 + checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" 4237 + dependencies = [ 4238 + "bitflags 2.11.1", 4239 + "memchr", 4240 + "unicase", 4241 + ] 4242 + 4243 + [[package]] 4244 + name = "pulldown-cmark-to-cmark" 4245 + version = "22.0.0" 4246 + source = "registry+https://github.com/rust-lang/crates.io-index" 4247 + checksum = "50793def1b900256624a709439404384204a5dc3a6ec580281bfaac35e882e90" 4248 + dependencies = [ 4249 + "pulldown-cmark", 4250 + ] 4251 + 2977 4252 [[package]] 2978 4253 name = "quick_cache" 2979 4254 version = "0.6.22" ··· 3147 4422 source = "registry+https://github.com/rust-lang/crates.io-index" 3148 4423 checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" 3149 4424 dependencies = [ 3150 - "bitflags", 4425 + "bitflags 2.11.1", 3151 4426 ] 3152 4427 3153 4428 [[package]] ··· 3304 4579 source = "registry+https://github.com/rust-lang/crates.io-index" 3305 4580 checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" 3306 4581 dependencies = [ 3307 - "bitflags", 4582 + "bitflags 2.11.1", 3308 4583 "errno", 3309 4584 "libc", 3310 4585 "linux-raw-sys", ··· 3445 4720 source = "registry+https://github.com/rust-lang/crates.io-index" 3446 4721 checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" 3447 4722 dependencies = [ 3448 - "bitflags", 4723 + "bitflags 2.11.1", 3449 4724 "core-foundation 0.10.1", 3450 4725 "core-foundation-sys", 3451 4726 "libc", ··· 3639 4914 checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 3640 4915 dependencies = [ 3641 4916 "cfg-if", 3642 - "cpufeatures", 3643 - "digest", 4917 + "cpufeatures 0.2.17", 4918 + "digest 0.10.7", 4919 + ] 4920 + 4921 + [[package]] 4922 + name = "sha1-checked" 4923 + version = "0.10.0" 4924 + source = "registry+https://github.com/rust-lang/crates.io-index" 4925 + checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" 4926 + dependencies = [ 4927 + "digest 0.10.7", 4928 + "sha1", 3644 4929 ] 3645 4930 3646 4931 [[package]] ··· 3650 4935 checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" 3651 4936 dependencies = [ 3652 4937 "cfg-if", 3653 - "cpufeatures", 3654 - "digest", 4938 + "cpufeatures 0.2.17", 4939 + "digest 0.10.7", 4940 + ] 4941 + 4942 + [[package]] 4943 + name = "sha2" 4944 + version = "0.11.0" 4945 + source = "registry+https://github.com/rust-lang/crates.io-index" 4946 + checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" 4947 + dependencies = [ 4948 + "cfg-if", 4949 + "cpufeatures 0.3.0", 4950 + "digest 0.11.3", 3655 4951 ] 3656 4952 3657 4953 [[package]] ··· 3663 4959 "lazy_static", 3664 4960 ] 3665 4961 4962 + [[package]] 4963 + name = "shell-words" 4964 + version = "1.1.1" 4965 + source = "registry+https://github.com/rust-lang/crates.io-index" 4966 + checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" 4967 + 3666 4968 [[package]] 3667 4969 name = "shlex" 3668 4970 version = "1.3.0" ··· 3689 4991 "tracing-subscriber", 3690 4992 ] 3691 4993 4994 + [[package]] 4995 + name = "signal-hook" 4996 + version = "0.4.4" 4997 + source = "registry+https://github.com/rust-lang/crates.io-index" 4998 + checksum = "b2a0c28ca5908dbdbcd52e6fdaa00358ab88637f8ab33e1f188dd510eb44b53d" 4999 + dependencies = [ 5000 + "libc", 5001 + "signal-hook-registry", 5002 + ] 5003 + 3692 5004 [[package]] 3693 5005 name = "signal-hook-registry" 3694 5006 version = "1.4.8" ··· 3705 5017 source = "registry+https://github.com/rust-lang/crates.io-index" 3706 5018 checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" 3707 5019 dependencies = [ 3708 - "digest", 5020 + "digest 0.10.7", 3709 5021 "rand_core 0.6.4", 3710 5022 ] 3711 5023 ··· 3848 5160 source = "registry+https://github.com/rust-lang/crates.io-index" 3849 5161 checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" 3850 5162 dependencies = [ 3851 - "bitflags", 5163 + "bitflags 2.11.1", 3852 5164 "core-foundation 0.9.4", 3853 5165 "system-configuration-sys", 3854 5166 ] ··· 4216 5528 "futures-core", 4217 5529 "pin-project-lite", 4218 5530 "tokio", 5531 + "tokio-util", 4219 5532 ] 4220 5533 4221 5534 [[package]] ··· 4335 5648 source = "registry+https://github.com/rust-lang/crates.io-index" 4336 5649 checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" 4337 5650 5651 + [[package]] 5652 + name = "tonic" 5653 + version = "0.14.6" 5654 + source = "registry+https://github.com/rust-lang/crates.io-index" 5655 + checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" 5656 + dependencies = [ 5657 + "async-trait", 5658 + "axum", 5659 + "base64", 5660 + "bytes", 5661 + "h2", 5662 + "http", 5663 + "http-body", 5664 + "http-body-util", 5665 + "hyper", 5666 + "hyper-timeout", 5667 + "hyper-util", 5668 + "percent-encoding", 5669 + "pin-project", 5670 + "socket2", 5671 + "sync_wrapper", 5672 + "tokio", 5673 + "tokio-stream", 5674 + "tower", 5675 + "tower-layer", 5676 + "tower-service", 5677 + "tracing", 5678 + ] 5679 + 5680 + [[package]] 5681 + name = "tonic-build" 5682 + version = "0.14.6" 5683 + source = "registry+https://github.com/rust-lang/crates.io-index" 5684 + checksum = "c68f61875ac5293cf72e6c8cf0158086428c82c37229e98c840878f1706b0322" 5685 + dependencies = [ 5686 + "prettyplease", 5687 + "proc-macro2", 5688 + "quote", 5689 + "syn", 5690 + ] 5691 + 5692 + [[package]] 5693 + name = "tonic-prost" 5694 + version = "0.14.6" 5695 + source = "registry+https://github.com/rust-lang/crates.io-index" 5696 + checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" 5697 + dependencies = [ 5698 + "bytes", 5699 + "prost", 5700 + "tonic", 5701 + ] 5702 + 5703 + [[package]] 5704 + name = "tonic-prost-build" 5705 + version = "0.14.6" 5706 + source = "registry+https://github.com/rust-lang/crates.io-index" 5707 + checksum = "654e5643eff75d7f8c99197ce1440ed19a3474eada74c12bbac488b2cafdae27" 5708 + dependencies = [ 5709 + "prettyplease", 5710 + "proc-macro2", 5711 + "prost-build", 5712 + "prost-types", 5713 + "quote", 5714 + "syn", 5715 + "tempfile", 5716 + "tonic-build", 5717 + ] 5718 + 4338 5719 [[package]] 4339 5720 name = "tower" 4340 5721 version = "0.5.3" ··· 4343 5724 dependencies = [ 4344 5725 "futures-core", 4345 5726 "futures-util", 5727 + "indexmap", 4346 5728 "pin-project-lite", 5729 + "slab", 4347 5730 "sync_wrapper", 4348 5731 "tokio", 5732 + "tokio-util", 4349 5733 "tower-layer", 4350 5734 "tower-service", 4351 5735 "tracing", ··· 4358 5742 checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" 4359 5743 dependencies = [ 4360 5744 "async-compression", 4361 - "bitflags", 5745 + "bitflags 2.11.1", 4362 5746 "bytes", 4363 5747 "futures-core", 4364 5748 "futures-util", ··· 4559 5943 "syn", 4560 5944 ] 4561 5945 5946 + [[package]] 5947 + name = "uluru" 5948 + version = "3.1.0" 5949 + source = "registry+https://github.com/rust-lang/crates.io-index" 5950 + checksum = "7c8a2469e56e6e5095c82ccd3afb98dad95f7af7929aab6d8ba8d6e0f73657da" 5951 + dependencies = [ 5952 + "arrayvec", 5953 + ] 5954 + 5955 + [[package]] 5956 + name = "unicase" 5957 + version = "2.9.0" 5958 + source = "registry+https://github.com/rust-lang/crates.io-index" 5959 + checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" 5960 + 5961 + [[package]] 5962 + name = "unicode-bom" 5963 + version = "2.0.3" 5964 + source = "registry+https://github.com/rust-lang/crates.io-index" 5965 + checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" 5966 + 4562 5967 [[package]] 4563 5968 name = "unicode-ident" 4564 5969 version = "1.0.24" 4565 5970 source = "registry+https://github.com/rust-lang/crates.io-index" 4566 5971 checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" 4567 5972 5973 + [[package]] 5974 + name = "unicode-normalization" 5975 + version = "0.1.25" 5976 + source = "registry+https://github.com/rust-lang/crates.io-index" 5977 + checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" 5978 + dependencies = [ 5979 + "tinyvec", 5980 + ] 5981 + 4568 5982 [[package]] 4569 5983 name = "unicode-segmentation" 4570 5984 version = "1.13.2" ··· 4805 6219 source = "registry+https://github.com/rust-lang/crates.io-index" 4806 6220 checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" 4807 6221 dependencies = [ 4808 - "bitflags", 6222 + "bitflags 2.11.1", 4809 6223 "hashbrown 0.15.5", 4810 6224 "indexmap", 4811 6225 "semver", ··· 5214 6628 checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" 5215 6629 dependencies = [ 5216 6630 "anyhow", 5217 - "bitflags", 6631 + "bitflags 2.11.1", 5218 6632 "indexmap", 5219 6633 "log", 5220 6634 "serde", ··· 5359 6773 "syn", 5360 6774 ] 5361 6775 6776 + [[package]] 6777 + name = "zlib-rs" 6778 + version = "0.6.4" 6779 + source = "registry+https://github.com/rust-lang/crates.io-index" 6780 + checksum = "977347db8caa080403f6b6b7c1cda9479a8e869316f7e13a59b19076a40f94e3" 6781 + 5362 6782 [[package]] 5363 6783 name = "zmij" 5364 6784 version = "1.0.21"
+6 -1
Cargo.toml
··· 1 1 [workspace] 2 2 resolver = "2" 3 - members = ["bobbin/crates/*", "shuttle"] 3 + members = ["bobbin/crates/*", "shuttle", "gitmirror"] 4 4 exclude = ["sites"] 5 5 6 6 [workspace.package] ··· 85 85 tracing = "0.1" 86 86 tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "json"] } 87 87 88 + tonic = "0.14" 89 + tonic-prost = "0.14" 90 + tonic-prost-build = "0.14" 91 + prost = "0.14" 92 + 88 93 thiserror = "2" 89 94 90 95 confique = { version = "0.4", default-features = false, features = ["toml"] }
+29 -18
docker-compose.yml
··· 284 284 condition: service_started 285 285 networks: [tngl] 286 286 287 + gitmirror: 288 + build: 289 + context: . 290 + dockerfile: localinfra/gitmirror.Dockerfile 291 + restart: unless-stopped 292 + environment: 293 + RUST_LOG: gitmirror=debug 294 + GITMIRROR_ADDR: 0.0.0.0:9000 295 + GITMIRROR_REPO_BASE: /data/repos 296 + volumes: 297 + - knotmirror-data:/data 298 + - ./localinfra/certs/root.crt:/usr/local/share/ca-certificates/caddy.crt:ro 299 + networks: [tngl] 300 + 287 301 zoekt-webserver: 288 302 build: 289 303 context: https://tangled.org/boltless.me/zoekt.git#tngl ··· 312 326 313 327 314 328 329 + working_dir: /build 330 + init: true 331 + volumes: 332 + - .:/build:ro 333 + - ./appview/pages/static:/build/appview/pages/static 334 + command: 335 + ["-i", "input.css", "-o", "appview/pages/static/tw.css", "--watch=always"] 315 336 316 337 317 338 ··· 324 345 325 346 326 347 348 + TANGLED_JETSTREAM_ENDPOINT: wss://jetstream.tngl.boltless.dev/subscribe 349 + TANGLED_REDIS_ADDR: redis:6379 350 + TANGLED_KNOTMIRROR_URL: https://mirror.tngl.boltless.dev 351 + TANGLED_KNOTMIRROR_V2_HOST: gitmirror:9000 352 + TANGLED_CODESEARCH_ZOEKT_URL: https://zoekt.tngl.boltless.dev 353 + TANGLED_SSH_ENABLED: "true" 354 + TANGLED_SSH_LISTEN_ADDR: "0.0.0.0:3333" 327 355 328 356 329 357 ··· 376 404 377 405 378 406 379 - 380 - 381 - 382 - 383 - 384 - 385 - 386 - 387 - 388 - 389 - 390 - 391 - 392 - 393 - 394 - 395 - 396 - 397 407 - spindle.tngl.boltless.dev 398 408 - tngl.boltless.dev 399 409 - mirror.tngl.boltless.dev 410 + - gitmirror.tngl.boltless.dev 400 411 - zoekt.tngl.boltless.dev 401 412 - pdsls.tngl.boltless.dev 402 413
+3
flake.nix
··· 362 362 pkgs.protoc-gen-prost-crate 363 363 pkgs.protoc-gen-prost-serde 364 364 pkgs.protoc-gen-go 365 + pkgs.protoc-gen-go-grpc 365 366 (fenix.packages.${system}.combine [ 366 367 fenix.packages.${system}.stable.cargo 367 368 fenix.packages.${system}.stable.rustc ··· 476 477 cd "$rootDir" 477 478 echo ">>> regenerating protobuf files.." 478 479 buf generate 480 + echo ">>> regenerating gitmirror go stubs..." 481 + buf generate gitmirror --template gitmirror/buf.gen.yaml 479 482 echo ">>> generating file descriptor set for shuttle..." 480 483 buf build -o shuttle/src/gen/file_descriptor_set.bin 481 484 echo ">>> done"
+28
gitmirror/Cargo.toml
··· 1 + [package] 2 + name = "gitmirror" 3 + version = "0.1.0" 4 + edition.workspace = true 5 + license.workspace = true 6 + rust-version.workspace = true 7 + 8 + [dependencies] 9 + anyhow = { workspace = true } 10 + clap = { workspace = true } 11 + tonic = { workspace = true } 12 + tonic-prost = { workspace = true } 13 + prost = { workspace = true } 14 + tokio = { workspace = true, features = ["net", "rt-multi-thread", "sync"] } 15 + tokio-stream = { workspace = true, features = ["sync"] } 16 + tracing = "0.1" 17 + tracing-subscriber = { version = "0.3", features = ["env-filter"] } 18 + line-numbers = "0.4.0" 19 + rustc-hash = "2.1.2" 20 + gix = { version = "0.84", features = ["parallel", "blob-diff", "merge", "sha1", "sha256", "revision", "tree-editor"] } 21 + prost-types = "0.14.4" 22 + tempfile = "3" 23 + 24 + [build-dependencies] 25 + tonic-prost-build = { workspace = true } 26 + 27 + [lints] 28 + workspace = true
+4
gitmirror/README.md
··· 1 + # Gitmirror 2 + 3 + gitmirror is alternative name of Knotmirror v2. It is designed to be run under the AppView as git syncing service. 4 + Because it's internal sync service like tap or hydrant, it uses grpc over xrpc.
+12
gitmirror/buf.gen.yaml
··· 1 + version: v2 2 + plugins: 3 + - local: protoc-gen-go 4 + out: . 5 + opt: 6 + - paths=import 7 + - module=tangled.org/core 8 + - local: protoc-gen-go-grpc 9 + out: . 10 + opt: 11 + - paths=import 12 + - module=tangled.org/core
+3
gitmirror/buf.yaml
··· 1 + version: v2 2 + modules: 3 + - path: proto
+4
gitmirror/build.rs
··· 1 + fn main() -> Result<(), Box<dyn std::error::Error>> { 2 + tonic_prost_build::compile_protos("proto/gitmirror/v1/gitmirror.proto")?; 3 + Ok(()) 4 + }
+42
gitmirror/example/client.go
··· 1 + //go:build ignore 2 + 3 + // Example: how appview will call gitmirror's GitMirrorService. 4 + // 5 + // This file is build-ignored so it never breaks `go build ./...`. It is a 6 + // copy-ready reference for wiring the client into appview once the Go stubs 7 + // are generated (see gitmirror/buf.gen.yaml). 8 + package main 9 + 10 + import ( 11 + "context" 12 + "log" 13 + "time" 14 + 15 + "google.golang.org/grpc" 16 + "google.golang.org/grpc/credentials/insecure" 17 + 18 + gitmirrorv1 "tangled.org/core/gitmirror/proto/gen" 19 + ) 20 + 21 + func main() { 22 + conn, err := grpc.NewClient("127.0.0.1:9000", 23 + grpc.WithTransportCredentials(insecure.NewCredentials())) 24 + if err != nil { 25 + log.Fatal(err) 26 + } 27 + defer conn.Close() 28 + 29 + client := gitmirrorv1.NewGitMirrorServiceClient(conn) 30 + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 31 + defer cancel() 32 + 33 + // appview converts its syntax.DID to a string via did.String(). 34 + resp, err := client.Commits(ctx, &gitmirrorv1.CommitsRequest{ 35 + Repo: "did:plc:example", 36 + Options: &gitmirrorv1.CommitsOptions{}, 37 + }) 38 + if err != nil { 39 + log.Fatal(err) 40 + } 41 + log.Println(resp.GetCommits()) 42 + }
+1470
gitmirror/proto/gen/gitmirror.pb.go
··· 1 + // Code generated by protoc-gen-go. DO NOT EDIT. 2 + // versions: 3 + // protoc-gen-go v1.36.11 4 + // protoc (unknown) 5 + // source: gitmirror/v1/gitmirror.proto 6 + 7 + package gitmirrorv1 8 + 9 + import ( 10 + protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 + protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 + timestamppb "google.golang.org/protobuf/types/known/timestamppb" 13 + reflect "reflect" 14 + sync "sync" 15 + unsafe "unsafe" 16 + ) 17 + 18 + const ( 19 + // Verify that this generated code is sufficiently up-to-date. 20 + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 21 + // Verify that runtime/protoimpl is sufficiently up-to-date. 22 + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 23 + ) 24 + 25 + type DiffRequest_ComparisonType int32 26 + 27 + const ( 28 + DiffRequest_COMPARISON_TYPE_UNSPECIFIED DiffRequest_ComparisonType = 0 29 + // Corresponds to the BASE..HEAD syntax that only returns any commits that 30 + // are in HEAD but not in BASE. 31 + DiffRequest_COMPARISON_TYPE_ONLY_IN_HEAD DiffRequest_ComparisonType = 1 32 + // Corresponds to the BASE...HEAD syntax that returns any commits that are 33 + // not in both BASE and HEAD. 34 + DiffRequest_COMPARISON_TYPE_INTERSECTION DiffRequest_ComparisonType = 2 35 + ) 36 + 37 + // Enum value maps for DiffRequest_ComparisonType. 38 + var ( 39 + DiffRequest_ComparisonType_name = map[int32]string{ 40 + 0: "COMPARISON_TYPE_UNSPECIFIED", 41 + 1: "COMPARISON_TYPE_ONLY_IN_HEAD", 42 + 2: "COMPARISON_TYPE_INTERSECTION", 43 + } 44 + DiffRequest_ComparisonType_value = map[string]int32{ 45 + "COMPARISON_TYPE_UNSPECIFIED": 0, 46 + "COMPARISON_TYPE_ONLY_IN_HEAD": 1, 47 + "COMPARISON_TYPE_INTERSECTION": 2, 48 + } 49 + ) 50 + 51 + func (x DiffRequest_ComparisonType) Enum() *DiffRequest_ComparisonType { 52 + p := new(DiffRequest_ComparisonType) 53 + *p = x 54 + return p 55 + } 56 + 57 + func (x DiffRequest_ComparisonType) String() string { 58 + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) 59 + } 60 + 61 + func (DiffRequest_ComparisonType) Descriptor() protoreflect.EnumDescriptor { 62 + return file_gitmirror_v1_gitmirror_proto_enumTypes[0].Descriptor() 63 + } 64 + 65 + func (DiffRequest_ComparisonType) Type() protoreflect.EnumType { 66 + return &file_gitmirror_v1_gitmirror_proto_enumTypes[0] 67 + } 68 + 69 + func (x DiffRequest_ComparisonType) Number() protoreflect.EnumNumber { 70 + return protoreflect.EnumNumber(x) 71 + } 72 + 73 + // Deprecated: Use DiffRequest_ComparisonType.Descriptor instead. 74 + func (DiffRequest_ComparisonType) EnumDescriptor() ([]byte, []int) { 75 + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{10, 0} 76 + } 77 + 78 + type ResolveRevRequest struct { 79 + state protoimpl.MessageState `protogen:"open.v1"` 80 + // repo carries the DID as a string. 81 + Repo string `protobuf:"bytes,1,opt,name=repo,proto3" json:"repo,omitempty"` 82 + // rev is a SIMPLE revspec: a branch name, tag name, or short/full commit oid. 83 + // HEAD and revspec navigation (HEAD~2, foo^, ref@{...}, a..b, path:file) are rejected. 84 + Rev []byte `protobuf:"bytes,2,opt,name=rev,proto3" json:"rev,omitempty"` 85 + unknownFields protoimpl.UnknownFields 86 + sizeCache protoimpl.SizeCache 87 + } 88 + 89 + func (x *ResolveRevRequest) Reset() { 90 + *x = ResolveRevRequest{} 91 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[0] 92 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 93 + ms.StoreMessageInfo(mi) 94 + } 95 + 96 + func (x *ResolveRevRequest) String() string { 97 + return protoimpl.X.MessageStringOf(x) 98 + } 99 + 100 + func (*ResolveRevRequest) ProtoMessage() {} 101 + 102 + func (x *ResolveRevRequest) ProtoReflect() protoreflect.Message { 103 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[0] 104 + if x != nil { 105 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 106 + if ms.LoadMessageInfo() == nil { 107 + ms.StoreMessageInfo(mi) 108 + } 109 + return ms 110 + } 111 + return mi.MessageOf(x) 112 + } 113 + 114 + // Deprecated: Use ResolveRevRequest.ProtoReflect.Descriptor instead. 115 + func (*ResolveRevRequest) Descriptor() ([]byte, []int) { 116 + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{0} 117 + } 118 + 119 + func (x *ResolveRevRequest) GetRepo() string { 120 + if x != nil { 121 + return x.Repo 122 + } 123 + return "" 124 + } 125 + 126 + func (x *ResolveRevRequest) GetRev() []byte { 127 + if x != nil { 128 + return x.Rev 129 + } 130 + return nil 131 + } 132 + 133 + type ResolveRevResponse struct { 134 + state protoimpl.MessageState `protogen:"open.v1"` 135 + // commit is the resolved commit object id, hex-encoded. 136 + Commit string `protobuf:"bytes,1,opt,name=commit,proto3" json:"commit,omitempty"` 137 + unknownFields protoimpl.UnknownFields 138 + sizeCache protoimpl.SizeCache 139 + } 140 + 141 + func (x *ResolveRevResponse) Reset() { 142 + *x = ResolveRevResponse{} 143 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[1] 144 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 145 + ms.StoreMessageInfo(mi) 146 + } 147 + 148 + func (x *ResolveRevResponse) String() string { 149 + return protoimpl.X.MessageStringOf(x) 150 + } 151 + 152 + func (*ResolveRevResponse) ProtoMessage() {} 153 + 154 + func (x *ResolveRevResponse) ProtoReflect() protoreflect.Message { 155 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[1] 156 + if x != nil { 157 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 158 + if ms.LoadMessageInfo() == nil { 159 + ms.StoreMessageInfo(mi) 160 + } 161 + return ms 162 + } 163 + return mi.MessageOf(x) 164 + } 165 + 166 + // Deprecated: Use ResolveRevResponse.ProtoReflect.Descriptor instead. 167 + func (*ResolveRevResponse) Descriptor() ([]byte, []int) { 168 + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{1} 169 + } 170 + 171 + func (x *ResolveRevResponse) GetCommit() string { 172 + if x != nil { 173 + return x.Commit 174 + } 175 + return "" 176 + } 177 + 178 + // A commit in a specific repo, addressed by the repo's DID. 179 + type RepoCommit struct { 180 + state protoimpl.MessageState `protogen:"open.v1"` 181 + // repo carries the DID as a string. 182 + Repo string `protobuf:"bytes,1,opt,name=repo,proto3" json:"repo,omitempty"` 183 + // commit is a full commit object id, hex-encoded (NOT a revspec/branch/tag). 184 + Commit []byte `protobuf:"bytes,2,opt,name=commit,proto3" json:"commit,omitempty"` 185 + unknownFields protoimpl.UnknownFields 186 + sizeCache protoimpl.SizeCache 187 + } 188 + 189 + func (x *RepoCommit) Reset() { 190 + *x = RepoCommit{} 191 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[2] 192 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 193 + ms.StoreMessageInfo(mi) 194 + } 195 + 196 + func (x *RepoCommit) String() string { 197 + return protoimpl.X.MessageStringOf(x) 198 + } 199 + 200 + func (*RepoCommit) ProtoMessage() {} 201 + 202 + func (x *RepoCommit) ProtoReflect() protoreflect.Message { 203 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[2] 204 + if x != nil { 205 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 206 + if ms.LoadMessageInfo() == nil { 207 + ms.StoreMessageInfo(mi) 208 + } 209 + return ms 210 + } 211 + return mi.MessageOf(x) 212 + } 213 + 214 + // Deprecated: Use RepoCommit.ProtoReflect.Descriptor instead. 215 + func (*RepoCommit) Descriptor() ([]byte, []int) { 216 + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{2} 217 + } 218 + 219 + func (x *RepoCommit) GetRepo() string { 220 + if x != nil { 221 + return x.Repo 222 + } 223 + return "" 224 + } 225 + 226 + func (x *RepoCommit) GetCommit() []byte { 227 + if x != nil { 228 + return x.Commit 229 + } 230 + return nil 231 + } 232 + 233 + type MergeCheckRequest struct { 234 + state protoimpl.MessageState `protogen:"open.v1"` 235 + // Branch/commit being merged into (the "ours" side of the 3-way merge). 236 + Target *RepoCommit `protobuf:"bytes,1,opt,name=target,proto3" json:"target,omitempty"` 237 + // PR head being merged (the "theirs" side of the 3-way merge). 238 + Source *RepoCommit `protobuf:"bytes,2,opt,name=source,proto3" json:"source,omitempty"` 239 + unknownFields protoimpl.UnknownFields 240 + sizeCache protoimpl.SizeCache 241 + } 242 + 243 + func (x *MergeCheckRequest) Reset() { 244 + *x = MergeCheckRequest{} 245 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[3] 246 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 247 + ms.StoreMessageInfo(mi) 248 + } 249 + 250 + func (x *MergeCheckRequest) String() string { 251 + return protoimpl.X.MessageStringOf(x) 252 + } 253 + 254 + func (*MergeCheckRequest) ProtoMessage() {} 255 + 256 + func (x *MergeCheckRequest) ProtoReflect() protoreflect.Message { 257 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[3] 258 + if x != nil { 259 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 260 + if ms.LoadMessageInfo() == nil { 261 + ms.StoreMessageInfo(mi) 262 + } 263 + return ms 264 + } 265 + return mi.MessageOf(x) 266 + } 267 + 268 + // Deprecated: Use MergeCheckRequest.ProtoReflect.Descriptor instead. 269 + func (*MergeCheckRequest) Descriptor() ([]byte, []int) { 270 + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{3} 271 + } 272 + 273 + func (x *MergeCheckRequest) GetTarget() *RepoCommit { 274 + if x != nil { 275 + return x.Target 276 + } 277 + return nil 278 + } 279 + 280 + func (x *MergeCheckRequest) GetSource() *RepoCommit { 281 + if x != nil { 282 + return x.Source 283 + } 284 + return nil 285 + } 286 + 287 + type MergeCheckResponse struct { 288 + state protoimpl.MessageState `protogen:"open.v1"` 289 + // Whether the 3-way merge has unresolved conflicts. 290 + IsConflicted bool `protobuf:"varint,1,opt,name=is_conflicted,json=isConflicted,proto3" json:"is_conflicted,omitempty"` 291 + // One entry per conflicted file. 292 + Conflicts []*MergeConflict `protobuf:"bytes,2,rep,name=conflicts,proto3" json:"conflicts,omitempty"` 293 + // Optional human-readable summary of the merge check. 294 + Message *string `protobuf:"bytes,3,opt,name=message,proto3,oneof" json:"message,omitempty"` 295 + unknownFields protoimpl.UnknownFields 296 + sizeCache protoimpl.SizeCache 297 + } 298 + 299 + func (x *MergeCheckResponse) Reset() { 300 + *x = MergeCheckResponse{} 301 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[4] 302 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 303 + ms.StoreMessageInfo(mi) 304 + } 305 + 306 + func (x *MergeCheckResponse) String() string { 307 + return protoimpl.X.MessageStringOf(x) 308 + } 309 + 310 + func (*MergeCheckResponse) ProtoMessage() {} 311 + 312 + func (x *MergeCheckResponse) ProtoReflect() protoreflect.Message { 313 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[4] 314 + if x != nil { 315 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 316 + if ms.LoadMessageInfo() == nil { 317 + ms.StoreMessageInfo(mi) 318 + } 319 + return ms 320 + } 321 + return mi.MessageOf(x) 322 + } 323 + 324 + // Deprecated: Use MergeCheckResponse.ProtoReflect.Descriptor instead. 325 + func (*MergeCheckResponse) Descriptor() ([]byte, []int) { 326 + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{4} 327 + } 328 + 329 + func (x *MergeCheckResponse) GetIsConflicted() bool { 330 + if x != nil { 331 + return x.IsConflicted 332 + } 333 + return false 334 + } 335 + 336 + func (x *MergeCheckResponse) GetConflicts() []*MergeConflict { 337 + if x != nil { 338 + return x.Conflicts 339 + } 340 + return nil 341 + } 342 + 343 + func (x *MergeCheckResponse) GetMessage() string { 344 + if x != nil && x.Message != nil { 345 + return *x.Message 346 + } 347 + return "" 348 + } 349 + 350 + type MergeConflict struct { 351 + state protoimpl.MessageState `protogen:"open.v1"` 352 + Filename string `protobuf:"bytes,1,opt,name=filename,proto3" json:"filename,omitempty"` 353 + Reason string `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"` 354 + unknownFields protoimpl.UnknownFields 355 + sizeCache protoimpl.SizeCache 356 + } 357 + 358 + func (x *MergeConflict) Reset() { 359 + *x = MergeConflict{} 360 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[5] 361 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 362 + ms.StoreMessageInfo(mi) 363 + } 364 + 365 + func (x *MergeConflict) String() string { 366 + return protoimpl.X.MessageStringOf(x) 367 + } 368 + 369 + func (*MergeConflict) ProtoMessage() {} 370 + 371 + func (x *MergeConflict) ProtoReflect() protoreflect.Message { 372 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[5] 373 + if x != nil { 374 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 375 + if ms.LoadMessageInfo() == nil { 376 + ms.StoreMessageInfo(mi) 377 + } 378 + return ms 379 + } 380 + return mi.MessageOf(x) 381 + } 382 + 383 + // Deprecated: Use MergeConflict.ProtoReflect.Descriptor instead. 384 + func (*MergeConflict) Descriptor() ([]byte, []int) { 385 + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{5} 386 + } 387 + 388 + func (x *MergeConflict) GetFilename() string { 389 + if x != nil { 390 + return x.Filename 391 + } 392 + return "" 393 + } 394 + 395 + func (x *MergeConflict) GetReason() string { 396 + if x != nil { 397 + return x.Reason 398 + } 399 + return "" 400 + } 401 + 402 + type CommitLogRequest struct { 403 + state protoimpl.MessageState `protogen:"open.v1"` 404 + // repo DID 405 + Repo string `protobuf:"bytes,1,opt,name=repo,proto3" json:"repo,omitempty"` 406 + // Ranges to include in the git log (revspec, "A..B", "A...B", etc.). 407 + // At least one range, or all_refs must be specified. 408 + Ranges [][]byte `protobuf:"bytes,2,rep,name=ranges,proto3" json:"ranges,omitempty"` 409 + // If true, all refs are searched for commits. 410 + // Must not be true when ranges are given. 411 + AllRefs bool `protobuf:"varint,3,opt,name=all_refs,json=allRefs,proto3" json:"all_refs,omitempty"` 412 + // After is an optional parameter to specify the earliest commit to consider. 413 + After *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=after,proto3" json:"after,omitempty"` 414 + // Before is an optional parameter to specify the latest commit to consider. 415 + Before *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=before,proto3" json:"before,omitempty"` 416 + // MaxCommits is an optional parameter to specify the maximum number of commits 417 + // to return. If max_commits is 0, all commits that match the criteria will be 418 + // returned. 419 + MaxCommits uint32 `protobuf:"varint,6,opt,name=max_commits,json=maxCommits,proto3" json:"max_commits,omitempty"` 420 + // Skip is an optional parameter to specify the number of commits to skip. 421 + // This can be used to implement a poor mans pagination. 422 + Skip uint32 `protobuf:"varint,7,opt,name=skip,proto3" json:"skip,omitempty"` 423 + unknownFields protoimpl.UnknownFields 424 + sizeCache protoimpl.SizeCache 425 + } 426 + 427 + func (x *CommitLogRequest) Reset() { 428 + *x = CommitLogRequest{} 429 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[6] 430 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 431 + ms.StoreMessageInfo(mi) 432 + } 433 + 434 + func (x *CommitLogRequest) String() string { 435 + return protoimpl.X.MessageStringOf(x) 436 + } 437 + 438 + func (*CommitLogRequest) ProtoMessage() {} 439 + 440 + func (x *CommitLogRequest) ProtoReflect() protoreflect.Message { 441 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[6] 442 + if x != nil { 443 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 444 + if ms.LoadMessageInfo() == nil { 445 + ms.StoreMessageInfo(mi) 446 + } 447 + return ms 448 + } 449 + return mi.MessageOf(x) 450 + } 451 + 452 + // Deprecated: Use CommitLogRequest.ProtoReflect.Descriptor instead. 453 + func (*CommitLogRequest) Descriptor() ([]byte, []int) { 454 + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{6} 455 + } 456 + 457 + func (x *CommitLogRequest) GetRepo() string { 458 + if x != nil { 459 + return x.Repo 460 + } 461 + return "" 462 + } 463 + 464 + func (x *CommitLogRequest) GetRanges() [][]byte { 465 + if x != nil { 466 + return x.Ranges 467 + } 468 + return nil 469 + } 470 + 471 + func (x *CommitLogRequest) GetAllRefs() bool { 472 + if x != nil { 473 + return x.AllRefs 474 + } 475 + return false 476 + } 477 + 478 + func (x *CommitLogRequest) GetAfter() *timestamppb.Timestamp { 479 + if x != nil { 480 + return x.After 481 + } 482 + return nil 483 + } 484 + 485 + func (x *CommitLogRequest) GetBefore() *timestamppb.Timestamp { 486 + if x != nil { 487 + return x.Before 488 + } 489 + return nil 490 + } 491 + 492 + func (x *CommitLogRequest) GetMaxCommits() uint32 { 493 + if x != nil { 494 + return x.MaxCommits 495 + } 496 + return 0 497 + } 498 + 499 + func (x *CommitLogRequest) GetSkip() uint32 { 500 + if x != nil { 501 + return x.Skip 502 + } 503 + return 0 504 + } 505 + 506 + type CommitLogResponse struct { 507 + state protoimpl.MessageState `protogen:"open.v1"` 508 + Commits []*GitCommit `protobuf:"bytes,1,rep,name=commits,proto3" json:"commits,omitempty"` 509 + unknownFields protoimpl.UnknownFields 510 + sizeCache protoimpl.SizeCache 511 + } 512 + 513 + func (x *CommitLogResponse) Reset() { 514 + *x = CommitLogResponse{} 515 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[7] 516 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 517 + ms.StoreMessageInfo(mi) 518 + } 519 + 520 + func (x *CommitLogResponse) String() string { 521 + return protoimpl.X.MessageStringOf(x) 522 + } 523 + 524 + func (*CommitLogResponse) ProtoMessage() {} 525 + 526 + func (x *CommitLogResponse) ProtoReflect() protoreflect.Message { 527 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[7] 528 + if x != nil { 529 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 530 + if ms.LoadMessageInfo() == nil { 531 + ms.StoreMessageInfo(mi) 532 + } 533 + return ms 534 + } 535 + return mi.MessageOf(x) 536 + } 537 + 538 + // Deprecated: Use CommitLogResponse.ProtoReflect.Descriptor instead. 539 + func (*CommitLogResponse) Descriptor() ([]byte, []int) { 540 + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{7} 541 + } 542 + 543 + func (x *CommitLogResponse) GetCommits() []*GitCommit { 544 + if x != nil { 545 + return x.Commits 546 + } 547 + return nil 548 + } 549 + 550 + type GetBlobRequest struct { 551 + state protoimpl.MessageState `protogen:"open.v1"` 552 + // repo carries the DID as a string. 553 + Repo string `protobuf:"bytes,1,opt,name=repo,proto3" json:"repo,omitempty"` 554 + // oid is the blob object id (hex). 555 + Oid string `protobuf:"bytes,2,opt,name=oid,proto3" json:"oid,omitempty"` 556 + unknownFields protoimpl.UnknownFields 557 + sizeCache protoimpl.SizeCache 558 + } 559 + 560 + func (x *GetBlobRequest) Reset() { 561 + *x = GetBlobRequest{} 562 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[8] 563 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 564 + ms.StoreMessageInfo(mi) 565 + } 566 + 567 + func (x *GetBlobRequest) String() string { 568 + return protoimpl.X.MessageStringOf(x) 569 + } 570 + 571 + func (*GetBlobRequest) ProtoMessage() {} 572 + 573 + func (x *GetBlobRequest) ProtoReflect() protoreflect.Message { 574 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[8] 575 + if x != nil { 576 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 577 + if ms.LoadMessageInfo() == nil { 578 + ms.StoreMessageInfo(mi) 579 + } 580 + return ms 581 + } 582 + return mi.MessageOf(x) 583 + } 584 + 585 + // Deprecated: Use GetBlobRequest.ProtoReflect.Descriptor instead. 586 + func (*GetBlobRequest) Descriptor() ([]byte, []int) { 587 + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{8} 588 + } 589 + 590 + func (x *GetBlobRequest) GetRepo() string { 591 + if x != nil { 592 + return x.Repo 593 + } 594 + return "" 595 + } 596 + 597 + func (x *GetBlobRequest) GetOid() string { 598 + if x != nil { 599 + return x.Oid 600 + } 601 + return "" 602 + } 603 + 604 + type BlobChunk struct { 605 + state protoimpl.MessageState `protogen:"open.v1"` 606 + Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` 607 + unknownFields protoimpl.UnknownFields 608 + sizeCache protoimpl.SizeCache 609 + } 610 + 611 + func (x *BlobChunk) Reset() { 612 + *x = BlobChunk{} 613 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[9] 614 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 615 + ms.StoreMessageInfo(mi) 616 + } 617 + 618 + func (x *BlobChunk) String() string { 619 + return protoimpl.X.MessageStringOf(x) 620 + } 621 + 622 + func (*BlobChunk) ProtoMessage() {} 623 + 624 + func (x *BlobChunk) ProtoReflect() protoreflect.Message { 625 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[9] 626 + if x != nil { 627 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 628 + if ms.LoadMessageInfo() == nil { 629 + ms.StoreMessageInfo(mi) 630 + } 631 + return ms 632 + } 633 + return mi.MessageOf(x) 634 + } 635 + 636 + // Deprecated: Use BlobChunk.ProtoReflect.Descriptor instead. 637 + func (*BlobChunk) Descriptor() ([]byte, []int) { 638 + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{9} 639 + } 640 + 641 + func (x *BlobChunk) GetData() []byte { 642 + if x != nil { 643 + return x.Data 644 + } 645 + return nil 646 + } 647 + 648 + type DiffRequest struct { 649 + state protoimpl.MessageState `protogen:"open.v1"` 650 + Base *RepoCommit `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` 651 + Head *RepoCommit `protobuf:"bytes,2,opt,name=head,proto3" json:"head,omitempty"` 652 + ComparisonType DiffRequest_ComparisonType `protobuf:"varint,3,opt,name=comparison_type,json=comparisonType,proto3,enum=gitmirror.v1.DiffRequest_ComparisonType" json:"comparison_type,omitempty"` 653 + unknownFields protoimpl.UnknownFields 654 + sizeCache protoimpl.SizeCache 655 + } 656 + 657 + func (x *DiffRequest) Reset() { 658 + *x = DiffRequest{} 659 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[10] 660 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 661 + ms.StoreMessageInfo(mi) 662 + } 663 + 664 + func (x *DiffRequest) String() string { 665 + return protoimpl.X.MessageStringOf(x) 666 + } 667 + 668 + func (*DiffRequest) ProtoMessage() {} 669 + 670 + func (x *DiffRequest) ProtoReflect() protoreflect.Message { 671 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[10] 672 + if x != nil { 673 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 674 + if ms.LoadMessageInfo() == nil { 675 + ms.StoreMessageInfo(mi) 676 + } 677 + return ms 678 + } 679 + return mi.MessageOf(x) 680 + } 681 + 682 + // Deprecated: Use DiffRequest.ProtoReflect.Descriptor instead. 683 + func (*DiffRequest) Descriptor() ([]byte, []int) { 684 + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{10} 685 + } 686 + 687 + func (x *DiffRequest) GetBase() *RepoCommit { 688 + if x != nil { 689 + return x.Base 690 + } 691 + return nil 692 + } 693 + 694 + func (x *DiffRequest) GetHead() *RepoCommit { 695 + if x != nil { 696 + return x.Head 697 + } 698 + return nil 699 + } 700 + 701 + func (x *DiffRequest) GetComparisonType() DiffRequest_ComparisonType { 702 + if x != nil { 703 + return x.ComparisonType 704 + } 705 + return DiffRequest_COMPARISON_TYPE_UNSPECIFIED 706 + } 707 + 708 + type InterdiffRequest struct { 709 + state protoimpl.MessageState `protogen:"open.v1"` 710 + // repo carries the DID as a string. 711 + Repo string `protobuf:"bytes,1,opt,name=repo,proto3" json:"repo,omitempty"` 712 + // Old patch range (from_base..from_head) and new patch range (to_base..to_head). 713 + FromBase []byte `protobuf:"bytes,2,opt,name=from_base,json=fromBase,proto3" json:"from_base,omitempty"` 714 + FromHead []byte `protobuf:"bytes,3,opt,name=from_head,json=fromHead,proto3" json:"from_head,omitempty"` 715 + ToBase []byte `protobuf:"bytes,4,opt,name=to_base,json=toBase,proto3" json:"to_base,omitempty"` 716 + ToHead []byte `protobuf:"bytes,5,opt,name=to_head,json=toHead,proto3" json:"to_head,omitempty"` 717 + unknownFields protoimpl.UnknownFields 718 + sizeCache protoimpl.SizeCache 719 + } 720 + 721 + func (x *InterdiffRequest) Reset() { 722 + *x = InterdiffRequest{} 723 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[11] 724 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 725 + ms.StoreMessageInfo(mi) 726 + } 727 + 728 + func (x *InterdiffRequest) String() string { 729 + return protoimpl.X.MessageStringOf(x) 730 + } 731 + 732 + func (*InterdiffRequest) ProtoMessage() {} 733 + 734 + func (x *InterdiffRequest) ProtoReflect() protoreflect.Message { 735 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[11] 736 + if x != nil { 737 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 738 + if ms.LoadMessageInfo() == nil { 739 + ms.StoreMessageInfo(mi) 740 + } 741 + return ms 742 + } 743 + return mi.MessageOf(x) 744 + } 745 + 746 + // Deprecated: Use InterdiffRequest.ProtoReflect.Descriptor instead. 747 + func (*InterdiffRequest) Descriptor() ([]byte, []int) { 748 + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{11} 749 + } 750 + 751 + func (x *InterdiffRequest) GetRepo() string { 752 + if x != nil { 753 + return x.Repo 754 + } 755 + return "" 756 + } 757 + 758 + func (x *InterdiffRequest) GetFromBase() []byte { 759 + if x != nil { 760 + return x.FromBase 761 + } 762 + return nil 763 + } 764 + 765 + func (x *InterdiffRequest) GetFromHead() []byte { 766 + if x != nil { 767 + return x.FromHead 768 + } 769 + return nil 770 + } 771 + 772 + func (x *InterdiffRequest) GetToBase() []byte { 773 + if x != nil { 774 + return x.ToBase 775 + } 776 + return nil 777 + } 778 + 779 + func (x *InterdiffRequest) GetToHead() []byte { 780 + if x != nil { 781 + return x.ToHead 782 + } 783 + return nil 784 + } 785 + 786 + type FileContent struct { 787 + state protoimpl.MessageState `protogen:"open.v1"` 788 + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` 789 + Oid string `protobuf:"bytes,2,opt,name=oid,proto3" json:"oid,omitempty"` 790 + Size uint64 `protobuf:"varint,3,opt,name=size,proto3" json:"size,omitempty"` 791 + IsBinary bool `protobuf:"varint,4,opt,name=is_binary,json=isBinary,proto3" json:"is_binary,omitempty"` 792 + IsSubmodule bool `protobuf:"varint,5,opt,name=is_submodule,json=isSubmodule,proto3" json:"is_submodule,omitempty"` 793 + // Raw file bytes, inlined when the oid is not fetchable by the client 794 + // (e.g. interdiff) 795 + Content []byte `protobuf:"bytes,6,opt,name=content,proto3,oneof" json:"content,omitempty"` 796 + unknownFields protoimpl.UnknownFields 797 + sizeCache protoimpl.SizeCache 798 + } 799 + 800 + func (x *FileContent) Reset() { 801 + *x = FileContent{} 802 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[12] 803 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 804 + ms.StoreMessageInfo(mi) 805 + } 806 + 807 + func (x *FileContent) String() string { 808 + return protoimpl.X.MessageStringOf(x) 809 + } 810 + 811 + func (*FileContent) ProtoMessage() {} 812 + 813 + func (x *FileContent) ProtoReflect() protoreflect.Message { 814 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[12] 815 + if x != nil { 816 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 817 + if ms.LoadMessageInfo() == nil { 818 + ms.StoreMessageInfo(mi) 819 + } 820 + return ms 821 + } 822 + return mi.MessageOf(x) 823 + } 824 + 825 + // Deprecated: Use FileContent.ProtoReflect.Descriptor instead. 826 + func (*FileContent) Descriptor() ([]byte, []int) { 827 + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{12} 828 + } 829 + 830 + func (x *FileContent) GetPath() string { 831 + if x != nil { 832 + return x.Path 833 + } 834 + return "" 835 + } 836 + 837 + func (x *FileContent) GetOid() string { 838 + if x != nil { 839 + return x.Oid 840 + } 841 + return "" 842 + } 843 + 844 + func (x *FileContent) GetSize() uint64 { 845 + if x != nil { 846 + return x.Size 847 + } 848 + return 0 849 + } 850 + 851 + func (x *FileContent) GetIsBinary() bool { 852 + if x != nil { 853 + return x.IsBinary 854 + } 855 + return false 856 + } 857 + 858 + func (x *FileContent) GetIsSubmodule() bool { 859 + if x != nil { 860 + return x.IsSubmodule 861 + } 862 + return false 863 + } 864 + 865 + func (x *FileContent) GetContent() []byte { 866 + if x != nil { 867 + return x.Content 868 + } 869 + return nil 870 + } 871 + 872 + // One aligned line pair; a side is absent (None) when unset. 873 + type LinePair struct { 874 + state protoimpl.MessageState `protogen:"open.v1"` 875 + Lhs *uint32 `protobuf:"varint,1,opt,name=lhs,proto3,oneof" json:"lhs,omitempty"` 876 + Rhs *uint32 `protobuf:"varint,2,opt,name=rhs,proto3,oneof" json:"rhs,omitempty"` 877 + unknownFields protoimpl.UnknownFields 878 + sizeCache protoimpl.SizeCache 879 + } 880 + 881 + func (x *LinePair) Reset() { 882 + *x = LinePair{} 883 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[13] 884 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 885 + ms.StoreMessageInfo(mi) 886 + } 887 + 888 + func (x *LinePair) String() string { 889 + return protoimpl.X.MessageStringOf(x) 890 + } 891 + 892 + func (*LinePair) ProtoMessage() {} 893 + 894 + func (x *LinePair) ProtoReflect() protoreflect.Message { 895 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[13] 896 + if x != nil { 897 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 898 + if ms.LoadMessageInfo() == nil { 899 + ms.StoreMessageInfo(mi) 900 + } 901 + return ms 902 + } 903 + return mi.MessageOf(x) 904 + } 905 + 906 + // Deprecated: Use LinePair.ProtoReflect.Descriptor instead. 907 + func (*LinePair) Descriptor() ([]byte, []int) { 908 + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{13} 909 + } 910 + 911 + func (x *LinePair) GetLhs() uint32 { 912 + if x != nil && x.Lhs != nil { 913 + return *x.Lhs 914 + } 915 + return 0 916 + } 917 + 918 + func (x *LinePair) GetRhs() uint32 { 919 + if x != nil && x.Rhs != nil { 920 + return *x.Rhs 921 + } 922 + return 0 923 + } 924 + 925 + type Hunk struct { 926 + state protoimpl.MessageState `protogen:"open.v1"` 927 + // LineNumber (u32) sets containing novel content per side. 928 + NovelLhs []uint32 `protobuf:"varint,1,rep,packed,name=novel_lhs,json=novelLhs,proto3" json:"novel_lhs,omitempty"` 929 + NovelRhs []uint32 `protobuf:"varint,2,rep,packed,name=novel_rhs,json=novelRhs,proto3" json:"novel_rhs,omitempty"` 930 + Lines []*LinePair `protobuf:"bytes,3,rep,name=lines,proto3" json:"lines,omitempty"` 931 + unknownFields protoimpl.UnknownFields 932 + sizeCache protoimpl.SizeCache 933 + } 934 + 935 + func (x *Hunk) Reset() { 936 + *x = Hunk{} 937 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[14] 938 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 939 + ms.StoreMessageInfo(mi) 940 + } 941 + 942 + func (x *Hunk) String() string { 943 + return protoimpl.X.MessageStringOf(x) 944 + } 945 + 946 + func (*Hunk) ProtoMessage() {} 947 + 948 + func (x *Hunk) ProtoReflect() protoreflect.Message { 949 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[14] 950 + if x != nil { 951 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 952 + if ms.LoadMessageInfo() == nil { 953 + ms.StoreMessageInfo(mi) 954 + } 955 + return ms 956 + } 957 + return mi.MessageOf(x) 958 + } 959 + 960 + // Deprecated: Use Hunk.ProtoReflect.Descriptor instead. 961 + func (*Hunk) Descriptor() ([]byte, []int) { 962 + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{14} 963 + } 964 + 965 + func (x *Hunk) GetNovelLhs() []uint32 { 966 + if x != nil { 967 + return x.NovelLhs 968 + } 969 + return nil 970 + } 971 + 972 + func (x *Hunk) GetNovelRhs() []uint32 { 973 + if x != nil { 974 + return x.NovelRhs 975 + } 976 + return nil 977 + } 978 + 979 + func (x *Hunk) GetLines() []*LinePair { 980 + if x != nil { 981 + return x.Lines 982 + } 983 + return nil 984 + } 985 + 986 + // Number of bytes per side when the files differ (Rust Option<(usize, usize)>). 987 + type ByteChanges struct { 988 + state protoimpl.MessageState `protogen:"open.v1"` 989 + Lhs uint64 `protobuf:"varint,1,opt,name=lhs,proto3" json:"lhs,omitempty"` 990 + Rhs uint64 `protobuf:"varint,2,opt,name=rhs,proto3" json:"rhs,omitempty"` 991 + unknownFields protoimpl.UnknownFields 992 + sizeCache protoimpl.SizeCache 993 + } 994 + 995 + func (x *ByteChanges) Reset() { 996 + *x = ByteChanges{} 997 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[15] 998 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 999 + ms.StoreMessageInfo(mi) 1000 + } 1001 + 1002 + func (x *ByteChanges) String() string { 1003 + return protoimpl.X.MessageStringOf(x) 1004 + } 1005 + 1006 + func (*ByteChanges) ProtoMessage() {} 1007 + 1008 + func (x *ByteChanges) ProtoReflect() protoreflect.Message { 1009 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[15] 1010 + if x != nil { 1011 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 1012 + if ms.LoadMessageInfo() == nil { 1013 + ms.StoreMessageInfo(mi) 1014 + } 1015 + return ms 1016 + } 1017 + return mi.MessageOf(x) 1018 + } 1019 + 1020 + // Deprecated: Use ByteChanges.ProtoReflect.Descriptor instead. 1021 + func (*ByteChanges) Descriptor() ([]byte, []int) { 1022 + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{15} 1023 + } 1024 + 1025 + func (x *ByteChanges) GetLhs() uint64 { 1026 + if x != nil { 1027 + return x.Lhs 1028 + } 1029 + return 0 1030 + } 1031 + 1032 + func (x *ByteChanges) GetRhs() uint64 { 1033 + if x != nil { 1034 + return x.Rhs 1035 + } 1036 + return 0 1037 + } 1038 + 1039 + type FileDiff struct { 1040 + state protoimpl.MessageState `protogen:"open.v1"` 1041 + LhsSrc *FileContent `protobuf:"bytes,1,opt,name=lhs_src,json=lhsSrc,proto3" json:"lhs_src,omitempty"` 1042 + RhsSrc *FileContent `protobuf:"bytes,2,opt,name=rhs_src,json=rhsSrc,proto3" json:"rhs_src,omitempty"` 1043 + Hunks []*Hunk `protobuf:"bytes,3,rep,name=hunks,proto3" json:"hunks,omitempty"` 1044 + HasByteChanges *ByteChanges `protobuf:"bytes,4,opt,name=has_byte_changes,json=hasByteChanges,proto3,oneof" json:"has_byte_changes,omitempty"` 1045 + HasSyntacticChanges bool `protobuf:"varint,5,opt,name=has_syntactic_changes,json=hasSyntacticChanges,proto3" json:"has_syntactic_changes,omitempty"` // TODO: lhs_positions & rhs_positions 1046 + unknownFields protoimpl.UnknownFields 1047 + sizeCache protoimpl.SizeCache 1048 + } 1049 + 1050 + func (x *FileDiff) Reset() { 1051 + *x = FileDiff{} 1052 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[16] 1053 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 1054 + ms.StoreMessageInfo(mi) 1055 + } 1056 + 1057 + func (x *FileDiff) String() string { 1058 + return protoimpl.X.MessageStringOf(x) 1059 + } 1060 + 1061 + func (*FileDiff) ProtoMessage() {} 1062 + 1063 + func (x *FileDiff) ProtoReflect() protoreflect.Message { 1064 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[16] 1065 + if x != nil { 1066 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 1067 + if ms.LoadMessageInfo() == nil { 1068 + ms.StoreMessageInfo(mi) 1069 + } 1070 + return ms 1071 + } 1072 + return mi.MessageOf(x) 1073 + } 1074 + 1075 + // Deprecated: Use FileDiff.ProtoReflect.Descriptor instead. 1076 + func (*FileDiff) Descriptor() ([]byte, []int) { 1077 + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{16} 1078 + } 1079 + 1080 + func (x *FileDiff) GetLhsSrc() *FileContent { 1081 + if x != nil { 1082 + return x.LhsSrc 1083 + } 1084 + return nil 1085 + } 1086 + 1087 + func (x *FileDiff) GetRhsSrc() *FileContent { 1088 + if x != nil { 1089 + return x.RhsSrc 1090 + } 1091 + return nil 1092 + } 1093 + 1094 + func (x *FileDiff) GetHunks() []*Hunk { 1095 + if x != nil { 1096 + return x.Hunks 1097 + } 1098 + return nil 1099 + } 1100 + 1101 + func (x *FileDiff) GetHasByteChanges() *ByteChanges { 1102 + if x != nil { 1103 + return x.HasByteChanges 1104 + } 1105 + return nil 1106 + } 1107 + 1108 + func (x *FileDiff) GetHasSyntacticChanges() bool { 1109 + if x != nil { 1110 + return x.HasSyntacticChanges 1111 + } 1112 + return false 1113 + } 1114 + 1115 + type GitCommit struct { 1116 + state protoimpl.MessageState `protogen:"open.v1"` 1117 + Oid string `protobuf:"bytes,1,opt,name=oid,proto3" json:"oid,omitempty"` 1118 + Author *GitSignature `protobuf:"bytes,2,opt,name=author,proto3" json:"author,omitempty"` 1119 + Committer *GitSignature `protobuf:"bytes,3,opt,name=committer,proto3" json:"committer,omitempty"` 1120 + Message []byte `protobuf:"bytes,4,opt,name=message,proto3" json:"message,omitempty"` 1121 + Parents []string `protobuf:"bytes,5,rep,name=parents,proto3" json:"parents,omitempty"` 1122 + ExtraHeaders map[string]string `protobuf:"bytes,6,rep,name=extra_headers,json=extraHeaders,proto3" json:"extra_headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` 1123 + unknownFields protoimpl.UnknownFields 1124 + sizeCache protoimpl.SizeCache 1125 + } 1126 + 1127 + func (x *GitCommit) Reset() { 1128 + *x = GitCommit{} 1129 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[17] 1130 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 1131 + ms.StoreMessageInfo(mi) 1132 + } 1133 + 1134 + func (x *GitCommit) String() string { 1135 + return protoimpl.X.MessageStringOf(x) 1136 + } 1137 + 1138 + func (*GitCommit) ProtoMessage() {} 1139 + 1140 + func (x *GitCommit) ProtoReflect() protoreflect.Message { 1141 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[17] 1142 + if x != nil { 1143 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 1144 + if ms.LoadMessageInfo() == nil { 1145 + ms.StoreMessageInfo(mi) 1146 + } 1147 + return ms 1148 + } 1149 + return mi.MessageOf(x) 1150 + } 1151 + 1152 + // Deprecated: Use GitCommit.ProtoReflect.Descriptor instead. 1153 + func (*GitCommit) Descriptor() ([]byte, []int) { 1154 + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{17} 1155 + } 1156 + 1157 + func (x *GitCommit) GetOid() string { 1158 + if x != nil { 1159 + return x.Oid 1160 + } 1161 + return "" 1162 + } 1163 + 1164 + func (x *GitCommit) GetAuthor() *GitSignature { 1165 + if x != nil { 1166 + return x.Author 1167 + } 1168 + return nil 1169 + } 1170 + 1171 + func (x *GitCommit) GetCommitter() *GitSignature { 1172 + if x != nil { 1173 + return x.Committer 1174 + } 1175 + return nil 1176 + } 1177 + 1178 + func (x *GitCommit) GetMessage() []byte { 1179 + if x != nil { 1180 + return x.Message 1181 + } 1182 + return nil 1183 + } 1184 + 1185 + func (x *GitCommit) GetParents() []string { 1186 + if x != nil { 1187 + return x.Parents 1188 + } 1189 + return nil 1190 + } 1191 + 1192 + func (x *GitCommit) GetExtraHeaders() map[string]string { 1193 + if x != nil { 1194 + return x.ExtraHeaders 1195 + } 1196 + return nil 1197 + } 1198 + 1199 + type GitSignature struct { 1200 + state protoimpl.MessageState `protogen:"open.v1"` 1201 + Name []byte `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` 1202 + Email []byte `protobuf:"bytes,2,opt,name=email,proto3" json:"email,omitempty"` 1203 + Date *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=date,proto3" json:"date,omitempty"` 1204 + unknownFields protoimpl.UnknownFields 1205 + sizeCache protoimpl.SizeCache 1206 + } 1207 + 1208 + func (x *GitSignature) Reset() { 1209 + *x = GitSignature{} 1210 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[18] 1211 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 1212 + ms.StoreMessageInfo(mi) 1213 + } 1214 + 1215 + func (x *GitSignature) String() string { 1216 + return protoimpl.X.MessageStringOf(x) 1217 + } 1218 + 1219 + func (*GitSignature) ProtoMessage() {} 1220 + 1221 + func (x *GitSignature) ProtoReflect() protoreflect.Message { 1222 + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[18] 1223 + if x != nil { 1224 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 1225 + if ms.LoadMessageInfo() == nil { 1226 + ms.StoreMessageInfo(mi) 1227 + } 1228 + return ms 1229 + } 1230 + return mi.MessageOf(x) 1231 + } 1232 + 1233 + // Deprecated: Use GitSignature.ProtoReflect.Descriptor instead. 1234 + func (*GitSignature) Descriptor() ([]byte, []int) { 1235 + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{18} 1236 + } 1237 + 1238 + func (x *GitSignature) GetName() []byte { 1239 + if x != nil { 1240 + return x.Name 1241 + } 1242 + return nil 1243 + } 1244 + 1245 + func (x *GitSignature) GetEmail() []byte { 1246 + if x != nil { 1247 + return x.Email 1248 + } 1249 + return nil 1250 + } 1251 + 1252 + func (x *GitSignature) GetDate() *timestamppb.Timestamp { 1253 + if x != nil { 1254 + return x.Date 1255 + } 1256 + return nil 1257 + } 1258 + 1259 + var File_gitmirror_v1_gitmirror_proto protoreflect.FileDescriptor 1260 + 1261 + const file_gitmirror_v1_gitmirror_proto_rawDesc = "" + 1262 + "\n" + 1263 + "\x1cgitmirror/v1/gitmirror.proto\x12\fgitmirror.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"9\n" + 1264 + "\x11ResolveRevRequest\x12\x12\n" + 1265 + "\x04repo\x18\x01 \x01(\tR\x04repo\x12\x10\n" + 1266 + "\x03rev\x18\x02 \x01(\fR\x03rev\",\n" + 1267 + "\x12ResolveRevResponse\x12\x16\n" + 1268 + "\x06commit\x18\x01 \x01(\tR\x06commit\"8\n" + 1269 + "\n" + 1270 + "RepoCommit\x12\x12\n" + 1271 + "\x04repo\x18\x01 \x01(\tR\x04repo\x12\x16\n" + 1272 + "\x06commit\x18\x02 \x01(\fR\x06commit\"w\n" + 1273 + "\x11MergeCheckRequest\x120\n" + 1274 + "\x06target\x18\x01 \x01(\v2\x18.gitmirror.v1.RepoCommitR\x06target\x120\n" + 1275 + "\x06source\x18\x02 \x01(\v2\x18.gitmirror.v1.RepoCommitR\x06source\"\x9f\x01\n" + 1276 + "\x12MergeCheckResponse\x12#\n" + 1277 + "\ris_conflicted\x18\x01 \x01(\bR\fisConflicted\x129\n" + 1278 + "\tconflicts\x18\x02 \x03(\v2\x1b.gitmirror.v1.MergeConflictR\tconflicts\x12\x1d\n" + 1279 + "\amessage\x18\x03 \x01(\tH\x00R\amessage\x88\x01\x01B\n" + 1280 + "\n" + 1281 + "\b_message\"C\n" + 1282 + "\rMergeConflict\x12\x1a\n" + 1283 + "\bfilename\x18\x01 \x01(\tR\bfilename\x12\x16\n" + 1284 + "\x06reason\x18\x02 \x01(\tR\x06reason\"\xf4\x01\n" + 1285 + "\x10CommitLogRequest\x12\x12\n" + 1286 + "\x04repo\x18\x01 \x01(\tR\x04repo\x12\x16\n" + 1287 + "\x06ranges\x18\x02 \x03(\fR\x06ranges\x12\x19\n" + 1288 + "\ball_refs\x18\x03 \x01(\bR\aallRefs\x120\n" + 1289 + "\x05after\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\x05after\x122\n" + 1290 + "\x06before\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\x06before\x12\x1f\n" + 1291 + "\vmax_commits\x18\x06 \x01(\rR\n" + 1292 + "maxCommits\x12\x12\n" + 1293 + "\x04skip\x18\a \x01(\rR\x04skip\"F\n" + 1294 + "\x11CommitLogResponse\x121\n" + 1295 + "\acommits\x18\x01 \x03(\v2\x17.gitmirror.v1.GitCommitR\acommits\"6\n" + 1296 + "\x0eGetBlobRequest\x12\x12\n" + 1297 + "\x04repo\x18\x01 \x01(\tR\x04repo\x12\x10\n" + 1298 + "\x03oid\x18\x02 \x01(\tR\x03oid\"\x1f\n" + 1299 + "\tBlobChunk\x12\x12\n" + 1300 + "\x04data\x18\x01 \x01(\fR\x04data\"\xb3\x02\n" + 1301 + "\vDiffRequest\x12,\n" + 1302 + "\x04base\x18\x01 \x01(\v2\x18.gitmirror.v1.RepoCommitR\x04base\x12,\n" + 1303 + "\x04head\x18\x02 \x01(\v2\x18.gitmirror.v1.RepoCommitR\x04head\x12Q\n" + 1304 + "\x0fcomparison_type\x18\x03 \x01(\x0e2(.gitmirror.v1.DiffRequest.ComparisonTypeR\x0ecomparisonType\"u\n" + 1305 + "\x0eComparisonType\x12\x1f\n" + 1306 + "\x1bCOMPARISON_TYPE_UNSPECIFIED\x10\x00\x12 \n" + 1307 + "\x1cCOMPARISON_TYPE_ONLY_IN_HEAD\x10\x01\x12 \n" + 1308 + "\x1cCOMPARISON_TYPE_INTERSECTION\x10\x02\"\x92\x01\n" + 1309 + "\x10InterdiffRequest\x12\x12\n" + 1310 + "\x04repo\x18\x01 \x01(\tR\x04repo\x12\x1b\n" + 1311 + "\tfrom_base\x18\x02 \x01(\fR\bfromBase\x12\x1b\n" + 1312 + "\tfrom_head\x18\x03 \x01(\fR\bfromHead\x12\x17\n" + 1313 + "\ato_base\x18\x04 \x01(\fR\x06toBase\x12\x17\n" + 1314 + "\ato_head\x18\x05 \x01(\fR\x06toHead\"\xb2\x01\n" + 1315 + "\vFileContent\x12\x12\n" + 1316 + "\x04path\x18\x01 \x01(\tR\x04path\x12\x10\n" + 1317 + "\x03oid\x18\x02 \x01(\tR\x03oid\x12\x12\n" + 1318 + "\x04size\x18\x03 \x01(\x04R\x04size\x12\x1b\n" + 1319 + "\tis_binary\x18\x04 \x01(\bR\bisBinary\x12!\n" + 1320 + "\fis_submodule\x18\x05 \x01(\bR\visSubmodule\x12\x1d\n" + 1321 + "\acontent\x18\x06 \x01(\fH\x00R\acontent\x88\x01\x01B\n" + 1322 + "\n" + 1323 + "\b_content\"H\n" + 1324 + "\bLinePair\x12\x15\n" + 1325 + "\x03lhs\x18\x01 \x01(\rH\x00R\x03lhs\x88\x01\x01\x12\x15\n" + 1326 + "\x03rhs\x18\x02 \x01(\rH\x01R\x03rhs\x88\x01\x01B\x06\n" + 1327 + "\x04_lhsB\x06\n" + 1328 + "\x04_rhs\"n\n" + 1329 + "\x04Hunk\x12\x1b\n" + 1330 + "\tnovel_lhs\x18\x01 \x03(\rR\bnovelLhs\x12\x1b\n" + 1331 + "\tnovel_rhs\x18\x02 \x03(\rR\bnovelRhs\x12,\n" + 1332 + "\x05lines\x18\x03 \x03(\v2\x16.gitmirror.v1.LinePairR\x05lines\"1\n" + 1333 + "\vByteChanges\x12\x10\n" + 1334 + "\x03lhs\x18\x01 \x01(\x04R\x03lhs\x12\x10\n" + 1335 + "\x03rhs\x18\x02 \x01(\x04R\x03rhs\"\xaf\x02\n" + 1336 + "\bFileDiff\x122\n" + 1337 + "\alhs_src\x18\x01 \x01(\v2\x19.gitmirror.v1.FileContentR\x06lhsSrc\x122\n" + 1338 + "\arhs_src\x18\x02 \x01(\v2\x19.gitmirror.v1.FileContentR\x06rhsSrc\x12(\n" + 1339 + "\x05hunks\x18\x03 \x03(\v2\x12.gitmirror.v1.HunkR\x05hunks\x12H\n" + 1340 + "\x10has_byte_changes\x18\x04 \x01(\v2\x19.gitmirror.v1.ByteChangesH\x00R\x0ehasByteChanges\x88\x01\x01\x122\n" + 1341 + "\x15has_syntactic_changes\x18\x05 \x01(\bR\x13hasSyntacticChangesB\x13\n" + 1342 + "\x11_has_byte_changes\"\xd0\x02\n" + 1343 + "\tGitCommit\x12\x10\n" + 1344 + "\x03oid\x18\x01 \x01(\tR\x03oid\x122\n" + 1345 + "\x06author\x18\x02 \x01(\v2\x1a.gitmirror.v1.GitSignatureR\x06author\x128\n" + 1346 + "\tcommitter\x18\x03 \x01(\v2\x1a.gitmirror.v1.GitSignatureR\tcommitter\x12\x18\n" + 1347 + "\amessage\x18\x04 \x01(\fR\amessage\x12\x18\n" + 1348 + "\aparents\x18\x05 \x03(\tR\aparents\x12N\n" + 1349 + "\rextra_headers\x18\x06 \x03(\v2).gitmirror.v1.GitCommit.ExtraHeadersEntryR\fextraHeaders\x1a?\n" + 1350 + "\x11ExtraHeadersEntry\x12\x10\n" + 1351 + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + 1352 + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"h\n" + 1353 + "\fGitSignature\x12\x12\n" + 1354 + "\x04name\x18\x01 \x01(\fR\x04name\x12\x14\n" + 1355 + "\x05email\x18\x02 \x01(\fR\x05email\x12.\n" + 1356 + "\x04date\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x04date2\xcc\x03\n" + 1357 + "\x10GitMirrorService\x12N\n" + 1358 + "\tCommitLog\x12\x1e.gitmirror.v1.CommitLogRequest\x1a\x1f.gitmirror.v1.CommitLogResponse0\x01\x12B\n" + 1359 + "\aGetBlob\x12\x1c.gitmirror.v1.GetBlobRequest\x1a\x17.gitmirror.v1.BlobChunk0\x01\x12;\n" + 1360 + "\x04Diff\x12\x19.gitmirror.v1.DiffRequest\x1a\x16.gitmirror.v1.FileDiff0\x01\x12E\n" + 1361 + "\tInterdiff\x12\x1e.gitmirror.v1.InterdiffRequest\x1a\x16.gitmirror.v1.FileDiff0\x01\x12O\n" + 1362 + "\n" + 1363 + "MergeCheck\x12\x1f.gitmirror.v1.MergeCheckRequest\x1a .gitmirror.v1.MergeCheckResponse\x12O\n" + 1364 + "\n" + 1365 + "ResolveRev\x12\x1f.gitmirror.v1.ResolveRevRequest\x1a .gitmirror.v1.ResolveRevResponseB2Z0tangled.org/core/gitmirror/proto/gen;gitmirrorv1b\x06proto3" 1366 + 1367 + var ( 1368 + file_gitmirror_v1_gitmirror_proto_rawDescOnce sync.Once 1369 + file_gitmirror_v1_gitmirror_proto_rawDescData []byte 1370 + ) 1371 + 1372 + func file_gitmirror_v1_gitmirror_proto_rawDescGZIP() []byte { 1373 + file_gitmirror_v1_gitmirror_proto_rawDescOnce.Do(func() { 1374 + file_gitmirror_v1_gitmirror_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_gitmirror_v1_gitmirror_proto_rawDesc), len(file_gitmirror_v1_gitmirror_proto_rawDesc))) 1375 + }) 1376 + return file_gitmirror_v1_gitmirror_proto_rawDescData 1377 + } 1378 + 1379 + var file_gitmirror_v1_gitmirror_proto_enumTypes = make([]protoimpl.EnumInfo, 1) 1380 + var file_gitmirror_v1_gitmirror_proto_msgTypes = make([]protoimpl.MessageInfo, 20) 1381 + var file_gitmirror_v1_gitmirror_proto_goTypes = []any{ 1382 + (DiffRequest_ComparisonType)(0), // 0: gitmirror.v1.DiffRequest.ComparisonType 1383 + (*ResolveRevRequest)(nil), // 1: gitmirror.v1.ResolveRevRequest 1384 + (*ResolveRevResponse)(nil), // 2: gitmirror.v1.ResolveRevResponse 1385 + (*RepoCommit)(nil), // 3: gitmirror.v1.RepoCommit 1386 + (*MergeCheckRequest)(nil), // 4: gitmirror.v1.MergeCheckRequest 1387 + (*MergeCheckResponse)(nil), // 5: gitmirror.v1.MergeCheckResponse 1388 + (*MergeConflict)(nil), // 6: gitmirror.v1.MergeConflict 1389 + (*CommitLogRequest)(nil), // 7: gitmirror.v1.CommitLogRequest 1390 + (*CommitLogResponse)(nil), // 8: gitmirror.v1.CommitLogResponse 1391 + (*GetBlobRequest)(nil), // 9: gitmirror.v1.GetBlobRequest 1392 + (*BlobChunk)(nil), // 10: gitmirror.v1.BlobChunk 1393 + (*DiffRequest)(nil), // 11: gitmirror.v1.DiffRequest 1394 + (*InterdiffRequest)(nil), // 12: gitmirror.v1.InterdiffRequest 1395 + (*FileContent)(nil), // 13: gitmirror.v1.FileContent 1396 + (*LinePair)(nil), // 14: gitmirror.v1.LinePair 1397 + (*Hunk)(nil), // 15: gitmirror.v1.Hunk 1398 + (*ByteChanges)(nil), // 16: gitmirror.v1.ByteChanges 1399 + (*FileDiff)(nil), // 17: gitmirror.v1.FileDiff 1400 + (*GitCommit)(nil), // 18: gitmirror.v1.GitCommit 1401 + (*GitSignature)(nil), // 19: gitmirror.v1.GitSignature 1402 + nil, // 20: gitmirror.v1.GitCommit.ExtraHeadersEntry 1403 + (*timestamppb.Timestamp)(nil), // 21: google.protobuf.Timestamp 1404 + } 1405 + var file_gitmirror_v1_gitmirror_proto_depIdxs = []int32{ 1406 + 3, // 0: gitmirror.v1.MergeCheckRequest.target:type_name -> gitmirror.v1.RepoCommit 1407 + 3, // 1: gitmirror.v1.MergeCheckRequest.source:type_name -> gitmirror.v1.RepoCommit 1408 + 6, // 2: gitmirror.v1.MergeCheckResponse.conflicts:type_name -> gitmirror.v1.MergeConflict 1409 + 21, // 3: gitmirror.v1.CommitLogRequest.after:type_name -> google.protobuf.Timestamp 1410 + 21, // 4: gitmirror.v1.CommitLogRequest.before:type_name -> google.protobuf.Timestamp 1411 + 18, // 5: gitmirror.v1.CommitLogResponse.commits:type_name -> gitmirror.v1.GitCommit 1412 + 3, // 6: gitmirror.v1.DiffRequest.base:type_name -> gitmirror.v1.RepoCommit 1413 + 3, // 7: gitmirror.v1.DiffRequest.head:type_name -> gitmirror.v1.RepoCommit 1414 + 0, // 8: gitmirror.v1.DiffRequest.comparison_type:type_name -> gitmirror.v1.DiffRequest.ComparisonType 1415 + 14, // 9: gitmirror.v1.Hunk.lines:type_name -> gitmirror.v1.LinePair 1416 + 13, // 10: gitmirror.v1.FileDiff.lhs_src:type_name -> gitmirror.v1.FileContent 1417 + 13, // 11: gitmirror.v1.FileDiff.rhs_src:type_name -> gitmirror.v1.FileContent 1418 + 15, // 12: gitmirror.v1.FileDiff.hunks:type_name -> gitmirror.v1.Hunk 1419 + 16, // 13: gitmirror.v1.FileDiff.has_byte_changes:type_name -> gitmirror.v1.ByteChanges 1420 + 19, // 14: gitmirror.v1.GitCommit.author:type_name -> gitmirror.v1.GitSignature 1421 + 19, // 15: gitmirror.v1.GitCommit.committer:type_name -> gitmirror.v1.GitSignature 1422 + 20, // 16: gitmirror.v1.GitCommit.extra_headers:type_name -> gitmirror.v1.GitCommit.ExtraHeadersEntry 1423 + 21, // 17: gitmirror.v1.GitSignature.date:type_name -> google.protobuf.Timestamp 1424 + 7, // 18: gitmirror.v1.GitMirrorService.CommitLog:input_type -> gitmirror.v1.CommitLogRequest 1425 + 9, // 19: gitmirror.v1.GitMirrorService.GetBlob:input_type -> gitmirror.v1.GetBlobRequest 1426 + 11, // 20: gitmirror.v1.GitMirrorService.Diff:input_type -> gitmirror.v1.DiffRequest 1427 + 12, // 21: gitmirror.v1.GitMirrorService.Interdiff:input_type -> gitmirror.v1.InterdiffRequest 1428 + 4, // 22: gitmirror.v1.GitMirrorService.MergeCheck:input_type -> gitmirror.v1.MergeCheckRequest 1429 + 1, // 23: gitmirror.v1.GitMirrorService.ResolveRev:input_type -> gitmirror.v1.ResolveRevRequest 1430 + 8, // 24: gitmirror.v1.GitMirrorService.CommitLog:output_type -> gitmirror.v1.CommitLogResponse 1431 + 10, // 25: gitmirror.v1.GitMirrorService.GetBlob:output_type -> gitmirror.v1.BlobChunk 1432 + 17, // 26: gitmirror.v1.GitMirrorService.Diff:output_type -> gitmirror.v1.FileDiff 1433 + 17, // 27: gitmirror.v1.GitMirrorService.Interdiff:output_type -> gitmirror.v1.FileDiff 1434 + 5, // 28: gitmirror.v1.GitMirrorService.MergeCheck:output_type -> gitmirror.v1.MergeCheckResponse 1435 + 2, // 29: gitmirror.v1.GitMirrorService.ResolveRev:output_type -> gitmirror.v1.ResolveRevResponse 1436 + 24, // [24:30] is the sub-list for method output_type 1437 + 18, // [18:24] is the sub-list for method input_type 1438 + 18, // [18:18] is the sub-list for extension type_name 1439 + 18, // [18:18] is the sub-list for extension extendee 1440 + 0, // [0:18] is the sub-list for field type_name 1441 + } 1442 + 1443 + func init() { file_gitmirror_v1_gitmirror_proto_init() } 1444 + func file_gitmirror_v1_gitmirror_proto_init() { 1445 + if File_gitmirror_v1_gitmirror_proto != nil { 1446 + return 1447 + } 1448 + file_gitmirror_v1_gitmirror_proto_msgTypes[4].OneofWrappers = []any{} 1449 + file_gitmirror_v1_gitmirror_proto_msgTypes[12].OneofWrappers = []any{} 1450 + file_gitmirror_v1_gitmirror_proto_msgTypes[13].OneofWrappers = []any{} 1451 + file_gitmirror_v1_gitmirror_proto_msgTypes[16].OneofWrappers = []any{} 1452 + type x struct{} 1453 + out := protoimpl.TypeBuilder{ 1454 + File: protoimpl.DescBuilder{ 1455 + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 1456 + RawDescriptor: unsafe.Slice(unsafe.StringData(file_gitmirror_v1_gitmirror_proto_rawDesc), len(file_gitmirror_v1_gitmirror_proto_rawDesc)), 1457 + NumEnums: 1, 1458 + NumMessages: 20, 1459 + NumExtensions: 0, 1460 + NumServices: 1, 1461 + }, 1462 + GoTypes: file_gitmirror_v1_gitmirror_proto_goTypes, 1463 + DependencyIndexes: file_gitmirror_v1_gitmirror_proto_depIdxs, 1464 + EnumInfos: file_gitmirror_v1_gitmirror_proto_enumTypes, 1465 + MessageInfos: file_gitmirror_v1_gitmirror_proto_msgTypes, 1466 + }.Build() 1467 + File_gitmirror_v1_gitmirror_proto = out.File 1468 + file_gitmirror_v1_gitmirror_proto_goTypes = nil 1469 + file_gitmirror_v1_gitmirror_proto_depIdxs = nil 1470 + }
+324
gitmirror/proto/gen/gitmirror_grpc.pb.go
··· 1 + // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 + // versions: 3 + // - protoc-gen-go-grpc v1.6.2 4 + // - protoc (unknown) 5 + // source: gitmirror/v1/gitmirror.proto 6 + 7 + package gitmirrorv1 8 + 9 + import ( 10 + context "context" 11 + grpc "google.golang.org/grpc" 12 + codes "google.golang.org/grpc/codes" 13 + status "google.golang.org/grpc/status" 14 + ) 15 + 16 + // This is a compile-time assertion to ensure that this generated file 17 + // is compatible with the grpc package it is being compiled against. 18 + // Requires gRPC-Go v1.64.0 or later. 19 + const _ = grpc.SupportPackageIsVersion9 20 + 21 + const ( 22 + GitMirrorService_CommitLog_FullMethodName = "/gitmirror.v1.GitMirrorService/CommitLog" 23 + GitMirrorService_GetBlob_FullMethodName = "/gitmirror.v1.GitMirrorService/GetBlob" 24 + GitMirrorService_Diff_FullMethodName = "/gitmirror.v1.GitMirrorService/Diff" 25 + GitMirrorService_Interdiff_FullMethodName = "/gitmirror.v1.GitMirrorService/Interdiff" 26 + GitMirrorService_MergeCheck_FullMethodName = "/gitmirror.v1.GitMirrorService/MergeCheck" 27 + GitMirrorService_ResolveRev_FullMethodName = "/gitmirror.v1.GitMirrorService/ResolveRev" 28 + ) 29 + 30 + // GitMirrorServiceClient is the client API for GitMirrorService service. 31 + // 32 + // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 33 + type GitMirrorServiceClient interface { 34 + CommitLog(ctx context.Context, in *CommitLogRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommitLogResponse], error) 35 + GetBlob(ctx context.Context, in *GetBlobRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[BlobChunk], error) 36 + Diff(ctx context.Context, in *DiffRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[FileDiff], error) 37 + Interdiff(ctx context.Context, in *InterdiffRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[FileDiff], error) 38 + MergeCheck(ctx context.Context, in *MergeCheckRequest, opts ...grpc.CallOption) (*MergeCheckResponse, error) 39 + ResolveRev(ctx context.Context, in *ResolveRevRequest, opts ...grpc.CallOption) (*ResolveRevResponse, error) 40 + } 41 + 42 + type gitMirrorServiceClient struct { 43 + cc grpc.ClientConnInterface 44 + } 45 + 46 + func NewGitMirrorServiceClient(cc grpc.ClientConnInterface) GitMirrorServiceClient { 47 + return &gitMirrorServiceClient{cc} 48 + } 49 + 50 + func (c *gitMirrorServiceClient) CommitLog(ctx context.Context, in *CommitLogRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommitLogResponse], error) { 51 + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 52 + stream, err := c.cc.NewStream(ctx, &GitMirrorService_ServiceDesc.Streams[0], GitMirrorService_CommitLog_FullMethodName, cOpts...) 53 + if err != nil { 54 + return nil, err 55 + } 56 + x := &grpc.GenericClientStream[CommitLogRequest, CommitLogResponse]{ClientStream: stream} 57 + if err := x.ClientStream.SendMsg(in); err != nil { 58 + return nil, err 59 + } 60 + if err := x.ClientStream.CloseSend(); err != nil { 61 + return nil, err 62 + } 63 + return x, nil 64 + } 65 + 66 + // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. 67 + type GitMirrorService_CommitLogClient = grpc.ServerStreamingClient[CommitLogResponse] 68 + 69 + func (c *gitMirrorServiceClient) GetBlob(ctx context.Context, in *GetBlobRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[BlobChunk], error) { 70 + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 71 + stream, err := c.cc.NewStream(ctx, &GitMirrorService_ServiceDesc.Streams[1], GitMirrorService_GetBlob_FullMethodName, cOpts...) 72 + if err != nil { 73 + return nil, err 74 + } 75 + x := &grpc.GenericClientStream[GetBlobRequest, BlobChunk]{ClientStream: stream} 76 + if err := x.ClientStream.SendMsg(in); err != nil { 77 + return nil, err 78 + } 79 + if err := x.ClientStream.CloseSend(); err != nil { 80 + return nil, err 81 + } 82 + return x, nil 83 + } 84 + 85 + // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. 86 + type GitMirrorService_GetBlobClient = grpc.ServerStreamingClient[BlobChunk] 87 + 88 + func (c *gitMirrorServiceClient) Diff(ctx context.Context, in *DiffRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[FileDiff], error) { 89 + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 90 + stream, err := c.cc.NewStream(ctx, &GitMirrorService_ServiceDesc.Streams[2], GitMirrorService_Diff_FullMethodName, cOpts...) 91 + if err != nil { 92 + return nil, err 93 + } 94 + x := &grpc.GenericClientStream[DiffRequest, FileDiff]{ClientStream: stream} 95 + if err := x.ClientStream.SendMsg(in); err != nil { 96 + return nil, err 97 + } 98 + if err := x.ClientStream.CloseSend(); err != nil { 99 + return nil, err 100 + } 101 + return x, nil 102 + } 103 + 104 + // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. 105 + type GitMirrorService_DiffClient = grpc.ServerStreamingClient[FileDiff] 106 + 107 + func (c *gitMirrorServiceClient) Interdiff(ctx context.Context, in *InterdiffRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[FileDiff], error) { 108 + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 109 + stream, err := c.cc.NewStream(ctx, &GitMirrorService_ServiceDesc.Streams[3], GitMirrorService_Interdiff_FullMethodName, cOpts...) 110 + if err != nil { 111 + return nil, err 112 + } 113 + x := &grpc.GenericClientStream[InterdiffRequest, FileDiff]{ClientStream: stream} 114 + if err := x.ClientStream.SendMsg(in); err != nil { 115 + return nil, err 116 + } 117 + if err := x.ClientStream.CloseSend(); err != nil { 118 + return nil, err 119 + } 120 + return x, nil 121 + } 122 + 123 + // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. 124 + type GitMirrorService_InterdiffClient = grpc.ServerStreamingClient[FileDiff] 125 + 126 + func (c *gitMirrorServiceClient) MergeCheck(ctx context.Context, in *MergeCheckRequest, opts ...grpc.CallOption) (*MergeCheckResponse, error) { 127 + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 128 + out := new(MergeCheckResponse) 129 + err := c.cc.Invoke(ctx, GitMirrorService_MergeCheck_FullMethodName, in, out, cOpts...) 130 + if err != nil { 131 + return nil, err 132 + } 133 + return out, nil 134 + } 135 + 136 + func (c *gitMirrorServiceClient) ResolveRev(ctx context.Context, in *ResolveRevRequest, opts ...grpc.CallOption) (*ResolveRevResponse, error) { 137 + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 138 + out := new(ResolveRevResponse) 139 + err := c.cc.Invoke(ctx, GitMirrorService_ResolveRev_FullMethodName, in, out, cOpts...) 140 + if err != nil { 141 + return nil, err 142 + } 143 + return out, nil 144 + } 145 + 146 + // GitMirrorServiceServer is the server API for GitMirrorService service. 147 + // All implementations must embed UnimplementedGitMirrorServiceServer 148 + // for forward compatibility. 149 + type GitMirrorServiceServer interface { 150 + CommitLog(*CommitLogRequest, grpc.ServerStreamingServer[CommitLogResponse]) error 151 + GetBlob(*GetBlobRequest, grpc.ServerStreamingServer[BlobChunk]) error 152 + Diff(*DiffRequest, grpc.ServerStreamingServer[FileDiff]) error 153 + Interdiff(*InterdiffRequest, grpc.ServerStreamingServer[FileDiff]) error 154 + MergeCheck(context.Context, *MergeCheckRequest) (*MergeCheckResponse, error) 155 + ResolveRev(context.Context, *ResolveRevRequest) (*ResolveRevResponse, error) 156 + mustEmbedUnimplementedGitMirrorServiceServer() 157 + } 158 + 159 + // UnimplementedGitMirrorServiceServer must be embedded to have 160 + // forward compatible implementations. 161 + // 162 + // NOTE: this should be embedded by value instead of pointer to avoid a nil 163 + // pointer dereference when methods are called. 164 + type UnimplementedGitMirrorServiceServer struct{} 165 + 166 + func (UnimplementedGitMirrorServiceServer) CommitLog(*CommitLogRequest, grpc.ServerStreamingServer[CommitLogResponse]) error { 167 + return status.Error(codes.Unimplemented, "method CommitLog not implemented") 168 + } 169 + func (UnimplementedGitMirrorServiceServer) GetBlob(*GetBlobRequest, grpc.ServerStreamingServer[BlobChunk]) error { 170 + return status.Error(codes.Unimplemented, "method GetBlob not implemented") 171 + } 172 + func (UnimplementedGitMirrorServiceServer) Diff(*DiffRequest, grpc.ServerStreamingServer[FileDiff]) error { 173 + return status.Error(codes.Unimplemented, "method Diff not implemented") 174 + } 175 + func (UnimplementedGitMirrorServiceServer) Interdiff(*InterdiffRequest, grpc.ServerStreamingServer[FileDiff]) error { 176 + return status.Error(codes.Unimplemented, "method Interdiff not implemented") 177 + } 178 + func (UnimplementedGitMirrorServiceServer) MergeCheck(context.Context, *MergeCheckRequest) (*MergeCheckResponse, error) { 179 + return nil, status.Error(codes.Unimplemented, "method MergeCheck not implemented") 180 + } 181 + func (UnimplementedGitMirrorServiceServer) ResolveRev(context.Context, *ResolveRevRequest) (*ResolveRevResponse, error) { 182 + return nil, status.Error(codes.Unimplemented, "method ResolveRev not implemented") 183 + } 184 + func (UnimplementedGitMirrorServiceServer) mustEmbedUnimplementedGitMirrorServiceServer() {} 185 + func (UnimplementedGitMirrorServiceServer) testEmbeddedByValue() {} 186 + 187 + // UnsafeGitMirrorServiceServer may be embedded to opt out of forward compatibility for this service. 188 + // Use of this interface is not recommended, as added methods to GitMirrorServiceServer will 189 + // result in compilation errors. 190 + type UnsafeGitMirrorServiceServer interface { 191 + mustEmbedUnimplementedGitMirrorServiceServer() 192 + } 193 + 194 + func RegisterGitMirrorServiceServer(s grpc.ServiceRegistrar, srv GitMirrorServiceServer) { 195 + // If the following call panics, it indicates UnimplementedGitMirrorServiceServer was 196 + // embedded by pointer and is nil. This will cause panics if an 197 + // unimplemented method is ever invoked, so we test this at initialization 198 + // time to prevent it from happening at runtime later due to I/O. 199 + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { 200 + t.testEmbeddedByValue() 201 + } 202 + s.RegisterService(&GitMirrorService_ServiceDesc, srv) 203 + } 204 + 205 + func _GitMirrorService_CommitLog_Handler(srv interface{}, stream grpc.ServerStream) error { 206 + m := new(CommitLogRequest) 207 + if err := stream.RecvMsg(m); err != nil { 208 + return err 209 + } 210 + return srv.(GitMirrorServiceServer).CommitLog(m, &grpc.GenericServerStream[CommitLogRequest, CommitLogResponse]{ServerStream: stream}) 211 + } 212 + 213 + // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. 214 + type GitMirrorService_CommitLogServer = grpc.ServerStreamingServer[CommitLogResponse] 215 + 216 + func _GitMirrorService_GetBlob_Handler(srv interface{}, stream grpc.ServerStream) error { 217 + m := new(GetBlobRequest) 218 + if err := stream.RecvMsg(m); err != nil { 219 + return err 220 + } 221 + return srv.(GitMirrorServiceServer).GetBlob(m, &grpc.GenericServerStream[GetBlobRequest, BlobChunk]{ServerStream: stream}) 222 + } 223 + 224 + // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. 225 + type GitMirrorService_GetBlobServer = grpc.ServerStreamingServer[BlobChunk] 226 + 227 + func _GitMirrorService_Diff_Handler(srv interface{}, stream grpc.ServerStream) error { 228 + m := new(DiffRequest) 229 + if err := stream.RecvMsg(m); err != nil { 230 + return err 231 + } 232 + return srv.(GitMirrorServiceServer).Diff(m, &grpc.GenericServerStream[DiffRequest, FileDiff]{ServerStream: stream}) 233 + } 234 + 235 + // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. 236 + type GitMirrorService_DiffServer = grpc.ServerStreamingServer[FileDiff] 237 + 238 + func _GitMirrorService_Interdiff_Handler(srv interface{}, stream grpc.ServerStream) error { 239 + m := new(InterdiffRequest) 240 + if err := stream.RecvMsg(m); err != nil { 241 + return err 242 + } 243 + return srv.(GitMirrorServiceServer).Interdiff(m, &grpc.GenericServerStream[InterdiffRequest, FileDiff]{ServerStream: stream}) 244 + } 245 + 246 + // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. 247 + type GitMirrorService_InterdiffServer = grpc.ServerStreamingServer[FileDiff] 248 + 249 + func _GitMirrorService_MergeCheck_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 250 + in := new(MergeCheckRequest) 251 + if err := dec(in); err != nil { 252 + return nil, err 253 + } 254 + if interceptor == nil { 255 + return srv.(GitMirrorServiceServer).MergeCheck(ctx, in) 256 + } 257 + info := &grpc.UnaryServerInfo{ 258 + Server: srv, 259 + FullMethod: GitMirrorService_MergeCheck_FullMethodName, 260 + } 261 + handler := func(ctx context.Context, req interface{}) (interface{}, error) { 262 + return srv.(GitMirrorServiceServer).MergeCheck(ctx, req.(*MergeCheckRequest)) 263 + } 264 + return interceptor(ctx, in, info, handler) 265 + } 266 + 267 + func _GitMirrorService_ResolveRev_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 268 + in := new(ResolveRevRequest) 269 + if err := dec(in); err != nil { 270 + return nil, err 271 + } 272 + if interceptor == nil { 273 + return srv.(GitMirrorServiceServer).ResolveRev(ctx, in) 274 + } 275 + info := &grpc.UnaryServerInfo{ 276 + Server: srv, 277 + FullMethod: GitMirrorService_ResolveRev_FullMethodName, 278 + } 279 + handler := func(ctx context.Context, req interface{}) (interface{}, error) { 280 + return srv.(GitMirrorServiceServer).ResolveRev(ctx, req.(*ResolveRevRequest)) 281 + } 282 + return interceptor(ctx, in, info, handler) 283 + } 284 + 285 + // GitMirrorService_ServiceDesc is the grpc.ServiceDesc for GitMirrorService service. 286 + // It's only intended for direct use with grpc.RegisterService, 287 + // and not to be introspected or modified (even as a copy) 288 + var GitMirrorService_ServiceDesc = grpc.ServiceDesc{ 289 + ServiceName: "gitmirror.v1.GitMirrorService", 290 + HandlerType: (*GitMirrorServiceServer)(nil), 291 + Methods: []grpc.MethodDesc{ 292 + { 293 + MethodName: "MergeCheck", 294 + Handler: _GitMirrorService_MergeCheck_Handler, 295 + }, 296 + { 297 + MethodName: "ResolveRev", 298 + Handler: _GitMirrorService_ResolveRev_Handler, 299 + }, 300 + }, 301 + Streams: []grpc.StreamDesc{ 302 + { 303 + StreamName: "CommitLog", 304 + Handler: _GitMirrorService_CommitLog_Handler, 305 + ServerStreams: true, 306 + }, 307 + { 308 + StreamName: "GetBlob", 309 + Handler: _GitMirrorService_GetBlob_Handler, 310 + ServerStreams: true, 311 + }, 312 + { 313 + StreamName: "Diff", 314 + Handler: _GitMirrorService_Diff_Handler, 315 + ServerStreams: true, 316 + }, 317 + { 318 + StreamName: "Interdiff", 319 + Handler: _GitMirrorService_Interdiff_Handler, 320 + ServerStreams: true, 321 + }, 322 + }, 323 + Metadata: "gitmirror/v1/gitmirror.proto", 324 + }
+172
gitmirror/proto/gitmirror/v1/gitmirror.proto
··· 1 + syntax = "proto3"; 2 + 3 + package gitmirror.v1; 4 + 5 + import "google/protobuf/timestamp.proto"; 6 + 7 + option go_package = "tangled.org/core/gitmirror/proto/gen;gitmirrorv1"; 8 + 9 + service GitMirrorService { 10 + rpc CommitLog(CommitLogRequest) returns (stream CommitLogResponse); 11 + rpc GetBlob(GetBlobRequest) returns (stream BlobChunk); 12 + rpc Diff(DiffRequest) returns (stream FileDiff); 13 + rpc Interdiff(InterdiffRequest) returns (stream FileDiff); 14 + rpc MergeCheck(MergeCheckRequest) returns (MergeCheckResponse); 15 + rpc ResolveRev(ResolveRevRequest) returns (ResolveRevResponse); 16 + } 17 + 18 + message ResolveRevRequest { 19 + // repo carries the DID as a string. 20 + string repo = 1; 21 + // rev is a SIMPLE revspec: a branch name, tag name, or short/full commit oid. 22 + // HEAD and revspec navigation (HEAD~2, foo^, ref@{...}, a..b, path:file) are rejected. 23 + bytes rev = 2; 24 + } 25 + message ResolveRevResponse { 26 + // commit is the resolved commit object id, hex-encoded. 27 + string commit = 1; 28 + } 29 + 30 + // A commit in a specific repo, addressed by the repo's DID. 31 + message RepoCommit { 32 + // repo carries the DID as a string. 33 + string repo = 1; 34 + // commit is a full commit object id, hex-encoded (NOT a revspec/branch/tag). 35 + bytes commit = 2; 36 + } 37 + 38 + message MergeCheckRequest { 39 + // Branch/commit being merged into (the "ours" side of the 3-way merge). 40 + RepoCommit target = 1; 41 + // PR head being merged (the "theirs" side of the 3-way merge). 42 + RepoCommit source = 2; 43 + } 44 + message MergeCheckResponse { 45 + // Whether the 3-way merge has unresolved conflicts. 46 + bool is_conflicted = 1; 47 + // One entry per conflicted file. 48 + repeated MergeConflict conflicts = 2; 49 + // Optional human-readable summary of the merge check. 50 + optional string message = 3; 51 + } 52 + message MergeConflict { 53 + string filename = 1; 54 + string reason = 2; 55 + } 56 + 57 + message CommitLogRequest { 58 + // repo DID 59 + string repo = 1; 60 + // Ranges to include in the git log (revspec, "A..B", "A...B", etc.). 61 + // At least one range, or all_refs must be specified. 62 + repeated bytes ranges = 2; 63 + // If true, all refs are searched for commits. 64 + // Must not be true when ranges are given. 65 + bool all_refs = 3; 66 + // After is an optional parameter to specify the earliest commit to consider. 67 + google.protobuf.Timestamp after = 4; 68 + // Before is an optional parameter to specify the latest commit to consider. 69 + google.protobuf.Timestamp before = 5; 70 + // MaxCommits is an optional parameter to specify the maximum number of commits 71 + // to return. If max_commits is 0, all commits that match the criteria will be 72 + // returned. 73 + uint32 max_commits = 6; 74 + // Skip is an optional parameter to specify the number of commits to skip. 75 + // This can be used to implement a poor mans pagination. 76 + uint32 skip = 7; 77 + } 78 + message CommitLogResponse { 79 + repeated GitCommit commits = 1; 80 + } 81 + 82 + message GetBlobRequest { 83 + // repo carries the DID as a string. 84 + string repo = 1; 85 + // oid is the blob object id (hex). 86 + string oid = 2; 87 + } 88 + message BlobChunk { 89 + bytes data = 1; 90 + } 91 + 92 + message DiffRequest { 93 + RepoCommit base = 1; 94 + RepoCommit head = 2; 95 + ComparisonType comparison_type = 3; 96 + 97 + enum ComparisonType { 98 + COMPARISON_TYPE_UNSPECIFIED = 0; 99 + // Corresponds to the BASE..HEAD syntax that only returns any commits that 100 + // are in HEAD but not in BASE. 101 + COMPARISON_TYPE_ONLY_IN_HEAD = 1; 102 + // Corresponds to the BASE...HEAD syntax that returns any commits that are 103 + // not in both BASE and HEAD. 104 + COMPARISON_TYPE_INTERSECTION = 2; 105 + } 106 + } 107 + 108 + message InterdiffRequest { 109 + // repo carries the DID as a string. 110 + string repo = 1; 111 + // Old patch range (from_base..from_head) and new patch range (to_base..to_head). 112 + bytes from_base = 2; 113 + bytes from_head = 3; 114 + bytes to_base = 4; 115 + bytes to_head = 5; 116 + } 117 + 118 + // Diff and Interdiff stream one FileDiff per changed file. 119 + 120 + message FileContent { 121 + string path = 1; 122 + string oid = 2; 123 + uint64 size = 3; 124 + bool is_binary = 4; 125 + bool is_submodule = 5; 126 + // Raw file bytes, inlined when the oid is not fetchable by the client 127 + // (e.g. interdiff) 128 + optional bytes content = 6; 129 + } 130 + 131 + // One aligned line pair; a side is absent (None) when unset. 132 + message LinePair { 133 + optional uint32 lhs = 1; 134 + optional uint32 rhs = 2; 135 + } 136 + 137 + message Hunk { 138 + // LineNumber (u32) sets containing novel content per side. 139 + repeated uint32 novel_lhs = 1; 140 + repeated uint32 novel_rhs = 2; 141 + repeated LinePair lines = 3; 142 + } 143 + 144 + // Number of bytes per side when the files differ (Rust Option<(usize, usize)>). 145 + message ByteChanges { 146 + uint64 lhs = 1; 147 + uint64 rhs = 2; 148 + } 149 + 150 + message FileDiff { 151 + FileContent lhs_src = 1; 152 + FileContent rhs_src = 2; 153 + repeated Hunk hunks = 3; 154 + optional ByteChanges has_byte_changes = 4; 155 + bool has_syntactic_changes = 5; 156 + // TODO: lhs_positions & rhs_positions 157 + } 158 + 159 + message GitCommit { 160 + string oid = 1; 161 + GitSignature author = 2; 162 + GitSignature committer = 3; 163 + bytes message = 4; 164 + repeated string parents = 5; 165 + map<string, string> extra_headers = 6; 166 + } 167 + 168 + message GitSignature { 169 + bytes name = 1; 170 + bytes email = 2; 171 + google.protobuf.Timestamp date = 3; 172 + }
+365
gitmirror/src/diff.rs
··· 1 + use gix::ObjectId; 2 + use gix::bstr::ByteSlice as _; 3 + use gix::objs::tree::EntryKind; 4 + use line_numbers::{LineNumber, SingleLineSpan}; 5 + use rustc_hash::FxHashSet; 6 + use tracing::info; 7 + 8 + /// Blobs larger than this are treated as binary. 9 + const LARGE_FILE_THRESHOLD_BYTES: u64 = 1024 * 1024; 10 + 11 + #[derive(Debug)] 12 + pub struct FileContent { 13 + pub(crate) path: String, 14 + pub(crate) oid: String, 15 + pub(crate) size: usize, 16 + pub(crate) is_binary: bool, 17 + pub(crate) is_submodule: bool, 18 + /// Raw file bytes, inlined when the `oid` is not fetchable by the client (interdiff's 19 + /// synthetic rebased tree). `None` for ordinary diffs and for binary/oversized blobs. 20 + pub(crate) content: Option<Vec<u8>>, 21 + } 22 + 23 + impl From<FileContent> for crate::protocol::v1::FileContent { 24 + fn from(f: FileContent) -> Self { 25 + Self { 26 + path: f.path, 27 + oid: f.oid, 28 + size: f.size as u64, 29 + is_binary: f.is_binary, 30 + is_submodule: f.is_submodule, 31 + content: f.content, 32 + } 33 + } 34 + } 35 + 36 + #[derive(Debug, Clone)] 37 + pub struct Hunk { 38 + pub(crate) novel_lhs: FxHashSet<LineNumber>, 39 + pub(crate) novel_rhs: FxHashSet<LineNumber>, 40 + pub(crate) lines: Vec<(Option<LineNumber>, Option<LineNumber>)>, 41 + } 42 + 43 + impl From<Hunk> for crate::protocol::v1::Hunk { 44 + fn from(h: Hunk) -> Self { 45 + // Sort the novel line sets for deterministic output. 46 + let sorted = |set: FxHashSet<LineNumber>| -> Vec<u32> { 47 + let mut v: Vec<u32> = set.into_iter().map(|n| n.0).collect(); 48 + v.sort_unstable(); 49 + v 50 + }; 51 + Self { 52 + novel_lhs: sorted(h.novel_lhs), 53 + novel_rhs: sorted(h.novel_rhs), 54 + lines: h 55 + .lines 56 + .into_iter() 57 + .map(|(lhs, rhs)| crate::protocol::v1::LinePair { 58 + lhs: lhs.map(|n| n.0), 59 + rhs: rhs.map(|n| n.0), 60 + }) 61 + .collect(), 62 + } 63 + } 64 + } 65 + 66 + /// A matched token (an atom, a delimiter, or a comment word). 67 + #[derive(PartialEq, Eq, Debug, Clone)] 68 + pub enum MatchKind { 69 + // TBD 70 + } 71 + 72 + #[derive(Debug, Clone, PartialEq, Eq)] 73 + pub struct MatchedPos { 74 + pub(crate) kind: MatchKind, 75 + pub(crate) pos: SingleLineSpan, 76 + } 77 + 78 + #[derive(Debug)] 79 + pub struct Diff { 80 + pub(crate) lhs_src: FileContent, 81 + pub(crate) rhs_src: FileContent, 82 + pub(crate) hunks: Vec<Hunk>, 83 + 84 + #[allow(unused)] 85 + pub(crate) lhs_positions: Vec<MatchedPos>, 86 + #[allow(unused)] 87 + pub(crate) rhs_positions: Vec<MatchedPos>, 88 + 89 + /// If the two files do not have exactly the same bytes, the 90 + /// number of bytes in each file. 91 + pub(crate) has_byte_changes: Option<(usize, usize)>, 92 + pub(crate) has_syntactic_changes: bool, 93 + } 94 + 95 + impl From<Diff> for crate::protocol::v1::FileDiff { 96 + fn from(d: Diff) -> Self { 97 + Self { 98 + lhs_src: Some(d.lhs_src.into()), 99 + rhs_src: Some(d.rhs_src.into()), 100 + hunks: d.hunks.into_iter().map(Into::into).collect(), 101 + has_byte_changes: d.has_byte_changes.map(|(lhs, rhs)| { 102 + crate::protocol::v1::ByteChanges { 103 + lhs: lhs as u64, 104 + rhs: rhs as u64, 105 + } 106 + }), 107 + has_syntactic_changes: d.has_syntactic_changes, 108 + } 109 + } 110 + } 111 + 112 + /// Diff two trees, yielding one [`Diff`] per changed file (line-level [`Hunk`]s included). 113 + /// 114 + /// Returns a lazy iterator of `anyhow::Result<Diff>`. 115 + pub fn diff<'repo>( 116 + repo: &'repo gix::Repository, 117 + old_tree: &gix::Tree<'_>, 118 + new_tree: &gix::Tree<'_>, 119 + embed_content: bool, 120 + ) -> anyhow::Result<DiffIter<'repo>> { 121 + let changes = repo.diff_tree_to_tree(Some(old_tree), Some(new_tree), None)?; 122 + let mut cache = repo.diff_resource_cache_for_tree_diff()?; 123 + cache.filter.options.large_file_threshold_bytes = LARGE_FILE_THRESHOLD_BYTES; 124 + Ok(DiffIter { 125 + repo, 126 + changes: changes.into_iter(), 127 + cache, 128 + embed_content, 129 + }) 130 + } 131 + 132 + /// Lazy iterator over per-file [`Diff`]s. 133 + pub struct DiffIter<'repo> { 134 + repo: &'repo gix::Repository, 135 + changes: std::vec::IntoIter<gix::object::tree::diff::ChangeDetached>, 136 + cache: gix::diff::blob::Platform, 137 + embed_content: bool, 138 + } 139 + 140 + impl Iterator for DiffIter<'_> { 141 + type Item = anyhow::Result<Diff>; 142 + 143 + fn next(&mut self) -> Option<Self::Item> { 144 + loop { 145 + let change = self.changes.next()?; 146 + let (old, new) = change_sides(&change); 147 + // Trees are recursed into by gix and never diffed directly; skip defensively. 148 + if old.is_kind(EntryKind::Tree) || new.is_kind(EntryKind::Tree) { 149 + continue; 150 + } 151 + // Submodules (gitlinks) can't go through the blob pipeline β€” their commit lives in 152 + // another repo. Emit the commit oids directly for the caller to compare. 153 + if old.is_kind(EntryKind::Commit) || new.is_kind(EntryKind::Commit) { 154 + return Some(Ok(submodule_diff(self.repo, &change, old, new))); 155 + } 156 + let result = build_diff(self.repo, &change, &mut self.cache, self.embed_content); 157 + self.cache.clear_resource_cache_keep_allocation(); 158 + return Some(result); 159 + } 160 + } 161 + } 162 + 163 + enum Side { 164 + Absent, 165 + Present { oid: ObjectId, kind: EntryKind }, 166 + } 167 + 168 + impl Side { 169 + fn is_kind(&self, wanted: EntryKind) -> bool { 170 + matches!(self, Side::Present { kind, .. } if *kind == wanted) 171 + } 172 + } 173 + 174 + /// Decompose a change into its `(old, new)` sides regardless of variant. 175 + fn change_sides(change: &gix::object::tree::diff::ChangeDetached) -> (Side, Side) { 176 + use gix::object::tree::diff::ChangeDetached as C; 177 + let present = |oid, mode: gix::object::tree::EntryMode| Side::Present { 178 + oid, 179 + kind: mode.kind(), 180 + }; 181 + match change { 182 + C::Addition { id, entry_mode, .. } => (Side::Absent, present(*id, *entry_mode)), 183 + C::Deletion { id, entry_mode, .. } => (present(*id, *entry_mode), Side::Absent), 184 + C::Modification { 185 + previous_id, 186 + previous_entry_mode, 187 + id, 188 + entry_mode, 189 + .. 190 + } => ( 191 + present(*previous_id, *previous_entry_mode), 192 + present(*id, *entry_mode), 193 + ), 194 + C::Rewrite { 195 + source_id, 196 + source_entry_mode, 197 + id, 198 + entry_mode, 199 + .. 200 + } => ( 201 + present(*source_id, *source_entry_mode), 202 + present(*id, *entry_mode), 203 + ), 204 + } 205 + } 206 + 207 + /// Build a [`Diff`] for a submodule (gitlink) change: no hunks, both sides carry their commit 208 + /// oid with `is_submodule` set. The caller compares the oids as submodule pointers. 209 + fn submodule_diff( 210 + repo: &gix::Repository, 211 + change: &gix::object::tree::diff::ChangeDetached, 212 + old: Side, 213 + new: Side, 214 + ) -> Diff { 215 + let path = change.location().to_string(); 216 + let side = |s: Side| FileContent { 217 + path: path.clone(), 218 + oid: match s { 219 + Side::Absent => ObjectId::null(repo.object_hash()).to_string(), 220 + Side::Present { oid, .. } => oid.to_string(), 221 + }, 222 + size: 0, 223 + is_binary: false, 224 + is_submodule: true, 225 + content: None, 226 + }; 227 + Diff { 228 + lhs_src: side(old), 229 + rhs_src: side(new), 230 + hunks: Vec::new(), 231 + lhs_positions: Vec::new(), 232 + rhs_positions: Vec::new(), 233 + has_byte_changes: None, 234 + has_syntactic_changes: false, 235 + } 236 + } 237 + 238 + /// Build the [`Diff`] for a single changed file. 239 + fn build_diff( 240 + repo: &gix::Repository, 241 + change: &gix::object::tree::diff::ChangeDetached, 242 + cache: &mut gix::diff::blob::Platform, 243 + embed_content: bool, 244 + ) -> anyhow::Result<Diff> { 245 + use gix::diff::blob::platform::prepare_diff::Operation; 246 + use gix::diff::blob::platform::resource::Data; 247 + use gix::diff::blob::{Diff as BlobDiff, InternedInput}; 248 + use gix::prelude::TreeDiffChangeExt as _; 249 + 250 + let change = change.attach(repo, repo); 251 + let blob = change.diff(cache)?; 252 + let out = blob.resource_cache.prepare_diff()?; 253 + 254 + // Per-side file metadata, derived from the prepared resources. 255 + let side = 256 + |res: &gix::diff::blob::platform::Resource<'_>, embed_content: bool| -> FileContent { 257 + let (size, is_binary, content) = match res.data { 258 + // Only textual buffers carry inlinable bytes; binary/missing sides fall back to oid. 259 + Data::Buffer { buf, .. } => (buf.len(), false, embed_content.then(|| buf.to_vec())), 260 + Data::Binary { size } => (size as usize, true, None), 261 + Data::Missing => (0, false, None), 262 + }; 263 + FileContent { 264 + path: res.rela_path.to_string(), 265 + oid: res.id.to_string(), 266 + size, 267 + is_binary, 268 + is_submodule: false, 269 + content, 270 + } 271 + }; 272 + let mut lhs_src = side(&out.old, embed_content); 273 + let mut rhs_src = side(&out.new, false); 274 + 275 + let has_byte_changes = if lhs_src.size == rhs_src.size && out.old.id == out.new.id { 276 + None 277 + } else { 278 + Some((lhs_src.size, rhs_src.size)) 279 + }; 280 + 281 + let hunks = match out.operation { 282 + Operation::InternalDiff { algorithm } => { 283 + let input = InternedInput::new(out.old.intern_source(), out.new.intern_source()); 284 + let mut d = BlobDiff::compute(algorithm, &input); 285 + d.postprocess_lines(&input); 286 + d.hunks() 287 + .map(|h| { 288 + let before: Vec<LineNumber> = h.before.clone().map(LineNumber).collect(); 289 + let after: Vec<LineNumber> = h.after.clone().map(LineNumber).collect(); 290 + let n = before.len().max(after.len()); 291 + let lines = (0..n) 292 + .map(|i| (before.get(i).copied(), after.get(i).copied())) 293 + .collect(); 294 + Hunk { 295 + novel_lhs: before.iter().copied().collect(), 296 + novel_rhs: after.iter().copied().collect(), 297 + lines, 298 + } 299 + }) 300 + .collect() 301 + } 302 + Operation::SourceOrDestinationIsBinary => { 303 + lhs_src.is_binary = true; 304 + rhs_src.is_binary = true; 305 + Vec::new() 306 + } 307 + Operation::ExternalCommand { .. } => unreachable!("we disabled that"), 308 + }; 309 + 310 + Ok(Diff { 311 + lhs_src, 312 + rhs_src, 313 + hunks, 314 + lhs_positions: Vec::new(), 315 + rhs_positions: Vec::new(), 316 + has_byte_changes, 317 + has_syntactic_changes: false, 318 + }) 319 + } 320 + 321 + /// Compute the two trees whose diff is the interdiff between an old patch 322 + /// version (`from_base..from_head`) and a new one (`to_base..to_head`). 323 + /// 324 + /// Rebase `from_base..from_head` onto `to_base` and return rebased tree so 325 + /// caller can diff between `rebased_tree` and `to_head.tree` 326 + pub fn prepare_interdiff<'repo>( 327 + repo: &'repo gix::Repository, 328 + (from_base_id, from_head_id): (ObjectId, ObjectId), 329 + to_base_id: ObjectId, 330 + ) -> anyhow::Result<gix::Tree<'repo>> { 331 + let from_head = repo.find_commit(from_head_id)?; 332 + 333 + let from_base = repo.find_commit(from_base_id)?; 334 + let to_base = repo.find_commit(to_base_id)?; 335 + 336 + // Endpoint trees for the 3-way merge. 337 + let from_base_tree = from_base.tree_id()?.detach(); 338 + let from_head_tree = from_head.tree_id()?.detach(); 339 + let to_base_tree = to_base.tree_id()?.detach(); 340 + 341 + let options = repo.tree_merge_options()?; 342 + let labels = gix::merge::blob::builtin_driver::text::Labels { 343 + ancestor: Some("from.base".as_bytes().as_bstr()), 344 + current: Some("to.base".as_bytes().as_bstr()), 345 + other: Some("from.head".as_bytes().as_bstr()), 346 + }; 347 + let mut outcome = repo.merge_trees( 348 + from_base_tree, 349 + to_base_tree, 350 + from_head_tree, 351 + labels, 352 + options, 353 + )?; 354 + 355 + if !outcome.conflicts.is_empty() { 356 + info!( 357 + conflicts = outcome.conflicts.len(), 358 + "interdiff rebase produced conflicts; tree contains conflict markers" 359 + ); 360 + } 361 + 362 + let rebased_id = outcome.tree.write()?.detach(); 363 + let rebased_tree = repo.find_tree(rebased_id)?; 364 + Ok(rebased_tree) 365 + }
+46
gitmirror/src/main.rs
··· 1 + use std::net::SocketAddr; 2 + use std::path::PathBuf; 3 + 4 + use clap::{Parser, Subcommand}; 5 + 6 + mod diff; 7 + mod merge; 8 + mod protocol; 9 + mod service; 10 + 11 + #[derive(Parser)] 12 + #[command(name = "gitmirror", about = "Git mirror gRPC service")] 13 + struct Cli { 14 + #[command(subcommand)] 15 + cmd: Command, 16 + } 17 + 18 + #[derive(Subcommand)] 19 + enum Command { 20 + /// Run the gRPC server. 21 + Serve(ServeArgs), 22 + } 23 + 24 + #[derive(clap::Args)] 25 + struct ServeArgs { 26 + /// Address to bind the gRPC server to. 27 + #[arg(long, env = "GITMIRROR_ADDR", default_value = "127.0.0.1:9000")] 28 + addr: SocketAddr, 29 + 30 + /// Base directory holding bare mirror repos, one per DID (<base>/<did>). 31 + #[arg(long, env = "GITMIRROR_REPO_BASE", default_value = "repos")] 32 + repo_base: PathBuf, 33 + } 34 + 35 + #[tokio::main] 36 + async fn main() -> anyhow::Result<()> { 37 + tracing_subscriber::fmt() 38 + .with_env_filter( 39 + tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()), 40 + ) 41 + .init(); 42 + 43 + match Cli::parse().cmd { 44 + Command::Serve(args) => service::serve(args.addr, args.repo_base).await, 45 + } 46 + }
+289
gitmirror/src/merge.rs
··· 1 + use gix::bstr::ByteSlice as _; 2 + use gix::diff::tree_with_rewrites::Change; 3 + use gix::merge::tree::{Conflict, TreatAsUnresolved}; 4 + 5 + use crate::protocol::v1::{MergeCheckResponse, MergeConflict}; 6 + 7 + /// Perform a 3-way merge check between `target` (ours) and `source` (theirs), using their 8 + /// merge base as the common ancestor. 9 + /// 10 + /// This is read-only: it inspects only the conflicts of the in-memory merge and never persists 11 + /// the merged tree. The caller must have opened `repo` with [`gix::Repository::with_object_memory`] 12 + /// so that any objects `merge_trees` writes while merging land in memory rather than on disk β€” 13 + /// the mirrored repositories (reached read-only via alternates) are never mutated. 14 + pub fn merge_check( 15 + repo: &gix::Repository, 16 + target_oid: gix::ObjectId, 17 + source_oid: gix::ObjectId, 18 + ) -> anyhow::Result<MergeCheckResponse> { 19 + let our_tree = repo.find_commit(target_oid)?.tree_id()?.detach(); 20 + let their_tree = repo.find_commit(source_oid)?.tree_id()?.detach(); 21 + 22 + // Common ancestor for the 3-way merge. When the histories are unrelated (no merge base), 23 + // fall back to the empty tree β€” the same behaviour as `git merge --allow-unrelated-histories`, 24 + // which surfaces add/add conflicts with real filenames instead of erroring out. 25 + let (ancestor_tree, unrelated) = match repo.merge_base(target_oid, source_oid) { 26 + Ok(base) => (repo.find_commit(base.detach())?.tree_id()?.detach(), false), 27 + Err(_) => (gix::ObjectId::empty_tree(repo.object_hash()), true), 28 + }; 29 + 30 + let options = repo.tree_merge_options()?; 31 + let labels = gix::merge::blob::builtin_driver::text::Labels { 32 + ancestor: Some("ancestor".as_bytes().as_bstr()), 33 + current: Some("target".as_bytes().as_bstr()), 34 + other: Some("source".as_bytes().as_bstr()), 35 + }; 36 + 37 + let outcome = repo.merge_trees(ancestor_tree, our_tree, their_tree, labels, options)?; 38 + 39 + // Report only genuinely-unresolved conflicts (matching git's notion), so gix's 40 + // auto-resolutions don't show up as false positives. The merged tree is intentionally 41 + // never written. 42 + let mut conflicts = Vec::new(); 43 + for conflict in &outcome.conflicts { 44 + if !conflict.is_unresolved(TreatAsUnresolved::git()) { 45 + continue; 46 + } 47 + conflicts.push(MergeConflict { 48 + filename: conflict.ours.location().to_string(), 49 + reason: conflict_reason(conflict).to_string(), 50 + }); 51 + } 52 + 53 + let is_conflicted = !conflicts.is_empty(); 54 + let message = match (is_conflicted, unrelated) { 55 + (true, true) => Some(format!( 56 + "{} conflicting file(s); unrelated histories (no common ancestor)", 57 + conflicts.len() 58 + )), 59 + (true, false) => Some(format!("{} conflicting file(s)", conflicts.len())), 60 + (false, true) => Some("no common ancestor (unrelated histories)".to_string()), 61 + (false, false) => None, 62 + }; 63 + 64 + Ok(MergeCheckResponse { 65 + is_conflicted, 66 + conflicts, 67 + message, 68 + }) 69 + } 70 + 71 + /// Classify a conflict into a short, human-readable reason from the shape of the two changes. 72 + /// Derived from the change variants rather than the resolution failure so it stays stable 73 + /// across gix versions. 74 + fn conflict_reason(conflict: &Conflict) -> &'static str { 75 + match (&conflict.ours, &conflict.theirs) { 76 + (Change::Deletion { .. }, _) | (_, Change::Deletion { .. }) => "modify/delete conflict", 77 + (Change::Addition { .. }, Change::Addition { .. }) => "add/add conflict", 78 + (Change::Rewrite { .. }, _) | (_, Change::Rewrite { .. }) => "rename conflict", 79 + _ => "content conflict", 80 + } 81 + } 82 + 83 + #[cfg(test)] 84 + mod tests { 85 + use super::*; 86 + use std::path::Path; 87 + use std::process::Command; 88 + 89 + fn git(dir: &Path, args: &[&str]) { 90 + let status = Command::new("git") 91 + .args(args) 92 + .current_dir(dir) 93 + .env("GIT_AUTHOR_NAME", "t") 94 + .env("GIT_AUTHOR_EMAIL", "t@t") 95 + .env("GIT_COMMITTER_NAME", "t") 96 + .env("GIT_COMMITTER_EMAIL", "t@t") 97 + .status() 98 + .expect("run git"); 99 + assert!(status.success(), "git {args:?} failed"); 100 + } 101 + 102 + fn write(dir: &Path, name: &str, content: &str) { 103 + std::fs::write(dir.join(name), content).unwrap(); 104 + } 105 + 106 + fn rev_parse(dir: &Path, rev: &str) -> gix::ObjectId { 107 + let out = Command::new("git") 108 + .args(["rev-parse", rev]) 109 + .current_dir(dir) 110 + .output() 111 + .expect("rev-parse"); 112 + let hex = String::from_utf8(out.stdout).unwrap(); 113 + gix::ObjectId::from_hex(hex.trim().as_bytes()).unwrap() 114 + } 115 + 116 + /// Count loose object files under `<repo>/.git/objects`, ignoring `info` and `pack`. 117 + fn count_loose_objects(git_dir: &Path) -> usize { 118 + let objects = git_dir.join(".git").join("objects"); 119 + let mut n = 0; 120 + for shard in std::fs::read_dir(&objects).unwrap() { 121 + let shard = shard.unwrap(); 122 + let name = shard.file_name(); 123 + if name == "info" || name == "pack" || !shard.file_type().unwrap().is_dir() { 124 + continue; 125 + } 126 + n += std::fs::read_dir(shard.path()).unwrap().count(); 127 + } 128 + n 129 + } 130 + 131 + /// Build a repo with a shared base commit, a `main` (target/ours) tip, and a `source` 132 + /// (theirs) tip built by the two closures. 133 + fn fixture( 134 + target: impl FnOnce(&Path), 135 + source: impl FnOnce(&Path), 136 + ) -> (tempfile::TempDir, gix::ObjectId, gix::ObjectId) { 137 + let dir = tempfile::tempdir().unwrap(); 138 + let p = dir.path(); 139 + git(p, &["init", "-q", "-b", "main"]); 140 + write(p, "a.txt", "line1\nline2\nline3\n"); 141 + write(p, "b.txt", "keep\n"); 142 + git(p, &["add", "."]); 143 + git(p, &["commit", "-q", "-m", "base"]); 144 + 145 + git(p, &["branch", "source"]); 146 + // target edits on main 147 + target(p); 148 + git(p, &["add", "-A"]); 149 + git(p, &["commit", "-q", "-m", "target"]); 150 + let target_oid = rev_parse(p, "main"); 151 + 152 + git(p, &["checkout", "-q", "source"]); 153 + source(p); 154 + git(p, &["add", "-A"]); 155 + git(p, &["commit", "-q", "-m", "source"]); 156 + let source_oid = rev_parse(p, "source"); 157 + 158 + (dir, target_oid, source_oid) 159 + } 160 + 161 + fn open_in_memory(dir: &Path) -> gix::Repository { 162 + gix::open(dir).unwrap().with_object_memory() 163 + } 164 + 165 + /// Mirror of `GitMirror::open_merge_scratch`: a throwaway repo whose alternates point at the 166 + /// given object dirs, opened in-memory. 167 + fn scratch_with_alternates(object_dirs: &[std::path::PathBuf]) -> (tempfile::TempDir, gix::Repository) { 168 + let scratch = tempfile::tempdir().unwrap(); 169 + gix::init_bare(scratch.path()).unwrap(); 170 + let info = scratch.path().join("objects").join("info"); 171 + std::fs::create_dir_all(&info).unwrap(); 172 + let mut alternates = String::new(); 173 + for d in object_dirs { 174 + alternates.push_str(&std::fs::canonicalize(d).unwrap().display().to_string()); 175 + alternates.push('\n'); 176 + } 177 + std::fs::write(info.join("alternates"), alternates).unwrap(); 178 + let repo = gix::open(scratch.path()).unwrap().with_object_memory(); 179 + (scratch, repo) 180 + } 181 + 182 + #[test] 183 + fn cross_repo_via_alternates_never_touches_mirrors() { 184 + // Two separate repos sharing a base commit: `a` holds the target tip, `b` the source 185 + // tip. This is the real gitmirror shape (target and source live in different mirrors). 186 + let a = tempfile::tempdir().unwrap(); 187 + let ap = a.path(); 188 + git(ap, &["init", "-q", "-b", "main"]); 189 + write(ap, "a.txt", "line1\nline2\nline3\n"); 190 + git(ap, &["add", "."]); 191 + git(ap, &["commit", "-q", "-m", "base"]); 192 + // target edit in repo `a` 193 + write(ap, "a.txt", "line1\nOURS\nline3\n"); 194 + git(ap, &["add", "-A"]); 195 + git(ap, &["commit", "-q", "-m", "target"]); 196 + let target = rev_parse(ap, "main"); 197 + 198 + // Clone `a` β†’ `b`, reset to the shared base, and make a conflicting source edit. 199 + let b = tempfile::tempdir().unwrap(); 200 + let bp = b.path(); 201 + git(bp, &["clone", "-q", ap.to_str().unwrap(), "."]); 202 + git(bp, &["checkout", "-q", "-B", "source", &target.to_string()]); 203 + git(bp, &["reset", "-q", "--hard", "HEAD~1"]); // back to base 204 + write(bp, "a.txt", "line1\nTHEIRS\nline3\n"); 205 + git(bp, &["add", "-A"]); 206 + git(bp, &["commit", "-q", "-m", "source"]); 207 + let source = rev_parse(bp, "source"); 208 + 209 + let a_objs = ap.join(".git").join("objects"); 210 + let b_objs = bp.join(".git").join("objects"); 211 + let before_a = count_loose_objects(ap); 212 + let before_b = count_loose_objects(bp); 213 + 214 + let (_scratch, repo) = scratch_with_alternates(&[a_objs, b_objs]); 215 + let out = merge_check(&repo, target, source).unwrap(); 216 + 217 + assert!(out.is_conflicted); 218 + assert_eq!(out.conflicts.len(), 1); 219 + assert_eq!(out.conflicts[0].filename, "a.txt"); 220 + // Neither mirror gained loose objects β€” the merge was read-only against both. 221 + assert_eq!(count_loose_objects(ap), before_a, "target mirror mutated"); 222 + assert_eq!(count_loose_objects(bp), before_b, "source mirror mutated"); 223 + } 224 + 225 + #[test] 226 + fn non_overlapping_edits_are_clean() { 227 + let (dir, t, s) = fixture( 228 + |p| write(p, "a.txt", "line1\nCHANGED\nline3\n"), 229 + |p| write(p, "b.txt", "keep\nappended\n"), 230 + ); 231 + let repo = open_in_memory(dir.path()); 232 + let before = count_loose_objects(dir.path()); 233 + let out = merge_check(&repo, t, s).unwrap(); 234 + assert!(!out.is_conflicted, "expected clean, got {out:?}"); 235 + assert!(out.conflicts.is_empty()); 236 + // never-touch-the-mirror: the in-memory merge writes no loose objects to disk. 237 + assert_eq!(count_loose_objects(dir.path()), before); 238 + } 239 + 240 + #[test] 241 + fn same_line_edits_conflict() { 242 + let (dir, t, s) = fixture( 243 + |p| write(p, "a.txt", "line1\nOURS\nline3\n"), 244 + |p| write(p, "a.txt", "line1\nTHEIRS\nline3\n"), 245 + ); 246 + let repo = open_in_memory(dir.path()); 247 + let before = count_loose_objects(dir.path()); 248 + let out = merge_check(&repo, t, s).unwrap(); 249 + assert!(out.is_conflicted); 250 + assert_eq!(out.conflicts.len(), 1); 251 + assert_eq!(out.conflicts[0].filename, "a.txt"); 252 + assert_eq!(out.conflicts[0].reason, "content conflict"); 253 + assert_eq!(count_loose_objects(dir.path()), before); 254 + } 255 + 256 + #[test] 257 + fn modify_delete_conflict() { 258 + let (dir, t, s) = fixture( 259 + |p| write(p, "a.txt", "line1\nOURS\nline3\n"), 260 + |p| std::fs::remove_file(p.join("a.txt")).unwrap(), 261 + ); 262 + let repo = open_in_memory(dir.path()); 263 + let out = merge_check(&repo, t, s).unwrap(); 264 + assert!(out.is_conflicted); 265 + assert_eq!(out.conflicts[0].filename, "a.txt"); 266 + assert_eq!(out.conflicts[0].reason, "modify/delete conflict"); 267 + } 268 + 269 + #[test] 270 + fn source_is_ancestor_is_clean() { 271 + // Source is an ancestor of target (i.e. already merged / up to date) β†’ no conflicts. 272 + let dir = tempfile::tempdir().unwrap(); 273 + let p = dir.path(); 274 + git(p, &["init", "-q", "-b", "main"]); 275 + write(p, "a.txt", "line1\nline2\n"); 276 + git(p, &["add", "."]); 277 + git(p, &["commit", "-q", "-m", "base"]); 278 + let base = rev_parse(p, "main"); 279 + write(p, "a.txt", "line1\nline2\nline3\n"); 280 + git(p, &["add", "-A"]); 281 + git(p, &["commit", "-q", "-m", "target"]); 282 + let target = rev_parse(p, "main"); 283 + 284 + let repo = open_in_memory(p); 285 + let out = merge_check(&repo, target, base).unwrap(); 286 + assert!(!out.is_conflicted, "expected clean, got {out:?}"); 287 + assert!(out.conflicts.is_empty()); 288 + } 289 + }
+3
gitmirror/src/protocol.rs
··· 1 + pub mod v1 { 2 + tonic::include_proto!("gitmirror.v1"); 3 + }
+523
gitmirror/src/service.rs
··· 1 + use std::net::SocketAddr; 2 + use std::path::PathBuf; 3 + use std::pin::Pin; 4 + 5 + use gix::bstr::ByteSlice as _; 6 + use gix::revision::plumbing::Spec as RevSpec; 7 + use gix::revision::walk::Sorting; 8 + use tempfile::TempDir; 9 + use tokio::sync::mpsc; 10 + use tokio_stream::wrappers::ReceiverStream; 11 + use tokio_stream::Stream; 12 + use tonic::transport::Server; 13 + use tonic::{Request, Response, Status}; 14 + use tracing::{debug, info}; 15 + 16 + use crate::diff::{self, prepare_interdiff}; 17 + use crate::protocol::v1::diff_request::ComparisonType; 18 + use crate::protocol::v1::git_mirror_service_server::{GitMirrorService, GitMirrorServiceServer}; 19 + use crate::protocol::v1::{ 20 + BlobChunk, CommitLogRequest, CommitLogResponse, DiffRequest, FileDiff, GetBlobRequest, 21 + GitCommit, GitSignature, InterdiffRequest, MergeCheckRequest, MergeCheckResponse, 22 + ResolveRevRequest, ResolveRevResponse, 23 + }; 24 + 25 + type FileDiffStream = Pin<Box<dyn Stream<Item = Result<FileDiff, Status>> + Send>>; 26 + type BlobChunkStream = Pin<Box<dyn Stream<Item = Result<BlobChunk, Status>> + Send>>; 27 + type CommitLogResponseStream = 28 + Pin<Box<dyn Stream<Item = Result<CommitLogResponse, Status>> + Send>>; 29 + 30 + const BLOB_CHUNK_SIZE: usize = 64 * 1024; 31 + const COMMIT_LOG_BATCH_SIZE: usize = 128; 32 + 33 + struct GitMirror { 34 + repo_base: PathBuf, 35 + } 36 + 37 + /// A [`gix::Repository`] backed by a temporary directory that is removed on drop. Derefs to 38 + /// `gix::Repository`, so it is used just like one; keeping it alive keeps the scratch dir alive. 39 + struct TempRepository { 40 + repo: gix::Repository, 41 + // Declared AFTER `repo` so `repo` drops first: any file handles into the scratch dir close 42 + // before the dir itself is removed (Rust drops struct fields in declaration order). 43 + _dir: TempDir, 44 + } 45 + 46 + impl TempRepository { 47 + fn new(repo: gix::Repository, dir: TempDir) -> Self { 48 + Self { repo, _dir: dir } 49 + } 50 + } 51 + 52 + impl std::ops::Deref for TempRepository { 53 + type Target = gix::Repository; 54 + fn deref(&self) -> &gix::Repository { 55 + &self.repo 56 + } 57 + } 58 + 59 + impl GitMirror { 60 + fn open_repo(&self, did: &str) -> Result<gix::Repository, Status> { 61 + if !did.starts_with("did:") { 62 + return Err(Status::invalid_argument("repo must be a DID")); 63 + } 64 + gix::open(self.repo_base.join(did)) 65 + .map_err(|e| Status::not_found(format!("repo not found: {e}"))) 66 + } 67 + 68 + /// Build a throwaway bare repo whose `objects/info/alternates` points read-only at each of the 69 + /// given git repositories, so a single `gix::Repository` can see objects from all of them 70 + /// without ever mutating them. 71 + fn open_scratch(&self, dids: &[&str]) -> Result<TempRepository, Status> { 72 + let mut seen: Vec<&str> = Vec::new(); 73 + let mut object_dirs = Vec::new(); 74 + for &did in dids { 75 + if seen.contains(&did) { 76 + continue 77 + } 78 + if !did.starts_with("did:") { 79 + return Err(Status::invalid_argument("repo must be a DID")); 80 + } 81 + seen.push(did); 82 + let object_dir = std::fs::canonicalize(self.repo_base.join(did).join("objects")) 83 + .map_err(|_| Status::not_found(format!("repo not found: {did}")))?; 84 + object_dirs.push(object_dir); 85 + } 86 + if object_dirs.is_empty() { 87 + return Err(Status::internal("open_scratch requires at least one repo")); 88 + } 89 + 90 + let scratch = tempfile::tempdir().map_err(|e| Status::internal(e.to_string()))?; 91 + gix::init_bare(scratch.path()).map_err(|e| Status::internal(e.to_string()))?; 92 + 93 + let info_dir = scratch.path().join("objects").join("info"); 94 + std::fs::create_dir_all(&info_dir).map_err(|e| Status::internal(e.to_string()))?; 95 + let alternates = object_dirs 96 + .iter() 97 + .map(|p| p.display().to_string()) 98 + .collect::<Vec<_>>() 99 + .join("\n"); 100 + std::fs::write(info_dir.join("alternates"), format!("{alternates}\n")) 101 + .map_err(|e| Status::internal(e.to_string()))?; 102 + 103 + let repo = gix::open(scratch.path()) 104 + .map_err(|e| Status::internal(e.to_string()))? 105 + .with_object_memory(); 106 + Ok(TempRepository::new(repo, scratch)) 107 + } 108 + } 109 + 110 + #[tonic::async_trait] 111 + impl GitMirrorService for GitMirror { 112 + type DiffStream = FileDiffStream; 113 + 114 + async fn diff( 115 + &self, 116 + request: Request<DiffRequest>, 117 + ) -> Result<Response<Self::DiffStream>, Status> { 118 + let req = request.into_inner(); 119 + let head = req 120 + .head 121 + .ok_or_else(|| Status::invalid_argument("head is required"))?; 122 + let base = req.base; 123 + debug!( 124 + base_repo = ?base.as_ref().map(|b| b.repo.as_str()), 125 + base_commit = ?base.as_ref().map(|b| b.commit.as_bstr()), 126 + head_repo = %head.repo, 127 + head_commit = %head.commit.as_bstr(), 128 + "diff request", 129 + ); 130 + let scratch = match &base { 131 + Some(base) => self.open_scratch(&[head.repo.as_str(), base.repo.as_str()])?, 132 + None => self.open_scratch(&[head.repo.as_str()])?, 133 + }; 134 + let head_commit_id = find_commit_by_sha(&scratch, &head.commit)?; 135 + let base_commit_id = base 136 + .as_ref() 137 + .map(|b| find_commit_by_sha(&scratch, &b.commit)) 138 + .transpose()?; 139 + let comparison_type = 140 + ComparisonType::try_from(req.comparison_type).unwrap_or(ComparisonType::Unspecified); 141 + 142 + let (tx, rx) = mpsc::channel::<Result<FileDiff, Status>>(16); 143 + tokio::task::spawn_blocking(move || { 144 + let repo = &*scratch; 145 + 146 + let run = || -> anyhow::Result<()> { 147 + let head_commit = repo.find_commit(head_commit_id)?; 148 + let old = match base_commit_id { 149 + Some(base_commit_id) => match comparison_type { 150 + ComparisonType::OnlyInHead => { 151 + repo.find_tree(repo.find_commit(base_commit_id)?.tree_id()?)? 152 + } 153 + ComparisonType::Unspecified | ComparisonType::Intersection => { 154 + let merge_base = 155 + repo.merge_base(base_commit_id, head_commit_id)?.detach(); 156 + repo.find_tree(repo.find_commit(merge_base)?.tree_id()?)? 157 + } 158 + }, 159 + None => match head_commit.parent_ids().next() { 160 + Some(parent_id) => { 161 + repo.find_tree(repo.find_commit(parent_id.detach())?.tree_id()?)? 162 + } 163 + None => repo.empty_tree(), 164 + }, 165 + }; 166 + let new = repo.find_tree(head_commit.tree_id()?)?; 167 + for item in diff::diff(repo, &old, &new, false)? { 168 + if tx.blocking_send(Ok(item?.into())).is_err() { 169 + break; 170 + } 171 + } 172 + Ok(()) 173 + }; 174 + if let Err(e) = run() { 175 + let _ = tx.blocking_send(Err(Status::internal(e.to_string()))); 176 + } 177 + }); 178 + 179 + Ok(Response::new(Box::pin(ReceiverStream::new(rx)))) 180 + } 181 + 182 + type InterdiffStream = FileDiffStream; 183 + 184 + async fn interdiff( 185 + &self, 186 + request: Request<InterdiffRequest>, 187 + ) -> Result<Response<Self::InterdiffStream>, Status> { 188 + let req = request.into_inner(); 189 + let repo = self.open_repo(&req.repo)?; 190 + let from_base_id = resolve_commit_oid(&repo, &req.from_base)?; 191 + let from_head_id = resolve_commit_oid(&repo, &req.from_head)?; 192 + let to_base_id = resolve_commit_oid(&repo, &req.to_base)?; 193 + let to_head_id = resolve_commit_oid(&repo, &req.to_head)?; 194 + let safe = repo.into_sync(); 195 + 196 + let (tx, rx) = mpsc::channel::<Result<FileDiff, Status>>(16); 197 + tokio::task::spawn_blocking(move || { 198 + let repo = safe.to_thread_local(); 199 + let run = || -> anyhow::Result<()> { 200 + let to_head = repo.find_commit(to_head_id)?; 201 + let to_head_tree = to_head.tree()?; 202 + let rebased_tree = 203 + prepare_interdiff(&repo, (from_base_id, from_head_id), to_base_id)?; 204 + for item in diff::diff(&repo, &rebased_tree, &to_head_tree, true)? { 205 + if tx.blocking_send(Ok(item?.into())).is_err() { 206 + break; 207 + } 208 + } 209 + Ok(()) 210 + }; 211 + if let Err(e) = run() { 212 + let _ = tx.blocking_send(Err(Status::internal(e.to_string()))); 213 + } 214 + }); 215 + 216 + Ok(Response::new(Box::pin(ReceiverStream::new(rx)))) 217 + } 218 + 219 + type GetBlobStream = BlobChunkStream; 220 + 221 + async fn get_blob( 222 + &self, 223 + request: Request<GetBlobRequest>, 224 + ) -> Result<Response<Self::GetBlobStream>, Status> { 225 + let req = request.into_inner(); 226 + let repo = self.open_repo(&req.repo)?; 227 + let oid = gix::ObjectId::from_hex(req.oid.as_bytes()) 228 + .map_err(|e| Status::invalid_argument(format!("bad oid '{}': {e}", req.oid)))?; 229 + let safe = repo.into_sync(); 230 + 231 + let (tx, rx) = mpsc::channel::<Result<BlobChunk, Status>>(16); 232 + tokio::task::spawn_blocking(move || { 233 + let repo = safe.to_thread_local(); 234 + let run = || -> anyhow::Result<()> { 235 + let blob = repo.find_object(oid)?.try_into_blob()?; 236 + for chunk in blob.data.chunks(BLOB_CHUNK_SIZE) { 237 + let msg = BlobChunk { 238 + data: chunk.to_vec(), 239 + }; 240 + if tx.blocking_send(Ok(msg)).is_err() { 241 + break; 242 + } 243 + } 244 + Ok(()) 245 + }; 246 + if let Err(e) = run() { 247 + let _ = tx.blocking_send(Err(Status::internal(e.to_string()))); 248 + } 249 + }); 250 + 251 + Ok(Response::new(Box::pin(ReceiverStream::new(rx)))) 252 + } 253 + 254 + type CommitLogStream = CommitLogResponseStream; 255 + 256 + async fn commit_log( 257 + &self, 258 + request: Request<CommitLogRequest>, 259 + ) -> Result<Response<Self::CommitLogStream>, Status> { 260 + let req = request.into_inner(); 261 + debug!( 262 + repo = %req.repo, 263 + all_refs = req.all_refs, 264 + ranges = ?req.ranges, 265 + after = ?req.after, 266 + before = ?req.before, 267 + max_commits = req.max_commits, 268 + skip = req.skip, 269 + "commit_log request", 270 + ); 271 + if req.all_refs != req.ranges.is_empty() { 272 + return Err(Status::invalid_argument( 273 + "specify exactly one of `ranges` or `all_refs`", 274 + )); 275 + } 276 + let repo = self.open_repo(&req.repo)?; 277 + let safe = repo.into_sync(); 278 + 279 + let (tx, rx) = mpsc::channel::<Result<CommitLogResponse, Status>>(16); 280 + tokio::task::spawn_blocking(move || { 281 + let repo = safe.to_thread_local(); 282 + let run = || -> anyhow::Result<()> { 283 + let after = req.after.map(|t| t.seconds); 284 + let before = req.before.map(|t| t.seconds); 285 + let max_commits = req.max_commits as usize; 286 + let mut skip = req.skip as usize; 287 + let mut sent = 0usize; 288 + let mut batch = Vec::with_capacity(COMMIT_LOG_BATCH_SIZE); 289 + 290 + for info in commit_log_walk(&repo, &req)? { 291 + let info = info?; 292 + let commit_time = info.commit_time(); 293 + if before.is_some_and(|b| commit_time > b) { 294 + continue; 295 + } 296 + if after.is_some_and(|a| commit_time < a) { 297 + break; 298 + } 299 + if skip > 0 { 300 + skip -= 1; 301 + continue; 302 + } 303 + if max_commits != 0 && sent >= max_commits { 304 + break; 305 + } 306 + batch.push(GitCommit::try_from(&info.object()?)?); 307 + sent += 1; 308 + if batch.len() == COMMIT_LOG_BATCH_SIZE { 309 + let msg = CommitLogResponse { 310 + commits: std::mem::take(&mut batch), 311 + }; 312 + if tx.blocking_send(Ok(msg)).is_err() { 313 + return Ok(()); // client hung up 314 + } 315 + } 316 + } 317 + if !batch.is_empty() { 318 + let _ = tx.blocking_send(Ok(CommitLogResponse { commits: batch })); 319 + } 320 + Ok(()) 321 + }; 322 + if let Err(e) = run() { 323 + let _ = tx.blocking_send(Err(Status::internal(e.to_string()))); 324 + } 325 + }); 326 + 327 + Ok(Response::new(Box::pin(ReceiverStream::new(rx)))) 328 + } 329 + 330 + async fn merge_check( 331 + &self, 332 + request: Request<MergeCheckRequest>, 333 + ) -> Result<Response<MergeCheckResponse>, Status> { 334 + let req = request.into_inner(); 335 + let target = req 336 + .target 337 + .ok_or_else(|| Status::invalid_argument("target is required"))?; 338 + let source = req 339 + .source 340 + .ok_or_else(|| Status::invalid_argument("source is required"))?; 341 + debug!( 342 + target_repo = %target.repo, 343 + target_commit = %target.commit.as_bstr(), 344 + source_repo = %source.repo, 345 + source_commit = %source.commit.as_bstr(), 346 + "merge_check request", 347 + ); 348 + 349 + let scratch = self.open_scratch(&[target.repo.as_str(), source.repo.as_str()])?; 350 + 351 + let response = 352 + tokio::task::spawn_blocking(move || -> Result<MergeCheckResponse, Status> { 353 + let target_oid = find_commit_by_sha(&scratch, &target.commit)?; 354 + let source_oid = find_commit_by_sha(&scratch, &source.commit)?; 355 + crate::merge::merge_check(&scratch, target_oid, source_oid) 356 + .map_err(|e| Status::internal(e.to_string())) 357 + }) 358 + .await 359 + .map_err(|e| Status::internal(format!("merge task panicked: {e}")))??; 360 + 361 + Ok(Response::new(response)) 362 + } 363 + 364 + async fn resolve_rev( 365 + &self, 366 + request: Request<ResolveRevRequest>, 367 + ) -> Result<Response<ResolveRevResponse>, Status> { 368 + let req = request.into_inner(); 369 + debug!(repo = %req.repo, rev = %req.rev.as_bstr(), "resolve_rev request"); 370 + let repo = self.open_repo(&req.repo)?; 371 + let commit = tokio::task::spawn_blocking(move || -> Result<String, Status> { 372 + Ok(resolve_simple_rev(&repo, &req.rev)?.to_string()) 373 + }) 374 + .await 375 + .map_err(|e| Status::internal(format!("resolve task panicked: {e}")))??; 376 + Ok(Response::new(ResolveRevResponse { commit })) 377 + } 378 + } 379 + 380 + fn resolve_commit_oid(repo: &gix::Repository, spec: &[u8]) -> Result<gix::ObjectId, Status> { 381 + let commit = repo 382 + .rev_parse_single(spec.as_bstr()) 383 + .map_err(|e| { 384 + Status::invalid_argument(format!("bad rev '{}': {e}", String::from_utf8_lossy(spec))) 385 + })? 386 + .object() 387 + .map_err(|e| Status::internal(e.to_string()))? 388 + .peel_to_commit() 389 + .map_err(|e| Status::internal(e.to_string()))?; 390 + Ok(commit.id) 391 + } 392 + 393 + fn resolve_simple_rev(repo: &gix::Repository, rev: &[u8]) -> Result<gix::ObjectId, Status> { 394 + let unsupported = rev == b"HEAD" 395 + || rev == b"@" 396 + || rev.windows(2).any(|w| w == b"@{") 397 + || rev.iter().any(|&b| matches!(b, b'~' | b'^' | b':')); 398 + if unsupported { 399 + return Err(Status::invalid_argument(format!( 400 + "unsupported rev '{}': only a branch, tag, or commit oid is allowed", 401 + rev.as_bstr() 402 + ))); 403 + } 404 + resolve_commit_oid(repo, rev) 405 + } 406 + 407 + fn find_commit_by_sha(repo: &gix::Repository, sha: &[u8]) -> Result<gix::ObjectId, Status> { 408 + let oid = gix::ObjectId::from_hex(sha).map_err(|e| { 409 + Status::invalid_argument(format!("bad commit sha '{}': {e}", sha.as_bstr())) 410 + })?; 411 + repo.find_commit(oid) 412 + .map_err(|_| Status::not_found(format!("commit not found: {oid}")))?; 413 + Ok(oid) 414 + } 415 + 416 + fn commit_log_walk<'repo>( 417 + repo: &'repo gix::Repository, 418 + req: &CommitLogRequest, 419 + ) -> anyhow::Result<gix::revision::Walk<'repo>> { 420 + let mut tips = Vec::new(); 421 + let mut hidden = Vec::new(); 422 + 423 + if req.all_refs { 424 + for r in repo.references()?.all()? { 425 + let mut r = r.map_err(|e| anyhow::anyhow!(e))?; 426 + if let Ok(commit) = r.peel_to_commit() { 427 + tips.push(commit.id); 428 + } 429 + } 430 + } else { 431 + for range in &req.ranges { 432 + let revspec = repo.rev_parse(range.as_bstr())?; 433 + let spec = revspec.detach(); 434 + match spec { 435 + RevSpec::Include(id) => tips.push(id), 436 + RevSpec::Range { from, to } => { 437 + tips.push(to); 438 + hidden.push(from); 439 + } 440 + _ => { 441 + anyhow::bail!("The spec isn't currently supported: {spec:?}") 442 + } 443 + } 444 + } 445 + } 446 + 447 + Ok(repo 448 + .rev_walk(tips) 449 + .sorting(Sorting::ByCommitTime(Default::default())) 450 + .with_hidden(hidden) 451 + .all()?) 452 + } 453 + 454 + impl TryFrom<&gix::Commit<'_>> for GitCommit { 455 + type Error = anyhow::Error; 456 + 457 + fn try_from(commit: &gix::Commit<'_>) -> anyhow::Result<Self> { 458 + let decoded = commit.decode()?; 459 + Ok(GitCommit { 460 + oid: commit.id.to_string(), 461 + author: Some(decoded.author()?.try_into()?), 462 + committer: Some(decoded.committer()?.try_into()?), 463 + message: decoded.message.to_vec(), 464 + parents: decoded 465 + .parents 466 + .into_iter() 467 + .map(|id| id.to_string()) 468 + .collect(), 469 + extra_headers: decoded 470 + .extra_headers 471 + .iter() 472 + .map(|(k, v)| (k.to_string(), v.to_string())) 473 + .collect(), 474 + }) 475 + } 476 + } 477 + 478 + impl TryFrom<gix::actor::SignatureRef<'_>> for GitSignature { 479 + type Error = anyhow::Error; 480 + 481 + fn try_from(sig: gix::actor::SignatureRef<'_>) -> anyhow::Result<Self> { 482 + let time = sig.time()?; 483 + Ok(GitSignature { 484 + name: sig.name.to_vec(), 485 + email: sig.email.to_vec(), 486 + date: Some(prost_types::Timestamp { 487 + seconds: time.seconds, 488 + nanos: 0, 489 + }), 490 + }) 491 + } 492 + } 493 + 494 + pub async fn serve(addr: SocketAddr, repo_base: PathBuf) -> anyhow::Result<()> { 495 + let service = GitMirror { repo_base }; 496 + info!(addr = %addr, "gitmirror gRPC server listening"); 497 + 498 + Server::builder() 499 + .add_service(GitMirrorServiceServer::new(service)) 500 + .serve(addr) 501 + .await?; 502 + 503 + Ok(()) 504 + } 505 + 506 + #[cfg(test)] 507 + mod tests { 508 + use super::*; 509 + 510 + #[test] 511 + fn temp_repository_removes_scratch_dir_on_drop() { 512 + let dir = tempfile::tempdir().unwrap(); 513 + gix::init_bare(dir.path()).unwrap(); 514 + let repo = gix::open(dir.path()).unwrap().with_object_memory(); 515 + let temp = TempRepository::new(repo, dir); 516 + let path = temp.git_dir().to_path_buf(); 517 + assert!(path.exists()); 518 + // Derefs to gix::Repository. 519 + assert!(temp.object_hash() == gix::hash::Kind::Sha1); 520 + drop(temp); 521 + assert!(!path.exists(), "scratch dir should be gone after drop"); 522 + } 523 + }
+10
localinfra/Caddyfile
··· 53 53 reverse_proxy knotmirror:7000 54 54 } 55 55 56 + gitmirror.tngl.boltless.dev { 57 + tls internal 58 + # gitmirror (tonic) serves h2c; Caddy defaults to HTTP/1.1 to the backend. 59 + reverse_proxy gitmirror:9000 { 60 + transport http { 61 + versions h2c 2 62 + } 63 + } 64 + } 65 + 56 66 # zoekt (internal service) 57 67 zoekt.tngl.boltless.dev { 58 68 tls internal
+29
localinfra/gitmirror.Dockerfile
··· 1 + # Development only. Not for production use. 2 + 3 + FROM rust:1.96-bookworm AS build 4 + 5 + RUN apt-get update && apt-get install -y --no-install-recommends protobuf-compiler libprotobuf-dev \ 6 + && rm -rf /var/lib/apt/lists/* 7 + 8 + WORKDIR /src 9 + COPY . . 10 + 11 + # Cache mounts on the cargo registry + target dir. The binary is copied out within the same 12 + # RUN because the target cache mount is unmounted afterwards (a later COPY --from could not 13 + # see it). 14 + RUN --mount=type=cache,target=/usr/local/cargo/registry \ 15 + --mount=type=cache,target=/src/target \ 16 + cargo build --release -p gitmirror \ 17 + && cp /src/target/release/gitmirror /usr/local/bin/gitmirror 18 + 19 + FROM debian:bookworm-slim 20 + 21 + RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates tini \ 22 + && rm -rf /var/lib/apt/lists/* 23 + 24 + COPY --from=build /usr/local/bin/gitmirror /usr/local/bin/gitmirror 25 + 26 + EXPOSE 9000 27 + 28 + ENTRYPOINT ["/usr/bin/tini", "--"] 29 + CMD ["sh", "-c", "if [ -f /usr/local/share/ca-certificates/caddy.crt ]; then update-ca-certificates; fi && exec /usr/local/bin/gitmirror serve"]
+5 -2
localinfra/tailwind.Dockerfile
··· 1 1 FROM node:22-alpine 2 2 3 - WORKDIR /build 3 + WORKDIR /deps 4 4 5 5 RUN npm install --no-save \ 6 6 tailwindcss@3.4.17 \ ··· 8 8 tailwindcss-animated@1.1.2 9 9 10 10 ENV BROWSERSLIST_IGNORE_OLD_DATA=true 11 + ENV NODE_PATH=/deps/node_modules 12 + 13 + WORKDIR /build 11 14 12 - ENTRYPOINT ["node_modules/.bin/tailwindcss"] 15 + ENTRYPOINT ["/deps/node_modules/.bin/tailwindcss"]
+2 -1
appview/config/config.go
··· 60 60 } 61 61 62 62 type KnotMirrorConfig struct { 63 - Url string `env:"URL, default=https://mirror.tangled.network"` 63 + Url string `env:"URL, default=https://mirror.tangled.network"` 64 + V2Host string `env:"V2_HOST, required"` 64 65 } 65 66 66 67 type JetstreamConfig struct {
+7
appview/models/pull.go
··· 87 87 Repo *Repo 88 88 } 89 89 90 + func (p *Pull) SourceRepoDid() syntax.DID { 91 + if p.PullSource != nil && p.PullSource.RepoDid != nil { 92 + return *p.PullSource.RepoDid 93 + } 94 + return p.RepoDid 95 + } 96 + 90 97 // NOTE: This method does not include patch blob in returned atproto record 91 98 func (p Pull) AsRecord() tangled.RepoPull { 92 99 mentions := make([]string, len(p.Mentions))
+45
appview/pages/pages.go
··· 1547 1547 IsSubscribed *bool 1548 1548 } 1549 1549 1550 + type PullPageBaseParams struct { 1551 + BaseParams 1552 + Pull *models.Pull 1553 + 1554 + Backlinks []models.RichReferenceLink 1555 + Comments []models.Comment 1556 + Commits []types.Commit // all commits between <target>..<pr/head> 1557 + 1558 + LabelDefs map[string]*models.LabelDefinition 1559 + Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData 1560 + UserReacted map[syntax.ATURI]map[models.ReactionKind]bool 1561 + VouchRelationships map[syntax.DID]*models.VouchRelationship 1562 + VouchSkips map[syntax.DID]bool 1563 + 1564 + // diff, branch-delete-status, merge-check, resubmit-check, pipelines will be lazy-loaded. 1565 + } 1566 + 1567 + // /pulls/123/2/1a2b3c..d4e5f6 1568 + type PullDiffParams struct { 1569 + PullPageBaseParams 1570 + Version int 1571 + BaseCommitId string 1572 + HeadCommitId string 1573 + 1574 + ErrorMsg string 1575 + } 1576 + 1577 + // /pulls/123/1..2/abcdef 1578 + type PullInterdiffParams struct { 1579 + PullPageBaseParams 1580 + Version1 int 1581 + Version2 int 1582 + ChangeId string // optional change-id filter 1583 + 1584 + ErrorMsg string 1585 + } 1586 + 1587 + func (p *Pages) PullDiff(w io.Writer, params PullDiffParams) error { 1588 + panic("unimplemented") 1589 + } 1590 + 1591 + func (p *Pages) PullInterdiff(w io.Writer, params PullInterdiffParams) error { 1592 + panic("unimplemented") 1593 + } 1594 + 1550 1595 func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 1551 1596 params.Active = "pulls" 1552 1597 return p.executeRepo("repo/pulls/pull", w, params)
+280
appview/pulls/pull2.go
··· 1 + package pulls 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "net/http" 7 + "strconv" 8 + 9 + "github.com/bluesky-social/indigo/atproto/syntax" 10 + "github.com/bluesky-social/indigo/lex/util" 11 + indigoxrpc "github.com/bluesky-social/indigo/xrpc" 12 + "github.com/go-chi/chi/v5" 13 + "golang.org/x/sync/errgroup" 14 + "tangled.org/core/api/tangled" 15 + "tangled.org/core/appview/models" 16 + "tangled.org/core/appview/pages" 17 + "tangled.org/core/types" 18 + ) 19 + 20 + // NOTE: parsing object in middleware is bad pattern 21 + // you will have to check if object exist in context "just in case" 22 + // so it's better to make helper function that can read the url pattern instead. 23 + 24 + 25 + // A -- B -- C 26 + // (master) (pr/123/0) 27 + // 28 + // A -- B -- C 29 + // \ (pr/123/0) 30 + // `-- D <- B' <- C' 31 + // (master) (pr/123/1) 32 + 33 + // 1. rebase B<-C to D 34 + // 2. compare tree of C and D 35 + 36 + // PullInterDiff is router for /pulls/{pull}/{version}..{version}/{change} 37 + // 38 + // Examples: 39 + // - /pulls/123/0..2/all 40 + // - /pulls/123/0..2/nrpytyzw 41 + func (s *Pulls) PullInterDiff(w http.ResponseWriter, r *http.Request) { 42 + l := s.logger.With("handler", "PullRound") 43 + ctx := r.Context() 44 + 45 + pull, ok := r.Context().Value("pull").(*models.Pull) 46 + if !ok { 47 + l.Error("failed to get pull") 48 + s.pages.Error500(w) 49 + return 50 + } 51 + 52 + var ( 53 + version1 = 0 54 + version2 = 0 55 + changeId = chi.URLParam(r, "*") 56 + ) 57 + if changeId == "all" { 58 + changeId = "" 59 + } 60 + 61 + // defer render 62 + var params pages.PullInterdiffParams 63 + params.Pull = pull 64 + params.Version1 = version1 65 + params.Version2 = version2 66 + params.ChangeId = changeId 67 + defer s.pages.PullInterdiff(w, params) 68 + 69 + // 1. resolve target branch -> (branch, commit) 70 + xrpcc := &indigoxrpc.Client{Host: s.config.KnotMirror.Url} 71 + branch, err := tangled.GitTempGetBranch(ctx, xrpcc, pull.TargetBranch, pull.RepoDid.String()) 72 + if err != nil { 73 + panic("unimplemented") 74 + } 75 + 76 + base := branch.Hash 77 + head1 := "" // pull.Versions[version1].Head 78 + head2 := "" // pull.Versions[version2].Head 79 + 80 + // 1. log commits from base..head1 and base..head2 81 + var commits1, commits2 []types.Commit 82 + g, gctx := errgroup.WithContext(ctx) 83 + g.Go(func() error { 84 + commits1, err = getTempListCommits(gctx, xrpcc, pull.SourceRepoDid(), base, head1) 85 + return err 86 + }) 87 + g.Go(func() error { 88 + commits2, err = getTempListCommits(gctx, xrpcc, pull.SourceRepoDid(), base, head2) 89 + return err 90 + }) 91 + if err := g.Wait(); err != nil { 92 + params.ErrorMsg = "something something" 93 + panic("unimplemented") 94 + } 95 + 96 + if changeId != "" { 97 + // interdiff by change-id 98 + var old, new *types.Commit 99 + for _, commit := range commits1 { 100 + if commit.ChangeId == changeId { 101 + old = &commit 102 + break 103 + } 104 + } 105 + for _, commit := range commits2 { 106 + if commit.ChangeId == changeId { 107 + new = &commit 108 + break 109 + } 110 + } 111 + _, _ = old, new 112 + panic("unimplemented") 113 + } else { 114 + // interdiff of two commit ranges 115 + panic("unimplemented") 116 + } 117 + } 118 + 119 + // PullDiff is router for /pulls/{pull}/{version}/{commit}..{commit} 120 + // 121 + // Examples: 122 + // - /pulls/123/latest 123 + // - /pulls/123/2/head 124 + // - /pulls/123/2/base..head 125 + // - /pulls/123/2/a53ab251e..d8add468c 126 + // - /pulls/123/2/d8add468c 127 + func (s *Pulls) PullDiff(w http.ResponseWriter, r *http.Request) { 128 + l := s.logger.With("handler", "PullRound") 129 + ctx := r.Context() 130 + 131 + pull, ok := r.Context().Value("pull").(*models.Pull) 132 + if !ok { 133 + l.Error("failed to get pull") 134 + s.pages.Error500(w) 135 + return 136 + } 137 + 138 + var err error 139 + 140 + var version int 141 + var versionRaw = chi.URLParam(r, "version") 142 + if versionRaw == "latest" { 143 + version = pull.LastRoundNumber() 144 + } else { 145 + version, err = strconv.Atoi(versionRaw) 146 + if err != nil { 147 + // invalid version number. redirect 148 + http.Redirect(w, r, 149 + fmt.Sprintf("/%s/pulls/%d/latest", pull.Repo.RepoIdentifier(), pull.ID), 150 + http.StatusSeeOther, 151 + ) 152 + return 153 + } 154 + } 155 + 156 + var range_ = chi.URLParam(r, "*") 157 + base, head, err := parseRevRange(range_) 158 + if err != nil { 159 + http.Redirect(w, r, 160 + fmt.Sprintf("/%s/pulls/%d/%s", pull.Repo.RepoIdentifier(), pull.ID, versionRaw), 161 + http.StatusSeeOther, 162 + ) 163 + return 164 + } 165 + 166 + // defer render 167 + var params pages.PullDiffParams 168 + params.Pull = pull 169 + params.Version = version 170 + defer s.pages.PullDiff(w, params) 171 + 172 + // 1. resolve target branch -> (branch, commit) 173 + xrpcc := &indigoxrpc.Client{Host: s.config.KnotMirror.Url} 174 + branch, err := tangled.GitTempGetBranch(ctx, xrpcc, pull.TargetBranch, pull.RepoDid.String()) 175 + if err != nil { 176 + l.Warn("Failed to resolve target branch", "branch", pull.TargetBranch, "err", err) 177 + params.ErrorMsg = fmt.Sprintf("Failed to resolve target branch %q", pull.TargetBranch) 178 + return 179 + } 180 + 181 + if base == "base" { 182 + base = branch.Hash 183 + } 184 + if head == "head" { 185 + head = pull.HEAD() 186 + } 187 + 188 + sourceRepoDid := pull.SourceRepoDid() 189 + 190 + // 2. list diverged commits using knotmirror (BASE..HEAD) -> ([]commit) 191 + // - knotmirror needs on-demand fetch implementation for this 192 + commits, err := getTempListCommits(ctx, xrpcc, sourceRepoDid, base, head) 193 + if err != nil { 194 + panic("unimplemented") 195 + } 196 + 197 + // 3. list every commits in UI. They will be lazy-loaded 198 + params.Commits = commits 199 + } 200 + 201 + // htmx fragment. render diff between commits 202 + func (s *Pulls) PullDiffFragment(w http.ResponseWriter, r *http.Request) { 203 + // var ( 204 + // base = r.URL.Query().Get("base") // base commit ID 205 + // head = r.URL.Query().Get("head") // head commit ID 206 + // unified = r.URL.Query().Get("view") == "unified" 207 + // ) 208 + 209 + // 1. get commit object 210 + // 2. get diff between parent..commit (knotmirror), parse that diff 211 + // 3. fetch each file entries (& run syntax highlight) <- skip this part for stage 1. we will do this at stage 2. 212 + // 4. render diff 213 + } 214 + 215 + // htmx fragment. render interdiff between changes 216 + func (s *Pulls) PullInterdiffFragment(w http.ResponseWriter, r *http.Request) { 217 + // var ( 218 + // base1 = r.URL.Query().Get("base1") // base1 commit ID 219 + // base2 = r.URL.Query().Get("base2") // base2 commit ID 220 + // head1 = r.URL.Query().Get("head1") // head1 commit ID 221 + // head2 = r.URL.Query().Get("head2") // head1 commit ID 222 + // unified = r.URL.Query().Get("view") == "unified" 223 + // ) 224 + 225 + // 1. compute interdiff. (knotmirror) 226 + // 2. return rich diff data. (knotmirror) 227 + // 3. load old/new blobs & run syntax highlight 228 + // 4. render diff 229 + } 230 + 231 + // parseRevRange parses <head>..<base> string. 232 + // base and head will default to "base" and "head" when omitted. 233 + func parseRevRange(range_ string) (base string, head string, err error) { 234 + panic("unimplemented") 235 + } 236 + 237 + // parseVersionRange parses <version>..<version> string. 238 + // Each versions will default to "base" and "latest" when omitted. 239 + func parseVersionRange(range_ string) (base string, head string, err error) { 240 + panic("unimplemented") 241 + } 242 + 243 + func getTempListCommits(ctx context.Context, xrpcc util.LexClient, repo syntax.DID, base, head string) ([]types.Commit, error) { 244 + panic("unimplemented") 245 + // raw, err := tangled.GitTempListCommits(ctx, xrpcc, "", 1000, head, repo.String()) 246 + // if err != nil { 247 + // return nil, err 248 + // } 249 + // 250 + // var xrpcResp types.RepoLogResponse 251 + // if err := json.Unmarshal(raw, &xrpcResp); err != nil { 252 + // return nil, fmt.Errorf("failed to decode XRPC response: %w", err) 253 + // } 254 + // 255 + // return xrpcResp.Commits, nil 256 + } 257 + 258 + // htmx fragment. render interdiff between commits 259 + func (s *Pulls) PullInterDiffFragment(w http.ResponseWriter, r *http.Request) { 260 + panic("unimplemented") 261 + } 262 + 263 + // gitmirror 264 + // - git.ListCommitsSinceMergeBase(repo, base, head) 265 + // - git.Diff(repo, base, head, mode) 266 + // - git.Interdiff(repo, 267 + 268 + // for interdiff, we want: from{start,end}, to{start,end} 269 + // 1. squash from.start ~ from.end into one commit 270 + // 2. rebase that commit to to.start.parent() 271 + // 3. diff from_squashed.tree and to.end.tree 272 + 273 + // we want git log BASE..HEAD (only commits in HEAD) diverged=false 274 + // and git diff BASE...HEAD (changes from HEAD since merge-base) absolute=false 275 + 276 + // commands.go:56 picks comparison type: 277 + // - diff BASE..HEAD (COMPARISON_TYPE_ONLY_IN_HEAD) = direct 278 + // - diff BASE...HEAD (COMPARISON_TYPE_INTERSECTION) = merge-base. Server resolves merge-base via g.MergeBase() first (diff.go:43) then diffs. 279 + // we want second one. we should compute merge-base first. 280 + // we can have
+4
appview/pulls/pulls.go
··· 19 19 "tangled.org/core/appview/oauth" 20 20 "tangled.org/core/appview/pages" 21 21 "tangled.org/core/appview/reporesolver" 22 + knotmirror "tangled.org/core/gitmirror/proto/gen" 22 23 "tangled.org/core/idresolver" 23 24 "tangled.org/core/ogre" 24 25 "tangled.org/core/patchutil" ··· 49 50 indexer *pulls_indexer.Indexer 50 51 ogreClient *ogre.Client 51 52 diffCache *expirable.LRU[string, types.DiffRenderer] 53 + gitmirror knotmirror.GitMirrorServiceClient 52 54 } 53 55 54 56 func New( ··· 63 65 acl *knotacl.Service, 64 66 indexer *pulls_indexer.Indexer, 65 67 logger *slog.Logger, 68 + gitmirror knotmirror.GitMirrorServiceClient, 66 69 ) *Pulls { 67 70 return &Pulls{ 68 71 oauth: oauth, ··· 78 81 indexer: indexer, 79 82 ogreClient: ogre.NewClient(config.Ogre.Host), 80 83 diffCache: expirable.NewLRU[string, types.DiffRenderer](diffCacheSize, nil, diffCacheTTL), 84 + gitmirror: gitmirror, 81 85 } 82 86 } 83 87
+1
appview/state/router.go
··· 416 416 s.aclService, 417 417 s.indexer.Pulls, 418 418 log.SubLogger(s.logger, "pulls"), 419 + s.gitmirror, 419 420 ) 420 421 return pulls.Router(mw) 421 422 }
+11
appview/state/state.go
··· 11 11 "strings" 12 12 "time" 13 13 14 + "google.golang.org/grpc" 15 + "google.golang.org/grpc/credentials/insecure" 14 16 "tangled.org/core/api/tangled" 15 17 "tangled.org/core/appview" 16 18 "tangled.org/core/appview/bsky" ··· 37 39 "tangled.org/core/appview/reporesolver" 38 40 "tangled.org/core/consts" 39 41 "tangled.org/core/eventconsumer" 42 + knotmirror "tangled.org/core/gitmirror/proto/gen" 40 43 "tangled.org/core/idresolver" 41 44 "tangled.org/core/jetstream" 42 45 "tangled.org/core/log" ··· 75 78 logger *slog.Logger 76 79 cfClient *cloudflare.Client 77 80 codesearch *codesearch.CodeSearch 81 + gitmirror knotmirror.GitMirrorServiceClient 78 82 } 79 83 80 84 func Make(ctx context.Context, config *config.Config) (*State, error) { ··· 112 116 return nil, fmt.Errorf("failed to create posthog client: %w", err) 113 117 } 114 118 119 + conn, err := grpc.NewClient(config.KnotMirror.V2Host, grpc.WithTransportCredentials(insecure.NewCredentials())) 120 + if err != nil { 121 + return nil, fmt.Errorf("failed to create gitmirror client: %w", err) 122 + } 123 + gitmirror := knotmirror.NewGitMirrorServiceClient(conn) 124 + 115 125 pages := pages.NewPages(config, res, d, rdb, log.SubLogger(logger, "pages")) 116 126 knotcompat.UseNativeLatch(knotacl.NewLatch(d, log.SubLogger(logger, "knotacl-latch"))) 117 127 aclService := knotacl.NewService(enforcer, d, config.Core.Dev, log.SubLogger(logger, "knotacl")) ··· 243 253 logger: logger, 244 254 cfClient: cfClient, 245 255 codesearch: &codesearch.CodeSearch{Host: config.CodeSearch.ZoektUrl}, 256 + gitmirror: gitmirror, 246 257 } 247 258 248 259 // fetch initial bluesky posts if configured
+1 -1
go.mod
··· 84 84 golang.org/x/sync v0.20.0 85 85 golang.org/x/sys v0.45.0 86 86 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da 87 + google.golang.org/grpc v1.80.0 87 88 google.golang.org/protobuf v1.36.11 88 89 gopkg.in/yaml.v3 v3.0.1 89 90 ) ··· 313 314 golang.org/x/tools v0.44.0 // indirect 314 315 google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect 315 316 google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect 316 - google.golang.org/grpc v1.80.0 // indirect 317 317 gopkg.in/fsnotify.v1 v1.4.7 // indirect 318 318 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 319 319 gopkg.in/warnings.v0 v0.1.2 // indirect

History

2 rounds 0 comments
Sign up or Login to add to the discussion
14 commits
Expand
lexicons: git.keepCommit
knotserver/xrpc: git.keepCommit
lexicons: git.temp.getMergeBase
knotmirror/xrpc: git.getMergeBase
lexicons: new git.merge* method schema
knotserver/xrpc: git.mergeCheck and git.mergeCommit
lexicons/git: git.formatPatch
knotmirror/xrpc: git.formatPatch
lexicons/pulls: rename PR round to version
cargo: fix rust build linker on aarch64-darwin
gitmirror: introduce gitmirror
docker-compose: use directory mount
appview: use gitmirror
appview: ref based PR
Checking mergeability…
Expand 0 comments
13 commits
Expand
lexicons: git.keepCommit
knotserver/xrpc: git.keepCommit
lexicons: git.temp.getMergeBase
knotmirror/xrpc: git.getMergeBase
lexicons: new git.merge* method schema
knotserver/xrpc: git.mergeCheck and git.mergeCommit
lexicons/git: git.formatPatch
knotmirror/xrpc: git.formatPatch
lexicons/pulls: rename PR round to version
cargo: fix rust build linker on aarch64-darwin
gitmirror: introduce gitmirror
docker-compose: use directory mount
appview: use gitmirror
Expand 0 comments