knope 0.23.0

A command line tool for automating common development tasks
use miette::Diagnostic;
use serde_json::json;
use tracing::{debug, info};

use super::initialize_state;
use crate::{
    app_config, config,
    integrations::{
        ApiRequestError, PullRequest, git,
        http::{Client, handle_response},
    },
    state,
    state::RunType,
};

pub(crate) async fn create_or_update_pull_request(
    title: &str,
    body: &str,
    base: &str,
    state: RunType<state::Gitea>,
    config: &config::Gitea,
) -> Result<state::Gitea, Error> {
    let branch_ref = git::current_branch()?;
    let current_branch = branch_ref.split('/').next_back().ok_or(Error::GitRef)?;
    let state = match state {
        RunType::DryRun(state) => {
            info!("Would create or update a pull request from {current_branch} to {base}:");
            info!("\tTitle: {title}");
            info!("\tBody: {body}");
            return Ok(state);
        }
        RunType::Real(state) => state,
    };
    let (token, client) = initialize_state(&config.host, state)?;

    let resp = client
        .get(config.get_pulls_url())
        .header("Accept", "application/json")
        .query(&[
            ("state", "open"),
            (
                "head",
                &format!("{owner}:{current_branch}", owner = config.owner),
            ),
            ("base", base),
            ("access_token", &token),
        ])
        .send()
        .await;
    let resp = handle_response(
        resp,
        config.host.clone(),
        "fetching existing pull requests".to_string(),
    )
    .await?;
    let existing_pulls: Vec<PullRequest> =
        resp.json().await.map_err(|source| Error::ApiResponse {
            source,
            activity: "fetching existing pull requests",
            host: config.host.clone(),
        })?;

    // Update the existing PR
    if let Some(pr) = existing_pulls.first() {
        debug!("Updating existing pull request: {}", pr.url);
        update_pull_request(&client, config, &token, pr.number, title, body).await?;
    // Create a new PR
    } else {
        debug!("No matching existing pull request found, creating a new one.");
        create_pull_request(&client, config, &token, base, current_branch, title, body).await?;
    }

    Ok(state::Gitea::Initialized { token, client })
}

async fn update_pull_request(
    client: &Client,
    config: &config::Gitea,
    token: &str,
    number: u32,
    title: &str,
    body: &str,
) -> Result<(), Error> {
    let resp = client
        .patch(config.get_pull_url(number))
        .header("Accept", "application/json")
        .query(&[("access_token", token)])
        .json(&json!({
            "body": body,
            "title": title
        }))
        .send()
        .await;
    handle_response(
        resp,
        config.host.clone(),
        "updating pull request".to_string(),
    )
    .await?;
    Ok(())
}

async fn create_pull_request(
    client: &Client,
    config: &config::Gitea,
    token: &str,
    base: &str,
    head: &str,
    title: &str,
    body: &str,
) -> Result<(), Error> {
    let resp = client
        .post(config.get_pulls_url())
        .header("Accept", "application/json")
        .query(&[("access_token", token)])
        .json(&json!({
            "title": title,
            "body": body,
            "head": head,
            "base": base,
        }))
        .send()
        .await;
    let resp = handle_response(
        resp,
        config.host.clone(),
        "creating pull request".to_string(),
    )
    .await?;
    let new_pr = resp
        .json::<PullRequest>()
        .await
        .map_err(|source| Error::ApiResponse {
            source,
            activity: "creating pull request",
            host: config.host.clone(),
        })?;

    debug!("Created new pull request: {pr_url}", pr_url = new_pr.url);
    Ok(())
}

#[derive(Debug, Diagnostic, thiserror::Error)]
pub(crate) enum Error {
    #[error(transparent)]
    #[diagnostic(transparent)]
    ApiRequest(#[from] ApiRequestError),
    #[error("Trouble decoding the response from Gitea while {activity}: {source}")]
    #[diagnostic(
        code(gitea::api_response_error),
        help(
            "Failure to decode a response from the Gitea instance at {host} is probably a bug. Please report it at https://github.com/knope-dev/knope"
        )
    )]
    ApiResponse {
        source: reqwest::Error,
        activity: &'static str,
        host: String,
    },
    #[error(transparent)]
    #[diagnostic(transparent)]
    Git(#[from] git::Error),
    #[error("Trouble getting the head branch")]
    #[diagnostic(
        code(gitea::failed_getting_current_branch),
        help(
            "The current branch could not be parsed from the git ref path. This is a bug, please report it at https://github.com/knope-dev/knope "
        )
    )]
    GitRef,
    #[error(transparent)]
    #[diagnostic(transparent)]
    AppConfig(#[from] app_config::Error),
}