Skip to content

Commit dcfb315

Browse files
authored
Merge pull request #96 from jonasehrlich/je/include-references
Include references in API and show in Git Dialog
2 parents 7e0ff90 + 8b65887 commit dcfb315

File tree

22 files changed

+952
-137
lines changed

22 files changed

+952
-137
lines changed

Cargo.lock

Lines changed: 39 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/app/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ utoipa-axum = { version = "^0.2.0" }
2727
utoipa-rapidoc = { version = "^6.0.0", features = ["axum"] }
2828
chrono = { version = "^0.4.41", features = ["serde"] }
2929
hannibal = "^0.12"
30+
axum-extra = { version = "0.10.1", features = ["query"] }

backend/app/src/actors/git.rs

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use git2_ox::ReferenceKindFilter;
12
use hannibal::prelude::*;
23
use std::path::Path;
34

@@ -17,7 +18,10 @@ impl GitActor {
1718
Ok(Self { repository })
1819
}
1920

20-
fn filter_commit(filter: &str, commit: &git2_ox::Commit) -> bool {
21+
fn filter_commit<CommitLikeT>(filter: &str, commit: &CommitLikeT) -> bool
22+
where
23+
CommitLikeT: git2_ox::CommitProperties,
24+
{
2125
let id_matches = commit
2226
.id()
2327
.to_string()
@@ -31,7 +35,7 @@ impl GitActor {
3135
}
3236
}
3337

34-
#[message(response = Result<git2_ox::Commit, git2_ox::error::Error>)]
38+
#[message(response = Result<git2_ox::CommitWithReferences, git2_ox::error::Error>)]
3539
pub struct GetRevision {
3640
pub revision: String,
3741
}
@@ -41,12 +45,12 @@ impl Handler<GetRevision> for GitActor {
4145
&mut self,
4246
_ctx: &mut Context<Self>,
4347
msg: GetRevision,
44-
) -> Result<git2_ox::Commit, git2_ox::error::Error> {
48+
) -> Result<git2_ox::CommitWithReferences, git2_ox::error::Error> {
4549
self.repository.get_commit_for_revision(&msg.revision)
4650
}
4751
}
4852

49-
#[message(response = Result<git2_ox::Commit, git2_ox::error::Error>)]
53+
#[message(response = Result<git2_ox::CommitWithReferences, git2_ox::error::Error>)]
5054
pub struct CheckoutRevision {
5155
pub revision: String,
5256
}
@@ -56,12 +60,12 @@ impl Handler<CheckoutRevision> for GitActor {
5660
&mut self,
5761
_ctx: &mut Context<Self>,
5862
msg: CheckoutRevision,
59-
) -> Result<git2_ox::Commit, git2_ox::error::Error> {
63+
) -> Result<git2_ox::CommitWithReferences, git2_ox::error::Error> {
6064
self.repository.checkout_revision(&msg.revision)
6165
}
6266
}
6367

64-
#[message(response = Result<Vec<git2_ox::Commit>, git2_ox::error::Error>)]
68+
#[message(response = Result<Vec<git2_ox::CommitWithReferences>, git2_ox::error::Error>)]
6569
pub struct ListCommits {
6670
pub base_rev: Option<String>,
6771
pub head_rev: Option<String>,
@@ -73,7 +77,7 @@ impl Handler<ListCommits> for GitActor {
7377
&mut self,
7478
_ctx: &mut Context<Self>,
7579
msg: ListCommits,
76-
) -> Result<Vec<git2_ox::Commit>, git2_ox::error::Error> {
80+
) -> Result<Vec<git2_ox::CommitWithReferences>, git2_ox::error::Error> {
7781
let commits_iter = self
7882
.repository
7983
.iter_commits(msg.base_rev.as_deref(), msg.head_rev.as_deref())?;
@@ -118,11 +122,12 @@ impl Handler<ListTags> for GitActor {
118122
_ctx: &mut Context<Self>,
119123
msg: ListTags,
120124
) -> Result<Vec<git2_ox::TaggedCommit>, git2_ox::error::Error> {
121-
let tags_iter = self.repository.iter_tags(msg.filter.as_deref())?;
122-
let mut tags = Vec::new();
123-
for tag_result in tags_iter {
124-
tags.push(tag_result?);
125-
}
125+
let filter = msg.filter.unwrap_or("".to_string());
126+
let tags = self
127+
.repository
128+
.iter_tags()?
129+
.filter(|tag| tag.name().to_lowercase().contains(&filter.to_lowercase()))
130+
.collect();
126131
Ok(tags)
127132
}
128133
}
@@ -156,9 +161,16 @@ impl Handler<ListBranches> for GitActor {
156161
_ctx: &mut Context<Self>,
157162
msg: ListBranches,
158163
) -> Result<Vec<git2_ox::Branch>, git2_ox::error::Error> {
164+
let filter = msg.filter.unwrap_or("".to_string());
159165
let branches = self
160166
.repository
161-
.iter_branches(msg.filter.as_deref())?
167+
.iter_branches()?
168+
.filter(|branch| {
169+
branch
170+
.name()
171+
.to_lowercase()
172+
.contains(&filter.to_lowercase())
173+
})
162174
.collect();
163175
Ok(branches)
164176
}
@@ -184,7 +196,7 @@ impl Handler<CreateBranch> for GitActor {
184196

185197
#[derive(Debug, Clone)]
186198
pub struct RepositoryStatus {
187-
pub head: git2_ox::Commit,
199+
pub head: git2_ox::CommitWithReferences,
188200
pub current_branch: Option<String>,
189201
}
190202

@@ -205,3 +217,45 @@ impl Handler<GetRepositoryStatus> for GitActor {
205217
})
206218
}
207219
}
220+
221+
#[message(response = Result<Vec<git2_ox::ResolvedReference>, git2_ox::error::Error>)]
222+
pub struct ListReferences {
223+
pub filter: Option<String>,
224+
pub filter_kinds: Option<ReferenceKindFilter>,
225+
}
226+
227+
impl Handler<ListReferences> for GitActor {
228+
async fn handle(
229+
&mut self,
230+
_ctx: &mut Context<Self>,
231+
msg: ListReferences,
232+
) -> Result<Vec<git2_ox::ResolvedReference>, git2_ox::error::Error> {
233+
let filter = msg.filter.as_deref().unwrap_or("");
234+
235+
let references = self
236+
.repository
237+
.iter_references()?
238+
.filter_map(|r| {
239+
let ref_kind = r.kind();
240+
241+
let ref_kind_ok = match &msg.filter_kinds {
242+
None => true,
243+
Some(ReferenceKindFilter::Include {
244+
include: include_kinds,
245+
}) => include_kinds.contains(&ref_kind),
246+
Some(ReferenceKindFilter::Exclude {
247+
exclude: exclude_kinds,
248+
}) => !exclude_kinds.contains(&ref_kind),
249+
};
250+
if !ref_kind_ok {
251+
return None;
252+
}
253+
if !r.name().contains(filter) {
254+
return None;
255+
}
256+
Some(r)
257+
})
258+
.collect();
259+
Ok(references)
260+
}
261+
}

