diff --git a/appview/pages/pages.go b/appview/pages/pages.go
index 2c6e2da3..c56b1ff0 100644
--- a/appview/pages/pages.go
+++ b/appview/pages/pages.go
@@ -1464,6 +1464,10 @@ type PullInterdiffParams struct {
ErrorMsg string
}
+type PullDiffFragmentParams struct {
+ Diff string
+}
+
func (p *Pages) PullDiff(w io.Writer, params PullDiffParams) error {
panic("unimplemented")
}
@@ -1472,6 +1476,10 @@ func (p *Pages) PullInterdiff(w io.Writer, params PullInterdiffParams) error {
panic("unimplemented")
}
+func (p *Pages) PullDiffFragment(w io.Writer, params PullDiffFragmentParams) error {
+ return p.executePlain("repo/pulls/fragments/diff", w, params)
+}
+
func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error {
params.Active = "pulls"
return p.executeRepo("repo/pulls/pull", w, params)
diff --git a/appview/pages/templates/repo/pulls/fragments/diff.html b/appview/pages/templates/repo/pulls/fragments/diff.html
new file mode 100644
index 00000000..035758da
--- /dev/null
+++ b/appview/pages/templates/repo/pulls/fragments/diff.html
@@ -0,0 +1,5 @@
+{{ define "repo/pulls/fragments/diff" }}
+
+{{ end }}
diff --git a/appview/pulls/pull2.go b/appview/pulls/pull2.go
index 589aa0be..d970eb0c 100644
--- a/appview/pulls/pull2.go
+++ b/appview/pulls/pull2.go
@@ -2,9 +2,12 @@ package pulls
import (
"context"
+ "errors"
"fmt"
+ "io"
"net/http"
"strconv"
+ "strings"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/bluesky-social/indigo/lex/util"
@@ -198,50 +201,157 @@ func (s *Pulls) PullDiff(w http.ResponseWriter, r *http.Request) {
params.Commits = commits
}
+// fileDiff pairs a streamed FileDiff with the line content of its base/head blobs.
+type fileDiff struct {
+ diff *gitmirrorv1.FileDiff
+ baseLines []string // lines of the base (lhs) blob; nil for binary/submodule/absent
+ headLines []string // lines of the head (rhs) blob; nil for binary/submodule/absent
+}
+
// htmx fragment. render diff between commits
func (s *Pulls) PullDiffFragment(w http.ResponseWriter, r *http.Request) {
var (
- repoRaw = r.URL.Query().Get("repo")
+ // repoRaw = r.URL.Query().Get("repo")
base = r.URL.Query().Get("base") // base commit ID
head = r.URL.Query().Get("head") // head commit ID
unified = r.URL.Query().Get("view") == "unified"
)
- repo, err := syntax.ParseDID(repoRaw)
+ // repo, err := syntax.ParseDID(repoRaw)
+ // if err != nil {
+ // http.Error(w, fmt.Sprintf("invalid repo DID: %q", repoRaw), http.StatusBadRequest)
+ // return
+ // }
+ repo, err := s.repoResolver.Resolve(r)
if err != nil {
- http.Error(w, fmt.Sprintf("invalid repo DID: %q", repoRaw), http.StatusBadRequest)
- return
+ panic("unimplemented")
}
+ l := s.logger
ctx := r.Context()
+ var params pages.PullDiffFragmentParams
+ defer func() {
+ s.pages.PullDiffFragment(w, params)
+ }()
+
+ // a. drain the diff stream into one fileDiff per changed file.
stream, err := s.gitmirror.Diff(ctx, &gitmirrorv1.DiffRequest{
- Repo: repo.String(),
+ Repo: repo.RepoDid,
Base: base,
Head: head,
})
if err != nil {
- panic("unimplemented")
+ panic(err)
+ }
+ var files []*fileDiff
+ for {
+ fd, err := stream.Recv()
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ if err != nil {
+ panic(err)
+ }
+ files = append(files, &fileDiff{diff: fd})
+ }
+
+ // b. fetch each file's base/head blob in parallel and split into lines.
+ g, gctx := errgroup.WithContext(ctx)
+ for i, f := range files {
+ l.Debug("diff", "file", i, "diff", f.diff)
+ g.Go(func() error {
+ lhs, rhs := f.diff.GetLhsSrc(), f.diff.GetRhsSrc()
+ // Binary/submodule files have no line content to fetch.
+ if isBinaryOrSubmodule(lhs) || isBinaryOrSubmodule(rhs) {
+ return nil
+ }
+ baseBlob, err := s.getBlob(gctx, repo.RepoDid, lhs.GetOid())
+ if err != nil {
+ return err
+ }
+ headBlob, err := s.getBlob(gctx, repo.RepoDid, rhs.GetOid())
+ if err != nil {
+ return err
+ }
+ f.baseLines = splitLines(baseBlob)
+ f.headLines = splitLines(headBlob)
+ return nil
+ })
+ }
+ if err := g.Wait(); err != nil {
+ panic(err)
}
- diff, err := stream.Recv()
+ // TODO: implement split view
+ _ = unified
+
+ // TODO: implement context lines
+ for _, f := range files {
+ params.Diff += f.diff.RhsSrc.Path + "\n\n"
+ for _, hunk := range f.diff.Hunks {
+ params.Diff += "@@@\n"
+ for _, line := range hunk.Lines {
+ if line.Lhs != nil {
+ params.Diff += fmt.Sprintf("%d\t\t - %s\n", *line.Lhs+1, f.baseLines[*line.Lhs])
+ }
+ }
+ for _, line := range hunk.Lines {
+ if line.Rhs != nil {
+ params.Diff += fmt.Sprintf("\t%d\t + %s\n", *line.Rhs+1, f.headLines[*line.Rhs])
+ }
+ }
+ }
+ }
+}
+
+// getBlob streams a blob's bytes from gitmirror by OID and concatenates them.
+// A null (all-zero) oid — the absent side of an addition/deletion — yields nil.
+func (s *Pulls) getBlob(ctx context.Context, repo, oid string) ([]byte, error) {
+ if isNullOid(oid) {
+ return nil, nil
+ }
+ stream, err := s.gitmirror.GetBlob(ctx, &gitmirrorv1.GetBlobRequest{Repo: repo, Oid: oid})
if err != nil {
- panic("unimplemented")
+ return nil, err
}
+ var buf []byte
+ for {
+ chunk, err := stream.Recv()
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ if err != nil {
+ return nil, err
+ }
+ buf = append(buf, chunk.GetData()...)
+ }
+ return buf, nil
+}
- _ = diff
+func isBinaryOrSubmodule(fc *gitmirrorv1.FileContent) bool {
+ return fc != nil && (fc.GetIsBinary() || fc.GetIsSubmodule())
+}
- // // 1. fetch each blob in parallel
- // baseBlob, err := s.gitmirror.GetBlob(ctx, ...)
- // headBlob, err := s.gitmirror.GetBlob(ctx, ...)
+// isNullOid reports whether oid is empty or the all-zero object id.
+func isNullOid(oid string) bool {
+ if oid == "" {
+ return true
+ }
+ for _, c := range oid {
+ if c != '0' {
+ return false
+ }
+ }
+ return true
+}
- // 2. render diff by merging diff & blob content
- if unified {
- // render unified diff
- // var lines []string
- } else {
- // render split diff
- // var leftLines, rightLines []string
+// splitLines splits blob bytes into lines, dropping the trailing empty element
+// produced by a final newline.
+func splitLines(b []byte) []string {
+ if len(b) == 0 {
+ return nil
}
+ return strings.Split(strings.TrimSuffix(string(b), "\n"), "\n")
}
// htmx fragment. render interdiff between changes
diff --git a/appview/pulls/pulls.go b/appview/pulls/pulls.go
index e9dac606..7ab22574 100644
--- a/appview/pulls/pulls.go
+++ b/appview/pulls/pulls.go
@@ -24,10 +24,15 @@ import (
"tangled.org/core/patchutil"
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials/insecure"
)
const ApplicationGzip = "application/gzip"
+// TODO: move to config. gitmirror serves h2c; talk to it over plaintext for now.
+const gitmirrorHost = "gitmirror:9000"
+
type Pulls struct {
oauth *oauth.OAuth
repoResolver *reporesolver.RepoResolver
@@ -57,6 +62,10 @@ func New(
indexer *pulls_indexer.Indexer,
logger *slog.Logger,
) *Pulls {
+ conn, err := grpc.NewClient(gitmirrorHost, grpc.WithTransportCredentials(insecure.NewCredentials()))
+ if err != nil {
+ panic(fmt.Sprintf("dial gitmirror: %v", err))
+ }
return &Pulls{
oauth: oauth,
repoResolver: repoResolver,
@@ -70,6 +79,7 @@ func New(
logger: logger,
indexer: indexer,
ogreClient: ogre.NewClient(config.Ogre.Host),
+ gitmirror: knotmirror.NewGitMirrorServiceClient(conn),
}
}
diff --git a/appview/pulls/router.go b/appview/pulls/router.go
index 530d4941..94d8200f 100644
--- a/appview/pulls/router.go
+++ b/appview/pulls/router.go
@@ -9,6 +9,7 @@ import (
func (s *Pulls) Router(mw *middleware.Middleware) http.Handler {
r := chi.NewRouter()
+ r.Get("/test", s.PullDiffFragment)
r.With(middleware.Paginate).Get("/", s.RepoPulls)
r.With(middleware.AuthMiddleware(s.oauth)).Route("/new", func(r chi.Router) {
r.Get("/", s.NewPull)
diff --git a/gitmirror/proto/gen/gitmirror.pb.go b/gitmirror/proto/gen/gitmirror.pb.go
index d9eaf289..569923fe 100644
--- a/gitmirror/proto/gen/gitmirror.pb.go
+++ b/gitmirror/proto/gen/gitmirror.pb.go
@@ -21,6 +21,105 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
+type GetBlobRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ // repo carries the DID as a string.
+ Repo string `protobuf:"bytes,1,opt,name=repo,proto3" json:"repo,omitempty"`
+ // oid is the blob object id (hex).
+ Oid string `protobuf:"bytes,2,opt,name=oid,proto3" json:"oid,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *GetBlobRequest) Reset() {
+ *x = GetBlobRequest{}
+ mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *GetBlobRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetBlobRequest) ProtoMessage() {}
+
+func (x *GetBlobRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[0]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetBlobRequest.ProtoReflect.Descriptor instead.
+func (*GetBlobRequest) Descriptor() ([]byte, []int) {
+ return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *GetBlobRequest) GetRepo() string {
+ if x != nil {
+ return x.Repo
+ }
+ return ""
+}
+
+func (x *GetBlobRequest) GetOid() string {
+ if x != nil {
+ return x.Oid
+ }
+ return ""
+}
+
+// One chunk of a blob's raw bytes; concatenate in arrival order.
+type BlobChunk struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *BlobChunk) Reset() {
+ *x = BlobChunk{}
+ mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *BlobChunk) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*BlobChunk) ProtoMessage() {}
+
+func (x *BlobChunk) ProtoReflect() protoreflect.Message {
+ mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[1]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use BlobChunk.ProtoReflect.Descriptor instead.
+func (*BlobChunk) Descriptor() ([]byte, []int) {
+ return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *BlobChunk) GetData() []byte {
+ if x != nil {
+ return x.Data
+ }
+ return nil
+}
+
type CommitsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// repo carries the DID as a string; parsing/validation is an impl detail.
@@ -32,7 +131,7 @@ type CommitsRequest struct {
func (x *CommitsRequest) Reset() {
*x = CommitsRequest{}
- mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[0]
+ mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -44,7 +143,7 @@ func (x *CommitsRequest) String() string {
func (*CommitsRequest) ProtoMessage() {}
func (x *CommitsRequest) ProtoReflect() protoreflect.Message {
- mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[0]
+ mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -57,7 +156,7 @@ func (x *CommitsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CommitsRequest.ProtoReflect.Descriptor instead.
func (*CommitsRequest) Descriptor() ([]byte, []int) {
- return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{0}
+ return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{2}
}
func (x *CommitsRequest) GetRepo() string {
@@ -82,7 +181,7 @@ type CommitsOptions struct {
func (x *CommitsOptions) Reset() {
*x = CommitsOptions{}
- mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[1]
+ mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -94,7 +193,7 @@ func (x *CommitsOptions) String() string {
func (*CommitsOptions) ProtoMessage() {}
func (x *CommitsOptions) ProtoReflect() protoreflect.Message {
- mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[1]
+ mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -107,7 +206,7 @@ func (x *CommitsOptions) ProtoReflect() protoreflect.Message {
// Deprecated: Use CommitsOptions.ProtoReflect.Descriptor instead.
func (*CommitsOptions) Descriptor() ([]byte, []int) {
- return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{1}
+ return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{3}
}
type CommitsResponse struct {
@@ -120,7 +219,7 @@ type CommitsResponse struct {
func (x *CommitsResponse) Reset() {
*x = CommitsResponse{}
- mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[2]
+ mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -132,7 +231,7 @@ func (x *CommitsResponse) String() string {
func (*CommitsResponse) ProtoMessage() {}
func (x *CommitsResponse) ProtoReflect() protoreflect.Message {
- mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[2]
+ mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -145,7 +244,7 @@ func (x *CommitsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use CommitsResponse.ProtoReflect.Descriptor instead.
func (*CommitsResponse) Descriptor() ([]byte, []int) {
- return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{2}
+ return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{4}
}
func (x *CommitsResponse) GetCommits() []string {
@@ -168,7 +267,7 @@ type DiffRequest struct {
func (x *DiffRequest) Reset() {
*x = DiffRequest{}
- mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[3]
+ mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -180,7 +279,7 @@ func (x *DiffRequest) String() string {
func (*DiffRequest) ProtoMessage() {}
func (x *DiffRequest) ProtoReflect() protoreflect.Message {
- mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[3]
+ mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -193,7 +292,7 @@ func (x *DiffRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use DiffRequest.ProtoReflect.Descriptor instead.
func (*DiffRequest) Descriptor() ([]byte, []int) {
- return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{3}
+ return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{5}
}
func (x *DiffRequest) GetRepo() string {
@@ -232,7 +331,7 @@ type InterdiffRequest struct {
func (x *InterdiffRequest) Reset() {
*x = InterdiffRequest{}
- mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[4]
+ mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -244,7 +343,7 @@ func (x *InterdiffRequest) String() string {
func (*InterdiffRequest) ProtoMessage() {}
func (x *InterdiffRequest) ProtoReflect() protoreflect.Message {
- mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[4]
+ mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -257,7 +356,7 @@ func (x *InterdiffRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use InterdiffRequest.ProtoReflect.Descriptor instead.
func (*InterdiffRequest) Descriptor() ([]byte, []int) {
- return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{4}
+ return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{6}
}
func (x *InterdiffRequest) GetRepo() string {
@@ -301,13 +400,14 @@ type FileContent struct {
Oid string `protobuf:"bytes,2,opt,name=oid,proto3" json:"oid,omitempty"`
Size uint64 `protobuf:"varint,3,opt,name=size,proto3" json:"size,omitempty"`
IsBinary bool `protobuf:"varint,4,opt,name=is_binary,json=isBinary,proto3" json:"is_binary,omitempty"`
+ IsSubmodule bool `protobuf:"varint,5,opt,name=is_submodule,json=isSubmodule,proto3" json:"is_submodule,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *FileContent) Reset() {
*x = FileContent{}
- mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[5]
+ mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -319,7 +419,7 @@ func (x *FileContent) String() string {
func (*FileContent) ProtoMessage() {}
func (x *FileContent) ProtoReflect() protoreflect.Message {
- mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[5]
+ mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -332,7 +432,7 @@ func (x *FileContent) ProtoReflect() protoreflect.Message {
// Deprecated: Use FileContent.ProtoReflect.Descriptor instead.
func (*FileContent) Descriptor() ([]byte, []int) {
- return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{5}
+ return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{7}
}
func (x *FileContent) GetPath() string {
@@ -363,6 +463,13 @@ func (x *FileContent) GetIsBinary() bool {
return false
}
+func (x *FileContent) GetIsSubmodule() bool {
+ if x != nil {
+ return x.IsSubmodule
+ }
+ return false
+}
+
// One aligned line pair; a side is absent (None) when unset.
type LinePair struct {
state protoimpl.MessageState `protogen:"open.v1"`
@@ -374,7 +481,7 @@ type LinePair struct {
func (x *LinePair) Reset() {
*x = LinePair{}
- mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[6]
+ mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -386,7 +493,7 @@ func (x *LinePair) String() string {
func (*LinePair) ProtoMessage() {}
func (x *LinePair) ProtoReflect() protoreflect.Message {
- mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[6]
+ mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -399,7 +506,7 @@ func (x *LinePair) ProtoReflect() protoreflect.Message {
// Deprecated: Use LinePair.ProtoReflect.Descriptor instead.
func (*LinePair) Descriptor() ([]byte, []int) {
- return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{6}
+ return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{8}
}
func (x *LinePair) GetLhs() uint32 {
@@ -428,7 +535,7 @@ type Hunk struct {
func (x *Hunk) Reset() {
*x = Hunk{}
- mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[7]
+ mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -440,7 +547,7 @@ func (x *Hunk) String() string {
func (*Hunk) ProtoMessage() {}
func (x *Hunk) ProtoReflect() protoreflect.Message {
- mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[7]
+ mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -453,7 +560,7 @@ func (x *Hunk) ProtoReflect() protoreflect.Message {
// Deprecated: Use Hunk.ProtoReflect.Descriptor instead.
func (*Hunk) Descriptor() ([]byte, []int) {
- return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{7}
+ return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{9}
}
func (x *Hunk) GetNovelLhs() []uint32 {
@@ -488,7 +595,7 @@ type ByteChanges struct {
func (x *ByteChanges) Reset() {
*x = ByteChanges{}
- mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[8]
+ mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -500,7 +607,7 @@ func (x *ByteChanges) String() string {
func (*ByteChanges) ProtoMessage() {}
func (x *ByteChanges) ProtoReflect() protoreflect.Message {
- mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[8]
+ mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[10]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -513,7 +620,7 @@ func (x *ByteChanges) ProtoReflect() protoreflect.Message {
// Deprecated: Use ByteChanges.ProtoReflect.Descriptor instead.
func (*ByteChanges) Descriptor() ([]byte, []int) {
- return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{8}
+ return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{10}
}
func (x *ByteChanges) GetLhs() uint64 {
@@ -543,7 +650,7 @@ type FileDiff struct {
func (x *FileDiff) Reset() {
*x = FileDiff{}
- mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[9]
+ mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -555,7 +662,7 @@ func (x *FileDiff) String() string {
func (*FileDiff) ProtoMessage() {}
func (x *FileDiff) ProtoReflect() protoreflect.Message {
- mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[9]
+ mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[11]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -568,7 +675,7 @@ func (x *FileDiff) ProtoReflect() protoreflect.Message {
// Deprecated: Use FileDiff.ProtoReflect.Descriptor instead.
func (*FileDiff) Descriptor() ([]byte, []int) {
- return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{9}
+ return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{11}
}
func (x *FileDiff) GetLhsSrc() *FileContent {
@@ -610,7 +717,12 @@ var File_gitmirror_v1_gitmirror_proto protoreflect.FileDescriptor
const file_gitmirror_v1_gitmirror_proto_rawDesc = "" +
"\n" +
- "\x1cgitmirror/v1/gitmirror.proto\x12\fgitmirror.v1\"\\\n" +
+ "\x1cgitmirror/v1/gitmirror.proto\x12\fgitmirror.v1\"6\n" +
+ "\x0eGetBlobRequest\x12\x12\n" +
+ "\x04repo\x18\x01 \x01(\tR\x04repo\x12\x10\n" +
+ "\x03oid\x18\x02 \x01(\tR\x03oid\"\x1f\n" +
+ "\tBlobChunk\x12\x12\n" +
+ "\x04data\x18\x01 \x01(\fR\x04data\"\\\n" +
"\x0eCommitsRequest\x12\x12\n" +
"\x04repo\x18\x01 \x01(\tR\x04repo\x126\n" +
"\aoptions\x18\x02 \x01(\v2\x1c.gitmirror.v1.CommitsOptionsR\aoptions\"\x10\n" +
@@ -626,12 +738,13 @@ const file_gitmirror_v1_gitmirror_proto_rawDesc = "" +
"\tfrom_base\x18\x02 \x01(\tR\bfromBase\x12\x1b\n" +
"\tfrom_head\x18\x03 \x01(\tR\bfromHead\x12\x17\n" +
"\ato_base\x18\x04 \x01(\tR\x06toBase\x12\x17\n" +
- "\ato_head\x18\x05 \x01(\tR\x06toHead\"d\n" +
+ "\ato_head\x18\x05 \x01(\tR\x06toHead\"\x87\x01\n" +
"\vFileContent\x12\x12\n" +
"\x04path\x18\x01 \x01(\tR\x04path\x12\x10\n" +
"\x03oid\x18\x02 \x01(\tR\x03oid\x12\x12\n" +
"\x04size\x18\x03 \x01(\x04R\x04size\x12\x1b\n" +
- "\tis_binary\x18\x04 \x01(\bR\bisBinary\"H\n" +
+ "\tis_binary\x18\x04 \x01(\bR\bisBinary\x12!\n" +
+ "\fis_submodule\x18\x05 \x01(\bR\visSubmodule\"H\n" +
"\bLinePair\x12\x15\n" +
"\x03lhs\x18\x01 \x01(\rH\x00R\x03lhs\x88\x01\x01\x12\x15\n" +
"\x03rhs\x18\x02 \x01(\rH\x01R\x03rhs\x88\x01\x01B\x06\n" +
@@ -650,11 +763,12 @@ const file_gitmirror_v1_gitmirror_proto_rawDesc = "" +
"\x05hunks\x18\x03 \x03(\v2\x12.gitmirror.v1.HunkR\x05hunks\x12H\n" +
"\x10has_byte_changes\x18\x04 \x01(\v2\x19.gitmirror.v1.ByteChangesH\x00R\x0ehasByteChanges\x88\x01\x01\x122\n" +
"\x15has_syntactic_changes\x18\x05 \x01(\bR\x13hasSyntacticChangesB\x13\n" +
- "\x11_has_byte_changes2\xde\x01\n" +
+ "\x11_has_byte_changes2\xa2\x02\n" +
"\x10GitMirrorService\x12F\n" +
"\aCommits\x12\x1c.gitmirror.v1.CommitsRequest\x1a\x1d.gitmirror.v1.CommitsResponse\x12;\n" +
"\x04Diff\x12\x19.gitmirror.v1.DiffRequest\x1a\x16.gitmirror.v1.FileDiff0\x01\x12E\n" +
- "\tInterdiff\x12\x1e.gitmirror.v1.InterdiffRequest\x1a\x16.gitmirror.v1.FileDiff0\x01B2Z0tangled.org/core/gitmirror/proto/gen;gitmirrorv1b\x06proto3"
+ "\tInterdiff\x12\x1e.gitmirror.v1.InterdiffRequest\x1a\x16.gitmirror.v1.FileDiff0\x01\x12B\n" +
+ "\aGetBlob\x12\x1c.gitmirror.v1.GetBlobRequest\x1a\x17.gitmirror.v1.BlobChunk0\x01B2Z0tangled.org/core/gitmirror/proto/gen;gitmirrorv1b\x06proto3"
var (
file_gitmirror_v1_gitmirror_proto_rawDescOnce sync.Once
@@ -668,37 +782,41 @@ func file_gitmirror_v1_gitmirror_proto_rawDescGZIP() []byte {
return file_gitmirror_v1_gitmirror_proto_rawDescData
}
-var file_gitmirror_v1_gitmirror_proto_msgTypes = make([]protoimpl.MessageInfo, 10)
+var file_gitmirror_v1_gitmirror_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
var file_gitmirror_v1_gitmirror_proto_goTypes = []any{
- (*CommitsRequest)(nil), // 0: gitmirror.v1.CommitsRequest
- (*CommitsOptions)(nil), // 1: gitmirror.v1.CommitsOptions
- (*CommitsResponse)(nil), // 2: gitmirror.v1.CommitsResponse
- (*DiffRequest)(nil), // 3: gitmirror.v1.DiffRequest
- (*InterdiffRequest)(nil), // 4: gitmirror.v1.InterdiffRequest
- (*FileContent)(nil), // 5: gitmirror.v1.FileContent
- (*LinePair)(nil), // 6: gitmirror.v1.LinePair
- (*Hunk)(nil), // 7: gitmirror.v1.Hunk
- (*ByteChanges)(nil), // 8: gitmirror.v1.ByteChanges
- (*FileDiff)(nil), // 9: gitmirror.v1.FileDiff
+ (*GetBlobRequest)(nil), // 0: gitmirror.v1.GetBlobRequest
+ (*BlobChunk)(nil), // 1: gitmirror.v1.BlobChunk
+ (*CommitsRequest)(nil), // 2: gitmirror.v1.CommitsRequest
+ (*CommitsOptions)(nil), // 3: gitmirror.v1.CommitsOptions
+ (*CommitsResponse)(nil), // 4: gitmirror.v1.CommitsResponse
+ (*DiffRequest)(nil), // 5: gitmirror.v1.DiffRequest
+ (*InterdiffRequest)(nil), // 6: gitmirror.v1.InterdiffRequest
+ (*FileContent)(nil), // 7: gitmirror.v1.FileContent
+ (*LinePair)(nil), // 8: gitmirror.v1.LinePair
+ (*Hunk)(nil), // 9: gitmirror.v1.Hunk
+ (*ByteChanges)(nil), // 10: gitmirror.v1.ByteChanges
+ (*FileDiff)(nil), // 11: gitmirror.v1.FileDiff
}
var file_gitmirror_v1_gitmirror_proto_depIdxs = []int32{
- 1, // 0: gitmirror.v1.CommitsRequest.options:type_name -> gitmirror.v1.CommitsOptions
- 6, // 1: gitmirror.v1.Hunk.lines:type_name -> gitmirror.v1.LinePair
- 5, // 2: gitmirror.v1.FileDiff.lhs_src:type_name -> gitmirror.v1.FileContent
- 5, // 3: gitmirror.v1.FileDiff.rhs_src:type_name -> gitmirror.v1.FileContent
- 7, // 4: gitmirror.v1.FileDiff.hunks:type_name -> gitmirror.v1.Hunk
- 8, // 5: gitmirror.v1.FileDiff.has_byte_changes:type_name -> gitmirror.v1.ByteChanges
- 0, // 6: gitmirror.v1.GitMirrorService.Commits:input_type -> gitmirror.v1.CommitsRequest
- 3, // 7: gitmirror.v1.GitMirrorService.Diff:input_type -> gitmirror.v1.DiffRequest
- 4, // 8: gitmirror.v1.GitMirrorService.Interdiff:input_type -> gitmirror.v1.InterdiffRequest
- 2, // 9: gitmirror.v1.GitMirrorService.Commits:output_type -> gitmirror.v1.CommitsResponse
- 9, // 10: gitmirror.v1.GitMirrorService.Diff:output_type -> gitmirror.v1.FileDiff
- 9, // 11: gitmirror.v1.GitMirrorService.Interdiff:output_type -> gitmirror.v1.FileDiff
- 9, // [9:12] is the sub-list for method output_type
- 6, // [6:9] is the sub-list for method input_type
- 6, // [6:6] is the sub-list for extension type_name
- 6, // [6:6] is the sub-list for extension extendee
- 0, // [0:6] is the sub-list for field type_name
+ 3, // 0: gitmirror.v1.CommitsRequest.options:type_name -> gitmirror.v1.CommitsOptions
+ 8, // 1: gitmirror.v1.Hunk.lines:type_name -> gitmirror.v1.LinePair
+ 7, // 2: gitmirror.v1.FileDiff.lhs_src:type_name -> gitmirror.v1.FileContent
+ 7, // 3: gitmirror.v1.FileDiff.rhs_src:type_name -> gitmirror.v1.FileContent
+ 9, // 4: gitmirror.v1.FileDiff.hunks:type_name -> gitmirror.v1.Hunk
+ 10, // 5: gitmirror.v1.FileDiff.has_byte_changes:type_name -> gitmirror.v1.ByteChanges
+ 2, // 6: gitmirror.v1.GitMirrorService.Commits:input_type -> gitmirror.v1.CommitsRequest
+ 5, // 7: gitmirror.v1.GitMirrorService.Diff:input_type -> gitmirror.v1.DiffRequest
+ 6, // 8: gitmirror.v1.GitMirrorService.Interdiff:input_type -> gitmirror.v1.InterdiffRequest
+ 0, // 9: gitmirror.v1.GitMirrorService.GetBlob:input_type -> gitmirror.v1.GetBlobRequest
+ 4, // 10: gitmirror.v1.GitMirrorService.Commits:output_type -> gitmirror.v1.CommitsResponse
+ 11, // 11: gitmirror.v1.GitMirrorService.Diff:output_type -> gitmirror.v1.FileDiff
+ 11, // 12: gitmirror.v1.GitMirrorService.Interdiff:output_type -> gitmirror.v1.FileDiff
+ 1, // 13: gitmirror.v1.GitMirrorService.GetBlob:output_type -> gitmirror.v1.BlobChunk
+ 10, // [10:14] is the sub-list for method output_type
+ 6, // [6:10] is the sub-list for method input_type
+ 6, // [6:6] is the sub-list for extension type_name
+ 6, // [6:6] is the sub-list for extension extendee
+ 0, // [0:6] is the sub-list for field type_name
}
func init() { file_gitmirror_v1_gitmirror_proto_init() }
@@ -706,15 +824,15 @@ func file_gitmirror_v1_gitmirror_proto_init() {
if File_gitmirror_v1_gitmirror_proto != nil {
return
}
- file_gitmirror_v1_gitmirror_proto_msgTypes[6].OneofWrappers = []any{}
- file_gitmirror_v1_gitmirror_proto_msgTypes[9].OneofWrappers = []any{}
+ file_gitmirror_v1_gitmirror_proto_msgTypes[8].OneofWrappers = []any{}
+ file_gitmirror_v1_gitmirror_proto_msgTypes[11].OneofWrappers = []any{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_gitmirror_v1_gitmirror_proto_rawDesc), len(file_gitmirror_v1_gitmirror_proto_rawDesc)),
NumEnums: 0,
- NumMessages: 10,
+ NumMessages: 12,
NumExtensions: 0,
NumServices: 1,
},
diff --git a/gitmirror/proto/gen/gitmirror_grpc.pb.go b/gitmirror/proto/gen/gitmirror_grpc.pb.go
index ec5d5948..4e6ad768 100644
--- a/gitmirror/proto/gen/gitmirror_grpc.pb.go
+++ b/gitmirror/proto/gen/gitmirror_grpc.pb.go
@@ -22,6 +22,7 @@ const (
GitMirrorService_Commits_FullMethodName = "/gitmirror.v1.GitMirrorService/Commits"
GitMirrorService_Diff_FullMethodName = "/gitmirror.v1.GitMirrorService/Diff"
GitMirrorService_Interdiff_FullMethodName = "/gitmirror.v1.GitMirrorService/Interdiff"
+ GitMirrorService_GetBlob_FullMethodName = "/gitmirror.v1.GitMirrorService/GetBlob"
)
// GitMirrorServiceClient is the client API for GitMirrorService service.
@@ -31,6 +32,8 @@ type GitMirrorServiceClient interface {
Commits(ctx context.Context, in *CommitsRequest, opts ...grpc.CallOption) (*CommitsResponse, error)
Diff(ctx context.Context, in *DiffRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[FileDiff], error)
Interdiff(ctx context.Context, in *InterdiffRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[FileDiff], error)
+ // GetBlob streams a blob's raw bytes by OID, in chunks.
+ GetBlob(ctx context.Context, in *GetBlobRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[BlobChunk], error)
}
type gitMirrorServiceClient struct {
@@ -89,6 +92,25 @@ func (c *gitMirrorServiceClient) Interdiff(ctx context.Context, in *InterdiffReq
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type GitMirrorService_InterdiffClient = grpc.ServerStreamingClient[FileDiff]
+func (c *gitMirrorServiceClient) GetBlob(ctx context.Context, in *GetBlobRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[BlobChunk], error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ stream, err := c.cc.NewStream(ctx, &GitMirrorService_ServiceDesc.Streams[2], GitMirrorService_GetBlob_FullMethodName, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ x := &grpc.GenericClientStream[GetBlobRequest, BlobChunk]{ClientStream: stream}
+ if err := x.ClientStream.SendMsg(in); err != nil {
+ return nil, err
+ }
+ if err := x.ClientStream.CloseSend(); err != nil {
+ return nil, err
+ }
+ return x, nil
+}
+
+// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
+type GitMirrorService_GetBlobClient = grpc.ServerStreamingClient[BlobChunk]
+
// GitMirrorServiceServer is the server API for GitMirrorService service.
// All implementations must embed UnimplementedGitMirrorServiceServer
// for forward compatibility.
@@ -96,6 +118,8 @@ type GitMirrorServiceServer interface {
Commits(context.Context, *CommitsRequest) (*CommitsResponse, error)
Diff(*DiffRequest, grpc.ServerStreamingServer[FileDiff]) error
Interdiff(*InterdiffRequest, grpc.ServerStreamingServer[FileDiff]) error
+ // GetBlob streams a blob's raw bytes by OID, in chunks.
+ GetBlob(*GetBlobRequest, grpc.ServerStreamingServer[BlobChunk]) error
mustEmbedUnimplementedGitMirrorServiceServer()
}
@@ -115,6 +139,9 @@ func (UnimplementedGitMirrorServiceServer) Diff(*DiffRequest, grpc.ServerStreami
func (UnimplementedGitMirrorServiceServer) Interdiff(*InterdiffRequest, grpc.ServerStreamingServer[FileDiff]) error {
return status.Error(codes.Unimplemented, "method Interdiff not implemented")
}
+func (UnimplementedGitMirrorServiceServer) GetBlob(*GetBlobRequest, grpc.ServerStreamingServer[BlobChunk]) error {
+ return status.Error(codes.Unimplemented, "method GetBlob not implemented")
+}
func (UnimplementedGitMirrorServiceServer) mustEmbedUnimplementedGitMirrorServiceServer() {}
func (UnimplementedGitMirrorServiceServer) testEmbeddedByValue() {}
@@ -176,6 +203,17 @@ func _GitMirrorService_Interdiff_Handler(srv interface{}, stream grpc.ServerStre
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type GitMirrorService_InterdiffServer = grpc.ServerStreamingServer[FileDiff]
+func _GitMirrorService_GetBlob_Handler(srv interface{}, stream grpc.ServerStream) error {
+ m := new(GetBlobRequest)
+ if err := stream.RecvMsg(m); err != nil {
+ return err
+ }
+ return srv.(GitMirrorServiceServer).GetBlob(m, &grpc.GenericServerStream[GetBlobRequest, BlobChunk]{ServerStream: stream})
+}
+
+// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
+type GitMirrorService_GetBlobServer = grpc.ServerStreamingServer[BlobChunk]
+
// GitMirrorService_ServiceDesc is the grpc.ServiceDesc for GitMirrorService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -199,6 +237,11 @@ var GitMirrorService_ServiceDesc = grpc.ServiceDesc{
Handler: _GitMirrorService_Interdiff_Handler,
ServerStreams: true,
},
+ {
+ StreamName: "GetBlob",
+ Handler: _GitMirrorService_GetBlob_Handler,
+ ServerStreams: true,
+ },
},
Metadata: "gitmirror/v1/gitmirror.proto",
}
diff --git a/gitmirror/proto/gitmirror/v1/gitmirror.proto b/gitmirror/proto/gitmirror/v1/gitmirror.proto
index 79973948..aea16488 100644
--- a/gitmirror/proto/gitmirror/v1/gitmirror.proto
+++ b/gitmirror/proto/gitmirror/v1/gitmirror.proto
@@ -8,6 +8,20 @@ service GitMirrorService {
rpc Commits(CommitsRequest) returns (CommitsResponse);
rpc Diff(DiffRequest) returns (stream FileDiff);
rpc Interdiff(InterdiffRequest) returns (stream FileDiff);
+ // GetBlob streams a blob's raw bytes by OID, in chunks.
+ rpc GetBlob(GetBlobRequest) returns (stream BlobChunk);
+}
+
+message GetBlobRequest {
+ // repo carries the DID as a string.
+ string repo = 1;
+ // oid is the blob object id (hex).
+ string oid = 2;
+}
+
+// One chunk of a blob's raw bytes; concatenate in arrival order.
+message BlobChunk {
+ bytes data = 1;
}
message CommitsRequest {
diff --git a/gitmirror/src/main.rs b/gitmirror/src/main.rs
index a601e4fb..6b28b206 100644
--- a/gitmirror/src/main.rs
+++ b/gitmirror/src/main.rs
@@ -23,11 +23,15 @@ use tokio_stream::wrappers::ReceiverStream;
use gitmirror::v1::git_mirror_service_server::{GitMirrorService, GitMirrorServiceServer};
use gitmirror::v1::{
- ByteChanges, CommitsRequest, CommitsResponse, DiffRequest, FileContent, FileDiff, Hunk,
- InterdiffRequest, LinePair,
+ BlobChunk, ByteChanges, CommitsRequest, CommitsResponse, DiffRequest, FileContent, FileDiff,
+ GetBlobRequest, Hunk, InterdiffRequest, LinePair,
};
type FileDiffStream = Pin> + Send>>;
+type BlobChunkStream = Pin> + Send>>;
+
+/// Blob bytes are streamed in chunks of this size.
+const BLOB_CHUNK_SIZE: usize = 64 * 1024;
#[derive(Parser)]
#[command(name = "gitmirror", about = "Git mirror gRPC service")]
@@ -108,6 +112,41 @@ impl GitMirrorService for GitMirror {
) -> Result, Status> {
Err(Status::unimplemented("not implemented"))
}
+
+ type GetBlobStream = BlobChunkStream;
+
+ async fn get_blob(
+ &self,
+ request: Request,
+ ) -> Result, Status> {
+ let req = request.into_inner();
+ let repo = self.open_repo(&req.repo)?;
+ let oid = gix::ObjectId::from_hex(req.oid.as_bytes())
+ .map_err(|e| Status::invalid_argument(format!("bad oid '{}': {e}", req.oid)))?;
+ let safe = repo.into_sync();
+
+ let (tx, rx) = mpsc::channel::>(16);
+ tokio::task::spawn_blocking(move || {
+ let repo = safe.to_thread_local();
+ let run = || -> anyhow::Result<()> {
+ let blob = repo.find_object(oid)?.try_into_blob()?;
+ for chunk in blob.data.chunks(BLOB_CHUNK_SIZE) {
+ let msg = BlobChunk {
+ data: chunk.to_vec(),
+ };
+ if tx.blocking_send(Ok(msg)).is_err() {
+ break; // client hung up
+ }
+ }
+ Ok(())
+ };
+ if let Err(e) = run() {
+ let _ = tx.blocking_send(Err(Status::internal(e.to_string())));
+ }
+ });
+
+ Ok(Response::new(Box::pin(ReceiverStream::new(rx))))
+ }
}
/// Resolve a commit-ish ref string (oid, short oid, branch/tag) to its tree oid.
diff --git a/shuttle/src/gen/gitmirror/v1/gitmirror.v1.rs b/shuttle/src/gen/gitmirror/v1/gitmirror.v1.rs
new file mode 100644
index 00000000..d245bdb1
--- /dev/null
+++ b/shuttle/src/gen/gitmirror/v1/gitmirror.v1.rs
@@ -0,0 +1,118 @@
+// @generated
+// This file is @generated by prost-build.
+#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
+pub struct GetBlobRequest {
+ /// repo carries the DID as a string.
+ #[prost(string, tag="1")]
+ pub repo: ::prost::alloc::string::String,
+ /// oid is the blob object id (hex).
+ #[prost(string, tag="2")]
+ pub oid: ::prost::alloc::string::String,
+}
+/// One chunk of a blob's raw bytes; concatenate in arrival order.
+#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
+pub struct BlobChunk {
+ #[prost(bytes="bytes", tag="1")]
+ pub data: ::prost::bytes::Bytes,
+}
+#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
+pub struct CommitsRequest {
+ /// repo carries the DID as a string; parsing/validation is an impl detail.
+ #[prost(string, tag="1")]
+ pub repo: ::prost::alloc::string::String,
+ #[prost(message, optional, tag="2")]
+ pub options: ::core::option::Option,
+}
+/// TODO: fields for the Go CommitsOptions (e.g. ref/branch, limit, cursor) —
+/// unspecified for now.
+#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
+pub struct CommitsOptions {
+}
+#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
+pub struct CommitsResponse {
+ /// maps the Go \[\]string return.
+ #[prost(string, repeated, tag="1")]
+ pub commits: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
+}
+#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
+pub struct DiffRequest {
+ /// repo carries the DID as a string; parsing/validation is an impl detail.
+ #[prost(string, tag="1")]
+ pub repo: ::prost::alloc::string::String,
+ /// base/head are commit-ish ref strings.
+ #[prost(string, tag="2")]
+ pub base: ::prost::alloc::string::String,
+ #[prost(string, tag="3")]
+ pub head: ::prost::alloc::string::String,
+}
+#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
+pub struct InterdiffRequest {
+ /// repo carries the DID as a string.
+ #[prost(string, tag="1")]
+ pub repo: ::prost::alloc::string::String,
+ /// Old patch range (from_base..from_head) and new patch range (to_base..to_head).
+ #[prost(string, tag="2")]
+ pub from_base: ::prost::alloc::string::String,
+ #[prost(string, tag="3")]
+ pub from_head: ::prost::alloc::string::String,
+ #[prost(string, tag="4")]
+ pub to_base: ::prost::alloc::string::String,
+ #[prost(string, tag="5")]
+ pub to_head: ::prost::alloc::string::String,
+}
+// Diff and Interdiff stream one FileDiff per changed file.
+
+#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
+pub struct FileContent {
+ #[prost(string, tag="1")]
+ pub path: ::prost::alloc::string::String,
+ #[prost(string, tag="2")]
+ pub oid: ::prost::alloc::string::String,
+ #[prost(uint64, tag="3")]
+ pub size: u64,
+ #[prost(bool, tag="4")]
+ pub is_binary: bool,
+ #[prost(bool, tag="5")]
+ pub is_submodule: bool,
+}
+/// One aligned line pair; a side is absent (None) when unset.
+#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
+pub struct LinePair {
+ #[prost(uint32, optional, tag="1")]
+ pub lhs: ::core::option::Option,
+ #[prost(uint32, optional, tag="2")]
+ pub rhs: ::core::option::Option,
+}
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct Hunk {
+ /// LineNumber (u32) sets containing novel content per side.
+ #[prost(uint32, repeated, tag="1")]
+ pub novel_lhs: ::prost::alloc::vec::Vec,
+ #[prost(uint32, repeated, tag="2")]
+ pub novel_rhs: ::prost::alloc::vec::Vec,
+ #[prost(message, repeated, tag="3")]
+ pub lines: ::prost::alloc::vec::Vec,
+}
+/// Number of bytes per side when the files differ (Rust Option<(usize, usize)>).
+#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
+pub struct ByteChanges {
+ #[prost(uint64, tag="1")]
+ pub lhs: u64,
+ #[prost(uint64, tag="2")]
+ pub rhs: u64,
+}
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct FileDiff {
+ #[prost(message, optional, tag="1")]
+ pub lhs_src: ::core::option::Option,
+ #[prost(message, optional, tag="2")]
+ pub rhs_src: ::core::option::Option,
+ #[prost(message, repeated, tag="3")]
+ pub hunks: ::prost::alloc::vec::Vec,
+ #[prost(message, optional, tag="4")]
+ pub has_byte_changes: ::core::option::Option,
+ /// TODO: lhs_positions / rhs_positions (MatchedPos) — AST, added later.
+ #[prost(bool, tag="5")]
+ pub has_syntactic_changes: bool,
+}
+// @@protoc_insertion_point(module)
diff --git a/shuttle/src/gen/spindle/agent/v1/spindle.agent.v1.rs b/shuttle/src/gen/spindle/agent/v1/spindle.agent.v1.rs
index 04a12079..a274c2a2 100644
--- a/shuttle/src/gen/spindle/agent/v1/spindle.agent.v1.rs
+++ b/shuttle/src/gen/spindle/agent/v1/spindle.agent.v1.rs
@@ -2,145 +2,146 @@
// This file is @generated by prost-build.
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct Hello {
- #[prost(uint32, tag = "1")]
+ #[prost(uint32, tag="1")]
pub protocol_version: u32,
- #[prost(string, tag = "2")]
+ #[prost(string, tag="2")]
pub agent_version: ::prost::alloc::string::String,
- #[prost(string, tag = "3")]
+ #[prost(string, tag="3")]
pub boot_id: ::prost::alloc::string::String,
- #[prost(string, tag = "4")]
+ #[prost(string, tag="4")]
pub nix_version: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct Init {
- #[prost(string, tag = "1")]
+ #[prost(string, tag="1")]
pub job_id: ::prost::alloc::string::String,
- #[prost(string, repeated, tag = "2")]
+ #[prost(string, repeated, tag="2")]
pub cache_trusted_public_keys: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
- #[prost(uint32, tag = "3")]
+ #[prost(uint32, tag="3")]
pub cache_read_proxy_port: u32,
- #[prost(uint32, tag = "4")]
+ #[prost(uint32, tag="4")]
pub cache_upload_proxy_port: u32,
- #[prost(uint32, tag = "5")]
+ #[prost(uint32, tag="5")]
pub dns_proxy_port: u32,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct ExecStart {
- #[prost(string, repeated, tag = "1")]
+ #[prost(string, repeated, tag="1")]
pub argv: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
- #[prost(string, repeated, tag = "2")]
+ #[prost(string, repeated, tag="2")]
pub env: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
- #[prost(string, tag = "3")]
+ #[prost(string, tag="3")]
pub cwd: ::prost::alloc::string::String,
- #[prost(string, tag = "4")]
+ #[prost(string, tag="4")]
pub user: ::prost::alloc::string::String,
- #[prost(uint32, tag = "5")]
+ #[prost(uint32, tag="5")]
pub timeout_seconds: u32,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct ExecStdout {
- #[prost(string, tag = "1")]
+ #[prost(string, tag="1")]
pub data: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct ExecStderr {
- #[prost(string, tag = "1")]
+ #[prost(string, tag="1")]
pub data: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct ExecExit {
- #[prost(int32, tag = "1")]
+ #[prost(int32, tag="1")]
pub exit_code: i32,
- #[prost(string, tag = "2")]
+ #[prost(string, tag="2")]
pub error: ::prost::alloc::string::String,
/// set when the guest killed the step on its own timeout timer, so the host
/// can classify it as a timeout rather than inferring failure from exit_code.
- #[prost(bool, tag = "3")]
+ #[prost(bool, tag="3")]
pub timed_out: bool,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct ActivateConfig {
- #[prost(string, tag = "1")]
+ #[prost(string, tag="1")]
pub config_key: ::prost::alloc::string::String,
- #[prost(string, tag = "2")]
+ #[prost(string, tag="2")]
pub base_config_hash: ::prost::alloc::string::String,
- #[prost(string, tag = "3")]
+ #[prost(string, tag="3")]
pub user_config: ::prost::alloc::string::String,
- #[prost(string, tag = "4")]
+ #[prost(string, tag="4")]
pub toplevel: ::prost::alloc::string::String,
- #[prost(uint32, tag = "5")]
+ #[prost(uint32, tag="5")]
pub timeout_seconds: u32,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct ActivateConfigResult {
- #[prost(string, tag = "1")]
+ #[prost(string, tag="1")]
pub config_key: ::prost::alloc::string::String,
- #[prost(string, tag = "2")]
+ #[prost(string, tag="2")]
pub toplevel: ::prost::alloc::string::String,
- #[prost(string, tag = "3")]
+ #[prost(string, tag="3")]
pub error: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct BuiltPaths {
- #[prost(string, repeated, tag = "1")]
+ #[prost(string, repeated, tag="1")]
pub paths: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
- #[prost(string, tag = "2")]
+ #[prost(string, tag="2")]
pub reason: ::prost::alloc::string::String,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct CacheDrain {
- #[prost(uint32, tag = "1")]
+ #[prost(uint32, tag="1")]
pub timeout_seconds: u32,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct CacheDrainResult {
- #[prost(string, tag = "1")]
+ #[prost(string, tag="1")]
pub error: ::prost::alloc::string::String,
- #[prost(uint32, tag = "2")]
+ #[prost(uint32, tag="2")]
pub cache_queued: u32,
- #[prost(uint32, tag = "3")]
+ #[prost(uint32, tag="3")]
pub cache_active: u32,
- #[prost(uint32, tag = "4")]
+ #[prost(uint32, tag="4")]
pub cache_uploaded: u32,
- #[prost(uint32, tag = "5")]
+ #[prost(uint32, tag="5")]
pub cache_failed: u32,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
-pub struct Poweroff {}
+pub struct Poweroff {
+}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct PoweroffResult {
- #[prost(string, tag = "1")]
+ #[prost(string, tag="1")]
pub error: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct Message {
- #[prost(string, tag = "1")]
+ #[prost(string, tag="1")]
pub id: ::prost::alloc::string::String,
- #[prost(message, optional, tag = "2")]
+ #[prost(message, optional, tag="2")]
pub hello: ::core::option::Option,
- #[prost(message, optional, tag = "3")]
+ #[prost(message, optional, tag="3")]
pub init: ::core::option::Option,
- #[prost(message, optional, tag = "4")]
+ #[prost(message, optional, tag="4")]
pub exec_start: ::core::option::Option,
- #[prost(message, optional, tag = "5")]
+ #[prost(message, optional, tag="5")]
pub exec_stdout: ::core::option::Option,
- #[prost(message, optional, tag = "6")]
+ #[prost(message, optional, tag="6")]
pub exec_stderr: ::core::option::Option,
- #[prost(message, optional, tag = "7")]
+ #[prost(message, optional, tag="7")]
pub exec_exit: ::core::option::Option,
- #[prost(message, optional, tag = "8")]
+ #[prost(message, optional, tag="8")]
pub activate_config: ::core::option::Option,
- #[prost(message, optional, tag = "9")]
+ #[prost(message, optional, tag="9")]
pub activate_config_result: ::core::option::Option,
- #[prost(message, optional, tag = "10")]
+ #[prost(message, optional, tag="10")]
pub built_paths: ::core::option::Option,
- #[prost(message, optional, tag = "11")]
+ #[prost(message, optional, tag="11")]
pub cache_drain: ::core::option::Option,
- #[prost(message, optional, tag = "12")]
+ #[prost(message, optional, tag="12")]
pub cache_drain_result: ::core::option::Option,
- #[prost(message, optional, tag = "13")]
+ #[prost(message, optional, tag="13")]
pub poweroff: ::core::option::Option,
- #[prost(message, optional, tag = "14")]
+ #[prost(message, optional, tag="14")]
pub poweroff_result: ::core::option::Option,
}
// @@protoc_insertion_point(module)