Skip to content
Merged
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
27 changes: 1 addition & 26 deletions crates/mergify-ci/src/detector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,32 +184,7 @@ fn read_github_event_pull_request_number() -> Result<Option<u64>, CliError> {
#[cfg(test)]
mod tests {
use super::*;

/// Clear every CI-provider env var the detector inspects, then
/// apply the test-specific overrides on top. Without this, a
/// test running on a real CI host inherits provider state and
/// the detector picks the wrong branch.
pub(crate) fn with_ci_env<F: FnOnce() -> R, R>(extra: &[(&str, Option<&str>)], f: F) -> R {
let mut vars: Vec<(String, Option<String>)> = [
"JENKINS_URL",
"GITHUB_ACTIONS",
"GITHUB_REPOSITORY",
"GITHUB_EVENT_PATH",
"CIRCLECI",
"CIRCLE_REPOSITORY_URL",
"BUILDKITE",
"BUILDKITE_REPO",
"BUILDKITE_PULL_REQUEST",
"GIT_URL",
]
.into_iter()
.map(|k| (k.to_string(), None))
.collect();
for (k, v) in extra {
vars.push((k.to_string(), v.map(ToString::to_string)));
}
temp_env::with_vars(vars, f)
}
use crate::testing::with_ci_env;

#[test]
fn ci_provider_jenkins_takes_precedence() {
Expand Down
3 changes: 3 additions & 0 deletions crates/mergify-ci/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ pub mod queue_info;
pub mod queue_metadata;
pub mod scopes_send;
pub mod tests_show;

#[cfg(test)]
mod testing;
54 changes: 2 additions & 52 deletions crates/mergify-ci/src/scopes_send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,58 +156,8 @@ mod tests {
use wiremock::matchers::path;

use super::*;

/// Clear every CI-provider env var the resolver inspects, then
/// apply the test-specific overrides on top. Without this, a test
/// running on a real CI host (Buildkite, Actions, …) inherits
/// provider env vars and the new provider-aware resolver picks
/// the wrong branch.
fn with_ci_env<F: FnOnce() -> R, R>(extra: &[(&str, Option<&str>)], f: F) -> R {
let mut vars: Vec<(String, Option<String>)> = [
"JENKINS_URL",
"GITHUB_ACTIONS",
"GITHUB_REPOSITORY",
"GITHUB_EVENT_PATH",
"CIRCLECI",
"CIRCLE_REPOSITORY_URL",
"BUILDKITE",
"BUILDKITE_REPO",
"BUILDKITE_PULL_REQUEST",
"GIT_URL",
]
.into_iter()
.map(|k| (k.to_string(), None))
.collect();
for (k, v) in extra {
vars.push((k.to_string(), v.map(ToString::to_string)));
}
temp_env::with_vars(vars, f)
}

async fn with_ci_env_async<F: std::future::Future<Output = R>, R>(
extra: &[(&str, Option<&str>)],
f: F,
) -> R {
let mut vars: Vec<(String, Option<String>)> = [
"JENKINS_URL",
"GITHUB_ACTIONS",
"GITHUB_REPOSITORY",
"GITHUB_EVENT_PATH",
"CIRCLECI",
"CIRCLE_REPOSITORY_URL",
"BUILDKITE",
"BUILDKITE_REPO",
"BUILDKITE_PULL_REQUEST",
"GIT_URL",
]
.into_iter()
.map(|k| (k.to_string(), None))
.collect();
for (k, v) in extra {
vars.push((k.to_string(), v.map(ToString::to_string)));
}
temp_env::async_with_vars(vars, f).await
}
use crate::testing::with_ci_env;
use crate::testing::with_ci_env_async;

#[test]
fn resolve_repository_prefers_flag_over_env() {
Expand Down
67 changes: 67 additions & 0 deletions crates/mergify-ci/src/testing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//! Test-only helpers shared between `detector` and `scopes_send`.
//!
//! Both modules test CI-provider-aware code paths and need to scrub
//! the host's CI env vars before running each case — otherwise a
//! test running on a real Buildkite/Actions/Circle/Jenkins host
//! inherits provider state and the detector picks the wrong branch.
//! Two flavors: a sync `with_ci_env` and an async `with_ci_env_async`
//! (used by the `#[tokio::test]` cases in `scopes_send`).

use std::future::Future;

/// Env vars the CI-provider detection chain inspects. Clear every
/// one of them before applying the test-specific overrides, so the
/// host environment can't leak into the test.
///
/// `GITHUB_OUTPUT` belongs on this list too — when the suite runs
/// *on* a GHA runner that var points at the runner's real
/// step-output file, and any test that exercises a code path
/// appending a heredoc (e.g. `ci scopes` →
/// `MERGIFY_SCOPES<<ghadelimiter_…`) will splice its own
/// delimiter into the runner's file. GHA then fails the step with
/// "Matching delimiter not found". Scrubbing it forces the no-op
/// `env::var("GITHUB_OUTPUT").ok()` branch unless the test
/// explicitly points it at a temp file.
const CI_ENV_VARS: &[&str] = &[
"JENKINS_URL",
"GITHUB_ACTIONS",
"GITHUB_REPOSITORY",
"GITHUB_EVENT_PATH",
"GITHUB_OUTPUT",
"CIRCLECI",
"CIRCLE_REPOSITORY_URL",
"BUILDKITE",
"BUILDKITE_REPO",
"BUILDKITE_PULL_REQUEST",
"GIT_URL",
];

fn merged_overrides(extra: &[(&str, Option<&str>)]) -> Vec<(String, Option<String>)> {
let mut vars: Vec<(String, Option<String>)> = CI_ENV_VARS
.iter()
.map(|k| ((*k).to_string(), None))
.collect();
for (k, v) in extra {
vars.push(((*k).to_string(), v.map(ToString::to_string)));
}
vars
}

/// Run `f` with the CI-provider env vars cleared, plus the
/// `extra` overrides applied on top.
pub(crate) fn with_ci_env<F, R>(extra: &[(&str, Option<&str>)], f: F) -> R
where
F: FnOnce() -> R,
{
temp_env::with_vars(merged_overrides(extra), f)
}

/// Async counterpart to [`with_ci_env`]. Used by `#[tokio::test]`
/// cases in `scopes_send` — the sync variant can't bridge `.await`
/// points.
pub(crate) async fn with_ci_env_async<F, R>(extra: &[(&str, Option<&str>)], f: F) -> R
where
F: Future<Output = R>,
{
temp_env::async_with_vars(merged_overrides(extra), f).await
}
4 changes: 2 additions & 2 deletions crates/mergify-config/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Native Rust implementation of the `mergify config` subcommands.
//!
//! Phase 1.3 ports `config validate`. Phase 1.3b adds `config
//! simulate`. Both share the config-file resolver in [`paths`].
//! Hosts `config validate` and `config simulate`; both share the
//! config-file resolver in [`paths`].

pub mod paths;
pub mod simulate;
Expand Down
7 changes: 3 additions & 4 deletions crates/mergify-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
//! the appropriate `ExitCode` and writes a human-readable message
//! to stderr before exiting.
//!
//! This enum grows as new error sources are added. Today it covers
//! the categories needed to port the `config` pilot (Phase 1.3);
//! subsequent sub-phases add variants for HTTP failures, git
//! subprocess failures, and so on.
//! This enum grows as new error sources are added — add a variant
//! per error category, never a generic `String` catch-all for new
//! kinds of failure that have their own exit code.

use std::io;

Expand Down
6 changes: 3 additions & 3 deletions crates/mergify-core/src/exit_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
//!
//! Mirrors `mergify_cli.exit_codes.ExitCode` in the Python
//! implementation. The contract — which (command, failure mode)
//! maps to which exit code — is locked by Phase 0.1 and enforced by
//! the compat-test harness. Changing a variant's numeric value is a
//! breaking change for downstream scripts.
//! maps to which exit code — is enforced by the compat-test
//! harness. Changing a variant's numeric value is a breaking change
//! for downstream scripts.

use std::process::ExitCode as ProcessExitCode;

Expand Down
10 changes: 5 additions & 5 deletions crates/mergify-core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
//! Shared foundations for the mergify CLI Rust port.
//!
//! Phase 1.2 populates this crate with:
//!
//! - [`exit_code::ExitCode`] — typed exit codes mirroring the
//! Python `exit_codes.py` contract.
//! - [`error::CliError`] — top-level error enum with deterministic
Expand All @@ -11,9 +9,11 @@
//! in.
//! - [`http::Client`] — wraps `reqwest` with bearer auth, retry,
//! and typed error mapping for the Mergify and GitHub APIs.
//!
//! Git operations, interactive prompts, and config loading arrive
//! in subsequent sub-phases.
//! - [`auth`] — resolve `--repository` / `--token` / `--api-url`
//! from the same flag → env → fallback chain the Python CLI uses.
//! - [`command_context::CommandContext`] — bundle the resolved
//! trio + a pre-configured Mergify HTTP client for the
//! queue/freeze command preludes.

pub mod auth;
pub mod command_context;
Expand Down
2 changes: 1 addition & 1 deletion crates/mergify-core/src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
//! directly. This keeps the JSON and human rendering paths
//! symmetric, lets commands stay test-friendly, and gives a single
//! place to enforce the "stdout must be a single JSON document
//! under `--json`" invariant (Phase 0.3).
//! under `--json`" invariant.
//!
//! The trait is deliberately small. Commands emit one "result"
//! value that knows how to render itself both as JSON (via
Expand Down
12 changes: 4 additions & 8 deletions crates/mergify-freeze/src/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,16 +188,12 @@ fn write_row(
spaces = " ".repeat(pad),
)?;
} else if HEADERS[i] == "Status" {
// `Theme::fg` already collapses to `Style::new()` when
// colors are disabled — no need for an extra branch.
let style = if cell == "active" {
if theme.enabled {
theme.fg(AnsiColor::Green)
} else {
anstyle::Style::new()
}
} else if theme.enabled {
theme.fg(AnsiColor::Yellow)
theme.fg(AnsiColor::Green)
} else {
anstyle::Style::new()
theme.fg(AnsiColor::Yellow)
};
write!(
w,
Expand Down
3 changes: 2 additions & 1 deletion crates/mergify-py-shim/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
//! native impl first and only falls back to [`run`] for un-ported
//! commands. The plan is for each port PR to delete its Python
//! implementation in the same change, so the shim's reach shrinks
//! one command at a time. Phase 6 deletes this crate entirely.
//! one command at a time; this crate is deleted entirely once
//! everything is ported.

use std::env;
use std::io;
Expand Down
11 changes: 3 additions & 8 deletions crates/mergify-queue/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
//! Native Rust implementation of the `mergify queue` subcommands.
//!
//! Phase 1.5 ported `pause` and `unpause` — two idempotent API
//! calls that rest on the HTTP client added in 1.2b and the
//! `put`/`delete_if_exists` methods added alongside this crate.
//! Phase 1.7 ports `status`, the read-only command that fetches
//! the merge-queue snapshot and renders it either as a JSON
//! passthrough or as the human-friendly batch tree + waiting list.
//! `queue show` stays shimmed until its conditions/checks tree
//! ports next.
//! Hosts `pause` / `unpause` (idempotent API mutations), `status`
//! (read-only batch tree + waiting list, with JSON passthrough),
//! and `show` (per-PR detail with checks + conditions tree).

pub mod pause;
pub mod show;
Expand Down
42 changes: 23 additions & 19 deletions crates/mergify-queue/src/show.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@
use std::io::Write;

use anstyle::AnsiColor;
use anstyle::Style;
use chrono::DateTime;
use chrono::Utc;
use mergify_core::CliError;
use mergify_core::CommandContext;
use mergify_core::Output;
use mergify_tui::StyledGlyph;
use mergify_tui::Theme;
use mergify_tui::relative_time;
use mergify_tui::tree;
Expand Down Expand Up @@ -230,11 +230,12 @@ fn print_checks_section(
now: DateTime<Utc>,
) -> std::io::Result<()> {
writeln!(w)?;
let (icon, style) = check_state_glyph(theme, &mc.ci_state);
let glyph = check_state_glyph(theme, &mc.ci_state);
write!(
w,
" CI State: {S}{icon} {state}{R}",
S = style,
S = glyph.style,
icon = glyph.icon,
state = mc.ci_state,
R = theme.reset,
)?;
Expand Down Expand Up @@ -267,7 +268,7 @@ fn print_checks_table(w: &mut dyn Write, theme: &Theme, checks: &[Check]) -> std
.max()
.unwrap_or(0);
for check in checks {
let (icon, style) = check_state_glyph(theme, &check.state);
let glyph = check_state_glyph(theme, &check.state);
let pad = name_width.saturating_sub(check.name.chars().count());
writeln!(
w,
Expand All @@ -276,7 +277,8 @@ fn print_checks_table(w: &mut dyn Write, theme: &Theme, checks: &[Check]) -> std
name = check.name,
spaces = " ".repeat(pad),
R = theme.reset,
S = style,
S = glyph.style,
icon = glyph.icon,
state = check.state,
)?;
}
Expand Down Expand Up @@ -325,11 +327,12 @@ fn print_checks_summary(w: &mut dyn Write, theme: &Theme, checks: &[Check]) -> s
check.state.as_str(),
"failure" | "error" | "timed_out" | "action_required"
) {
let (icon, style) = check_state_glyph(theme, &check.state);
let glyph = check_state_glyph(theme, &check.state);
writeln!(
w,
" {S}{icon} {state}{R} {D}{name}{R}",
S = style,
S = glyph.style,
icon = glyph.icon,
state = check.state,
R = theme.reset,
D = theme.dim,
Expand All @@ -340,17 +343,17 @@ fn print_checks_summary(w: &mut dyn Write, theme: &Theme, checks: &[Check]) -> s
Ok(())
}

/// Map a check state string to (icon, ANSI style). Mirrors Python's
/// Map a check state string to its [`StyledGlyph`]. Mirrors Python's
/// `CHECK_STATE_STYLES`; unknown states fall back to a dim `?` so
/// the renderer never crashes on a new API code.
fn check_state_glyph(theme: &Theme, state: &str) -> (&'static str, Style) {
fn check_state_glyph(theme: &Theme, state: &str) -> StyledGlyph {
match state {
"success" => ("✓", theme.fg(AnsiColor::Green)),
"pending" => ("◌", theme.fg(AnsiColor::Yellow)),
"failure" | "error" | "action_required" => ("✗", theme.fg(AnsiColor::Red)),
"timed_out" => ("⏰", theme.fg(AnsiColor::Red)),
"cancelled" | "neutral" | "skipped" | "stale" => ("○", theme.dim),
_ => ("?", theme.dim),
"success" => StyledGlyph::new("✓", theme.fg(AnsiColor::Green)),
"pending" => StyledGlyph::new("◌", theme.fg(AnsiColor::Yellow)),
"failure" | "error" | "action_required" => StyledGlyph::new("✗", theme.fg(AnsiColor::Red)),
"timed_out" => StyledGlyph::new("⏰", theme.fg(AnsiColor::Red)),
"cancelled" | "neutral" | "skipped" | "stale" => StyledGlyph::new("○", theme.dim),
_ => StyledGlyph::new("?", theme.dim),
}
}

Expand Down Expand Up @@ -446,15 +449,16 @@ fn write_condition_tree(
let last = nodes.len() - 1;
for (i, node) in nodes.iter().enumerate() {
let (branch, continuation) = tree::branch_chars(i == last);
let (icon, style) = if node.r#match {
("✓", theme.fg(AnsiColor::Green))
let glyph = if node.r#match {
StyledGlyph::new("✓", theme.fg(AnsiColor::Green))
} else {
("✗", theme.fg(AnsiColor::Red))
StyledGlyph::new("✗", theme.fg(AnsiColor::Red))
};
writeln!(
w,
"{prefix}{branch}{S}{icon}{R} {label}",
S = style,
S = glyph.style,
icon = glyph.icon,
R = theme.reset,
label = node.label,
)?;
Expand Down
Loading
Loading