From 24b094cf30e0d720eaadd3150c529ca30f1be7c5 Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Mon, 29 Jun 2026 17:41:37 +0900 Subject: [PATCH] gitmirror: introduce gitmirror gitmirror is just another name of knotmirror/v2. Current Knotmirror will be gradually replaced by gitmirror except the knot-relay part. gitmirror implements cross-repo operations like diff or merge-check which cannot be done from individual knots when PR source is from different Knot. Signed-off-by: Seongmin Lee Signed-off-by: dawn --- .gitattributes | 2 + Cargo.lock | 140 ++ Cargo.toml | 9 +- docker-compose.yml | 15 + flake.nix | 3 + gitmirror/Cargo.toml | 28 + gitmirror/README.md | 4 + gitmirror/buf.gen.yaml | 12 + gitmirror/buf.yaml | 3 + gitmirror/build.rs | 4 + gitmirror/example/client.go | 42 + gitmirror/proto/gen/gitmirror.pb.go | 1470 ++++++++++++++++++ gitmirror/proto/gen/gitmirror_grpc.pb.go | 324 ++++ gitmirror/proto/gitmirror/v1/gitmirror.proto | 172 ++ gitmirror/src/diff.rs | 365 +++++ gitmirror/src/main.rs | 46 + gitmirror/src/merge.rs | 289 ++++ gitmirror/src/protocol.rs | 3 + gitmirror/src/service.rs | 523 +++++++ localinfra/Caddyfile | 10 + localinfra/gitmirror.Dockerfile | 29 + 21 files changed, 3491 insertions(+), 2 deletions(-) create mode 100644 gitmirror/Cargo.toml create mode 100644 gitmirror/README.md create mode 100644 gitmirror/buf.gen.yaml create mode 100644 gitmirror/buf.yaml create mode 100644 gitmirror/build.rs create mode 100644 gitmirror/example/client.go create mode 100644 gitmirror/proto/gen/gitmirror.pb.go create mode 100644 gitmirror/proto/gen/gitmirror_grpc.pb.go create mode 100644 gitmirror/proto/gitmirror/v1/gitmirror.proto create mode 100644 gitmirror/src/diff.rs create mode 100644 gitmirror/src/main.rs create mode 100644 gitmirror/src/merge.rs create mode 100644 gitmirror/src/protocol.rs create mode 100644 gitmirror/src/service.rs create mode 100644 localinfra/gitmirror.Dockerfile diff --git a/.gitattributes b/.gitattributes index 47ca7d4a5..061a15b2b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,3 +2,5 @@ api/tangled/** linguist-generated -diff api/tangled/*_ext.go -linguist-generated diff flake.lock -diff web/src/lib/api/lexicons/** linguist-generated -diff +Cargo.lock -diff +gitmirror/proto/gen/** linguist-generated -diff diff --git a/Cargo.lock b/Cargo.lock index d55447c4f..149cd9276 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2539,6 +2539,27 @@ dependencies = [ "polyval", ] +[[package]] +name = "gitmirror" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "gix", + "line-numbers", + "prost", + "prost-types", + "rustc-hash", + "tempfile", + "tokio", + "tokio-stream", + "tonic", + "tonic-prost", + "tonic-prost-build", + "tracing", + "tracing-subscriber", +] + [[package]] name = "gix" version = "0.84.0" @@ -2568,6 +2589,7 @@ dependencies = [ "gix-index", "gix-lock", "gix-mailmap", + "gix-merge", "gix-negotiate", "gix-object", "gix-odb", @@ -3007,6 +3029,32 @@ dependencies = [ "gix-error", ] +[[package]] +name = "gix-merge" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7543e1eceb25fbbd1a29459c794148d8bafc1c77555eb386617a2d99b5371971" +dependencies = [ + "bstr", + "gix-command", + "gix-diff", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-imara-diff", + "gix-index", + "gix-object", + "gix-path", + "gix-quote", + "gix-revision", + "gix-revwalk", + "gix-tempfile", + "gix-trace", + "gix-worktree", + "nonempty 0.12.0", + "thiserror 2.0.18", +] + [[package]] name = "gix-negotiate" version = "0.32.0" @@ -3863,6 +3911,19 @@ dependencies = [ "webpki-roots 1.0.8", ] +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -5104,6 +5165,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "line-numbers" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ab6409a2f9ffae158ae823a012a7d7d68386663a14bedc260ca66eacf65c2c" + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -6181,6 +6248,8 @@ dependencies = [ "prettyplease", "prost", "prost-types", + "pulldown-cmark", + "pulldown-cmark-to-cmark", "regex", "syn 2.0.118", "tempfile", @@ -8172,6 +8241,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util", ] [[package]] @@ -8291,6 +8361,74 @@ version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +[[package]] +name = "tonic" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" +dependencies = [ + "async-trait", + "axum", + "base64", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "socket2", + "sync_wrapper", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-build" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c68f61875ac5293cf72e6c8cf0158086428c82c37229e98c840878f1706b0322" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tonic-prost" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" +dependencies = [ + "bytes", + "prost", + "tonic", +] + +[[package]] +name = "tonic-prost-build" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "654e5643eff75d7f8c99197ce1440ed19a3474eada74c12bbac488b2cafdae27" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn 2.0.118", + "tempfile", + "tonic-build", +] + [[package]] name = "tower" version = "0.5.3" @@ -8299,7 +8437,9 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", + "indexmap", "pin-project-lite", + "slab", "sync_wrapper", "tokio", "tokio-util", diff --git a/Cargo.toml b/Cargo.toml index fd3a77f42..a32f35569 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [workspace] resolver = "3" -members = ["bobbin/crates/*", "crates/*", "shuttle", "knot2/crates/*"] -default-members = ["bobbin/crates/*", "crates/*", "knot2/crates/*"] +members = ["bobbin/crates/*", "crates/*", "shuttle", "knot2/crates/*", "gitmirror"] +default-members = ["bobbin/crates/*", "crates/*", "knot2/crates/*", "gitmirror"] exclude = ["sites"] [workspace.package] @@ -169,6 +169,11 @@ tikv-jemalloc-ctl = "0.7" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "json"] } +tonic = "0.14" +tonic-prost = "0.14" +tonic-prost-build = "0.14" +prost = "0.14" + thiserror = "2" confique = { version = "0.4", default-features = false, features = ["toml"] } diff --git a/docker-compose.yml b/docker-compose.yml index f9893c5dc..3b4392ce7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -321,6 +321,20 @@ services: condition: service_started networks: [tngl] + gitmirror: + build: + context: . + dockerfile: localinfra/gitmirror.Dockerfile + restart: unless-stopped + environment: + RUST_LOG: gitmirror=debug + GITMIRROR_ADDR: 0.0.0.0:9000 + GITMIRROR_REPO_BASE: /data/repos + volumes: + - knotmirror-data:/data + - ./localinfra/certs/root.crt:/usr/local/share/ca-certificates/caddy.crt:ro + networks: [tngl] + zoekt-webserver: build: context: https://tangled.org/boltless.me/zoekt.git#tngl @@ -630,6 +644,7 @@ services: - spindle.tngl.boltless.dev - tngl.boltless.dev - mirror.tngl.boltless.dev + - gitmirror.tngl.boltless.dev - zoekt.tngl.boltless.dev - pdsls.tngl.boltless.dev - bobbin.tngl.boltless.dev diff --git a/flake.nix b/flake.nix index e2a59d28b..fd1c8504e 100644 --- a/flake.nix +++ b/flake.nix @@ -470,6 +470,7 @@ pkgs.protoc-gen-prost-crate pkgs.protoc-gen-prost-serde pkgs.protoc-gen-go + pkgs.protoc-gen-go-grpc (fenix.packages.${system}.combine [ fenix.packages.${system}.stable.cargo fenix.packages.${system}.stable.rustc @@ -593,6 +594,8 @@ cd "$rootDir" echo ">>> regenerating protobuf files.." buf generate + echo ">>> regenerating gitmirror go stubs..." + buf generate gitmirror --template gitmirror/buf.gen.yaml echo ">>> generating file descriptor set for shuttle..." buf build -o shuttle/src/gen/file_descriptor_set.bin echo ">>> done" diff --git a/gitmirror/Cargo.toml b/gitmirror/Cargo.toml new file mode 100644 index 000000000..ce6187351 --- /dev/null +++ b/gitmirror/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "gitmirror" +version = "0.1.0" +edition.workspace = true +license.workspace = true +rust-version.workspace = true + +[dependencies] +anyhow = { workspace = true } +clap = { workspace = true } +tonic = { workspace = true } +tonic-prost = { workspace = true } +prost = { workspace = true } +tokio = { workspace = true, features = ["net", "rt-multi-thread", "sync"] } +tokio-stream = { workspace = true, features = ["sync"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +line-numbers = "0.4.0" +rustc-hash = "2.1.2" +gix = { version = "0.84", features = ["parallel", "blob-diff", "merge", "sha1", "sha256", "revision", "tree-editor"] } +prost-types = "0.14.4" +tempfile = "3" + +[build-dependencies] +tonic-prost-build = { workspace = true } + +[lints] +workspace = true diff --git a/gitmirror/README.md b/gitmirror/README.md new file mode 100644 index 000000000..656afc251 --- /dev/null +++ b/gitmirror/README.md @@ -0,0 +1,4 @@ +# Gitmirror + +gitmirror is alternative name of Knotmirror v2. It is designed to be run under the AppView as git syncing service. +Because it's internal sync service like tap or hydrant, it uses grpc over xrpc. diff --git a/gitmirror/buf.gen.yaml b/gitmirror/buf.gen.yaml new file mode 100644 index 000000000..fc9cd944d --- /dev/null +++ b/gitmirror/buf.gen.yaml @@ -0,0 +1,12 @@ +version: v2 +plugins: + - local: protoc-gen-go + out: . + opt: + - paths=import + - module=tangled.org/core + - local: protoc-gen-go-grpc + out: . + opt: + - paths=import + - module=tangled.org/core diff --git a/gitmirror/buf.yaml b/gitmirror/buf.yaml new file mode 100644 index 000000000..ba5ddf507 --- /dev/null +++ b/gitmirror/buf.yaml @@ -0,0 +1,3 @@ +version: v2 +modules: + - path: proto diff --git a/gitmirror/build.rs b/gitmirror/build.rs new file mode 100644 index 000000000..f5b16bc66 --- /dev/null +++ b/gitmirror/build.rs @@ -0,0 +1,4 @@ +fn main() -> Result<(), Box> { + tonic_prost_build::compile_protos("proto/gitmirror/v1/gitmirror.proto")?; + Ok(()) +} diff --git a/gitmirror/example/client.go b/gitmirror/example/client.go new file mode 100644 index 000000000..93f584b28 --- /dev/null +++ b/gitmirror/example/client.go @@ -0,0 +1,42 @@ +//go:build ignore + +// Example: how appview will call gitmirror's GitMirrorService. +// +// This file is build-ignored so it never breaks `go build ./...`. It is a +// copy-ready reference for wiring the client into appview once the Go stubs +// are generated (see gitmirror/buf.gen.yaml). +package main + +import ( + "context" + "log" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + gitmirrorv1 "tangled.org/core/gitmirror/proto/gen" +) + +func main() { + conn, err := grpc.NewClient("127.0.0.1:9000", + grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Fatal(err) + } + defer conn.Close() + + client := gitmirrorv1.NewGitMirrorServiceClient(conn) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // appview converts its syntax.DID to a string via did.String(). + resp, err := client.Commits(ctx, &gitmirrorv1.CommitsRequest{ + Repo: "did:plc:example", + Options: &gitmirrorv1.CommitsOptions{}, + }) + if err != nil { + log.Fatal(err) + } + log.Println(resp.GetCommits()) +} diff --git a/gitmirror/proto/gen/gitmirror.pb.go b/gitmirror/proto/gen/gitmirror.pb.go new file mode 100644 index 000000000..6003bde58 --- /dev/null +++ b/gitmirror/proto/gen/gitmirror.pb.go @@ -0,0 +1,1470 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: gitmirror/v1/gitmirror.proto + +package gitmirrorv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type DiffRequest_ComparisonType int32 + +const ( + DiffRequest_COMPARISON_TYPE_UNSPECIFIED DiffRequest_ComparisonType = 0 + // Corresponds to the BASE..HEAD syntax that only returns any commits that + // are in HEAD but not in BASE. + DiffRequest_COMPARISON_TYPE_ONLY_IN_HEAD DiffRequest_ComparisonType = 1 + // Corresponds to the BASE...HEAD syntax that returns any commits that are + // not in both BASE and HEAD. + DiffRequest_COMPARISON_TYPE_INTERSECTION DiffRequest_ComparisonType = 2 +) + +// Enum value maps for DiffRequest_ComparisonType. +var ( + DiffRequest_ComparisonType_name = map[int32]string{ + 0: "COMPARISON_TYPE_UNSPECIFIED", + 1: "COMPARISON_TYPE_ONLY_IN_HEAD", + 2: "COMPARISON_TYPE_INTERSECTION", + } + DiffRequest_ComparisonType_value = map[string]int32{ + "COMPARISON_TYPE_UNSPECIFIED": 0, + "COMPARISON_TYPE_ONLY_IN_HEAD": 1, + "COMPARISON_TYPE_INTERSECTION": 2, + } +) + +func (x DiffRequest_ComparisonType) Enum() *DiffRequest_ComparisonType { + p := new(DiffRequest_ComparisonType) + *p = x + return p +} + +func (x DiffRequest_ComparisonType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (DiffRequest_ComparisonType) Descriptor() protoreflect.EnumDescriptor { + return file_gitmirror_v1_gitmirror_proto_enumTypes[0].Descriptor() +} + +func (DiffRequest_ComparisonType) Type() protoreflect.EnumType { + return &file_gitmirror_v1_gitmirror_proto_enumTypes[0] +} + +func (x DiffRequest_ComparisonType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use DiffRequest_ComparisonType.Descriptor instead. +func (DiffRequest_ComparisonType) EnumDescriptor() ([]byte, []int) { + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{10, 0} +} + +type ResolveRevRequest 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"` + // rev is a SIMPLE revspec: a branch name, tag name, or short/full commit oid. + // HEAD and revspec navigation (HEAD~2, foo^, ref@{...}, a..b, path:file) are rejected. + Rev []byte `protobuf:"bytes,2,opt,name=rev,proto3" json:"rev,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResolveRevRequest) Reset() { + *x = ResolveRevRequest{} + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResolveRevRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResolveRevRequest) ProtoMessage() {} + +func (x *ResolveRevRequest) 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 ResolveRevRequest.ProtoReflect.Descriptor instead. +func (*ResolveRevRequest) Descriptor() ([]byte, []int) { + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{0} +} + +func (x *ResolveRevRequest) GetRepo() string { + if x != nil { + return x.Repo + } + return "" +} + +func (x *ResolveRevRequest) GetRev() []byte { + if x != nil { + return x.Rev + } + return nil +} + +type ResolveRevResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // commit is the resolved commit object id, hex-encoded. + Commit string `protobuf:"bytes,1,opt,name=commit,proto3" json:"commit,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResolveRevResponse) Reset() { + *x = ResolveRevResponse{} + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResolveRevResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResolveRevResponse) ProtoMessage() {} + +func (x *ResolveRevResponse) 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 ResolveRevResponse.ProtoReflect.Descriptor instead. +func (*ResolveRevResponse) Descriptor() ([]byte, []int) { + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{1} +} + +func (x *ResolveRevResponse) GetCommit() string { + if x != nil { + return x.Commit + } + return "" +} + +// A commit in a specific repo, addressed by the repo's DID. +type RepoCommit 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"` + // commit is a full commit object id, hex-encoded (NOT a revspec/branch/tag). + Commit []byte `protobuf:"bytes,2,opt,name=commit,proto3" json:"commit,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RepoCommit) Reset() { + *x = RepoCommit{} + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RepoCommit) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepoCommit) ProtoMessage() {} + +func (x *RepoCommit) ProtoReflect() protoreflect.Message { + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[2] + 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 RepoCommit.ProtoReflect.Descriptor instead. +func (*RepoCommit) Descriptor() ([]byte, []int) { + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{2} +} + +func (x *RepoCommit) GetRepo() string { + if x != nil { + return x.Repo + } + return "" +} + +func (x *RepoCommit) GetCommit() []byte { + if x != nil { + return x.Commit + } + return nil +} + +type MergeCheckRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Branch/commit being merged into (the "ours" side of the 3-way merge). + Target *RepoCommit `protobuf:"bytes,1,opt,name=target,proto3" json:"target,omitempty"` + // PR head being merged (the "theirs" side of the 3-way merge). + Source *RepoCommit `protobuf:"bytes,2,opt,name=source,proto3" json:"source,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MergeCheckRequest) Reset() { + *x = MergeCheckRequest{} + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MergeCheckRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MergeCheckRequest) ProtoMessage() {} + +func (x *MergeCheckRequest) ProtoReflect() protoreflect.Message { + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[3] + 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 MergeCheckRequest.ProtoReflect.Descriptor instead. +func (*MergeCheckRequest) Descriptor() ([]byte, []int) { + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{3} +} + +func (x *MergeCheckRequest) GetTarget() *RepoCommit { + if x != nil { + return x.Target + } + return nil +} + +func (x *MergeCheckRequest) GetSource() *RepoCommit { + if x != nil { + return x.Source + } + return nil +} + +type MergeCheckResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Whether the 3-way merge has unresolved conflicts. + IsConflicted bool `protobuf:"varint,1,opt,name=is_conflicted,json=isConflicted,proto3" json:"is_conflicted,omitempty"` + // One entry per conflicted file. + Conflicts []*MergeConflict `protobuf:"bytes,2,rep,name=conflicts,proto3" json:"conflicts,omitempty"` + // Optional human-readable summary of the merge check. + Message *string `protobuf:"bytes,3,opt,name=message,proto3,oneof" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MergeCheckResponse) Reset() { + *x = MergeCheckResponse{} + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MergeCheckResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MergeCheckResponse) ProtoMessage() {} + +func (x *MergeCheckResponse) ProtoReflect() protoreflect.Message { + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[4] + 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 MergeCheckResponse.ProtoReflect.Descriptor instead. +func (*MergeCheckResponse) Descriptor() ([]byte, []int) { + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{4} +} + +func (x *MergeCheckResponse) GetIsConflicted() bool { + if x != nil { + return x.IsConflicted + } + return false +} + +func (x *MergeCheckResponse) GetConflicts() []*MergeConflict { + if x != nil { + return x.Conflicts + } + return nil +} + +func (x *MergeCheckResponse) GetMessage() string { + if x != nil && x.Message != nil { + return *x.Message + } + return "" +} + +type MergeConflict struct { + state protoimpl.MessageState `protogen:"open.v1"` + Filename string `protobuf:"bytes,1,opt,name=filename,proto3" json:"filename,omitempty"` + Reason string `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MergeConflict) Reset() { + *x = MergeConflict{} + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MergeConflict) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MergeConflict) ProtoMessage() {} + +func (x *MergeConflict) ProtoReflect() protoreflect.Message { + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[5] + 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 MergeConflict.ProtoReflect.Descriptor instead. +func (*MergeConflict) Descriptor() ([]byte, []int) { + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{5} +} + +func (x *MergeConflict) GetFilename() string { + if x != nil { + return x.Filename + } + return "" +} + +func (x *MergeConflict) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +type CommitLogRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // repo DID + Repo string `protobuf:"bytes,1,opt,name=repo,proto3" json:"repo,omitempty"` + // Ranges to include in the git log (revspec, "A..B", "A...B", etc.). + // At least one range, or all_refs must be specified. + Ranges [][]byte `protobuf:"bytes,2,rep,name=ranges,proto3" json:"ranges,omitempty"` + // If true, all refs are searched for commits. + // Must not be true when ranges are given. + AllRefs bool `protobuf:"varint,3,opt,name=all_refs,json=allRefs,proto3" json:"all_refs,omitempty"` + // After is an optional parameter to specify the earliest commit to consider. + After *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=after,proto3" json:"after,omitempty"` + // Before is an optional parameter to specify the latest commit to consider. + Before *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=before,proto3" json:"before,omitempty"` + // MaxCommits is an optional parameter to specify the maximum number of commits + // to return. If max_commits is 0, all commits that match the criteria will be + // returned. + MaxCommits uint32 `protobuf:"varint,6,opt,name=max_commits,json=maxCommits,proto3" json:"max_commits,omitempty"` + // Skip is an optional parameter to specify the number of commits to skip. + // This can be used to implement a poor mans pagination. + Skip uint32 `protobuf:"varint,7,opt,name=skip,proto3" json:"skip,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CommitLogRequest) Reset() { + *x = CommitLogRequest{} + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CommitLogRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CommitLogRequest) ProtoMessage() {} + +func (x *CommitLogRequest) ProtoReflect() protoreflect.Message { + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[6] + 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 CommitLogRequest.ProtoReflect.Descriptor instead. +func (*CommitLogRequest) Descriptor() ([]byte, []int) { + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{6} +} + +func (x *CommitLogRequest) GetRepo() string { + if x != nil { + return x.Repo + } + return "" +} + +func (x *CommitLogRequest) GetRanges() [][]byte { + if x != nil { + return x.Ranges + } + return nil +} + +func (x *CommitLogRequest) GetAllRefs() bool { + if x != nil { + return x.AllRefs + } + return false +} + +func (x *CommitLogRequest) GetAfter() *timestamppb.Timestamp { + if x != nil { + return x.After + } + return nil +} + +func (x *CommitLogRequest) GetBefore() *timestamppb.Timestamp { + if x != nil { + return x.Before + } + return nil +} + +func (x *CommitLogRequest) GetMaxCommits() uint32 { + if x != nil { + return x.MaxCommits + } + return 0 +} + +func (x *CommitLogRequest) GetSkip() uint32 { + if x != nil { + return x.Skip + } + return 0 +} + +type CommitLogResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Commits []*GitCommit `protobuf:"bytes,1,rep,name=commits,proto3" json:"commits,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CommitLogResponse) Reset() { + *x = CommitLogResponse{} + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CommitLogResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CommitLogResponse) ProtoMessage() {} + +func (x *CommitLogResponse) ProtoReflect() protoreflect.Message { + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[7] + 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 CommitLogResponse.ProtoReflect.Descriptor instead. +func (*CommitLogResponse) Descriptor() ([]byte, []int) { + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{7} +} + +func (x *CommitLogResponse) GetCommits() []*GitCommit { + if x != nil { + return x.Commits + } + return nil +} + +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[8] + 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[8] + 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{8} +} + +func (x *GetBlobRequest) GetRepo() string { + if x != nil { + return x.Repo + } + return "" +} + +func (x *GetBlobRequest) GetOid() string { + if x != nil { + return x.Oid + } + return "" +} + +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[9] + 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[9] + 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{9} +} + +func (x *BlobChunk) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +type DiffRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Base *RepoCommit `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + Head *RepoCommit `protobuf:"bytes,2,opt,name=head,proto3" json:"head,omitempty"` + ComparisonType DiffRequest_ComparisonType `protobuf:"varint,3,opt,name=comparison_type,json=comparisonType,proto3,enum=gitmirror.v1.DiffRequest_ComparisonType" json:"comparison_type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DiffRequest) Reset() { + *x = DiffRequest{} + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DiffRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DiffRequest) ProtoMessage() {} + +func (x *DiffRequest) ProtoReflect() protoreflect.Message { + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[10] + 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 DiffRequest.ProtoReflect.Descriptor instead. +func (*DiffRequest) Descriptor() ([]byte, []int) { + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{10} +} + +func (x *DiffRequest) GetBase() *RepoCommit { + if x != nil { + return x.Base + } + return nil +} + +func (x *DiffRequest) GetHead() *RepoCommit { + if x != nil { + return x.Head + } + return nil +} + +func (x *DiffRequest) GetComparisonType() DiffRequest_ComparisonType { + if x != nil { + return x.ComparisonType + } + return DiffRequest_COMPARISON_TYPE_UNSPECIFIED +} + +type InterdiffRequest 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"` + // Old patch range (from_base..from_head) and new patch range (to_base..to_head). + FromBase []byte `protobuf:"bytes,2,opt,name=from_base,json=fromBase,proto3" json:"from_base,omitempty"` + FromHead []byte `protobuf:"bytes,3,opt,name=from_head,json=fromHead,proto3" json:"from_head,omitempty"` + ToBase []byte `protobuf:"bytes,4,opt,name=to_base,json=toBase,proto3" json:"to_base,omitempty"` + ToHead []byte `protobuf:"bytes,5,opt,name=to_head,json=toHead,proto3" json:"to_head,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InterdiffRequest) Reset() { + *x = InterdiffRequest{} + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InterdiffRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InterdiffRequest) ProtoMessage() {} + +func (x *InterdiffRequest) ProtoReflect() protoreflect.Message { + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[11] + 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 InterdiffRequest.ProtoReflect.Descriptor instead. +func (*InterdiffRequest) Descriptor() ([]byte, []int) { + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{11} +} + +func (x *InterdiffRequest) GetRepo() string { + if x != nil { + return x.Repo + } + return "" +} + +func (x *InterdiffRequest) GetFromBase() []byte { + if x != nil { + return x.FromBase + } + return nil +} + +func (x *InterdiffRequest) GetFromHead() []byte { + if x != nil { + return x.FromHead + } + return nil +} + +func (x *InterdiffRequest) GetToBase() []byte { + if x != nil { + return x.ToBase + } + return nil +} + +func (x *InterdiffRequest) GetToHead() []byte { + if x != nil { + return x.ToHead + } + return nil +} + +type FileContent struct { + state protoimpl.MessageState `protogen:"open.v1"` + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + 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"` + // Raw file bytes, inlined when the oid is not fetchable by the client + // (e.g. interdiff) + Content []byte `protobuf:"bytes,6,opt,name=content,proto3,oneof" json:"content,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FileContent) Reset() { + *x = FileContent{} + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FileContent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FileContent) ProtoMessage() {} + +func (x *FileContent) ProtoReflect() protoreflect.Message { + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[12] + 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 FileContent.ProtoReflect.Descriptor instead. +func (*FileContent) Descriptor() ([]byte, []int) { + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{12} +} + +func (x *FileContent) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *FileContent) GetOid() string { + if x != nil { + return x.Oid + } + return "" +} + +func (x *FileContent) GetSize() uint64 { + if x != nil { + return x.Size + } + return 0 +} + +func (x *FileContent) GetIsBinary() bool { + if x != nil { + return x.IsBinary + } + return false +} + +func (x *FileContent) GetIsSubmodule() bool { + if x != nil { + return x.IsSubmodule + } + return false +} + +func (x *FileContent) GetContent() []byte { + if x != nil { + return x.Content + } + return nil +} + +// One aligned line pair; a side is absent (None) when unset. +type LinePair struct { + state protoimpl.MessageState `protogen:"open.v1"` + Lhs *uint32 `protobuf:"varint,1,opt,name=lhs,proto3,oneof" json:"lhs,omitempty"` + Rhs *uint32 `protobuf:"varint,2,opt,name=rhs,proto3,oneof" json:"rhs,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LinePair) Reset() { + *x = LinePair{} + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LinePair) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LinePair) ProtoMessage() {} + +func (x *LinePair) ProtoReflect() protoreflect.Message { + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[13] + 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 LinePair.ProtoReflect.Descriptor instead. +func (*LinePair) Descriptor() ([]byte, []int) { + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{13} +} + +func (x *LinePair) GetLhs() uint32 { + if x != nil && x.Lhs != nil { + return *x.Lhs + } + return 0 +} + +func (x *LinePair) GetRhs() uint32 { + if x != nil && x.Rhs != nil { + return *x.Rhs + } + return 0 +} + +type Hunk struct { + state protoimpl.MessageState `protogen:"open.v1"` + // LineNumber (u32) sets containing novel content per side. + NovelLhs []uint32 `protobuf:"varint,1,rep,packed,name=novel_lhs,json=novelLhs,proto3" json:"novel_lhs,omitempty"` + NovelRhs []uint32 `protobuf:"varint,2,rep,packed,name=novel_rhs,json=novelRhs,proto3" json:"novel_rhs,omitempty"` + Lines []*LinePair `protobuf:"bytes,3,rep,name=lines,proto3" json:"lines,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Hunk) Reset() { + *x = Hunk{} + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Hunk) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Hunk) ProtoMessage() {} + +func (x *Hunk) ProtoReflect() protoreflect.Message { + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[14] + 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 Hunk.ProtoReflect.Descriptor instead. +func (*Hunk) Descriptor() ([]byte, []int) { + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{14} +} + +func (x *Hunk) GetNovelLhs() []uint32 { + if x != nil { + return x.NovelLhs + } + return nil +} + +func (x *Hunk) GetNovelRhs() []uint32 { + if x != nil { + return x.NovelRhs + } + return nil +} + +func (x *Hunk) GetLines() []*LinePair { + if x != nil { + return x.Lines + } + return nil +} + +// Number of bytes per side when the files differ (Rust Option<(usize, usize)>). +type ByteChanges struct { + state protoimpl.MessageState `protogen:"open.v1"` + Lhs uint64 `protobuf:"varint,1,opt,name=lhs,proto3" json:"lhs,omitempty"` + Rhs uint64 `protobuf:"varint,2,opt,name=rhs,proto3" json:"rhs,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ByteChanges) Reset() { + *x = ByteChanges{} + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ByteChanges) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ByteChanges) ProtoMessage() {} + +func (x *ByteChanges) ProtoReflect() protoreflect.Message { + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[15] + 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 ByteChanges.ProtoReflect.Descriptor instead. +func (*ByteChanges) Descriptor() ([]byte, []int) { + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{15} +} + +func (x *ByteChanges) GetLhs() uint64 { + if x != nil { + return x.Lhs + } + return 0 +} + +func (x *ByteChanges) GetRhs() uint64 { + if x != nil { + return x.Rhs + } + return 0 +} + +type FileDiff struct { + state protoimpl.MessageState `protogen:"open.v1"` + LhsSrc *FileContent `protobuf:"bytes,1,opt,name=lhs_src,json=lhsSrc,proto3" json:"lhs_src,omitempty"` + RhsSrc *FileContent `protobuf:"bytes,2,opt,name=rhs_src,json=rhsSrc,proto3" json:"rhs_src,omitempty"` + Hunks []*Hunk `protobuf:"bytes,3,rep,name=hunks,proto3" json:"hunks,omitempty"` + HasByteChanges *ByteChanges `protobuf:"bytes,4,opt,name=has_byte_changes,json=hasByteChanges,proto3,oneof" json:"has_byte_changes,omitempty"` + HasSyntacticChanges bool `protobuf:"varint,5,opt,name=has_syntactic_changes,json=hasSyntacticChanges,proto3" json:"has_syntactic_changes,omitempty"` // TODO: lhs_positions & rhs_positions + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FileDiff) Reset() { + *x = FileDiff{} + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FileDiff) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FileDiff) ProtoMessage() {} + +func (x *FileDiff) ProtoReflect() protoreflect.Message { + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[16] + 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 FileDiff.ProtoReflect.Descriptor instead. +func (*FileDiff) Descriptor() ([]byte, []int) { + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{16} +} + +func (x *FileDiff) GetLhsSrc() *FileContent { + if x != nil { + return x.LhsSrc + } + return nil +} + +func (x *FileDiff) GetRhsSrc() *FileContent { + if x != nil { + return x.RhsSrc + } + return nil +} + +func (x *FileDiff) GetHunks() []*Hunk { + if x != nil { + return x.Hunks + } + return nil +} + +func (x *FileDiff) GetHasByteChanges() *ByteChanges { + if x != nil { + return x.HasByteChanges + } + return nil +} + +func (x *FileDiff) GetHasSyntacticChanges() bool { + if x != nil { + return x.HasSyntacticChanges + } + return false +} + +type GitCommit struct { + state protoimpl.MessageState `protogen:"open.v1"` + Oid string `protobuf:"bytes,1,opt,name=oid,proto3" json:"oid,omitempty"` + Author *GitSignature `protobuf:"bytes,2,opt,name=author,proto3" json:"author,omitempty"` + Committer *GitSignature `protobuf:"bytes,3,opt,name=committer,proto3" json:"committer,omitempty"` + Message []byte `protobuf:"bytes,4,opt,name=message,proto3" json:"message,omitempty"` + Parents []string `protobuf:"bytes,5,rep,name=parents,proto3" json:"parents,omitempty"` + 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"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GitCommit) Reset() { + *x = GitCommit{} + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GitCommit) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GitCommit) ProtoMessage() {} + +func (x *GitCommit) ProtoReflect() protoreflect.Message { + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[17] + 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 GitCommit.ProtoReflect.Descriptor instead. +func (*GitCommit) Descriptor() ([]byte, []int) { + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{17} +} + +func (x *GitCommit) GetOid() string { + if x != nil { + return x.Oid + } + return "" +} + +func (x *GitCommit) GetAuthor() *GitSignature { + if x != nil { + return x.Author + } + return nil +} + +func (x *GitCommit) GetCommitter() *GitSignature { + if x != nil { + return x.Committer + } + return nil +} + +func (x *GitCommit) GetMessage() []byte { + if x != nil { + return x.Message + } + return nil +} + +func (x *GitCommit) GetParents() []string { + if x != nil { + return x.Parents + } + return nil +} + +func (x *GitCommit) GetExtraHeaders() map[string]string { + if x != nil { + return x.ExtraHeaders + } + return nil +} + +type GitSignature struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name []byte `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Email []byte `protobuf:"bytes,2,opt,name=email,proto3" json:"email,omitempty"` + Date *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=date,proto3" json:"date,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GitSignature) Reset() { + *x = GitSignature{} + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GitSignature) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GitSignature) ProtoMessage() {} + +func (x *GitSignature) ProtoReflect() protoreflect.Message { + mi := &file_gitmirror_v1_gitmirror_proto_msgTypes[18] + 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 GitSignature.ProtoReflect.Descriptor instead. +func (*GitSignature) Descriptor() ([]byte, []int) { + return file_gitmirror_v1_gitmirror_proto_rawDescGZIP(), []int{18} +} + +func (x *GitSignature) GetName() []byte { + if x != nil { + return x.Name + } + return nil +} + +func (x *GitSignature) GetEmail() []byte { + if x != nil { + return x.Email + } + return nil +} + +func (x *GitSignature) GetDate() *timestamppb.Timestamp { + if x != nil { + return x.Date + } + return nil +} + +var File_gitmirror_v1_gitmirror_proto protoreflect.FileDescriptor + +const file_gitmirror_v1_gitmirror_proto_rawDesc = "" + + "\n" + + "\x1cgitmirror/v1/gitmirror.proto\x12\fgitmirror.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"9\n" + + "\x11ResolveRevRequest\x12\x12\n" + + "\x04repo\x18\x01 \x01(\tR\x04repo\x12\x10\n" + + "\x03rev\x18\x02 \x01(\fR\x03rev\",\n" + + "\x12ResolveRevResponse\x12\x16\n" + + "\x06commit\x18\x01 \x01(\tR\x06commit\"8\n" + + "\n" + + "RepoCommit\x12\x12\n" + + "\x04repo\x18\x01 \x01(\tR\x04repo\x12\x16\n" + + "\x06commit\x18\x02 \x01(\fR\x06commit\"w\n" + + "\x11MergeCheckRequest\x120\n" + + "\x06target\x18\x01 \x01(\v2\x18.gitmirror.v1.RepoCommitR\x06target\x120\n" + + "\x06source\x18\x02 \x01(\v2\x18.gitmirror.v1.RepoCommitR\x06source\"\x9f\x01\n" + + "\x12MergeCheckResponse\x12#\n" + + "\ris_conflicted\x18\x01 \x01(\bR\fisConflicted\x129\n" + + "\tconflicts\x18\x02 \x03(\v2\x1b.gitmirror.v1.MergeConflictR\tconflicts\x12\x1d\n" + + "\amessage\x18\x03 \x01(\tH\x00R\amessage\x88\x01\x01B\n" + + "\n" + + "\b_message\"C\n" + + "\rMergeConflict\x12\x1a\n" + + "\bfilename\x18\x01 \x01(\tR\bfilename\x12\x16\n" + + "\x06reason\x18\x02 \x01(\tR\x06reason\"\xf4\x01\n" + + "\x10CommitLogRequest\x12\x12\n" + + "\x04repo\x18\x01 \x01(\tR\x04repo\x12\x16\n" + + "\x06ranges\x18\x02 \x03(\fR\x06ranges\x12\x19\n" + + "\ball_refs\x18\x03 \x01(\bR\aallRefs\x120\n" + + "\x05after\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\x05after\x122\n" + + "\x06before\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\x06before\x12\x1f\n" + + "\vmax_commits\x18\x06 \x01(\rR\n" + + "maxCommits\x12\x12\n" + + "\x04skip\x18\a \x01(\rR\x04skip\"F\n" + + "\x11CommitLogResponse\x121\n" + + "\acommits\x18\x01 \x03(\v2\x17.gitmirror.v1.GitCommitR\acommits\"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\"\xb3\x02\n" + + "\vDiffRequest\x12,\n" + + "\x04base\x18\x01 \x01(\v2\x18.gitmirror.v1.RepoCommitR\x04base\x12,\n" + + "\x04head\x18\x02 \x01(\v2\x18.gitmirror.v1.RepoCommitR\x04head\x12Q\n" + + "\x0fcomparison_type\x18\x03 \x01(\x0e2(.gitmirror.v1.DiffRequest.ComparisonTypeR\x0ecomparisonType\"u\n" + + "\x0eComparisonType\x12\x1f\n" + + "\x1bCOMPARISON_TYPE_UNSPECIFIED\x10\x00\x12 \n" + + "\x1cCOMPARISON_TYPE_ONLY_IN_HEAD\x10\x01\x12 \n" + + "\x1cCOMPARISON_TYPE_INTERSECTION\x10\x02\"\x92\x01\n" + + "\x10InterdiffRequest\x12\x12\n" + + "\x04repo\x18\x01 \x01(\tR\x04repo\x12\x1b\n" + + "\tfrom_base\x18\x02 \x01(\fR\bfromBase\x12\x1b\n" + + "\tfrom_head\x18\x03 \x01(\fR\bfromHead\x12\x17\n" + + "\ato_base\x18\x04 \x01(\fR\x06toBase\x12\x17\n" + + "\ato_head\x18\x05 \x01(\fR\x06toHead\"\xb2\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\x12!\n" + + "\fis_submodule\x18\x05 \x01(\bR\visSubmodule\x12\x1d\n" + + "\acontent\x18\x06 \x01(\fH\x00R\acontent\x88\x01\x01B\n" + + "\n" + + "\b_content\"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" + + "\x04_lhsB\x06\n" + + "\x04_rhs\"n\n" + + "\x04Hunk\x12\x1b\n" + + "\tnovel_lhs\x18\x01 \x03(\rR\bnovelLhs\x12\x1b\n" + + "\tnovel_rhs\x18\x02 \x03(\rR\bnovelRhs\x12,\n" + + "\x05lines\x18\x03 \x03(\v2\x16.gitmirror.v1.LinePairR\x05lines\"1\n" + + "\vByteChanges\x12\x10\n" + + "\x03lhs\x18\x01 \x01(\x04R\x03lhs\x12\x10\n" + + "\x03rhs\x18\x02 \x01(\x04R\x03rhs\"\xaf\x02\n" + + "\bFileDiff\x122\n" + + "\alhs_src\x18\x01 \x01(\v2\x19.gitmirror.v1.FileContentR\x06lhsSrc\x122\n" + + "\arhs_src\x18\x02 \x01(\v2\x19.gitmirror.v1.FileContentR\x06rhsSrc\x12(\n" + + "\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_changes\"\xd0\x02\n" + + "\tGitCommit\x12\x10\n" + + "\x03oid\x18\x01 \x01(\tR\x03oid\x122\n" + + "\x06author\x18\x02 \x01(\v2\x1a.gitmirror.v1.GitSignatureR\x06author\x128\n" + + "\tcommitter\x18\x03 \x01(\v2\x1a.gitmirror.v1.GitSignatureR\tcommitter\x12\x18\n" + + "\amessage\x18\x04 \x01(\fR\amessage\x12\x18\n" + + "\aparents\x18\x05 \x03(\tR\aparents\x12N\n" + + "\rextra_headers\x18\x06 \x03(\v2).gitmirror.v1.GitCommit.ExtraHeadersEntryR\fextraHeaders\x1a?\n" + + "\x11ExtraHeadersEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"h\n" + + "\fGitSignature\x12\x12\n" + + "\x04name\x18\x01 \x01(\fR\x04name\x12\x14\n" + + "\x05email\x18\x02 \x01(\fR\x05email\x12.\n" + + "\x04date\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x04date2\xcc\x03\n" + + "\x10GitMirrorService\x12N\n" + + "\tCommitLog\x12\x1e.gitmirror.v1.CommitLogRequest\x1a\x1f.gitmirror.v1.CommitLogResponse0\x01\x12B\n" + + "\aGetBlob\x12\x1c.gitmirror.v1.GetBlobRequest\x1a\x17.gitmirror.v1.BlobChunk0\x01\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\x01\x12O\n" + + "\n" + + "MergeCheck\x12\x1f.gitmirror.v1.MergeCheckRequest\x1a .gitmirror.v1.MergeCheckResponse\x12O\n" + + "\n" + + "ResolveRev\x12\x1f.gitmirror.v1.ResolveRevRequest\x1a .gitmirror.v1.ResolveRevResponseB2Z0tangled.org/core/gitmirror/proto/gen;gitmirrorv1b\x06proto3" + +var ( + file_gitmirror_v1_gitmirror_proto_rawDescOnce sync.Once + file_gitmirror_v1_gitmirror_proto_rawDescData []byte +) + +func file_gitmirror_v1_gitmirror_proto_rawDescGZIP() []byte { + file_gitmirror_v1_gitmirror_proto_rawDescOnce.Do(func() { + 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))) + }) + return file_gitmirror_v1_gitmirror_proto_rawDescData +} + +var file_gitmirror_v1_gitmirror_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_gitmirror_v1_gitmirror_proto_msgTypes = make([]protoimpl.MessageInfo, 20) +var file_gitmirror_v1_gitmirror_proto_goTypes = []any{ + (DiffRequest_ComparisonType)(0), // 0: gitmirror.v1.DiffRequest.ComparisonType + (*ResolveRevRequest)(nil), // 1: gitmirror.v1.ResolveRevRequest + (*ResolveRevResponse)(nil), // 2: gitmirror.v1.ResolveRevResponse + (*RepoCommit)(nil), // 3: gitmirror.v1.RepoCommit + (*MergeCheckRequest)(nil), // 4: gitmirror.v1.MergeCheckRequest + (*MergeCheckResponse)(nil), // 5: gitmirror.v1.MergeCheckResponse + (*MergeConflict)(nil), // 6: gitmirror.v1.MergeConflict + (*CommitLogRequest)(nil), // 7: gitmirror.v1.CommitLogRequest + (*CommitLogResponse)(nil), // 8: gitmirror.v1.CommitLogResponse + (*GetBlobRequest)(nil), // 9: gitmirror.v1.GetBlobRequest + (*BlobChunk)(nil), // 10: gitmirror.v1.BlobChunk + (*DiffRequest)(nil), // 11: gitmirror.v1.DiffRequest + (*InterdiffRequest)(nil), // 12: gitmirror.v1.InterdiffRequest + (*FileContent)(nil), // 13: gitmirror.v1.FileContent + (*LinePair)(nil), // 14: gitmirror.v1.LinePair + (*Hunk)(nil), // 15: gitmirror.v1.Hunk + (*ByteChanges)(nil), // 16: gitmirror.v1.ByteChanges + (*FileDiff)(nil), // 17: gitmirror.v1.FileDiff + (*GitCommit)(nil), // 18: gitmirror.v1.GitCommit + (*GitSignature)(nil), // 19: gitmirror.v1.GitSignature + nil, // 20: gitmirror.v1.GitCommit.ExtraHeadersEntry + (*timestamppb.Timestamp)(nil), // 21: google.protobuf.Timestamp +} +var file_gitmirror_v1_gitmirror_proto_depIdxs = []int32{ + 3, // 0: gitmirror.v1.MergeCheckRequest.target:type_name -> gitmirror.v1.RepoCommit + 3, // 1: gitmirror.v1.MergeCheckRequest.source:type_name -> gitmirror.v1.RepoCommit + 6, // 2: gitmirror.v1.MergeCheckResponse.conflicts:type_name -> gitmirror.v1.MergeConflict + 21, // 3: gitmirror.v1.CommitLogRequest.after:type_name -> google.protobuf.Timestamp + 21, // 4: gitmirror.v1.CommitLogRequest.before:type_name -> google.protobuf.Timestamp + 18, // 5: gitmirror.v1.CommitLogResponse.commits:type_name -> gitmirror.v1.GitCommit + 3, // 6: gitmirror.v1.DiffRequest.base:type_name -> gitmirror.v1.RepoCommit + 3, // 7: gitmirror.v1.DiffRequest.head:type_name -> gitmirror.v1.RepoCommit + 0, // 8: gitmirror.v1.DiffRequest.comparison_type:type_name -> gitmirror.v1.DiffRequest.ComparisonType + 14, // 9: gitmirror.v1.Hunk.lines:type_name -> gitmirror.v1.LinePair + 13, // 10: gitmirror.v1.FileDiff.lhs_src:type_name -> gitmirror.v1.FileContent + 13, // 11: gitmirror.v1.FileDiff.rhs_src:type_name -> gitmirror.v1.FileContent + 15, // 12: gitmirror.v1.FileDiff.hunks:type_name -> gitmirror.v1.Hunk + 16, // 13: gitmirror.v1.FileDiff.has_byte_changes:type_name -> gitmirror.v1.ByteChanges + 19, // 14: gitmirror.v1.GitCommit.author:type_name -> gitmirror.v1.GitSignature + 19, // 15: gitmirror.v1.GitCommit.committer:type_name -> gitmirror.v1.GitSignature + 20, // 16: gitmirror.v1.GitCommit.extra_headers:type_name -> gitmirror.v1.GitCommit.ExtraHeadersEntry + 21, // 17: gitmirror.v1.GitSignature.date:type_name -> google.protobuf.Timestamp + 7, // 18: gitmirror.v1.GitMirrorService.CommitLog:input_type -> gitmirror.v1.CommitLogRequest + 9, // 19: gitmirror.v1.GitMirrorService.GetBlob:input_type -> gitmirror.v1.GetBlobRequest + 11, // 20: gitmirror.v1.GitMirrorService.Diff:input_type -> gitmirror.v1.DiffRequest + 12, // 21: gitmirror.v1.GitMirrorService.Interdiff:input_type -> gitmirror.v1.InterdiffRequest + 4, // 22: gitmirror.v1.GitMirrorService.MergeCheck:input_type -> gitmirror.v1.MergeCheckRequest + 1, // 23: gitmirror.v1.GitMirrorService.ResolveRev:input_type -> gitmirror.v1.ResolveRevRequest + 8, // 24: gitmirror.v1.GitMirrorService.CommitLog:output_type -> gitmirror.v1.CommitLogResponse + 10, // 25: gitmirror.v1.GitMirrorService.GetBlob:output_type -> gitmirror.v1.BlobChunk + 17, // 26: gitmirror.v1.GitMirrorService.Diff:output_type -> gitmirror.v1.FileDiff + 17, // 27: gitmirror.v1.GitMirrorService.Interdiff:output_type -> gitmirror.v1.FileDiff + 5, // 28: gitmirror.v1.GitMirrorService.MergeCheck:output_type -> gitmirror.v1.MergeCheckResponse + 2, // 29: gitmirror.v1.GitMirrorService.ResolveRev:output_type -> gitmirror.v1.ResolveRevResponse + 24, // [24:30] is the sub-list for method output_type + 18, // [18:24] is the sub-list for method input_type + 18, // [18:18] is the sub-list for extension type_name + 18, // [18:18] is the sub-list for extension extendee + 0, // [0:18] is the sub-list for field type_name +} + +func init() { file_gitmirror_v1_gitmirror_proto_init() } +func file_gitmirror_v1_gitmirror_proto_init() { + if File_gitmirror_v1_gitmirror_proto != nil { + return + } + file_gitmirror_v1_gitmirror_proto_msgTypes[4].OneofWrappers = []any{} + file_gitmirror_v1_gitmirror_proto_msgTypes[12].OneofWrappers = []any{} + file_gitmirror_v1_gitmirror_proto_msgTypes[13].OneofWrappers = []any{} + file_gitmirror_v1_gitmirror_proto_msgTypes[16].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: 1, + NumMessages: 20, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_gitmirror_v1_gitmirror_proto_goTypes, + DependencyIndexes: file_gitmirror_v1_gitmirror_proto_depIdxs, + EnumInfos: file_gitmirror_v1_gitmirror_proto_enumTypes, + MessageInfos: file_gitmirror_v1_gitmirror_proto_msgTypes, + }.Build() + File_gitmirror_v1_gitmirror_proto = out.File + file_gitmirror_v1_gitmirror_proto_goTypes = nil + file_gitmirror_v1_gitmirror_proto_depIdxs = nil +} diff --git a/gitmirror/proto/gen/gitmirror_grpc.pb.go b/gitmirror/proto/gen/gitmirror_grpc.pb.go new file mode 100644 index 000000000..387f85b45 --- /dev/null +++ b/gitmirror/proto/gen/gitmirror_grpc.pb.go @@ -0,0 +1,324 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.2 +// - protoc (unknown) +// source: gitmirror/v1/gitmirror.proto + +package gitmirrorv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + GitMirrorService_CommitLog_FullMethodName = "/gitmirror.v1.GitMirrorService/CommitLog" + GitMirrorService_GetBlob_FullMethodName = "/gitmirror.v1.GitMirrorService/GetBlob" + GitMirrorService_Diff_FullMethodName = "/gitmirror.v1.GitMirrorService/Diff" + GitMirrorService_Interdiff_FullMethodName = "/gitmirror.v1.GitMirrorService/Interdiff" + GitMirrorService_MergeCheck_FullMethodName = "/gitmirror.v1.GitMirrorService/MergeCheck" + GitMirrorService_ResolveRev_FullMethodName = "/gitmirror.v1.GitMirrorService/ResolveRev" +) + +// GitMirrorServiceClient is the client API for GitMirrorService service. +// +// 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. +type GitMirrorServiceClient interface { + CommitLog(ctx context.Context, in *CommitLogRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommitLogResponse], error) + GetBlob(ctx context.Context, in *GetBlobRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[BlobChunk], 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) + MergeCheck(ctx context.Context, in *MergeCheckRequest, opts ...grpc.CallOption) (*MergeCheckResponse, error) + ResolveRev(ctx context.Context, in *ResolveRevRequest, opts ...grpc.CallOption) (*ResolveRevResponse, error) +} + +type gitMirrorServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewGitMirrorServiceClient(cc grpc.ClientConnInterface) GitMirrorServiceClient { + return &gitMirrorServiceClient{cc} +} + +func (c *gitMirrorServiceClient) CommitLog(ctx context.Context, in *CommitLogRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommitLogResponse], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &GitMirrorService_ServiceDesc.Streams[0], GitMirrorService_CommitLog_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[CommitLogRequest, CommitLogResponse]{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_CommitLogClient = grpc.ServerStreamingClient[CommitLogResponse] + +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[1], 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] + +func (c *gitMirrorServiceClient) Diff(ctx context.Context, in *DiffRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[FileDiff], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &GitMirrorService_ServiceDesc.Streams[2], GitMirrorService_Diff_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[DiffRequest, FileDiff]{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_DiffClient = grpc.ServerStreamingClient[FileDiff] + +func (c *gitMirrorServiceClient) Interdiff(ctx context.Context, in *InterdiffRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[FileDiff], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &GitMirrorService_ServiceDesc.Streams[3], GitMirrorService_Interdiff_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[InterdiffRequest, FileDiff]{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_InterdiffClient = grpc.ServerStreamingClient[FileDiff] + +func (c *gitMirrorServiceClient) MergeCheck(ctx context.Context, in *MergeCheckRequest, opts ...grpc.CallOption) (*MergeCheckResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(MergeCheckResponse) + err := c.cc.Invoke(ctx, GitMirrorService_MergeCheck_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *gitMirrorServiceClient) ResolveRev(ctx context.Context, in *ResolveRevRequest, opts ...grpc.CallOption) (*ResolveRevResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResolveRevResponse) + err := c.cc.Invoke(ctx, GitMirrorService_ResolveRev_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// GitMirrorServiceServer is the server API for GitMirrorService service. +// All implementations must embed UnimplementedGitMirrorServiceServer +// for forward compatibility. +type GitMirrorServiceServer interface { + CommitLog(*CommitLogRequest, grpc.ServerStreamingServer[CommitLogResponse]) error + GetBlob(*GetBlobRequest, grpc.ServerStreamingServer[BlobChunk]) error + Diff(*DiffRequest, grpc.ServerStreamingServer[FileDiff]) error + Interdiff(*InterdiffRequest, grpc.ServerStreamingServer[FileDiff]) error + MergeCheck(context.Context, *MergeCheckRequest) (*MergeCheckResponse, error) + ResolveRev(context.Context, *ResolveRevRequest) (*ResolveRevResponse, error) + mustEmbedUnimplementedGitMirrorServiceServer() +} + +// UnimplementedGitMirrorServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedGitMirrorServiceServer struct{} + +func (UnimplementedGitMirrorServiceServer) CommitLog(*CommitLogRequest, grpc.ServerStreamingServer[CommitLogResponse]) error { + return status.Error(codes.Unimplemented, "method CommitLog not implemented") +} +func (UnimplementedGitMirrorServiceServer) GetBlob(*GetBlobRequest, grpc.ServerStreamingServer[BlobChunk]) error { + return status.Error(codes.Unimplemented, "method GetBlob not implemented") +} +func (UnimplementedGitMirrorServiceServer) Diff(*DiffRequest, grpc.ServerStreamingServer[FileDiff]) error { + return status.Error(codes.Unimplemented, "method Diff not implemented") +} +func (UnimplementedGitMirrorServiceServer) Interdiff(*InterdiffRequest, grpc.ServerStreamingServer[FileDiff]) error { + return status.Error(codes.Unimplemented, "method Interdiff not implemented") +} +func (UnimplementedGitMirrorServiceServer) MergeCheck(context.Context, *MergeCheckRequest) (*MergeCheckResponse, error) { + return nil, status.Error(codes.Unimplemented, "method MergeCheck not implemented") +} +func (UnimplementedGitMirrorServiceServer) ResolveRev(context.Context, *ResolveRevRequest) (*ResolveRevResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ResolveRev not implemented") +} +func (UnimplementedGitMirrorServiceServer) mustEmbedUnimplementedGitMirrorServiceServer() {} +func (UnimplementedGitMirrorServiceServer) testEmbeddedByValue() {} + +// UnsafeGitMirrorServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to GitMirrorServiceServer will +// result in compilation errors. +type UnsafeGitMirrorServiceServer interface { + mustEmbedUnimplementedGitMirrorServiceServer() +} + +func RegisterGitMirrorServiceServer(s grpc.ServiceRegistrar, srv GitMirrorServiceServer) { + // If the following call panics, it indicates UnimplementedGitMirrorServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&GitMirrorService_ServiceDesc, srv) +} + +func _GitMirrorService_CommitLog_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(CommitLogRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(GitMirrorServiceServer).CommitLog(m, &grpc.GenericServerStream[CommitLogRequest, CommitLogResponse]{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_CommitLogServer = grpc.ServerStreamingServer[CommitLogResponse] + +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] + +func _GitMirrorService_Diff_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(DiffRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(GitMirrorServiceServer).Diff(m, &grpc.GenericServerStream[DiffRequest, FileDiff]{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_DiffServer = grpc.ServerStreamingServer[FileDiff] + +func _GitMirrorService_Interdiff_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(InterdiffRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(GitMirrorServiceServer).Interdiff(m, &grpc.GenericServerStream[InterdiffRequest, FileDiff]{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_InterdiffServer = grpc.ServerStreamingServer[FileDiff] + +func _GitMirrorService_MergeCheck_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MergeCheckRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GitMirrorServiceServer).MergeCheck(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GitMirrorService_MergeCheck_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GitMirrorServiceServer).MergeCheck(ctx, req.(*MergeCheckRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GitMirrorService_ResolveRev_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ResolveRevRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GitMirrorServiceServer).ResolveRev(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GitMirrorService_ResolveRev_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GitMirrorServiceServer).ResolveRev(ctx, req.(*ResolveRevRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// 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) +var GitMirrorService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "gitmirror.v1.GitMirrorService", + HandlerType: (*GitMirrorServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "MergeCheck", + Handler: _GitMirrorService_MergeCheck_Handler, + }, + { + MethodName: "ResolveRev", + Handler: _GitMirrorService_ResolveRev_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "CommitLog", + Handler: _GitMirrorService_CommitLog_Handler, + ServerStreams: true, + }, + { + StreamName: "GetBlob", + Handler: _GitMirrorService_GetBlob_Handler, + ServerStreams: true, + }, + { + StreamName: "Diff", + Handler: _GitMirrorService_Diff_Handler, + ServerStreams: true, + }, + { + StreamName: "Interdiff", + Handler: _GitMirrorService_Interdiff_Handler, + ServerStreams: true, + }, + }, + Metadata: "gitmirror/v1/gitmirror.proto", +} diff --git a/gitmirror/proto/gitmirror/v1/gitmirror.proto b/gitmirror/proto/gitmirror/v1/gitmirror.proto new file mode 100644 index 000000000..bda84c970 --- /dev/null +++ b/gitmirror/proto/gitmirror/v1/gitmirror.proto @@ -0,0 +1,172 @@ +syntax = "proto3"; + +package gitmirror.v1; + +import "google/protobuf/timestamp.proto"; + +option go_package = "tangled.org/core/gitmirror/proto/gen;gitmirrorv1"; + +service GitMirrorService { + rpc CommitLog(CommitLogRequest) returns (stream CommitLogResponse); + rpc GetBlob(GetBlobRequest) returns (stream BlobChunk); + rpc Diff(DiffRequest) returns (stream FileDiff); + rpc Interdiff(InterdiffRequest) returns (stream FileDiff); + rpc MergeCheck(MergeCheckRequest) returns (MergeCheckResponse); + rpc ResolveRev(ResolveRevRequest) returns (ResolveRevResponse); +} + +message ResolveRevRequest { + // repo carries the DID as a string. + string repo = 1; + // rev is a SIMPLE revspec: a branch name, tag name, or short/full commit oid. + // HEAD and revspec navigation (HEAD~2, foo^, ref@{...}, a..b, path:file) are rejected. + bytes rev = 2; +} +message ResolveRevResponse { + // commit is the resolved commit object id, hex-encoded. + string commit = 1; +} + +// A commit in a specific repo, addressed by the repo's DID. +message RepoCommit { + // repo carries the DID as a string. + string repo = 1; + // commit is a full commit object id, hex-encoded (NOT a revspec/branch/tag). + bytes commit = 2; +} + +message MergeCheckRequest { + // Branch/commit being merged into (the "ours" side of the 3-way merge). + RepoCommit target = 1; + // PR head being merged (the "theirs" side of the 3-way merge). + RepoCommit source = 2; +} +message MergeCheckResponse { + // Whether the 3-way merge has unresolved conflicts. + bool is_conflicted = 1; + // One entry per conflicted file. + repeated MergeConflict conflicts = 2; + // Optional human-readable summary of the merge check. + optional string message = 3; +} +message MergeConflict { + string filename = 1; + string reason = 2; +} + +message CommitLogRequest { + // repo DID + string repo = 1; + // Ranges to include in the git log (revspec, "A..B", "A...B", etc.). + // At least one range, or all_refs must be specified. + repeated bytes ranges = 2; + // If true, all refs are searched for commits. + // Must not be true when ranges are given. + bool all_refs = 3; + // After is an optional parameter to specify the earliest commit to consider. + google.protobuf.Timestamp after = 4; + // Before is an optional parameter to specify the latest commit to consider. + google.protobuf.Timestamp before = 5; + // MaxCommits is an optional parameter to specify the maximum number of commits + // to return. If max_commits is 0, all commits that match the criteria will be + // returned. + uint32 max_commits = 6; + // Skip is an optional parameter to specify the number of commits to skip. + // This can be used to implement a poor mans pagination. + uint32 skip = 7; +} +message CommitLogResponse { + repeated GitCommit commits = 1; +} + +message GetBlobRequest { + // repo carries the DID as a string. + string repo = 1; + // oid is the blob object id (hex). + string oid = 2; +} +message BlobChunk { + bytes data = 1; +} + +message DiffRequest { + RepoCommit base = 1; + RepoCommit head = 2; + ComparisonType comparison_type = 3; + + enum ComparisonType { + COMPARISON_TYPE_UNSPECIFIED = 0; + // Corresponds to the BASE..HEAD syntax that only returns any commits that + // are in HEAD but not in BASE. + COMPARISON_TYPE_ONLY_IN_HEAD = 1; + // Corresponds to the BASE...HEAD syntax that returns any commits that are + // not in both BASE and HEAD. + COMPARISON_TYPE_INTERSECTION = 2; + } +} + +message InterdiffRequest { + // repo carries the DID as a string. + string repo = 1; + // Old patch range (from_base..from_head) and new patch range (to_base..to_head). + bytes from_base = 2; + bytes from_head = 3; + bytes to_base = 4; + bytes to_head = 5; +} + +// Diff and Interdiff stream one FileDiff per changed file. + +message FileContent { + string path = 1; + string oid = 2; + uint64 size = 3; + bool is_binary = 4; + bool is_submodule = 5; + // Raw file bytes, inlined when the oid is not fetchable by the client + // (e.g. interdiff) + optional bytes content = 6; +} + +// One aligned line pair; a side is absent (None) when unset. +message LinePair { + optional uint32 lhs = 1; + optional uint32 rhs = 2; +} + +message Hunk { + // LineNumber (u32) sets containing novel content per side. + repeated uint32 novel_lhs = 1; + repeated uint32 novel_rhs = 2; + repeated LinePair lines = 3; +} + +// Number of bytes per side when the files differ (Rust Option<(usize, usize)>). +message ByteChanges { + uint64 lhs = 1; + uint64 rhs = 2; +} + +message FileDiff { + FileContent lhs_src = 1; + FileContent rhs_src = 2; + repeated Hunk hunks = 3; + optional ByteChanges has_byte_changes = 4; + bool has_syntactic_changes = 5; + // TODO: lhs_positions & rhs_positions +} + +message GitCommit { + string oid = 1; + GitSignature author = 2; + GitSignature committer = 3; + bytes message = 4; + repeated string parents = 5; + map extra_headers = 6; +} + +message GitSignature { + bytes name = 1; + bytes email = 2; + google.protobuf.Timestamp date = 3; +} diff --git a/gitmirror/src/diff.rs b/gitmirror/src/diff.rs new file mode 100644 index 000000000..5560cd8c4 --- /dev/null +++ b/gitmirror/src/diff.rs @@ -0,0 +1,365 @@ +use gix::ObjectId; +use gix::bstr::ByteSlice as _; +use gix::objs::tree::EntryKind; +use line_numbers::{LineNumber, SingleLineSpan}; +use rustc_hash::FxHashSet; +use tracing::info; + +/// Blobs larger than this are treated as binary. +const LARGE_FILE_THRESHOLD_BYTES: u64 = 1024 * 1024; + +#[derive(Debug)] +pub struct FileContent { + pub(crate) path: String, + pub(crate) oid: String, + pub(crate) size: usize, + pub(crate) is_binary: bool, + pub(crate) is_submodule: bool, + /// Raw file bytes, inlined when the `oid` is not fetchable by the client (interdiff's + /// synthetic rebased tree). `None` for ordinary diffs and for binary/oversized blobs. + pub(crate) content: Option>, +} + +impl From for crate::protocol::v1::FileContent { + fn from(f: FileContent) -> Self { + Self { + path: f.path, + oid: f.oid, + size: f.size as u64, + is_binary: f.is_binary, + is_submodule: f.is_submodule, + content: f.content, + } + } +} + +#[derive(Debug, Clone)] +pub struct Hunk { + pub(crate) novel_lhs: FxHashSet, + pub(crate) novel_rhs: FxHashSet, + pub(crate) lines: Vec<(Option, Option)>, +} + +impl From for crate::protocol::v1::Hunk { + fn from(h: Hunk) -> Self { + // Sort the novel line sets for deterministic output. + let sorted = |set: FxHashSet| -> Vec { + let mut v: Vec = set.into_iter().map(|n| n.0).collect(); + v.sort_unstable(); + v + }; + Self { + novel_lhs: sorted(h.novel_lhs), + novel_rhs: sorted(h.novel_rhs), + lines: h + .lines + .into_iter() + .map(|(lhs, rhs)| crate::protocol::v1::LinePair { + lhs: lhs.map(|n| n.0), + rhs: rhs.map(|n| n.0), + }) + .collect(), + } + } +} + +/// A matched token (an atom, a delimiter, or a comment word). +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum MatchKind { + // TBD +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MatchedPos { + pub(crate) kind: MatchKind, + pub(crate) pos: SingleLineSpan, +} + +#[derive(Debug)] +pub struct Diff { + pub(crate) lhs_src: FileContent, + pub(crate) rhs_src: FileContent, + pub(crate) hunks: Vec, + + #[allow(unused)] + pub(crate) lhs_positions: Vec, + #[allow(unused)] + pub(crate) rhs_positions: Vec, + + /// If the two files do not have exactly the same bytes, the + /// number of bytes in each file. + pub(crate) has_byte_changes: Option<(usize, usize)>, + pub(crate) has_syntactic_changes: bool, +} + +impl From for crate::protocol::v1::FileDiff { + fn from(d: Diff) -> Self { + Self { + lhs_src: Some(d.lhs_src.into()), + rhs_src: Some(d.rhs_src.into()), + hunks: d.hunks.into_iter().map(Into::into).collect(), + has_byte_changes: d.has_byte_changes.map(|(lhs, rhs)| { + crate::protocol::v1::ByteChanges { + lhs: lhs as u64, + rhs: rhs as u64, + } + }), + has_syntactic_changes: d.has_syntactic_changes, + } + } +} + +/// Diff two trees, yielding one [`Diff`] per changed file (line-level [`Hunk`]s included). +/// +/// Returns a lazy iterator of `anyhow::Result`. +pub fn diff<'repo>( + repo: &'repo gix::Repository, + old_tree: &gix::Tree<'_>, + new_tree: &gix::Tree<'_>, + embed_content: bool, +) -> anyhow::Result> { + let changes = repo.diff_tree_to_tree(Some(old_tree), Some(new_tree), None)?; + let mut cache = repo.diff_resource_cache_for_tree_diff()?; + cache.filter.options.large_file_threshold_bytes = LARGE_FILE_THRESHOLD_BYTES; + Ok(DiffIter { + repo, + changes: changes.into_iter(), + cache, + embed_content, + }) +} + +/// Lazy iterator over per-file [`Diff`]s. +pub struct DiffIter<'repo> { + repo: &'repo gix::Repository, + changes: std::vec::IntoIter, + cache: gix::diff::blob::Platform, + embed_content: bool, +} + +impl Iterator for DiffIter<'_> { + type Item = anyhow::Result; + + fn next(&mut self) -> Option { + loop { + let change = self.changes.next()?; + let (old, new) = change_sides(&change); + // Trees are recursed into by gix and never diffed directly; skip defensively. + if old.is_kind(EntryKind::Tree) || new.is_kind(EntryKind::Tree) { + continue; + } + // Submodules (gitlinks) can't go through the blob pipeline — their commit lives in + // another repo. Emit the commit oids directly for the caller to compare. + if old.is_kind(EntryKind::Commit) || new.is_kind(EntryKind::Commit) { + return Some(Ok(submodule_diff(self.repo, &change, old, new))); + } + let result = build_diff(self.repo, &change, &mut self.cache, self.embed_content); + self.cache.clear_resource_cache_keep_allocation(); + return Some(result); + } + } +} + +enum Side { + Absent, + Present { oid: ObjectId, kind: EntryKind }, +} + +impl Side { + fn is_kind(&self, wanted: EntryKind) -> bool { + matches!(self, Side::Present { kind, .. } if *kind == wanted) + } +} + +/// Decompose a change into its `(old, new)` sides regardless of variant. +fn change_sides(change: &gix::object::tree::diff::ChangeDetached) -> (Side, Side) { + use gix::object::tree::diff::ChangeDetached as C; + let present = |oid, mode: gix::object::tree::EntryMode| Side::Present { + oid, + kind: mode.kind(), + }; + match change { + C::Addition { id, entry_mode, .. } => (Side::Absent, present(*id, *entry_mode)), + C::Deletion { id, entry_mode, .. } => (present(*id, *entry_mode), Side::Absent), + C::Modification { + previous_id, + previous_entry_mode, + id, + entry_mode, + .. + } => ( + present(*previous_id, *previous_entry_mode), + present(*id, *entry_mode), + ), + C::Rewrite { + source_id, + source_entry_mode, + id, + entry_mode, + .. + } => ( + present(*source_id, *source_entry_mode), + present(*id, *entry_mode), + ), + } +} + +/// Build a [`Diff`] for a submodule (gitlink) change: no hunks, both sides carry their commit +/// oid with `is_submodule` set. The caller compares the oids as submodule pointers. +fn submodule_diff( + repo: &gix::Repository, + change: &gix::object::tree::diff::ChangeDetached, + old: Side, + new: Side, +) -> Diff { + let path = change.location().to_string(); + let side = |s: Side| FileContent { + path: path.clone(), + oid: match s { + Side::Absent => ObjectId::null(repo.object_hash()).to_string(), + Side::Present { oid, .. } => oid.to_string(), + }, + size: 0, + is_binary: false, + is_submodule: true, + content: None, + }; + Diff { + lhs_src: side(old), + rhs_src: side(new), + hunks: Vec::new(), + lhs_positions: Vec::new(), + rhs_positions: Vec::new(), + has_byte_changes: None, + has_syntactic_changes: false, + } +} + +/// Build the [`Diff`] for a single changed file. +fn build_diff( + repo: &gix::Repository, + change: &gix::object::tree::diff::ChangeDetached, + cache: &mut gix::diff::blob::Platform, + embed_content: bool, +) -> anyhow::Result { + use gix::diff::blob::platform::prepare_diff::Operation; + use gix::diff::blob::platform::resource::Data; + use gix::diff::blob::{Diff as BlobDiff, InternedInput}; + use gix::prelude::TreeDiffChangeExt as _; + + let change = change.attach(repo, repo); + let blob = change.diff(cache)?; + let out = blob.resource_cache.prepare_diff()?; + + // Per-side file metadata, derived from the prepared resources. + let side = + |res: &gix::diff::blob::platform::Resource<'_>, embed_content: bool| -> FileContent { + let (size, is_binary, content) = match res.data { + // Only textual buffers carry inlinable bytes; binary/missing sides fall back to oid. + Data::Buffer { buf, .. } => (buf.len(), false, embed_content.then(|| buf.to_vec())), + Data::Binary { size } => (size as usize, true, None), + Data::Missing => (0, false, None), + }; + FileContent { + path: res.rela_path.to_string(), + oid: res.id.to_string(), + size, + is_binary, + is_submodule: false, + content, + } + }; + let mut lhs_src = side(&out.old, embed_content); + let mut rhs_src = side(&out.new, false); + + let has_byte_changes = if lhs_src.size == rhs_src.size && out.old.id == out.new.id { + None + } else { + Some((lhs_src.size, rhs_src.size)) + }; + + let hunks = match out.operation { + Operation::InternalDiff { algorithm } => { + let input = InternedInput::new(out.old.intern_source(), out.new.intern_source()); + let mut d = BlobDiff::compute(algorithm, &input); + d.postprocess_lines(&input); + d.hunks() + .map(|h| { + let before: Vec = h.before.clone().map(LineNumber).collect(); + let after: Vec = h.after.clone().map(LineNumber).collect(); + let n = before.len().max(after.len()); + let lines = (0..n) + .map(|i| (before.get(i).copied(), after.get(i).copied())) + .collect(); + Hunk { + novel_lhs: before.iter().copied().collect(), + novel_rhs: after.iter().copied().collect(), + lines, + } + }) + .collect() + } + Operation::SourceOrDestinationIsBinary => { + lhs_src.is_binary = true; + rhs_src.is_binary = true; + Vec::new() + } + Operation::ExternalCommand { .. } => unreachable!("we disabled that"), + }; + + Ok(Diff { + lhs_src, + rhs_src, + hunks, + lhs_positions: Vec::new(), + rhs_positions: Vec::new(), + has_byte_changes, + has_syntactic_changes: false, + }) +} + +/// Compute the two trees whose diff is the interdiff between an old patch +/// version (`from_base..from_head`) and a new one (`to_base..to_head`). +/// +/// Rebase `from_base..from_head` onto `to_base` and return rebased tree so +/// caller can diff between `rebased_tree` and `to_head.tree` +pub fn prepare_interdiff<'repo>( + repo: &'repo gix::Repository, + (from_base_id, from_head_id): (ObjectId, ObjectId), + to_base_id: ObjectId, +) -> anyhow::Result> { + let from_head = repo.find_commit(from_head_id)?; + + let from_base = repo.find_commit(from_base_id)?; + let to_base = repo.find_commit(to_base_id)?; + + // Endpoint trees for the 3-way merge. + let from_base_tree = from_base.tree_id()?.detach(); + let from_head_tree = from_head.tree_id()?.detach(); + let to_base_tree = to_base.tree_id()?.detach(); + + let options = repo.tree_merge_options()?; + let labels = gix::merge::blob::builtin_driver::text::Labels { + ancestor: Some("from.base".as_bytes().as_bstr()), + current: Some("to.base".as_bytes().as_bstr()), + other: Some("from.head".as_bytes().as_bstr()), + }; + let mut outcome = repo.merge_trees( + from_base_tree, + to_base_tree, + from_head_tree, + labels, + options, + )?; + + if !outcome.conflicts.is_empty() { + info!( + conflicts = outcome.conflicts.len(), + "interdiff rebase produced conflicts; tree contains conflict markers" + ); + } + + let rebased_id = outcome.tree.write()?.detach(); + let rebased_tree = repo.find_tree(rebased_id)?; + Ok(rebased_tree) +} diff --git a/gitmirror/src/main.rs b/gitmirror/src/main.rs new file mode 100644 index 000000000..52aa9bd9a --- /dev/null +++ b/gitmirror/src/main.rs @@ -0,0 +1,46 @@ +use std::net::SocketAddr; +use std::path::PathBuf; + +use clap::{Parser, Subcommand}; + +mod diff; +mod merge; +mod protocol; +mod service; + +#[derive(Parser)] +#[command(name = "gitmirror", about = "Git mirror gRPC service")] +struct Cli { + #[command(subcommand)] + cmd: Command, +} + +#[derive(Subcommand)] +enum Command { + /// Run the gRPC server. + Serve(ServeArgs), +} + +#[derive(clap::Args)] +struct ServeArgs { + /// Address to bind the gRPC server to. + #[arg(long, env = "GITMIRROR_ADDR", default_value = "127.0.0.1:9000")] + addr: SocketAddr, + + /// Base directory holding bare mirror repos, one per DID (/). + #[arg(long, env = "GITMIRROR_REPO_BASE", default_value = "repos")] + repo_base: PathBuf, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()), + ) + .init(); + + match Cli::parse().cmd { + Command::Serve(args) => service::serve(args.addr, args.repo_base).await, + } +} diff --git a/gitmirror/src/merge.rs b/gitmirror/src/merge.rs new file mode 100644 index 000000000..4e0dadd2c --- /dev/null +++ b/gitmirror/src/merge.rs @@ -0,0 +1,289 @@ +use gix::bstr::ByteSlice as _; +use gix::diff::tree_with_rewrites::Change; +use gix::merge::tree::{Conflict, TreatAsUnresolved}; + +use crate::protocol::v1::{MergeCheckResponse, MergeConflict}; + +/// Perform a 3-way merge check between `target` (ours) and `source` (theirs), using their +/// merge base as the common ancestor. +/// +/// This is read-only: it inspects only the conflicts of the in-memory merge and never persists +/// the merged tree. The caller must have opened `repo` with [`gix::Repository::with_object_memory`] +/// so that any objects `merge_trees` writes while merging land in memory rather than on disk — +/// the mirrored repositories (reached read-only via alternates) are never mutated. +pub fn merge_check( + repo: &gix::Repository, + target_oid: gix::ObjectId, + source_oid: gix::ObjectId, +) -> anyhow::Result { + let our_tree = repo.find_commit(target_oid)?.tree_id()?.detach(); + let their_tree = repo.find_commit(source_oid)?.tree_id()?.detach(); + + // Common ancestor for the 3-way merge. When the histories are unrelated (no merge base), + // fall back to the empty tree — the same behaviour as `git merge --allow-unrelated-histories`, + // which surfaces add/add conflicts with real filenames instead of erroring out. + let (ancestor_tree, unrelated) = match repo.merge_base(target_oid, source_oid) { + Ok(base) => (repo.find_commit(base.detach())?.tree_id()?.detach(), false), + Err(_) => (gix::ObjectId::empty_tree(repo.object_hash()), true), + }; + + let options = repo.tree_merge_options()?; + let labels = gix::merge::blob::builtin_driver::text::Labels { + ancestor: Some("ancestor".as_bytes().as_bstr()), + current: Some("target".as_bytes().as_bstr()), + other: Some("source".as_bytes().as_bstr()), + }; + + let outcome = repo.merge_trees(ancestor_tree, our_tree, their_tree, labels, options)?; + + // Report only genuinely-unresolved conflicts (matching git's notion), so gix's + // auto-resolutions don't show up as false positives. The merged tree is intentionally + // never written. + let mut conflicts = Vec::new(); + for conflict in &outcome.conflicts { + if !conflict.is_unresolved(TreatAsUnresolved::git()) { + continue; + } + conflicts.push(MergeConflict { + filename: conflict.ours.location().to_string(), + reason: conflict_reason(conflict).to_string(), + }); + } + + let is_conflicted = !conflicts.is_empty(); + let message = match (is_conflicted, unrelated) { + (true, true) => Some(format!( + "{} conflicting file(s); unrelated histories (no common ancestor)", + conflicts.len() + )), + (true, false) => Some(format!("{} conflicting file(s)", conflicts.len())), + (false, true) => Some("no common ancestor (unrelated histories)".to_string()), + (false, false) => None, + }; + + Ok(MergeCheckResponse { + is_conflicted, + conflicts, + message, + }) +} + +/// Classify a conflict into a short, human-readable reason from the shape of the two changes. +/// Derived from the change variants rather than the resolution failure so it stays stable +/// across gix versions. +fn conflict_reason(conflict: &Conflict) -> &'static str { + match (&conflict.ours, &conflict.theirs) { + (Change::Deletion { .. }, _) | (_, Change::Deletion { .. }) => "modify/delete conflict", + (Change::Addition { .. }, Change::Addition { .. }) => "add/add conflict", + (Change::Rewrite { .. }, _) | (_, Change::Rewrite { .. }) => "rename conflict", + _ => "content conflict", + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::Path; + use std::process::Command; + + fn git(dir: &Path, args: &[&str]) { + let status = Command::new("git") + .args(args) + .current_dir(dir) + .env("GIT_AUTHOR_NAME", "t") + .env("GIT_AUTHOR_EMAIL", "t@t") + .env("GIT_COMMITTER_NAME", "t") + .env("GIT_COMMITTER_EMAIL", "t@t") + .status() + .expect("run git"); + assert!(status.success(), "git {args:?} failed"); + } + + fn write(dir: &Path, name: &str, content: &str) { + std::fs::write(dir.join(name), content).unwrap(); + } + + fn rev_parse(dir: &Path, rev: &str) -> gix::ObjectId { + let out = Command::new("git") + .args(["rev-parse", rev]) + .current_dir(dir) + .output() + .expect("rev-parse"); + let hex = String::from_utf8(out.stdout).unwrap(); + gix::ObjectId::from_hex(hex.trim().as_bytes()).unwrap() + } + + /// Count loose object files under `/.git/objects`, ignoring `info` and `pack`. + fn count_loose_objects(git_dir: &Path) -> usize { + let objects = git_dir.join(".git").join("objects"); + let mut n = 0; + for shard in std::fs::read_dir(&objects).unwrap() { + let shard = shard.unwrap(); + let name = shard.file_name(); + if name == "info" || name == "pack" || !shard.file_type().unwrap().is_dir() { + continue; + } + n += std::fs::read_dir(shard.path()).unwrap().count(); + } + n + } + + /// Build a repo with a shared base commit, a `main` (target/ours) tip, and a `source` + /// (theirs) tip built by the two closures. + fn fixture( + target: impl FnOnce(&Path), + source: impl FnOnce(&Path), + ) -> (tempfile::TempDir, gix::ObjectId, gix::ObjectId) { + let dir = tempfile::tempdir().unwrap(); + let p = dir.path(); + git(p, &["init", "-q", "-b", "main"]); + write(p, "a.txt", "line1\nline2\nline3\n"); + write(p, "b.txt", "keep\n"); + git(p, &["add", "."]); + git(p, &["commit", "-q", "-m", "base"]); + + git(p, &["branch", "source"]); + // target edits on main + target(p); + git(p, &["add", "-A"]); + git(p, &["commit", "-q", "-m", "target"]); + let target_oid = rev_parse(p, "main"); + + git(p, &["checkout", "-q", "source"]); + source(p); + git(p, &["add", "-A"]); + git(p, &["commit", "-q", "-m", "source"]); + let source_oid = rev_parse(p, "source"); + + (dir, target_oid, source_oid) + } + + fn open_in_memory(dir: &Path) -> gix::Repository { + gix::open(dir).unwrap().with_object_memory() + } + + /// Mirror of `GitMirror::open_merge_scratch`: a throwaway repo whose alternates point at the + /// given object dirs, opened in-memory. + fn scratch_with_alternates(object_dirs: &[std::path::PathBuf]) -> (tempfile::TempDir, gix::Repository) { + let scratch = tempfile::tempdir().unwrap(); + gix::init_bare(scratch.path()).unwrap(); + let info = scratch.path().join("objects").join("info"); + std::fs::create_dir_all(&info).unwrap(); + let mut alternates = String::new(); + for d in object_dirs { + alternates.push_str(&std::fs::canonicalize(d).unwrap().display().to_string()); + alternates.push('\n'); + } + std::fs::write(info.join("alternates"), alternates).unwrap(); + let repo = gix::open(scratch.path()).unwrap().with_object_memory(); + (scratch, repo) + } + + #[test] + fn cross_repo_via_alternates_never_touches_mirrors() { + // Two separate repos sharing a base commit: `a` holds the target tip, `b` the source + // tip. This is the real gitmirror shape (target and source live in different mirrors). + let a = tempfile::tempdir().unwrap(); + let ap = a.path(); + git(ap, &["init", "-q", "-b", "main"]); + write(ap, "a.txt", "line1\nline2\nline3\n"); + git(ap, &["add", "."]); + git(ap, &["commit", "-q", "-m", "base"]); + // target edit in repo `a` + write(ap, "a.txt", "line1\nOURS\nline3\n"); + git(ap, &["add", "-A"]); + git(ap, &["commit", "-q", "-m", "target"]); + let target = rev_parse(ap, "main"); + + // Clone `a` → `b`, reset to the shared base, and make a conflicting source edit. + let b = tempfile::tempdir().unwrap(); + let bp = b.path(); + git(bp, &["clone", "-q", ap.to_str().unwrap(), "."]); + git(bp, &["checkout", "-q", "-B", "source", &target.to_string()]); + git(bp, &["reset", "-q", "--hard", "HEAD~1"]); // back to base + write(bp, "a.txt", "line1\nTHEIRS\nline3\n"); + git(bp, &["add", "-A"]); + git(bp, &["commit", "-q", "-m", "source"]); + let source = rev_parse(bp, "source"); + + let a_objs = ap.join(".git").join("objects"); + let b_objs = bp.join(".git").join("objects"); + let before_a = count_loose_objects(ap); + let before_b = count_loose_objects(bp); + + let (_scratch, repo) = scratch_with_alternates(&[a_objs, b_objs]); + let out = merge_check(&repo, target, source).unwrap(); + + assert!(out.is_conflicted); + assert_eq!(out.conflicts.len(), 1); + assert_eq!(out.conflicts[0].filename, "a.txt"); + // Neither mirror gained loose objects — the merge was read-only against both. + assert_eq!(count_loose_objects(ap), before_a, "target mirror mutated"); + assert_eq!(count_loose_objects(bp), before_b, "source mirror mutated"); + } + + #[test] + fn non_overlapping_edits_are_clean() { + let (dir, t, s) = fixture( + |p| write(p, "a.txt", "line1\nCHANGED\nline3\n"), + |p| write(p, "b.txt", "keep\nappended\n"), + ); + let repo = open_in_memory(dir.path()); + let before = count_loose_objects(dir.path()); + let out = merge_check(&repo, t, s).unwrap(); + assert!(!out.is_conflicted, "expected clean, got {out:?}"); + assert!(out.conflicts.is_empty()); + // never-touch-the-mirror: the in-memory merge writes no loose objects to disk. + assert_eq!(count_loose_objects(dir.path()), before); + } + + #[test] + fn same_line_edits_conflict() { + let (dir, t, s) = fixture( + |p| write(p, "a.txt", "line1\nOURS\nline3\n"), + |p| write(p, "a.txt", "line1\nTHEIRS\nline3\n"), + ); + let repo = open_in_memory(dir.path()); + let before = count_loose_objects(dir.path()); + let out = merge_check(&repo, t, s).unwrap(); + assert!(out.is_conflicted); + assert_eq!(out.conflicts.len(), 1); + assert_eq!(out.conflicts[0].filename, "a.txt"); + assert_eq!(out.conflicts[0].reason, "content conflict"); + assert_eq!(count_loose_objects(dir.path()), before); + } + + #[test] + fn modify_delete_conflict() { + let (dir, t, s) = fixture( + |p| write(p, "a.txt", "line1\nOURS\nline3\n"), + |p| std::fs::remove_file(p.join("a.txt")).unwrap(), + ); + let repo = open_in_memory(dir.path()); + let out = merge_check(&repo, t, s).unwrap(); + assert!(out.is_conflicted); + assert_eq!(out.conflicts[0].filename, "a.txt"); + assert_eq!(out.conflicts[0].reason, "modify/delete conflict"); + } + + #[test] + fn source_is_ancestor_is_clean() { + // Source is an ancestor of target (i.e. already merged / up to date) → no conflicts. + let dir = tempfile::tempdir().unwrap(); + let p = dir.path(); + git(p, &["init", "-q", "-b", "main"]); + write(p, "a.txt", "line1\nline2\n"); + git(p, &["add", "."]); + git(p, &["commit", "-q", "-m", "base"]); + let base = rev_parse(p, "main"); + write(p, "a.txt", "line1\nline2\nline3\n"); + git(p, &["add", "-A"]); + git(p, &["commit", "-q", "-m", "target"]); + let target = rev_parse(p, "main"); + + let repo = open_in_memory(p); + let out = merge_check(&repo, target, base).unwrap(); + assert!(!out.is_conflicted, "expected clean, got {out:?}"); + assert!(out.conflicts.is_empty()); + } +} diff --git a/gitmirror/src/protocol.rs b/gitmirror/src/protocol.rs new file mode 100644 index 000000000..d90533a9f --- /dev/null +++ b/gitmirror/src/protocol.rs @@ -0,0 +1,3 @@ +pub mod v1 { + tonic::include_proto!("gitmirror.v1"); +} diff --git a/gitmirror/src/service.rs b/gitmirror/src/service.rs new file mode 100644 index 000000000..b3585c98f --- /dev/null +++ b/gitmirror/src/service.rs @@ -0,0 +1,523 @@ +use std::net::SocketAddr; +use std::path::PathBuf; +use std::pin::Pin; + +use gix::bstr::ByteSlice as _; +use gix::revision::plumbing::Spec as RevSpec; +use gix::revision::walk::Sorting; +use tempfile::TempDir; +use tokio::sync::mpsc; +use tokio_stream::wrappers::ReceiverStream; +use tokio_stream::Stream; +use tonic::transport::Server; +use tonic::{Request, Response, Status}; +use tracing::{debug, info}; + +use crate::diff::{self, prepare_interdiff}; +use crate::protocol::v1::diff_request::ComparisonType; +use crate::protocol::v1::git_mirror_service_server::{GitMirrorService, GitMirrorServiceServer}; +use crate::protocol::v1::{ + BlobChunk, CommitLogRequest, CommitLogResponse, DiffRequest, FileDiff, GetBlobRequest, + GitCommit, GitSignature, InterdiffRequest, MergeCheckRequest, MergeCheckResponse, + ResolveRevRequest, ResolveRevResponse, +}; + +type FileDiffStream = Pin> + Send>>; +type BlobChunkStream = Pin> + Send>>; +type CommitLogResponseStream = + Pin> + Send>>; + +const BLOB_CHUNK_SIZE: usize = 64 * 1024; +const COMMIT_LOG_BATCH_SIZE: usize = 128; + +struct GitMirror { + repo_base: PathBuf, +} + +/// A [`gix::Repository`] backed by a temporary directory that is removed on drop. Derefs to +/// `gix::Repository`, so it is used just like one; keeping it alive keeps the scratch dir alive. +struct TempRepository { + repo: gix::Repository, + // Declared AFTER `repo` so `repo` drops first: any file handles into the scratch dir close + // before the dir itself is removed (Rust drops struct fields in declaration order). + _dir: TempDir, +} + +impl TempRepository { + fn new(repo: gix::Repository, dir: TempDir) -> Self { + Self { repo, _dir: dir } + } +} + +impl std::ops::Deref for TempRepository { + type Target = gix::Repository; + fn deref(&self) -> &gix::Repository { + &self.repo + } +} + +impl GitMirror { + fn open_repo(&self, did: &str) -> Result { + if !did.starts_with("did:") { + return Err(Status::invalid_argument("repo must be a DID")); + } + gix::open(self.repo_base.join(did)) + .map_err(|e| Status::not_found(format!("repo not found: {e}"))) + } + + /// Build a throwaway bare repo whose `objects/info/alternates` points read-only at each of the + /// given git repositories, so a single `gix::Repository` can see objects from all of them + /// without ever mutating them. + fn open_scratch(&self, dids: &[&str]) -> Result { + let mut seen: Vec<&str> = Vec::new(); + let mut object_dirs = Vec::new(); + for &did in dids { + if seen.contains(&did) { + continue + } + if !did.starts_with("did:") { + return Err(Status::invalid_argument("repo must be a DID")); + } + seen.push(did); + let object_dir = std::fs::canonicalize(self.repo_base.join(did).join("objects")) + .map_err(|_| Status::not_found(format!("repo not found: {did}")))?; + object_dirs.push(object_dir); + } + if object_dirs.is_empty() { + return Err(Status::internal("open_scratch requires at least one repo")); + } + + let scratch = tempfile::tempdir().map_err(|e| Status::internal(e.to_string()))?; + gix::init_bare(scratch.path()).map_err(|e| Status::internal(e.to_string()))?; + + let info_dir = scratch.path().join("objects").join("info"); + std::fs::create_dir_all(&info_dir).map_err(|e| Status::internal(e.to_string()))?; + let alternates = object_dirs + .iter() + .map(|p| p.display().to_string()) + .collect::>() + .join("\n"); + std::fs::write(info_dir.join("alternates"), format!("{alternates}\n")) + .map_err(|e| Status::internal(e.to_string()))?; + + let repo = gix::open(scratch.path()) + .map_err(|e| Status::internal(e.to_string()))? + .with_object_memory(); + Ok(TempRepository::new(repo, scratch)) + } +} + +#[tonic::async_trait] +impl GitMirrorService for GitMirror { + type DiffStream = FileDiffStream; + + async fn diff( + &self, + request: Request, + ) -> Result, Status> { + let req = request.into_inner(); + let head = req + .head + .ok_or_else(|| Status::invalid_argument("head is required"))?; + let base = req.base; + debug!( + base_repo = ?base.as_ref().map(|b| b.repo.as_str()), + base_commit = ?base.as_ref().map(|b| b.commit.as_bstr()), + head_repo = %head.repo, + head_commit = %head.commit.as_bstr(), + "diff request", + ); + let scratch = match &base { + Some(base) => self.open_scratch(&[head.repo.as_str(), base.repo.as_str()])?, + None => self.open_scratch(&[head.repo.as_str()])?, + }; + let head_commit_id = find_commit_by_sha(&scratch, &head.commit)?; + let base_commit_id = base + .as_ref() + .map(|b| find_commit_by_sha(&scratch, &b.commit)) + .transpose()?; + let comparison_type = + ComparisonType::try_from(req.comparison_type).unwrap_or(ComparisonType::Unspecified); + + let (tx, rx) = mpsc::channel::>(16); + tokio::task::spawn_blocking(move || { + let repo = &*scratch; + + let run = || -> anyhow::Result<()> { + let head_commit = repo.find_commit(head_commit_id)?; + let old = match base_commit_id { + Some(base_commit_id) => match comparison_type { + ComparisonType::OnlyInHead => { + repo.find_tree(repo.find_commit(base_commit_id)?.tree_id()?)? + } + ComparisonType::Unspecified | ComparisonType::Intersection => { + let merge_base = + repo.merge_base(base_commit_id, head_commit_id)?.detach(); + repo.find_tree(repo.find_commit(merge_base)?.tree_id()?)? + } + }, + None => match head_commit.parent_ids().next() { + Some(parent_id) => { + repo.find_tree(repo.find_commit(parent_id.detach())?.tree_id()?)? + } + None => repo.empty_tree(), + }, + }; + let new = repo.find_tree(head_commit.tree_id()?)?; + for item in diff::diff(repo, &old, &new, false)? { + if tx.blocking_send(Ok(item?.into())).is_err() { + break; + } + } + Ok(()) + }; + if let Err(e) = run() { + let _ = tx.blocking_send(Err(Status::internal(e.to_string()))); + } + }); + + Ok(Response::new(Box::pin(ReceiverStream::new(rx)))) + } + + type InterdiffStream = FileDiffStream; + + async fn interdiff( + &self, + request: Request, + ) -> Result, Status> { + let req = request.into_inner(); + let repo = self.open_repo(&req.repo)?; + let from_base_id = resolve_commit_oid(&repo, &req.from_base)?; + let from_head_id = resolve_commit_oid(&repo, &req.from_head)?; + let to_base_id = resolve_commit_oid(&repo, &req.to_base)?; + let to_head_id = resolve_commit_oid(&repo, &req.to_head)?; + 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 to_head = repo.find_commit(to_head_id)?; + let to_head_tree = to_head.tree()?; + let rebased_tree = + prepare_interdiff(&repo, (from_base_id, from_head_id), to_base_id)?; + for item in diff::diff(&repo, &rebased_tree, &to_head_tree, true)? { + if tx.blocking_send(Ok(item?.into())).is_err() { + break; + } + } + Ok(()) + }; + if let Err(e) = run() { + let _ = tx.blocking_send(Err(Status::internal(e.to_string()))); + } + }); + + Ok(Response::new(Box::pin(ReceiverStream::new(rx)))) + } + + 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; + } + } + Ok(()) + }; + if let Err(e) = run() { + let _ = tx.blocking_send(Err(Status::internal(e.to_string()))); + } + }); + + Ok(Response::new(Box::pin(ReceiverStream::new(rx)))) + } + + type CommitLogStream = CommitLogResponseStream; + + async fn commit_log( + &self, + request: Request, + ) -> Result, Status> { + let req = request.into_inner(); + debug!( + repo = %req.repo, + all_refs = req.all_refs, + ranges = ?req.ranges, + after = ?req.after, + before = ?req.before, + max_commits = req.max_commits, + skip = req.skip, + "commit_log request", + ); + if req.all_refs != req.ranges.is_empty() { + return Err(Status::invalid_argument( + "specify exactly one of `ranges` or `all_refs`", + )); + } + let repo = self.open_repo(&req.repo)?; + 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 after = req.after.map(|t| t.seconds); + let before = req.before.map(|t| t.seconds); + let max_commits = req.max_commits as usize; + let mut skip = req.skip as usize; + let mut sent = 0usize; + let mut batch = Vec::with_capacity(COMMIT_LOG_BATCH_SIZE); + + for info in commit_log_walk(&repo, &req)? { + let info = info?; + let commit_time = info.commit_time(); + if before.is_some_and(|b| commit_time > b) { + continue; + } + if after.is_some_and(|a| commit_time < a) { + break; + } + if skip > 0 { + skip -= 1; + continue; + } + if max_commits != 0 && sent >= max_commits { + break; + } + batch.push(GitCommit::try_from(&info.object()?)?); + sent += 1; + if batch.len() == COMMIT_LOG_BATCH_SIZE { + let msg = CommitLogResponse { + commits: std::mem::take(&mut batch), + }; + if tx.blocking_send(Ok(msg)).is_err() { + return Ok(()); // client hung up + } + } + } + if !batch.is_empty() { + let _ = tx.blocking_send(Ok(CommitLogResponse { commits: batch })); + } + Ok(()) + }; + if let Err(e) = run() { + let _ = tx.blocking_send(Err(Status::internal(e.to_string()))); + } + }); + + Ok(Response::new(Box::pin(ReceiverStream::new(rx)))) + } + + async fn merge_check( + &self, + request: Request, + ) -> Result, Status> { + let req = request.into_inner(); + let target = req + .target + .ok_or_else(|| Status::invalid_argument("target is required"))?; + let source = req + .source + .ok_or_else(|| Status::invalid_argument("source is required"))?; + debug!( + target_repo = %target.repo, + target_commit = %target.commit.as_bstr(), + source_repo = %source.repo, + source_commit = %source.commit.as_bstr(), + "merge_check request", + ); + + let scratch = self.open_scratch(&[target.repo.as_str(), source.repo.as_str()])?; + + let response = + tokio::task::spawn_blocking(move || -> Result { + let target_oid = find_commit_by_sha(&scratch, &target.commit)?; + let source_oid = find_commit_by_sha(&scratch, &source.commit)?; + crate::merge::merge_check(&scratch, target_oid, source_oid) + .map_err(|e| Status::internal(e.to_string())) + }) + .await + .map_err(|e| Status::internal(format!("merge task panicked: {e}")))??; + + Ok(Response::new(response)) + } + + async fn resolve_rev( + &self, + request: Request, + ) -> Result, Status> { + let req = request.into_inner(); + debug!(repo = %req.repo, rev = %req.rev.as_bstr(), "resolve_rev request"); + let repo = self.open_repo(&req.repo)?; + let commit = tokio::task::spawn_blocking(move || -> Result { + Ok(resolve_simple_rev(&repo, &req.rev)?.to_string()) + }) + .await + .map_err(|e| Status::internal(format!("resolve task panicked: {e}")))??; + Ok(Response::new(ResolveRevResponse { commit })) + } +} + +fn resolve_commit_oid(repo: &gix::Repository, spec: &[u8]) -> Result { + let commit = repo + .rev_parse_single(spec.as_bstr()) + .map_err(|e| { + Status::invalid_argument(format!("bad rev '{}': {e}", String::from_utf8_lossy(spec))) + })? + .object() + .map_err(|e| Status::internal(e.to_string()))? + .peel_to_commit() + .map_err(|e| Status::internal(e.to_string()))?; + Ok(commit.id) +} + +fn resolve_simple_rev(repo: &gix::Repository, rev: &[u8]) -> Result { + let unsupported = rev == b"HEAD" + || rev == b"@" + || rev.windows(2).any(|w| w == b"@{") + || rev.iter().any(|&b| matches!(b, b'~' | b'^' | b':')); + if unsupported { + return Err(Status::invalid_argument(format!( + "unsupported rev '{}': only a branch, tag, or commit oid is allowed", + rev.as_bstr() + ))); + } + resolve_commit_oid(repo, rev) +} + +fn find_commit_by_sha(repo: &gix::Repository, sha: &[u8]) -> Result { + let oid = gix::ObjectId::from_hex(sha).map_err(|e| { + Status::invalid_argument(format!("bad commit sha '{}': {e}", sha.as_bstr())) + })?; + repo.find_commit(oid) + .map_err(|_| Status::not_found(format!("commit not found: {oid}")))?; + Ok(oid) +} + +fn commit_log_walk<'repo>( + repo: &'repo gix::Repository, + req: &CommitLogRequest, +) -> anyhow::Result> { + let mut tips = Vec::new(); + let mut hidden = Vec::new(); + + if req.all_refs { + for r in repo.references()?.all()? { + let mut r = r.map_err(|e| anyhow::anyhow!(e))?; + if let Ok(commit) = r.peel_to_commit() { + tips.push(commit.id); + } + } + } else { + for range in &req.ranges { + let revspec = repo.rev_parse(range.as_bstr())?; + let spec = revspec.detach(); + match spec { + RevSpec::Include(id) => tips.push(id), + RevSpec::Range { from, to } => { + tips.push(to); + hidden.push(from); + } + _ => { + anyhow::bail!("The spec isn't currently supported: {spec:?}") + } + } + } + } + + Ok(repo + .rev_walk(tips) + .sorting(Sorting::ByCommitTime(Default::default())) + .with_hidden(hidden) + .all()?) +} + +impl TryFrom<&gix::Commit<'_>> for GitCommit { + type Error = anyhow::Error; + + fn try_from(commit: &gix::Commit<'_>) -> anyhow::Result { + let decoded = commit.decode()?; + Ok(GitCommit { + oid: commit.id.to_string(), + author: Some(decoded.author()?.try_into()?), + committer: Some(decoded.committer()?.try_into()?), + message: decoded.message.to_vec(), + parents: decoded + .parents + .into_iter() + .map(|id| id.to_string()) + .collect(), + extra_headers: decoded + .extra_headers + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(), + }) + } +} + +impl TryFrom> for GitSignature { + type Error = anyhow::Error; + + fn try_from(sig: gix::actor::SignatureRef<'_>) -> anyhow::Result { + let time = sig.time()?; + Ok(GitSignature { + name: sig.name.to_vec(), + email: sig.email.to_vec(), + date: Some(prost_types::Timestamp { + seconds: time.seconds, + nanos: 0, + }), + }) + } +} + +pub async fn serve(addr: SocketAddr, repo_base: PathBuf) -> anyhow::Result<()> { + let service = GitMirror { repo_base }; + info!(addr = %addr, "gitmirror gRPC server listening"); + + Server::builder() + .add_service(GitMirrorServiceServer::new(service)) + .serve(addr) + .await?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn temp_repository_removes_scratch_dir_on_drop() { + let dir = tempfile::tempdir().unwrap(); + gix::init_bare(dir.path()).unwrap(); + let repo = gix::open(dir.path()).unwrap().with_object_memory(); + let temp = TempRepository::new(repo, dir); + let path = temp.git_dir().to_path_buf(); + assert!(path.exists()); + // Derefs to gix::Repository. + assert!(temp.object_hash() == gix::hash::Kind::Sha1); + drop(temp); + assert!(!path.exists(), "scratch dir should be gone after drop"); + } +} diff --git a/localinfra/Caddyfile b/localinfra/Caddyfile index 207cd6e9d..7c308abbc 100644 --- a/localinfra/Caddyfile +++ b/localinfra/Caddyfile @@ -66,6 +66,16 @@ mirror.tngl.boltless.dev { } } +gitmirror.tngl.boltless.dev { + tls internal + # gitmirror (tonic) serves h2c; Caddy defaults to HTTP/1.1 to the backend. + reverse_proxy gitmirror:9000 { + transport http { + versions h2c 2 + } + } +} + # zoekt (internal service) zoekt.tngl.boltless.dev { tls internal diff --git a/localinfra/gitmirror.Dockerfile b/localinfra/gitmirror.Dockerfile new file mode 100644 index 000000000..16f9fe66c --- /dev/null +++ b/localinfra/gitmirror.Dockerfile @@ -0,0 +1,29 @@ +# Development only. Not for production use. + +FROM rust:1.96-bookworm AS build + +RUN apt-get update && apt-get install -y --no-install-recommends protobuf-compiler libprotobuf-dev \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /src +COPY . . + +# Cache mounts on the cargo registry + target dir. The binary is copied out within the same +# RUN because the target cache mount is unmounted afterwards (a later COPY --from could not +# see it). +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/src/target \ + cargo build --release -p gitmirror \ + && cp /src/target/release/gitmirror /usr/local/bin/gitmirror + +FROM debian:bookworm-slim + +RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates tini \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=build /usr/local/bin/gitmirror /usr/local/bin/gitmirror + +EXPOSE 9000 + +ENTRYPOINT ["/usr/bin/tini", "--"] +CMD ["sh", "-c", "if [ -f /usr/local/share/ca-certificates/caddy.crt ]; then update-ca-certificates; fi && exec /usr/local/bin/gitmirror serve"] -- 2.51.2