backend/app/src/web/api/v1/git.rs

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::{actors, web, web::api};
22

33
use axum::extract::{Path, Query, State};
44
use axum::{Json, routing};
5-
use git2_ox::commit;
5+
use git2_ox::{ReferenceKind, ReferenceKindFilter, ResolvedReference, commit};
66
use serde::{Deserialize, Serialize};
77
use utoipa::{IntoParams, ToSchema};
88

@@ -17,10 +17,19 @@ pub fn router() -> routing::Router<web::AppState> {
1717
.route("/tags", routing::get(list_tags).post(create_tag))
1818
.route("/branches", routing::get(list_branches).post(create_branch))
1919
.route("/repository/status", routing::get(get_repository_status))
20+
.route("/references", routing::get(list_references))
2021
}
2122

2223
#[derive(utoipa::OpenApi)]
23-
#[openapi(paths(get_revision, checkout_revision, list_commits, list_tags, create_tag, list_branches, create_branch, get_repository_status, get_diff), tags((name = "Git Repository", description="Git Repository related endpoints")) )]
24+
#[openapi(
25+
paths(
26+
get_revision, checkout_revision, list_commits, list_tags, create_tag, list_branches, create_branch,
27+
get_repository_status, get_diff, list_references
28+
),
29+
tags(
30+
(name = "Git Repository", description="Git Repository related endpoints")
31+
)
32+
)]
2433
pub(super) struct ApiDoc;
2534

