crates/gunnar-ui-server/proto/gunnar/v1/refs.proto
syntax = "proto3";
package gunnar.v1;
import "gunnar/v1/common.proto";
// Read access to a repository's refs, commit graph and objects.
//
// This service is READ-ONLY by construction. Refs move over the SSH listener
// through receive-pack and its policy gate, never over gRPC. There is no
// `Refs.Set`, and adding one would route a push around every gate.
service Refs {
rpc List(ListRefsRequest) returns (ListRefsResponse);
rpc Get(GetRefRequest) returns (GetRefResponse);
// Commit walk, paged. Served from the Arrow commit table of the `git` znippy
// package once the `git` package's `__gunnar_graph__` lands (wave 9b): a scan, not a walk.
rpc Log(LogRequest) returns (LogResponse);
rpc ReadObject(ReadObjectRequest) returns (ReadObjectResponse);
// One LFS object out of the repository's LFS store, by the sha256 its
// POINTER names. A file kept in LFS is a pointer in the tree, so a caller
// that reads the tree — a console drawing a README's link to a PDF — gets
// 131 bytes of text where the file should be, and no way to ask for the
// file. `git lfs` fetches these over the SSH adapter or the HTTP batch API;
// this is the same bytes for a caller that already speaks gRPC and holds
// Read on the repo. It is a whole small object, not a transfer adapter:
// see ReadLfsObjectRequest for the ceiling, and gunnar-lfs for the wires
// that move a firmware image.
rpc ReadLfsObject(ReadLfsObjectRequest) returns (ReadLfsObjectResponse);
rpc Diff(DiffRequest) returns (DiffResponse);
// Who wrote each line of a file at a revision, and a literal string across
// one revision's files. These run the SAME walks `gunnar blame` / `gunnar
// search` run — but against the RUNNING server's already-open store, so an
// operator can blame/search a SEALED appliance with no shell, where the
// store-local CLI verbs (an exclusive lock the live server holds) cannot run
// at all. Read on the repo, like every verb here.
rpc Blame(BlameRequest) returns (BlameResponse);
rpc Search(SearchRequest) returns (SearchResponse);
}
message ListRefsRequest {
RepoPath repo = 1;
// e.g. "refs/heads/", matched as a literal prefix, not a glob.
string prefix = 2;
uint32 page_size = 3;
string page_token = 4;
// Resolve annotated tags to the commit they name, filling `peeled_oid`.
bool peel = 5;
}
message ListRefsResponse {
repeated RefTarget refs = 1;
string next_page_token = 2;
}
message GetRefRequest {
RepoPath repo = 1;
// Full ref name ("refs/heads/main") or "HEAD".
string ref_name = 2;
bool peel = 3;
}
message GetRefResponse {
RefTarget ref_target = 1;
}
message LogRequest {
RepoPath repo = 1;
// A full ref name or a hex oid. Empty means the repo's default branch.
string start = 2;
uint32 page_size = 3;
string page_token = 4;
// Only commits that CHANGED this path (a file or a directory), judged
// against each commit's FIRST parent — the same commits `git log -- <path>`
// shows without rename-following or history simplification. Empty means no
// filter. The walk behind a filtered page is bounded, so a quiet path deep
// in a long history can come back short of `page_size` without being the
// end of history.
string path = 5;
// Walk from EVERY branch and tag tip (refs/heads/*, refs/tags/*, and HEAD)
// at once instead of from `start`, so commits that live only on a side
// branch show up. Same order (newest committer time first) and the same
// `page_size` cap as a single-start walk. `start` must be empty when this is
// set; a caller continuing a page passes a frontier oid in `start` instead.
bool all_refs = 6;
}
message LogResponse {
repeated Commit commits = 1;
string next_page_token = 2;
}
message ReadObjectRequest {
RepoPath repo = 1;
// Full hex oid. Abbreviated oids are a UI convenience resolved elsewhere;
// this field is exact (abbreviations are never on the git wire).
string oid = 2;
// 0 means "the whole object". A larger object is truncated, not refused, and
// `truncated` is set on the response.
uint64 max_bytes = 3;
}
message ReadObjectResponse {
string oid = 1;
ObjectKind kind = 2;
// The FULL uncompressed size, even when `data` is truncated.
uint64 size = 3;
bytes data = 4;
bool truncated = 5;
}
message ReadLfsObjectRequest {
RepoPath repo = 1;
// The sha256 out of the pointer file, lowercase hex, 64 characters — the
// name LFS stores the object under. Not a git oid: an LFS object is not in
// the object database at all.
string oid = 2;
// The largest response the caller will take. 0 means the server's own
// ceiling, and anything above it is lowered to it.
//
// **An object past the ceiling is REFUSED, never truncated.** A prefix of a
// git blob is a readable prefix of a file; a prefix of an LFS object is a
// broken PDF, and a caller that cannot tell the difference would serve one.
// A client that wants the whole of a large object wants `git lfs`, whose
// adapters stream it — see the `gunnar-lfs` crate — or a RANGED read below.
uint64 max_bytes = 3;
// **A range of the object**, for a caller that streams one larger than the
// ceiling as consecutive ranges (a console handing a browser a 6 MB
// picture), or wants only its head (a picture's size is in its header).
//
// Both 0 — the fields absent, as every client before them sends — is the
// whole-or-refused read above, unchanged. Otherwise this is a ranged read:
// the bytes from `offset`, at most `length` of them (0 = up to the
// ceiling), ending early at the object's end. The ceiling applies to the
// RANGE: a `length` over it is refused, never cut. An `offset` past the end
// of the object is refused (OutOfRange), naming the object's size.
uint64 offset = 4;
uint64 length = 5;
}
message ReadLfsObjectResponse {
string oid = 1;
// The WHOLE object's size. On a whole read it is also `data`'s length; on a
// ranged read `data` is the range, and `offset` + `data`'s length = `size`
// says it was the last one.
uint64 size = 2;
bytes data = 3;
// Where `data` starts in the object — the request's `offset`, echoed. A
// server older than ranged reads leaves it 0 and answers whole-or-refused,
// which is how a client asking for `offset` > 0 detects one.
uint64 offset = 4;
}
enum ChangeKind {
CHANGE_KIND_UNSPECIFIED = 0;
CHANGE_KIND_ADDED = 1;
CHANGE_KIND_MODIFIED = 2;
CHANGE_KIND_DELETED = 3;
CHANGE_KIND_RENAMED = 4;
CHANGE_KIND_COPIED = 5;
CHANGE_KIND_TYPE_CHANGED = 6;
}
message FileChange {
ChangeKind kind = 1;
string path = 2;
// Set for RENAMED and COPIED only.
string old_path = 3;
string old_oid = 4;
string new_oid = 5;
uint32 old_mode = 6;
uint32 new_mode = 7;
uint64 insertions = 8;
uint64 deletions = 9;
bool binary = 10;
}
message DiffRequest {
RepoPath repo = 1;
// Tree-ish oids or ref names.
string from = 2;
string to = 3;
// Only paths under these prefixes. Empty means the whole tree.
repeated string path_prefixes = 4;
}
message DiffResponse {
repeated FileChange changes = 1;
}
message BlameRequest {
RepoPath repo = 1;
// A full ref name, "HEAD", or a hex oid. Empty means the repo's default
// branch — resolved the way Log resolves `start`.
string rev = 2;
// The file to blame, as a path within the revision's tree.
string path = 3;
}
// One line's attribution, in file order.
message BlameLine {
// The full hex oid of the commit that last touched this line.
string commit = 1;
// The commit author's display name, lossily decoded (git does not promise a
// name is UTF-8).
string author = 2;
// The line's bytes at the blamed revision, without its trailing newline.
bytes line = 3;
}
message BlameResponse {
repeated BlameLine lines = 1;
}
message SearchRequest {
RepoPath repo = 1;
// A full ref name, "HEAD", or a hex oid. Empty means the default branch.
string rev = 2;
// A LITERAL string, not a regular expression. An empty pattern is refused.
string pattern = 3;
// Files larger than this are skipped without being read. 0 means the
// server's default (1 MiB).
uint64 max_file_bytes = 4;
// Stop after this many matches. 0 means the server's default (1000).
uint32 max_matches = 5;
}
// One matching line.
message SearchMatch {
// The file's path within the tree, `/`-joined.
string path = 1;
// The one-based line number, the way an editor and `grep` count.
uint32 line_number = 2;
// The line's bytes, without its trailing newline.
bytes line = 3;
}
message SearchResponse {
repeated SearchMatch matches = 1;
}