use anyhow::{Context, Result};
use super::json::{
first_json_item, optional_bool, optional_string, parse_body_field, parse_state, required_string,
};
use super::{ReviewProvider, ReviewRequest, command_output};
pub(super) struct GitLabProvider;
impl ReviewProvider for GitLabProvider {
fn review_for_branch(&self, branch: &str) -> Result<Option<ReviewRequest>> {
if let Some(review) = list_review(branch, None)? {
return Ok(Some(review));
}
list_review(branch, Some("--merged"))
}
fn review_for_branch_including_closed(&self, branch: &str) -> Result<Option<ReviewRequest>> {
if let Some(review) = self.review_for_branch(branch)? {
return Ok(Some(review));
}
list_review(branch, Some("--closed"))
}
fn create_review(&self, branch: &str, base: &str, draft: bool) -> Result<String> {
let mut args = vec![
"mr",
"create",
"--source-branch",
branch,
"--target-branch",
base,
"--fill",
];
if draft {
args.push("--draft");
}
command_output("glab", &args)
}
fn update_review_base(&self, review: &ReviewRequest, base: &str) -> Result<String> {
command_output(
"glab",
&["mr", "update", review.id_value(), "--target-branch", base],
)
}
fn review_body(&self, review: &ReviewRequest) -> Result<String> {
let output = command_output(
"glab",
&["mr", "view", review.id_value(), "--output", "json"],
)?;
parse_body_field(&output, "description")
}
fn update_review_body(&self, review: &ReviewRequest, body: &str) -> Result<String> {
command_output(
"glab",
&["mr", "update", review.id_value(), "--description", body],
)
}
fn merge_review(&self, review: &ReviewRequest, strategy: &str, auto: bool) -> Result<String> {
let mut args = vec!["mr", "merge", review.id_value()];
match strategy {
"rebase" => args.push("--rebase"),
"merge" => {}
_ => args.push("--squash"),
}
if auto {
args.push("--auto-merge");
}
command_output("glab", &args)
}
fn wait_for_checks(&self, review: &ReviewRequest) -> Result<bool> {
loop {
let output = command_output(
"glab",
&["mr", "view", review.id_value(), "--output", "json"],
)?;
let value: serde_json::Value =
serde_json::from_str(&output).context("failed to parse glab MR JSON")?;
let status = value
.get("head_pipeline")
.or_else(|| value.get("pipeline"))
.and_then(|pipeline| pipeline.get("status"))
.and_then(serde_json::Value::as_str);
match status {
None => return Ok(true),
Some("success") | Some("skipped") | Some("manual") => return Ok(true),
Some("failed") | Some("canceled") => return Ok(false),
_ => std::thread::sleep(std::time::Duration::from_secs(10)),
}
}
}
fn mark_ready(&self, review: &ReviewRequest) -> Result<String> {
command_output("glab", &["mr", "update", review.id_value(), "--ready"])
}
}
fn list_review(branch: &str, state_flag: Option<&str>) -> Result<Option<ReviewRequest>> {
let mut args = vec!["mr", "list", "--source-branch", branch];
if let Some(flag) = state_flag {
args.push(flag);
}
args.extend(["--output", "json"]);
let output = command_output("glab", &args)?;
parse_gitlab_review(&output)
}
fn parse_gitlab_review(output: &str) -> Result<Option<ReviewRequest>> {
let Some(review) = first_json_item(output)? else {
return Ok(None);
};
Ok(Some(ReviewRequest {
id: format!("!{}", required_string(&review, &["iid", "id"])?),
branch: required_string(&review, &["source_branch", "sourceBranch"])?,
base: required_string(&review, &["target_branch", "targetBranch"])?,
state: parse_state(&required_string(&review, &["state"])?),
url: required_string(&review, &["web_url", "webUrl", "url"])?,
title: optional_string(&review, "title"),
draft: optional_bool(&review, "draft"),
}))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::providers::{ReviewRequest, ReviewState};
#[test]
fn parse_gitlab_review_reads_snake_case_fields() {
let review = parse_gitlab_review(
r#"[{"iid":34,"state":"merged","target_branch":"feature/a","source_branch":"feature/b","web_url":"https://gitlab.com/owner/repo/-/merge_requests/34"}]"#,
)
.expect("parse review")
.expect("review exists");
assert_eq!(
review,
ReviewRequest {
id: "!34".to_owned(),
branch: "feature/b".to_owned(),
base: "feature/a".to_owned(),
state: ReviewState::Merged,
url: "https://gitlab.com/owner/repo/-/merge_requests/34".to_owned(),
title: String::new(),
draft: false,
}
);
}
#[test]
fn parse_gitlab_review_reads_camel_case_fields() {
let review = parse_gitlab_review(
r#"[{"id":34,"state":"closed","targetBranch":"feature/a","sourceBranch":"feature/b","webUrl":"https://gitlab.com/owner/repo/-/merge_requests/34"}]"#,
)
.expect("parse review")
.expect("review exists");
assert_eq!(review.id, "!34");
assert_eq!(review.branch, "feature/b");
assert_eq!(review.base, "feature/a");
assert_eq!(review.state, ReviewState::Closed);
assert_eq!(
review.url,
"https://gitlab.com/owner/repo/-/merge_requests/34"
);
}
#[test]
fn parse_gitlab_review_empty_array_returns_none() {
assert_eq!(parse_gitlab_review("[]").expect("parse review"), None);
}
}