2635
#[utoipa::path(
@@ -44,7 +53,7 @@ pub(super) struct ApiDoc;
4453
async fn get_revision(
4554
State(state): State<web::AppState>,
4655
Path(commit_id): Path<String>,
47-
) -> Result<Json<commit::Commit>, api::AppError> {
56+
) -> Result<Json<commit::CommitWithReferences>, api::AppError> {
4857
let actor = state.git_actor();
4958
let msg = actors::git::GetRevision {
5059
revision: commit_id,
@@ -76,7 +85,7 @@ async fn get_revision(
7685
async fn checkout_revision(
7786
State(state): State<web::AppState>,
7887
Path(commit_id): Path<String>,
79-
) -> Result<Json<commit::Commit>, api::AppError> {
88+
) -> Result<Json<commit::CommitWithReferences>, api::AppError> {
8089
let actor = state.git_actor();
8190
let msg = actors::git::CheckoutRevision {
8291
revision: commit_id,
@@ -89,14 +98,17 @@ async fn checkout_revision(
8998
#[serde(rename_all = "camelCase")]
9099
struct ListCommitsQuery {
91100
/// string filter for the commits. Filters commits by their ID or summary.
101+
#[param(nullable = false)]
92102
filter: Option<String>,
93103

94104
// serde(flatten) does not work here, see https://github.com/juhaku/utoipa/issues/841
95105
/// The base revision of the range, this can be short hash, full hash, a tag,
96106
/// or any other reference such a branch name. If empty, the first commit is used.
107+
#[param(nullable = false)]
97108
base_rev: Option<String>,
98109
/// The head revision of the range, this can be short hash, full hash, a tag,
99110
/// or any other reference such a branch name. If empty, the current HEAD is used.
111+
#[param(nullable = false)]
100112
head_rev: Option<String>,
101113
}
102114

@@ -105,7 +117,7 @@ struct ListCommitsQuery {
105117
struct ListCommitsResponse {
106118
/// Array of commits between the base and head commit IDs
107119
/// in reverse chronological order.
108-
commits: Vec<git2_ox::Commit>,
120+
commits: Vec<git2_ox::CommitWithReferences>,
109121
}
110122

111123
#[utoipa::path(
@@ -139,9 +151,11 @@ async fn list_commits(
139151
struct CommitRangeQuery {
140152
/// The base revision of the range, this can be short hash, full hash, a tag,
141153
/// or any other reference such a branch name. If empty, the first commit is used.
154+
#[param(nullable = false)]
142155
base_rev: Option<String>,
143156
/// The head revision of the range, this can be short hash, full hash, a tag,
144157
/// or any other reference such a branch name. If empty, the current HEAD is used.
158+
#[param(nullable = false)]
145159
head_rev: Option<String>,
146160
}
147161

@@ -183,6 +197,7 @@ async fn get_diff(
183197
#[serde(rename_all = "camelCase")]
184198
struct ListTagsQuery {
185199
/// String filter against which the tag name is matched.
200+
#[param(nullable = false)]
186201
filter: Option<String>,
187202
}
188203

@@ -260,6 +275,7 @@ struct ListBranchesResponse {
260275
#[serde(rename_all = "camelCase")]
261276
struct ListBranchesQuery {
262277
/// string filter against with the branch name is matched
278+
#[param(nullable = false)]
263279
filter: Option<String>,
264280
}
265281

@@ -326,7 +342,7 @@ async fn create_branch(
326342
#[serde(rename_all = "camelCase")]
327343
struct RepositoryStatusResponse {
328344
/// The current HEAD commit
329-
head: git2_ox::Commit,
345+
head: git2_ox::CommitWithReferences,
330346
/// The current branch name, not set if in a detached HEAD state
331347
current_branch: Option<String>,
332348
}
@@ -352,3 +368,61 @@ async fn get_repository_status(
352368
current_branch: status.current_branch,
353369
}))
354370
}
371+
372+
#[derive(Deserialize, IntoParams)]
373+
#[serde(rename_all = "camelCase")]
374+
struct ListReferencesQuery {
375+
/// String filter against with the reference name
376+
#[param(nullable = false)]
377+
filter: Option<String>,
378+
// Ideally, we would use ReferenceKindFilter with serde(flatten) here,
379+
// but IntoParams does not does not respect it,
380+
// see https://github.com/juhaku/utoipa/issues/841
381+
/// Reference kinds to include, mutually exclusive with `exclude`
382+
#[param(min_items = 1, nullable = false)]
383+
include: Option<Vec<ReferenceKind>>,
384+
/// Reference kinds to exclude, mutually exclusive with `include`
385+
#[param(min_items = 1, nullable = false)]
386+
exclude: Option<Vec<ReferenceKind>>,
387+
}
388+
389+
#[derive(ToSchema, Serialize, IntoParams)]
390+
#[serde(rename_all = "camelCase")]
391+
struct ListReferencesResponse {
392+
/// Array of references
393+
references: Vec<ResolvedReference>,
394+
}
395+
396+
/// List references in the repository
397+
#[utoipa::path(
398+
get,
399+
path = "/references",
400+
summary = "List references",
401+
description = "List all references in the repository, optionally filtered by a glob pattern and type.",
402+
params(ListReferencesQuery),
403+
responses(
404+
(status = http::StatusCode::OK, description = "List of references", body = ListReferencesResponse),
405+
(status = http::StatusCode::INTERNAL_SERVER_ERROR, description = "Internal server error", body = api::ApiStatusDetailResponse),
406+
)
407+
)]
408+
async fn list_references(
409+
State(state): State<web::AppState>,
410+
axum_extra::extract::Query(query): axum_extra::extract::Query<ListReferencesQuery>,
411+
) -> Result<Json<ListReferencesResponse>, api::AppError> {
412+
let filter_kinds = match (query.include, query.exclude) {
413+
(Some(_), Some(_)) => Err(api::AppError::BadRequest(
414+
"Include and exclude filters are mutually exclusive".to_string(),
415+
)),
416+
(Some(include), None) => Ok(Some(ReferenceKindFilter::include(include))),
417+
(None, Some(exclude)) => Ok(Some(ReferenceKindFilter::exclude(exclude))),
418+
(None, None) => Ok(None),
419+
}?;
420+
421+
let actor = state.git_actor();
422+
let msg = actors::git::ListReferences {
423+
filter: query.filter,
424+
filter_kinds,
425+
};
426+
let references = actor.call(msg).await??;
427+
Ok(Json(ListReferencesResponse { references }))
428+
}

0 commit comments

Comments
 (0)