diff --git a/rust/rhgitaly/build.rs b/rust/rhgitaly/build.rs
index 5ac1a84995048d5304b2c909e73f0e68fecc03b4_cnVzdC9yaGdpdGFseS9idWlsZC5ycw==..6b69b3baa4d3b4be7fdab626ae566f91f3bfba75_cnVzdC9yaGdpdGFseS9idWlsZC5ycw== 100644
--- a/rust/rhgitaly/build.rs
+++ b/rust/rhgitaly/build.rs
@@ -23,6 +23,7 @@
         .compile(
             &[
                 "proto/commit.proto",
+                "proto/ref.proto",
                 "proto/repository.proto",
                 "proto/server.proto",
             ],
diff --git a/rust/rhgitaly/proto/ref.proto b/rust/rhgitaly/proto/ref.proto
new file mode 100644
index 0000000000000000000000000000000000000000..6b69b3baa4d3b4be7fdab626ae566f91f3bfba75_cnVzdC9yaGdpdGFseS9wcm90by9yZWYucHJvdG8=
--- /dev/null
+++ b/rust/rhgitaly/proto/ref.proto
@@ -0,0 +1,497 @@
+syntax = "proto3";
+
+package gitaly;
+
+import "errors.proto";
+import "google/protobuf/timestamp.proto";
+import "lint.proto";
+import "shared.proto";
+
+option go_package = "gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypb";
+
+// RefService is a service that provides RPCs to list and modify Git references.
+service RefService {
+
+  // This comment is left unintentionally blank.
+  rpc RefExists(RefExistsRequest) returns (RefExistsResponse) {
+    option (op_type) = {
+      op: ACCESSOR
+    };
+  }
+
+}
+
+// This comment is left unintentionally blank.
+message FindDefaultBranchNameRequest {
+  // This comment is left unintentionally blank.
+  Repository repository = 1 [(target_repository)=true];
+}
+
+// This comment is left unintentionally blank.
+message FindDefaultBranchNameResponse {
+  // This comment is left unintentionally blank.
+  bytes name = 1;
+}
+
+// This comment is left unintentionally blank.
+message FindAllBranchNamesRequest {
+  // This comment is left unintentionally blank.
+  Repository repository = 1 [(target_repository)=true];
+}
+
+// This comment is left unintentionally blank.
+message FindAllBranchNamesResponse {
+  // This comment is left unintentionally blank.
+  repeated bytes names = 1;
+}
+
+// This comment is left unintentionally blank.
+message FindAllTagNamesRequest {
+  // This comment is left unintentionally blank.
+  Repository repository = 1 [(target_repository)=true];
+}
+
+// This comment is left unintentionally blank.
+message FindAllTagNamesResponse {
+  // This comment is left unintentionally blank.
+  repeated bytes names = 1;
+}
+
+// This comment is left unintentionally blank.
+message FindLocalBranchesRequest {
+  // This comment is left unintentionally blank.
+  enum SortBy {
+    // This comment is left unintentionally blank.
+    NAME = 0; // protolint:disable:this ENUM_FIELD_NAMES_PREFIX ENUM_FIELD_NAMES_ZERO_VALUE_END_WITH
+    // This comment is left unintentionally blank.
+    UPDATED_ASC = 1; // protolint:disable:this ENUM_FIELD_NAMES_PREFIX
+    // This comment is left unintentionally blank.
+    UPDATED_DESC = 2; // protolint:disable:this ENUM_FIELD_NAMES_PREFIX
+  }
+
+  // This comment is left unintentionally blank.
+  Repository repository = 1 [(target_repository)=true];
+  // This comment is left unintentionally blank.
+  SortBy sort_by = 2;
+  // The page token is the branch name, with the `refs/heads/` prefix, for
+  // example "refs/heads/master". After the first branch name is encountered
+  // which lexicographically exceeds the page token, it will be the first result
+  // send as part of the response.
+  PaginationParameter pagination_params = 3;
+}
+
+// This comment is left unintentionally blank.
+message FindLocalBranchesResponse {
+  // This field is duplicated by 'local_branches' and will be marked deprecated
+  // as we add the implementation for 'local_branches'.
+  // Issue: https://gitlab.com/gitlab-org/gitaly/-/issues/1294
+  repeated FindLocalBranchResponse branches = 1;
+  // This comment is left unintentionally blank.
+  repeated Branch local_branches = 2;
+}
+
+// This comment is left unintentionally blank.
+message FindLocalBranchResponse {
+  // This comment is left unintentionally blank.
+  bytes name = 1;
+  // This comment is left unintentionally blank.
+  string commit_id = 2;
+  // This comment is left unintentionally blank.
+  bytes commit_subject = 3;
+  // This comment is left unintentionally blank.
+  FindLocalBranchCommitAuthor commit_author = 4;
+  // This comment is left unintentionally blank.
+  FindLocalBranchCommitAuthor commit_committer = 5;
+  // This comment is left unintentionally blank.
+  GitCommit commit = 6;
+}
+
+// This comment is left unintentionally blank.
+message FindLocalBranchCommitAuthor {
+  // This comment is left unintentionally blank.
+  bytes name = 1;
+  // This comment is left unintentionally blank.
+  bytes email = 2;
+  // This comment is left unintentionally blank.
+  google.protobuf.Timestamp date = 3;
+  // This comment is left unintentionally blank.
+  bytes timezone = 4;
+}
+
+// This comment is left unintentionally blank.
+message FindAllBranchesRequest {
+  // This comment is left unintentionally blank.
+  Repository repository = 1 [(target_repository)=true];
+  // Only return branches that are merged into root ref
+  bool merged_only = 2;
+  // If merged_only is true, this is a list of branches from which we
+  // return those merged into the root ref
+  repeated bytes merged_branches = 3;
+}
+
+// This comment is left unintentionally blank.
+message FindAllBranchesResponse {
+  // This comment is left unintentionally blank.
+  message Branch {
+    // This comment is left unintentionally blank.
+    bytes name = 1;
+    // This comment is left unintentionally blank.
+    GitCommit target = 2;
+  }
+
+  // This comment is left unintentionally blank.
+  repeated Branch branches = 1;
+}
+
+// FindTagRequest is a request for the FindTag RPC.
+message FindTagRequest {
+  // Repository is the repository to look up the tag in.
+  Repository repository = 1 [(target_repository)=true];
+  // TagName is the name of the tag that should be looked up. The caller is supposed to pass in the
+  // tag name only, so if e.g. a tag `refs/tags/v1.0.0` exists, then the caller should pass `v1.0.0`
+  // as argument.
+  bytes tag_name = 2;
+}
+
+// FindTagResponse is a response for the FindTag RPC.
+message FindTagResponse {
+  // Tag is the tag that was found.
+  Tag tag = 1;
+}
+
+// FindTagError is an error that will be returned by the FindTag RPC under specific error
+// conditions.
+message FindTagError {
+  oneof error {
+    // TagNotFound indicates that the tag was not found.
+    ReferenceNotFoundError tag_not_found = 1;
+  }
+}
+
+// This comment is left unintentionally blank.
+message FindAllTagsRequest {
+  // SortBy allows to specify desired order of the elements.
+  message SortBy {
+    // Key is a key used for sorting.
+    enum Key {
+      // This comment is left unintentionally blank.
+      REFNAME     = 0; // protolint:disable:this ENUM_FIELD_NAMES_PREFIX ENUM_FIELD_NAMES_ZERO_VALUE_END_WITH
+      // This comment is left unintentionally blank.
+      CREATORDATE = 1; // protolint:disable:this ENUM_FIELD_NAMES_PREFIX
+      // VERSION_REFNAME sorts tags by their semantic versions (https://semver.org/).
+      // Tag names that are not semantic versions are sorted lexicographically. They come before
+      // the semantic versions if the direction is ascending and after the semantic versions if
+      // the direction is descending.
+      VERSION_REFNAME = 2; // protolint:disable:this ENUM_FIELD_NAMES_PREFIX
+    }
+
+    // This comment is left unintentionally blank.
+    Key           key       = 1;
+    // This comment is left unintentionally blank.
+    SortDirection direction = 2;
+  }
+
+  // This comment is left unintentionally blank.
+  Repository repository = 1 [(target_repository)=true];
+  // SortBy allows to request tags in particular order.
+  SortBy sort_by = 2;
+  // The page token is the tags name, with the `refs/tags/` prefix, for
+  // example "refs/tags/v1.0.0". When the tag name matches the page token,
+  // the tag following it will be the first result send as part of the response.
+  PaginationParameter pagination_params = 3;
+}
+
+// This comment is left unintentionally blank.
+message FindAllTagsResponse {
+  // This comment is left unintentionally blank.
+  repeated Tag tags = 1;
+}
+
+// This comment is left unintentionally blank.
+message RefExistsRequest {
+  // This comment is left unintentionally blank.
+  Repository repository = 1 [(target_repository)=true];
+  // Any ref, e.g. 'refs/heads/master' or 'refs/tags/v1.0.1'. Must start with 'refs/'.
+  bytes ref = 2;
+}
+
+// This comment is left unintentionally blank.
+message RefExistsResponse {
+  // This comment is left unintentionally blank.
+  bool value = 1;
+}
+
+// This comment is left unintentionally blank.
+message CreateBranchRequest {
+  // This comment is left unintentionally blank.
+  Repository repository = 1 [(target_repository)=true];
+  // This comment is left unintentionally blank.
+  bytes name = 2;
+  // This comment is left unintentionally blank.
+  bytes start_point = 3;
+}
+
+// This comment is left unintentionally blank.
+message CreateBranchResponse {
+  // This comment is left unintentionally blank.
+  enum Status {
+    // This comment is left unintentionally blank.
+    OK = 0; // protolint:disable:this ENUM_FIELD_NAMES_PREFIX ENUM_FIELD_NAMES_ZERO_VALUE_END_WITH
+    // This comment is left unintentionally blank.
+    ERR_EXISTS = 1; // protolint:disable:this ENUM_FIELD_NAMES_PREFIX
+    // This comment is left unintentionally blank.
+    ERR_INVALID = 2; // protolint:disable:this ENUM_FIELD_NAMES_PREFIX
+    // This comment is left unintentionally blank.
+    ERR_INVALID_START_POINT = 3; // protolint:disable:this ENUM_FIELD_NAMES_PREFIX
+  }
+
+  // This comment is left unintentionally blank.
+  Status status = 1;
+  // This comment is left unintentionally blank.
+  Branch branch = 2;
+}
+
+// This comment is left unintentionally blank.
+message DeleteBranchRequest {
+  // This comment is left unintentionally blank.
+  Repository repository = 1 [(target_repository)=true];
+  // This comment is left unintentionally blank.
+  bytes name = 2;
+}
+
+// Not clear if we need to do status signaling; we can add fields later.
+message DeleteBranchResponse {
+}
+
+// This comment is left unintentionally blank.
+message FindBranchRequest {
+  // repository is the repository in which the branch should be looked up.
+  Repository repository = 1 [(target_repository)=true];
+  // name is the name of the branch which should be looked up. This must be the
+  // branch name only, it must not have the "refs/heads/" prefix.
+  bytes name = 2;
+}
+
+// This comment is left unintentionally blank.
+message FindBranchResponse {
+  // This comment is left unintentionally blank.
+  Branch branch = 1;
+}
+
+// This comment is left unintentionally blank.
+message DeleteRefsRequest{
+  // This comment is left unintentionally blank.
+  Repository repository = 1 [(target_repository)=true];
+  // The following two fields are mutually exclusive
+  repeated bytes except_with_prefix = 2; // protolint:disable:this REPEATED_FIELD_NAMES_PLURALIZED
+  // This comment is left unintentionally blank.
+  repeated bytes refs = 3;
+}
+
+// This comment is left unintentionally blank.
+message DeleteRefsResponse {
+  // This comment is left unintentionally blank.
+  string git_error = 1;
+}
+
+// DeleteRefsError is returned when DeleteRefs fails to delete refs
+message DeleteRefsError {
+  oneof error {
+    // InvalidFormat is returned when one or more of the refs to be deleted
+    // have an invalid format.
+    InvalidRefFormatError invalid_format = 1;
+    // ReferencesLocked is returned when the references to be deleted are already
+    // locked by another process.
+    ReferencesLockedError references_locked = 2;
+  }
+}
+
+// This comment is left unintentionally blank.
+message ListBranchNamesContainingCommitRequest {
+  // This comment is left unintentionally blank.
+  Repository repository = 1 [(target_repository)=true];
+  // This comment is left unintentionally blank.
+  string commit_id = 2;
+  // Limit the number of tag names to be returned
+  // If the limit is set to zero, all items will be returned
+  uint32 limit = 3;
+}
+
+// This comment is left unintentionally blank.
+message ListBranchNamesContainingCommitResponse {
+  reserved 1;
+  // This comment is left unintentionally blank.
+  repeated bytes branch_names = 2;
+}
+
+// This comment is left unintentionally blank.
+message ListTagNamesContainingCommitRequest {
+  // This comment is left unintentionally blank.
+  Repository repository = 1 [(target_repository)=true];
+  // This comment is left unintentionally blank.
+  string commit_id = 2;
+  // Limit the number of tag names to be returned
+  // If the limit is set to zero, all items will be returned
+  uint32 limit = 3;
+}
+
+// This comment is left unintentionally blank.
+message ListTagNamesContainingCommitResponse {
+  reserved 1;
+  // This comment is left unintentionally blank.
+  repeated bytes tag_names = 2;
+}
+
+// GetTagSignaturesRequest is a request for the GetTagSignatures RPC.
+message GetTagSignaturesRequest {
+  // Repository is the repository in which tag signatures should be looked up.
+  Repository repository = 1 [(target_repository)=true];
+  // TagRevisions is the set of revisions which that should be looked up. Revisions
+  // supports the syntax as specified by gitrevisions(7). All revisions are expected
+  // to resolve to annotated tag objects. At least one revision must be provided.
+  repeated string tag_revisions = 2;
+}
+
+// GetTagSignaturesResponse is a response for a GetTagSignatures request. Each response
+// may contain multiple TagSignatures. In case TagSignatures don't fit into a single
+// response, signatures will be batched in multiple responses.
+message GetTagSignaturesResponse {
+  // TagSignature represents the signature of a signed tag.
+  message TagSignature {
+    // TagId is the resolved object ID of the tag.
+    string tag_id = 1;
+    // Signature contains the cryptographic signature of the tag. If the tag is not
+    // cryptographically signed, then the signature is unset.
+    bytes signature = 2;
+    // Content contains the contents which are signed by the signature. Contents
+    // include both the commit message, but also the commit metadata like author and
+    // subject.
+    bytes content = 3;
+  }
+
+  // Signatures is the set of signatures found.
+  repeated TagSignature signatures = 1;
+}
+
+// This comment is left unintentionally blank.
+message GetTagMessagesRequest {
+  reserved 2;
+  reserved "tag_names";
+
+  // This comment is left unintentionally blank.
+  Repository repository = 1 [(target_repository)=true];
+  // This comment is left unintentionally blank.
+  repeated string tag_ids = 3;
+}
+
+// This comment is left unintentionally blank.
+message GetTagMessagesResponse {
+  reserved 1;
+  reserved "tag_name";
+
+  // This comment is left unintentionally blank.
+  bytes message = 2;
+  // Only present for a new tag message
+  string tag_id = 3;
+}
+
+// This comment is left unintentionally blank.
+message FindAllRemoteBranchesRequest {
+  // This comment is left unintentionally blank.
+  Repository repository = 1 [(target_repository)=true];
+  // This comment is left unintentionally blank.
+  string remote_name = 2;
+}
+
+// This comment is left unintentionally blank.
+message FindAllRemoteBranchesResponse {
+  // This comment is left unintentionally blank.
+  repeated Branch branches = 1;
+}
+
+// This comment is left unintentionally blank.
+message PackRefsRequest {
+  // This comment is left unintentionally blank.
+  Repository repository = 1 [(target_repository)=true];
+
+  // DEPRECATED: https://gitlab.com/gitlab-org/gitaly/-/issues/3997
+  reserved 2;
+  reserved "all_refs";
+}
+
+// This comment is left unintentionally blank.
+message PackRefsResponse{
+}
+
+// ListRefsRequest is a request for the ListRefs RPC.
+message ListRefsRequest {
+  // This comment is left unintentionally blank.
+  message SortBy {
+    // This comment is left unintentionally blank.
+    enum Key {
+      // This comment is left unintentionally blank.
+      REFNAME       = 0; // protolint:disable:this ENUM_FIELD_NAMES_PREFIX ENUM_FIELD_NAMES_ZERO_VALUE_END_WITH
+      // This comment is left unintentionally blank.
+      CREATORDATE   = 1; // protolint:disable:this ENUM_FIELD_NAMES_PREFIX
+      // This comment is left unintentionally blank.
+      AUTHORDATE    = 2; // protolint:disable:this ENUM_FIELD_NAMES_PREFIX
+      // This comment is left unintentionally blank.
+      COMMITTERDATE = 3; // protolint:disable:this ENUM_FIELD_NAMES_PREFIX
+    }
+
+    // Key is a key used for sorting.
+    Key           key       = 1;
+    // This comment is left unintentionally blank.
+    SortDirection direction = 2;
+  }
+
+  // Repository is the repository in which references should be listed in.
+  Repository repository = 1 [(target_repository)=true];
+  // Patterns contains all patterns which shall be listed. Patterns should be in the format
+  // accepted by git-for-each-ref(1). At least one pattern must be given, otherwise an error
+  // is returned. Patterns which don't match any reference will be silently ignored.
+  repeated bytes patterns = 2;
+  // Head determines whether the RPC should also return the HEAD reference. By default,
+  // pseudo-refs are not included in the response.
+  bool head = 3;
+  // SortBy allows to request SHAs in particular order.
+  SortBy sort_by = 4;
+}
+
+// ListRefsResponse is a response for the ListRefs RPC. The RPC can return multiple responses
+// in case there are more references than fit into a single gRPC message.
+message ListRefsResponse{
+  // Reference is a direct Git reference. No symbolic references will ever be returned by this RPC.
+  message Reference {
+    // Name is the fully qualified name of the reference.
+    bytes name = 1;
+    // Target is the object ID the reference points to.
+    string target = 2;
+  }
+
+  // References is the set of references returned by the RPC.
+  repeated Reference references = 1;
+}
+
+// This comment is left unintentionally blank.
+message FindRefsByOIDRequest {
+  // repository is the repository in which references will be looked for.
+  Repository repository = 1 [(target_repository)=true];
+  // oid is an object ID to find references for.
+  string oid = 2;
+  // ref_patterns can be one of branch name, tag name or fully qualified ref name.
+  // Providing more than one pattern will yield refs that match any of the given patterns.
+  // If left empty, defaults to "refs/heads/" and "refs/tags/"
+  repeated string ref_patterns = 3;
+  // sort_field determines the sort order of the resulting refs.
+  // If left empty, defaults to "refname" (lexicographic refname order)
+  string sort_field = 4;
+  // limit limits the amount of results returned. 0 means no limit.
+  uint32 limit = 5;
+}
+
+// This comment is left unintentionally blank.
+message FindRefsByOIDResponse {
+  // refs is the set of fully-qualified references which have been found.
+  repeated string refs = 1;
+}