Skip to content

Commit 70c91dc

Browse files
committed
forge: merge API
Add an API to merge a given review by it’s ID.
1 parent b9a9152 commit 70c91dc

File tree

11 files changed

+199
-4
lines changed

11 files changed

+199
-4
lines changed

crates/but-api/src/legacy/forge.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,31 @@ pub async fn publish_review(
221221
)
222222
.await
223223
}
224+
225+
/// Merge a review on the forge.
226+
#[but_api]
227+
#[instrument(err(Debug))]
228+
pub async fn merge_review(ctx: ThreadSafeContext, review_id: usize) -> Result<()> {
229+
let (storage, base_branch, preferred_forge_user) = {
230+
let ctx = ctx.into_thread_local();
231+
let base_branch = gitbutler_branch_actions::base::get_base_branch_data(&ctx)?;
232+
(
233+
but_forge_storage::Controller::from_path(but_path::app_data_dir()?),
234+
base_branch,
235+
ctx.legacy_project.preferred_forge_user.clone(),
236+
)
237+
};
238+
but_forge::merge_review(
239+
&preferred_forge_user,
240+
&base_branch
241+
.forge_repo_info
242+
.context("No forge could be determined for this repository branch")?,
243+
review_id,
244+
&storage,
245+
)
246+
.await
247+
}
248+
224249
/// Update the stacked review descriptions to have the correct footers.
225250
#[but_api]
226251
#[instrument(err(Debug))]

crates/but-forge/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ pub use review::{
1111
CacheConfig, CreateForgeReviewParams, ForgeAccountValidity, ForgeReview, ForgeReviewDescriptionUpdate,
1212
ForgeReviewFilter, ReviewTemplateFunctions, available_review_templates, check_forge_account_is_valid,
1313
create_forge_review, get_forge_review, get_review_template_functions, list_forge_reviews_for_branch,
14-
list_forge_reviews_with_cache, update_review_description_tables,
14+
list_forge_reviews_with_cache, merge_review, update_review_description_tables,
1515
};
1616

1717
fn determine_forge_from_host(host: &str) -> Option<ForgeName> {

crates/but-forge/src/review.rs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::{
33
path::{self},
44
};
55

6-
use anyhow::{Error, Result};
6+
use anyhow::{Context as _, Error, Result};
77
use but_fs::list_files;
88
use but_github::CredentialCheckResult;
99
use but_gitlab::GitLabProjectId;
@@ -733,6 +733,50 @@ pub async fn get_forge_review(
733733
}
734734
}
735735

736+
/// Merge a review to it's target branch
737+
pub async fn merge_review(
738+
preferred_forge_user: &Option<crate::ForgeUser>,
739+
forge_repo_info: &crate::forge::ForgeRepoInfo,
740+
review_number: usize,
741+
storage: &but_forge_storage::Controller,
742+
) -> Result<()> {
743+
let crate::forge::ForgeRepoInfo { forge, owner, repo, .. } = forge_repo_info;
744+
match forge {
745+
ForgeName::GitHub => {
746+
let preferred_account = preferred_forge_user.as_ref().and_then(|user| user.github());
747+
let pr_number = review_number
748+
.try_into()
749+
.context("PR: Failed to cast usize to i64, somehow")?;
750+
let params = but_github::MergePullRequestParams {
751+
owner,
752+
repo,
753+
pr_number,
754+
commit_message: None,
755+
commit_title: None,
756+
merge_method: None,
757+
};
758+
but_github::pr::merge(preferred_account, params, storage).await
759+
}
760+
ForgeName::GitLab => {
761+
let preferred_account = preferred_forge_user.as_ref().and_then(|user| user.gitlab());
762+
let project_id = GitLabProjectId::new(owner, repo);
763+
let mr_iid = review_number
764+
.try_into()
765+
.context("MR: Failed to cast usize to i64, somehow")?;
766+
let params = but_gitlab::MergeMergeRequestParams {
767+
project_id,
768+
mr_iid,
769+
squash: None,
770+
};
771+
772+
but_gitlab::mr::merge(preferred_account, params, storage).await
773+
}
774+
_ => Err(Error::msg(format!(
775+
"Merging reviews for forge {forge:?} is not implemented yet.",
776+
))),
777+
}
778+
}
779+
736780
#[derive(Debug, Clone, Serialize, Deserialize)]
737781
#[serde(rename_all = "camelCase")]
738782
pub struct CreateForgeReviewParams {

crates/but-github/src/client.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,39 @@ impl GitHubClient {
232232
let pr: GitHubPullRequest = response.json().await?;
233233
Ok(pr.into())
234234
}
235+
236+
pub async fn merge_pull_request(&self, params: &MergePullRequestParams<'_>) -> Result<()> {
237+
#[derive(Serialize)]
238+
struct MergePullRequestBody<'a> {
239+
#[serde(skip_serializing_if = "Option::is_none")]
240+
commit_title: Option<&'a str>,
241+
#[serde(skip_serializing_if = "Option::is_none")]
242+
commit_message: Option<&'a str>,
243+
#[serde(skip_serializing_if = "Option::is_none")]
244+
merge_method: Option<&'a str>,
245+
}
246+
247+
let url = format!(
248+
"{}/repos/{}/{}/pulls/{}/merge",
249+
self.base_url, params.owner, params.repo, params.pr_number
250+
);
251+
252+
let merge_method = params.merge_method.as_ref().map(Into::into);
253+
254+
let body = MergePullRequestBody {
255+
commit_title: params.commit_title,
256+
commit_message: params.commit_message,
257+
merge_method,
258+
};
259+
260+
let response = self.client.put(&url).json(&body).send().await?;
261+
262+
if !response.status().is_success() {
263+
bail!("Failed to merge pull request: {}", response.status());
264+
}
265+
266+
Ok(())
267+
}
235268
}
236269

