Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions crates/mergify-core/src/command_context.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//! Per-command "resolved context" + Mergify HTTP client builder.
//!
//! Every Mergify-API command starts the same way: resolve the
//! repository slug, the bearer token, and the API URL via the
//! standard fallback chain (flag → env → `gh auth token` / git
//! remote / default), then build a typed [`HttpClient`] from them.
//! [`CommandContext`] bundles those three pieces with a
//! `mergify_client()` builder so the prelude shrinks from a four-line
//! ritual to two:
//!
//! ```ignore
//! let ctx = CommandContext::resolve(opts.repository, opts.token, opts.api_url)?;
//! let client = ctx.mergify_client()?;
//! ```
//!
//! Specialized commands that don't fit the shape (`config validate`
//! needs no repository; `ci scopes-send` resolves the repo from CI
//! env; `config simulate` derives it from a PR URL) keep wiring up
//! the lower-level [`auth::resolve_*`] / [`HttpClient::new`] calls
//! by hand.
//!
//! [`auth::resolve_*`]: crate::auth

use url::Url;

use crate::auth;
use crate::error::CliError;
use crate::http::ApiFlavor;
use crate::http::Client as HttpClient;

/// Resolved repository / token / API URL for a Mergify-API command.
pub struct CommandContext {
pub repository: String,
pub token: String,
pub api_url: Url,
}

impl CommandContext {
/// Resolve all three pieces of context using the standard
/// fallback chain. Used by the `queue` and `freeze` command
/// families — every member of those groups needs the same
/// shape.
///
/// # Errors
///
/// Surfaces the first resolution failure as
/// [`CliError::Configuration`].
pub fn resolve(
repository: Option<&str>,
token: Option<&str>,
api_url: Option<&str>,
) -> Result<Self, CliError> {
Ok(Self {
repository: auth::resolve_repository(repository)?,
token: auth::resolve_token(token)?,
api_url: auth::resolve_api_url(api_url)?,
})
}

/// Build a Mergify-flavored [`HttpClient`] from this context.
/// Clones the API URL so the [`CommandContext`] stays usable
/// afterwards — callers typically still need `self.repository`
/// to format URL paths.
pub fn mergify_client(&self) -> Result<HttpClient, CliError> {
HttpClient::new(self.api_url.clone(), &self.token, ApiFlavor::Mergify)
}
}
2 changes: 2 additions & 0 deletions crates/mergify-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
//! in subsequent sub-phases.

pub mod auth;
pub mod command_context;
pub mod error;
pub mod exit_code;
pub mod http;
pub mod output;

pub use command_context::CommandContext;
pub use error::CliError;
pub use exit_code::ExitCode;
pub use http::{ApiFlavor, Client as HttpClient, DeleteOutcome, RetryPolicy};
Expand Down
17 changes: 8 additions & 9 deletions crates/mergify-freeze/src/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,9 @@
use std::io::Write;

use chrono::NaiveDateTime;
use mergify_core::ApiFlavor;
use mergify_core::CliError;
use mergify_core::HttpClient;
use mergify_core::CommandContext;
use mergify_core::Output;
use mergify_core::auth;
use serde::Serialize;

use crate::common::NaiveDateTimeWire;
Expand Down Expand Up @@ -56,9 +54,7 @@ struct CreatePayload<'a> {

