Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions docs/components/backend/git-cli-proxy/openapi.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,57 @@
{
"components": {
"schemas": {
"AuthorRow": {
"description": "One distinct commit author, with a commit of theirs to look them up by.\n\nGit records an author as a name and an e-mail and knows nothing of vendor\naccounts, so a consumer that needs the account resolves it against the\nvendor — one lookup per author rather than one per commit, which is the\nwhole reason this endpoint exists.",
"properties": {
"author_email": {
"type": "string"
},
"author_name": {
"type": "string"
},
"commit_count": {
"format": "int64",
"minimum": 0,
"type": "integer"
},
"last_committed_date": {
"type": "string"
},
"sample_sha": {
"description": "A commit this author wrote, for the account lookup. The author's most\nrecent one: a vendor matches an e-mail against the accounts that carry\nit today, so the freshest commit is the likeliest to resolve.",
"type": "string"
}
},
"required": [
"author_email",
"author_name",
"sample_sha",
"last_committed_date",
"commit_count"
],
"type": "object"
},
"AuthorsPage": {
"properties": {
"items": {
"items": {
"$ref": "#/components/schemas/AuthorRow"
},
"type": "array"
},
"next_page_token": {
"type": [
"string",
"null"
]
}
},
"required": [
"items"
],
"type": "object"
},
"BranchRow": {
"properties": {
"head_committed_date": {
Expand Down Expand Up @@ -282,6 +333,137 @@
},
"openapi": "3.1.0",
"paths": {
"/v1/authors": {
"get": {
"operationId": "git_cli_proxy.authors.list",
"parameters": [
{
"description": "Clone URL of the repository (http/https)",
"in": "query",
"name": "repo",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "Lower bound on committed_date; an author whose commits all predate it is omitted",
"in": "query",
"name": "since",
"required": false,
"schema": {
"type": "string"
}
},
{
"description": "1..=1000, default 1000; larger values are clamped",
"in": "query",
"name": "page_size",
"required": false,
"schema": {
"type": "integer"
}
},
{
"description": "Cursor from a previous page; pins the snapshot and never fetches",
"in": "query",
"name": "page_token",
"required": false,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AuthorsPage"
}
}
},
"description": "One page of authors"
},
"400": {
"content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/Problem"
}
}
},
"description": "Bad Request"
},
"401": {
"content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/Problem"
}
}
},
"description": "Unauthorized"
},
"404": {
"content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/Problem"
}
}
},
"description": "Not Found"
},
"409": {
"content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/Problem"
}
}
},
"description": "Conflict"
},
"413": {
"content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/Problem"
}
}
},
"description": "Repository exceeds the configured per-repository size cap"
},
"429": {
"content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/Problem"
}
}
},
"description": "Too Many Requests"
},
"500": {
"content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/Problem"
}
}
},
"description": "Internal Server Error"
}
},
"security": [
{
"bearerAuth": []
}
],
"summary": "Distinct commit authors, ascending by e-mail"
}
},
"/v1/branches": {
"get": {
"operationId": "git_cli_proxy.branches.list",
Expand Down
65 changes: 64 additions & 1 deletion src/backend/services/git-cli-proxy/src/api/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use utoipa::ToSchema;

use crate::engine::key::CacheKey;
use crate::engine::page::PageToken;
use crate::engine::read::{self, Page, branches, commits, numstat, patches};
use crate::engine::read::{self, Page, authors, branches, commits, numstat, patches};
use crate::engine::runner::GitError;
use crate::engine::store::{Freshness, RepoGuard, StoreError};

Expand Down Expand Up @@ -52,6 +52,14 @@ pub struct BranchesQuery {
page_token: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct AuthorsQuery {
repo: Option<String>,
since: Option<String>,
page_size: Option<u32>,
page_token: Option<String>,
}

/// Concrete page wrappers, one per endpoint: `Page<T>` cannot be a schema
/// because the registry keys components on the type's own name, so all three
/// instantiations would collide on one component.
Expand All @@ -76,6 +84,13 @@ pub struct BranchesPage {
}
impl toolkit::api::api_dto::ResponseApiDto for BranchesPage {}

#[derive(Debug, Serialize, ToSchema)]
pub struct AuthorsPage {
pub items: Vec<authors::AuthorRow>,
pub next_page_token: Option<String>,
}
impl toolkit::api::api_dto::ResponseApiDto for AuthorsPage {}

impl From<Page<commits::CommitRow>> for CommitsPage {
fn from(page: Page<commits::CommitRow>) -> Self {
Self {
Expand Down Expand Up @@ -103,6 +118,15 @@ impl From<Page<branches::BranchRow>> for BranchesPage {
}
}

impl From<Page<authors::AuthorRow>> for AuthorsPage {
fn from(page: Page<authors::AuthorRow>) -> Self {
Self {
items: page.items,
next_page_token: page.next_page_token,
}
}
}

#[derive(Debug, Serialize, ToSchema)]
pub struct FileChangeRow {
pub sha: String,
Expand Down Expand Up @@ -406,6 +430,45 @@ pub async fn list_branches(
json_page(BranchesPage::from(page)).await
}

/// `GET /v1/authors` — one row per distinct commit author.
///
/// # Errors
///
/// [`ApiError`] on malformed input or an origin failure.
pub async fn list_authors(
Extension(state): Extension<Arc<AppState>>,
headers: HeaderMap,
ValidatedQuery(query): ValidatedQuery<AuthorsQuery>,
) -> Result<Response, ApiError> {
let repo = required_param(query.repo.as_deref(), "repo")?;
let context = RequestContext::from_parts(&headers, repo, state.clone_url_policy())?;
let paging = Paging::parse(query.page_token.as_deref(), query.page_size)?;

let page = read_snapshot(&state, &context, &paging, |guard: RepoGuard| {
let (state, context, paging) = (&state, &context, &paging);
let since = query.since.as_deref();
Box::pin(async move {
// Already sorted by e-mail, which is the ascending key the cursor
// pages on — an author has no date to order by, since the walk
// collapses every commit they wrote into one row.
let all =
authors::read(state.store.runner(), guard.git_dir(), &context.creds, since).await?;
let (items, cursor) =
read::slice_page(all, paging.token.as_ref(), paging.page_size, |row| {
(row.author_email.clone(), String::new())
});

Ok(Page {
items,
next_page_token: encode_cursor(cursor, &context.key, &guard),
})
})
})
.await?;

json_page(AuthorsPage::from(page)).await
}

/// One page of commit keys, its cursor, and — when the index answered — the
/// page's default-branch membership.
///
Expand Down
48 changes: 48 additions & 0 deletions src/backend/services/git-cli-proxy/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,54 @@ fn build_operations(router: Router, openapi: &dyn OpenApiRegistry) -> Router {
.handler(data::list_file_changes)
.register(router, openapi);

let router = OperationBuilder::get("/v1/authors")
.operation_id("git_cli_proxy.authors.list")
.summary("Distinct commit authors, ascending by e-mail")
.authenticated()
.no_license_required()
.query_param_typed(
"repo",
true,
"Clone URL of the repository (http/https)",
"string",
)
.query_param_typed(
"since",
false,
"Lower bound on committed_date; an author whose commits all predate it is omitted",
"string",
)
.query_param_typed(
"page_size",
false,
"1..=1000, default 1000; larger values are clamped",
"integer",
)
.query_param_typed(
"page_token",
false,
"Cursor from a previous page; pins the snapshot and never fetches",
"string",
)
.json_response_with_schema::<data::AuthorsPage>(
openapi,
StatusCode::OK,
"One page of authors",
)
.error_400(openapi)
.error_401(openapi)
.error_404(openapi)
.error_409(openapi)
.error_429(openapi)
.error_500(openapi)
.problem_response(
openapi,
StatusCode::PAYLOAD_TOO_LARGE,
"Repository exceeds the configured per-repository size cap",
)
.handler(data::list_authors)
.register(router, openapi);

OperationBuilder::get("/v1/branches")
.operation_id("git_cli_proxy.branches.list")
.summary("Branch heads, ascending by name")
Expand Down
Loading
Loading