237270
pub struct CreatePullRequestParams<'a> {
@@ -254,6 +287,35 @@ pub struct UpdatePullRequestParams<'a> {
254287
pub state: Option<&'a str>,
255288
}
256289

290+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
291+
#[serde(rename_all = "lowercase")]
292+
pub enum MergeMethod {
293+
#[default]
294+
Merge,
295+
Squash,
296+
Rebase,
297+
}
298+
299+
impl From<&MergeMethod> for &str {
300+
fn from(val: &MergeMethod) -> Self {
301+
match val {
302+
MergeMethod::Merge => "merge",
303+
MergeMethod::Rebase => "rebase",
304+
MergeMethod::Squash => "squash",
305+
}
306+
}
307+
}
308+
309+
#[derive(Debug, Clone)]
310+
pub struct MergePullRequestParams<'a> {
311+
pub owner: &'a str,
312+
pub repo: &'a str,
313+
pub pr_number: i64,
314+
pub commit_title: Option<&'a str>,
315+
pub commit_message: Option<&'a str>,
316+
pub merge_method: Option<MergeMethod>,
317+
}
318+
257319
#[derive(Debug, Serialize)]
258320
pub struct AuthenticatedUser {
259321
pub login: String,

crates/but-github/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ use serde::{Deserialize, Serialize};
88
mod client;
99
pub mod pr;
1010
pub use client::{
11-
CheckRun, CreatePullRequestParams, GitHubClient, GitHubPrLabel, GitHubUser, PullRequest, UpdatePullRequestParams,
11+
CheckRun, CreatePullRequestParams, GitHubClient, GitHubPrLabel, GitHubUser, MergeMethod, MergePullRequestParams,
12+
PullRequest, UpdatePullRequestParams,
1213
};
1314
mod token;
1415
pub use token::GithubAccountIdentifier;

crates/but-github/src/pr.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,14 @@ pub async fn update(
7070
.context("Failed to update pull request")?;
7171
Ok(pr)
7272
}
73+
74+
pub async fn merge(
75+
preferred_account: Option<&crate::GithubAccountIdentifier>,
76+
params: crate::client::MergePullRequestParams<'_>,
77+
storage: &but_forge_storage::Controller,
78+
) -> Result<()> {
79+
GitHubClient::from_storage(storage, preferred_account)?
80+
.merge_pull_request(&params)
81+
.await
82+
.context("Failed to merge PR")
83+
}

crates/but-gitlab/src/client.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,29 @@ impl GitLabClient {
183183
let mr: GitLabMergeRequest = response.json().await?;
184184
Ok(mr.into())
185185
}
186+
187+
pub async fn merge_merge_request(&self, params: &MergeMergeRequestParams) -> Result<()> {
188+
#[derive(Serialize)]
189+
struct MergeMergeRequestBody {
190+
#[serde(skip_serializing_if = "Option::is_none")]
191+
squash: Option<bool>,
192+
}
193+
194+
let url = format!(
195+
"{}/projects/{}/merge_requests/{}/merge",
196+
self.base_url, params.project_id, params.mr_iid
197+
);
198+
199+
let body = MergeMergeRequestBody { squash: params.squash };
200+
201+
let response = self.client.put(&url).json(&body).send().await?;
202+
203+
if !response.status().is_success() {
204+
bail!("Failed to merge merge request: {}", response.status());
205+
}
206+
207+
Ok(())
208+
}
186209
}
187210

188211
pub struct CreateMergeRequestParams<'a> {
@@ -192,6 +215,11 @@ pub struct CreateMergeRequestParams<'a> {
192215
pub target_branch: &'a str,
193216
pub project_id: GitLabProjectId,
194217
}
218+
pub struct MergeMergeRequestParams {
219+
pub project_id: GitLabProjectId,
220+
pub mr_iid: i64,
221+
pub squash: Option<bool>,
222+
}
195223

196224
#[derive(Debug, Serialize)]
197225
pub struct AuthenticatedUser {

crates/but-gitlab/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ use but_secret::Sensitive;
44
mod client;
55
pub mod mr;
66
mod project;
7-
pub use client::{CreateMergeRequestParams, GitLabClient, GitLabLabel, GitLabUser, MergeRequest};
7+
pub use client::{
8+
CreateMergeRequestParams, GitLabClient, GitLabLabel, GitLabUser, MergeMergeRequestParams, MergeRequest,
9+
};
810
pub use project::GitLabProjectId;
911
mod token;
1012
use serde::Serialize;

crates/but-gitlab/src/mr.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,14 @@ pub async fn get(
5656
.context("Failed to get merge request")?;
5757
Ok(mr)
5858
}
59+
60+
pub async fn merge(
61+
preferred_account: Option<&crate::GitlabAccountIdentifier>,
62+
params: crate::client::MergeMergeRequestParams,
63+
storage: &but_forge_storage::Controller,
64+
) -> Result<()> {
65+
GitLabClient::from_storage(storage, preferred_account)?
66+
.merge_merge_request(&params)
67+
.await
68+
.context("Faile to merge MR")
69+
}

crates/but-server/src/lib.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -927,6 +927,16 @@ async fn handle_command(
927927
Err(e) => Err(e),
928928
}
929929
}
930+
"merge_review" => {
931+
let params = deserialize_json(request.params);
932+
match params {
933+
Ok(params) => {
934+
let result = legacy::forge::merge_review_cmd(params).await;
935+
result.map(|_| json!({"result": "success"}))
936+
}
937+
Err(e) => Err(e),
938+
}
939+
}
930940
"update_review_footers" => {
931941
let params = deserialize_json(request.params);
932942
match params {

0 commit comments

Comments
 (0)