/// Run the `freeze create` command.
pub async fn run(opts: CreateOptions<'_>, output: &mut dyn Output) -> Result<(), CliError> {
let repository = auth::resolve_repository(opts.repository)?;
let token = auth::resolve_token(opts.token)?;
let api_url = auth::resolve_api_url(opts.api_url)?;
let ctx = CommandContext::resolve(opts.repository, opts.token, opts.api_url)?;
let timezone = match opts.timezone {
Some(tz) => tz.to_string(),
None => detect_local_timezone()?,
Expand All @@ -85,10 +81,13 @@ pub async fn run(opts: CreateOptions<'_>, output: &mut dyn Output) -> Result<(),
},
};

output.status(&format!("Creating scheduled freeze for {repository}…"))?;
output.status(&format!(
"Creating scheduled freeze for {repo}…",
repo = ctx.repository,
))?;

let client = HttpClient::new(api_url, token, ApiFlavor::Mergify)?;
let path = format!("/v1/repos/{repository}/scheduled_freeze");
let client = ctx.mergify_client()?;
let path = format!("/v1/repos/{}/scheduled_freeze", ctx.repository);
let freeze: ScheduledFreeze = client.post(&path, &payload).await?;

output.emit(&(), &mut |w: &mut dyn Write| {
Expand Down
16 changes: 7 additions & 9 deletions crates/mergify-freeze/src/delete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,9 @@

use std::io::Write;

use mergify_core::ApiFlavor;
use mergify_core::CliError;
use mergify_core::HttpClient;
use mergify_core::CommandContext;
use mergify_core::Output;
use mergify_core::auth;
use serde::Serialize;

pub struct DeleteOptions<'a> {
Expand All @@ -36,22 +34,22 @@ struct DeletePayload<'a> {

/// Run the `freeze delete` command.
pub async fn run(opts: DeleteOptions<'_>, output: &mut dyn Output) -> Result<(), CliError> {
let repository = auth::resolve_repository(opts.repository)?;
let token = auth::resolve_token(opts.token)?;
let api_url = auth::resolve_api_url(opts.api_url)?;
let ctx = CommandContext::resolve(opts.repository, opts.token, opts.api_url)?;

output.status(&format!(
"Deleting scheduled freeze {id} on {repository}…",
"Deleting scheduled freeze {id} on {repo}…",
id = opts.freeze_id,
repo = ctx.repository,
))?;

let payload = DeletePayload {
delete_reason: opts.delete_reason,
};

let client = HttpClient::new(api_url, token, ApiFlavor::Mergify)?;
let client = ctx.mergify_client()?;
let path = format!(
"/v1/repos/{repository}/scheduled_freeze/{id}/delete",
"/v1/repos/{repo}/scheduled_freeze/{id}/delete",
repo = ctx.repository,
id = opts.freeze_id,
);
client.post_no_response(&path, &payload).await?;
Expand Down
17 changes: 8 additions & 9 deletions crates/mergify-freeze/src/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,9 @@ use std::io::Write;
use anstyle::AnsiColor;
use chrono::DateTime;
use chrono::Utc;
use mergify_core::ApiFlavor;
use mergify_core::CliError;
use mergify_core::HttpClient;
use mergify_core::CommandContext;
use mergify_core::Output;
use mergify_core::auth;
use mergify_tui::Theme;

use crate::common::ScheduledFreeze;
Expand All @@ -39,14 +37,15 @@ pub struct ListOptions<'a> {

/// Run the `freeze list` command.
pub async fn run(opts: ListOptions<'_>, output: &mut dyn Output) -> Result<(), CliError> {
let repository = auth::resolve_repository(opts.repository)?;
let token = auth::resolve_token(opts.token)?;
let api_url = auth::resolve_api_url(opts.api_url)?;
let ctx = CommandContext::resolve(opts.repository, opts.token, opts.api_url)?;

output.status(&format!("Fetching scheduled freezes for {repository}…"))?;
output.status(&format!(
"Fetching scheduled freezes for {repo}…",
repo = ctx.repository,
))?;

let client = HttpClient::new(api_url, token, ApiFlavor::Mergify)?;
let path = format!("/v1/repos/{repository}/scheduled_freeze");
let client = ctx.mergify_client()?;
let path = format!("/v1/repos/{}/scheduled_freeze", ctx.repository);
let raw: serde_json::Value = client.get(&path).await?;

// Python's `list_freezes` returns `data["scheduled_freezes"]`
Expand Down
16 changes: 7 additions & 9 deletions crates/mergify-freeze/src/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@
use std::io::Write;

use chrono::NaiveDateTime;
use mergify_core::ApiFlavor;
use mergify_core::CliError;
use mergify_core::HttpClient;
use mergify_core::CommandContext;
use mergify_core::Output;
use mergify_core::auth;
use serde::Serialize;

use crate::common::NaiveDateTimeWire;
Expand Down Expand Up @@ -55,9 +53,7 @@ struct UpdatePayload<'a> {

/// Run the `freeze update` command.
pub async fn run(opts: UpdateOptions<'_>, output: &mut dyn Output) -> Result<(), CliError> {
let repository = auth::resolve_repository(opts.repository)?;
let token = auth::resolve_token(opts.token)?;
let api_url = auth::resolve_api_url(opts.api_url)?;
let ctx = CommandContext::resolve(opts.repository, opts.token, opts.api_url)?;

let payload = UpdatePayload {
reason: opts.reason,
Expand All @@ -69,13 +65,15 @@ pub async fn run(opts: UpdateOptions<'_>, output: &mut dyn Output) -> Result<(),
};

output.status(&format!(
"Updating scheduled freeze {id} on {repository}…",
"Updating scheduled freeze {id} on {repo}…",
id = opts.freeze_id,
repo = ctx.repository,
))?;

let client = HttpClient::new(api_url, token, ApiFlavor::Mergify)?;
let client = ctx.mergify_client()?;
let path = format!(
"/v1/repos/{repository}/scheduled_freeze/{id}",
"/v1/repos/{repo}/scheduled_freeze/{id}",
repo = ctx.repository,
id = opts.freeze_id,
);
let freeze: ScheduledFreeze = client.patch(&path, &payload).await?;
Expand Down
19 changes: 9 additions & 10 deletions crates/mergify-queue/src/pause.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,9 @@
use std::io::IsTerminal;
use std::io::Write;

use mergify_core::ApiFlavor;
use mergify_core::CliError;
use mergify_core::HttpClient;
use mergify_core::CommandContext;
use mergify_core::Output;
use mergify_core::auth;
use serde::Deserialize;
use serde::Serialize;

Expand Down Expand Up @@ -74,20 +72,21 @@ pub async fn run(opts: PauseOptions<'_>, output: &mut dyn Output) -> Result<(),
// Resolve auth/repo first so the prompt names the *actual* repo
// (including the `GITHUB_REPOSITORY` fallback) and so a missing
// repo or token fails loudly *before* we ask for confirmation.
let repository = auth::resolve_repository(opts.repository)?;
let token = auth::resolve_token(opts.token)?;
let api_url = auth::resolve_api_url(opts.api_url)?;
let ctx = CommandContext::resolve(opts.repository, opts.token, opts.api_url)?;

confirm(
opts.yes_i_am_sure,
std::io::stdin().is_terminal(),
&repository,
&ctx.repository,
)?;

output.status(&format!("Pausing merge queue for {repository}…"))?;
output.status(&format!(
"Pausing merge queue for {repo}…",
repo = ctx.repository,
))?;

let client = HttpClient::new(api_url, token, ApiFlavor::Mergify)?;
let path = format!("/v1/repos/{repository}/merge-queue/pause");
let client = ctx.mergify_client()?;
let path = format!("/v1/repos/{}/merge-queue/pause", ctx.repository);
let resp: PauseResponse = client
.put(
&path,
Expand Down
13 changes: 5 additions & 8 deletions crates/mergify-queue/src/show.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,9 @@ use anstyle::AnsiColor;
use anstyle::Style;
use chrono::DateTime;
use chrono::Utc;
use mergify_core::ApiFlavor;
use mergify_core::CliError;
use mergify_core::HttpClient;
use mergify_core::CommandContext;
use mergify_core::Output;
use mergify_core::auth;
use mergify_tui::Theme;
use mergify_tui::relative_time;
use mergify_tui::tree;
Expand Down Expand Up @@ -105,13 +103,12 @@ const fn default_match_true() -> bool {

/// Run the `queue show` command.
pub async fn run(opts: ShowOptions<'_>, output: &mut dyn Output) -> Result<(), CliError> {
let repository = auth::resolve_repository(opts.repository)?;
let token = auth::resolve_token(opts.token)?;
let api_url = auth::resolve_api_url(opts.api_url)?;
let ctx = CommandContext::resolve(opts.repository, opts.token, opts.api_url)?;

let client = HttpClient::new(api_url, token, ApiFlavor::Mergify)?;
let client = ctx.mergify_client()?;
let path = format!(
"/v1/repos/{repository}/merge-queue/pull/{pr_number}",
"/v1/repos/{repo}/merge-queue/pull/{pr_number}",
repo = ctx.repository,
pr_number = opts.pr_number,
);

Expand Down
19 changes: 9 additions & 10 deletions crates/mergify-queue/src/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,9 @@ use anstyle::Style;
use chrono::DateTime;
use chrono::Utc;
use indexmap::IndexMap;
use mergify_core::ApiFlavor;
use mergify_core::CliError;
use mergify_core::HttpClient;
use mergify_core::CommandContext;
use mergify_core::Output;
use mergify_core::auth;
use mergify_tui::Theme;
use mergify_tui::relative_time;
use mergify_tui::tree;
Expand Down Expand Up @@ -125,14 +123,15 @@ struct Author {

/// Run the `queue status` command.
pub async fn run(opts: StatusOptions<'_>, output: &mut dyn Output) -> Result<(), CliError> {
let repository = auth::resolve_repository(opts.repository)?;
let token = auth::resolve_token(opts.token)?;
let api_url = auth::resolve_api_url(opts.api_url)?;
let ctx = CommandContext::resolve(opts.repository, opts.token, opts.api_url)?;

output.status(&format!("Fetching merge queue status for {repository}…"))?;
output.status(&format!(
"Fetching merge queue status for {repo}…",
repo = ctx.repository,
))?;

let client = HttpClient::new(api_url, token, ApiFlavor::Mergify)?;
let path = build_path(&repository, opts.branch);
let client = ctx.mergify_client()?;
let path = build_path(&ctx.repository, opts.branch);

let raw: serde_json::Value = client.get(&path).await?;

Expand All @@ -141,7 +140,7 @@ pub async fn run(opts: StatusOptions<'_>, output: &mut dyn Output) -> Result<(),
} else {
let view: StatusView = serde_json::from_value(raw)
.map_err(|e| CliError::Generic(format!("decode merge queue status response: {e}")))?;
emit_human(output, &repository, &view)?;
emit_human(output, &ctx.repository, &view)?;
}
Ok(())
}
Expand Down
17 changes: 8 additions & 9 deletions crates/mergify-queue/src/unpause.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@

use std::io::Write;

use mergify_core::ApiFlavor;
use mergify_core::CliError;
use mergify_core::CommandContext;
use mergify_core::DeleteOutcome;
use mergify_core::HttpClient;
use mergify_core::Output;
use mergify_core::auth;

pub struct UnpauseOptions<'a> {
pub repository: Option<&'a str>,
Expand All @@ -22,14 +20,15 @@ pub struct UnpauseOptions<'a> {

/// Run the `queue unpause` command.
pub async fn run(opts: UnpauseOptions<'_>, output: &mut dyn Output) -> Result<(), CliError> {
let repository = auth::resolve_repository(opts.repository)?;
let token = auth::resolve_token(opts.token)?;
let api_url = auth::resolve_api_url(opts.api_url)?;
let ctx = CommandContext::resolve(opts.repository, opts.token, opts.api_url)?;

output.status(&format!("Unpausing merge queue for {repository}…"))?;
output.status(&format!(
"Unpausing merge queue for {repo}…",
repo = ctx.repository,
))?;

let client = HttpClient::new(api_url, token, ApiFlavor::Mergify)?;
let path = format!("/v1/repos/{repository}/merge-queue/pause");
let client = ctx.mergify_client()?;
let path = format!("/v1/repos/{}/merge-queue/pause", ctx.repository);

match client.delete_if_exists(&path).await? {
DeleteOutcome::Deleted => {
Expand Down
Loading