use gunnar_proto::pull_requests_server::PullRequests;
use gunnar_proto::{
event, ClosePullRequestRequest, ClosePullRequestResponse, CreatePullRequestRequest,
CreatePullRequestResponse, GetPullRequestRequest, GetPullRequestResponse,
ListPullRequestsRequest, ListPullRequestsResponse, MergePlan, MergePullRequestRequest,
MergePullRequestResponse, MergeSigningKeyRequest, MergeSigningKeyResponse, PullRequestClosed,
PullRequestMerged, PullRequestOpened, RefUpdated,
};
use tonic::{Request, Response, Status};
use crate::auth::Access;
use crate::seam::{MergeOptions, MergedRef};
use crate::service::PullsService;
use crate::status::repo_path;
use gunnar_proto::GrantLevel;
#[tonic::async_trait]
impl PullRequests for PullsService {
async fn create(
&self,
request: Request<CreatePullRequestRequest>,
) -> Result<Response<CreatePullRequestResponse>, Status> {
let caller = self.ctx.caller(&request)?;
let request = request.into_inner();
let path = repo_path(request.repo, "repo")?;
self.ctx.require(
&caller,
Access::repo(&path),
GrantLevel::Write,
"PullRequests.Create",
)?;
let pull = self.ctx.control.create_pull(
&path,
&caller.principal_id,
&request.title,
&request.description,
&request.source_ref,
&request.target_ref,
)?;
self.ctx
.bus
.publish(event::Payload::PullRequestOpened(PullRequestOpened {
repo: Some(path),
number: pull.number,
title: pull.title.clone(),
source_ref: pull.source_ref.clone(),
target_ref: pull.target_ref.clone(),
principal_id: caller.principal_id.clone(),
}));
Ok(Response::new(CreatePullRequestResponse {
pull: Some(pull),
}))
}
async fn list(
&self,
request: Request<ListPullRequestsRequest>,
) -> Result<Response<ListPullRequestsResponse>, Status> {
let caller = self.ctx.caller(&request)?;
let request = request.into_inner();
let path = repo_path(request.repo, "repo")?;
self.ctx.require(
&caller,
Access::repo(&path),
GrantLevel::Read,
"PullRequests.List",
)?;
let pulls = self.ctx.control.list_pulls(&path, request.state)?;
Ok(Response::new(ListPullRequestsResponse { pulls }))
}
async fn get(
&self,
request: Request<GetPullRequestRequest>,
) -> Result<Response<GetPullRequestResponse>, Status> {
let caller = self.ctx.caller(&request)?;
let request = request.into_inner();
let path = repo_path(request.repo, "repo")?;
self.ctx.require(
&caller,
Access::repo(&path),
GrantLevel::Read,
"PullRequests.Get",
)?;
let pull = self.ctx.control.get_pull(&path, request.number)?;
Ok(Response::new(GetPullRequestResponse { pull: Some(pull) }))
}
async fn close(
&self,
request: Request<ClosePullRequestRequest>,
) -> Result<Response<ClosePullRequestResponse>, Status> {
let caller = self.ctx.caller(&request)?;
let request = request.into_inner();
let path = repo_path(request.repo, "repo")?;
self.ctx.require(
&caller,
Access::repo(&path),
GrantLevel::Write,
"PullRequests.Close",
)?;
let pull = self.ctx.control.close_pull(&path, request.number)?;
self.ctx
.bus
.publish(event::Payload::PullRequestClosed(PullRequestClosed {
repo: Some(path),
number: pull.number,
title: pull.title.clone(),
principal_id: caller.principal_id.clone(),
}));
Ok(Response::new(ClosePullRequestResponse { pull: Some(pull) }))
}
async fn check_merge(
&self,
request: Request<MergePullRequestRequest>,
) -> Result<Response<MergePullRequestResponse>, Status> {
let caller = self.ctx.caller(&request)?;
let request = request.into_inner();
let path = repo_path(request.repo, "repo")?;
self.ctx.require(
&caller,
Access::repo(&path),
GrantLevel::Write,
"PullRequests.CheckMerge",
)?;
let options = MergeOptions::from_request(request.strategy, &request.expected_target_oid)?;
let (pull, would) =
self.ctx
.control
.check_merge(&path, request.number, &caller.principal_id, &options)?;
Ok(Response::new(MergePullRequestResponse {
pull: Some(pull),
plan: Some(plan_of(&would)),
}))
}
async fn merge_signing_key(
&self,
request: Request<MergeSigningKeyRequest>,
) -> Result<Response<MergeSigningKeyResponse>, Status> {
let _caller = self.ctx.caller(&request)?;
let (openssh, fingerprint_sha256) = self.ctx.control.merge_signing_key()?;
let allowed_signers = allowed_signers_line(&openssh);
Ok(Response::new(MergeSigningKeyResponse {
openssh,
fingerprint_sha256,
allowed_signers,
}))
}
async fn merge(
&self,
request: Request<MergePullRequestRequest>,
) -> Result<Response<MergePullRequestResponse>, Status> {
let caller = self.ctx.caller(&request)?;
let request = request.into_inner();
let path = repo_path(request.repo, "repo")?;
self.ctx.require(
&caller,
Access::repo(&path),
GrantLevel::Write,
"PullRequests.Merge",
)?;
let options = MergeOptions::from_request(request.strategy, &request.expected_target_oid)?;
let (pull, moved) =
self.ctx
.control
.merge_pull(&path, request.number, &caller.principal_id, &options)?;
let plan = plan_of(&moved);
if !moved.closed.is_empty() {
self.ctx
.bus
.publish(event::Payload::PullRequestClosed(PullRequestClosed {
repo: Some(path),
number: pull.number,
title: pull.title.clone(),
principal_id: caller.principal_id.clone(),
}));
return Ok(Response::new(MergePullRequestResponse {
pull: Some(pull),
plan: Some(plan),
}));
}
if moved.old_oid != moved.new_oid {
self.ctx.bus.publish(event::Payload::RefUpdated(RefUpdated {
repo: Some(path.clone()),
ref_name: moved.ref_name,
old_oid: moved.old_oid,
new_oid: moved.new_oid.clone(),
principal_id: caller.principal_id.clone(),
key_fingerprint: caller.fingerprint.clone(),
credential: format!("key:{}", caller.fingerprint),
forced: false,
}));
}
self.ctx
.bus
.publish(event::Payload::PullRequestMerged(PullRequestMerged {
repo: Some(path),
number: pull.number,
title: pull.title.clone(),
target_ref: pull.target_ref.clone(),
merge_oid: moved.new_oid,
principal_id: caller.principal_id.clone(),
}));
Ok(Response::new(MergePullRequestResponse {
pull: Some(pull),
plan: Some(plan),
}))
}
}
fn plan_of(merged: &MergedRef) -> MergePlan {
MergePlan {
strategy: merged.strategy as i32,
head_oid: merged.head_oid.clone(),
target_oid: merged.old_oid.clone(),
result_oid: merged.new_oid.clone(),
conflicts: merged.conflicts.clone(),
approvers: merged.approvers.clone(),
rechecked: merged.rechecked,
contained_in: merged.contained_in.clone(),
}
}
pub const MERGE_COMMITTER_EMAIL: &str = "noreply@gunnar.invalid";
pub fn allowed_signers_line(openssh: &str) -> String {
let mut fields = openssh.split_whitespace();
let (kind, blob) = (
fields.next().unwrap_or_default(),
fields.next().unwrap_or_default(),
);
format!("{MERGE_COMMITTER_EMAIL} namespaces=\"git\" {kind} {blob}")
}