From 1d374a9c1f71699ea22a1b920db211cb24e1d77a Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Tue, 19 May 2026 17:14:23 +0200 Subject: [PATCH] refactor(queue): drop indexmap, group_by_scope returns a Vec<(K, V)> MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `group_by_scope` in `queue status` returned `IndexMap>` because the human renderer needs first-seen insertion order (mirrors Python dict iteration). That's one hashmap-shaped use of the only `IndexMap` in the codebase — and the typical input size is 1–3 scopes per repo. At that N, the hashing/bucketing overhead of `IndexMap` is dead weight versus a `Vec<(K, V)>` with a linear `iter_mut().find()` on insert. Same wire shape from the caller's side (still iterable by `(scope, batches)`), one fewer crate in `Cargo.lock`. `indexmap` survives transitively (it's an internal dep of `jsonschema`), so the lockfile loses one direct line but keeps the package — that's fine, the goal here is to shed a *direct* dependency we control, not to fight upstream graphs. Co-Authored-By: Claude Opus 4.7 Change-Id: I7085cf297a6a294217b0a6656a4bc224a1dd2c58 --- Cargo.lock | 1 - crates/mergify-queue/Cargo.toml | 1 - crates/mergify-queue/src/status.rs | 28 ++++++++++++++++++++-------- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 147934ad..67a71ab7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1153,7 +1153,6 @@ version = "0.0.0" dependencies = [ "anstyle", "chrono", - "indexmap", "mergify-core", "mergify-test-support", "mergify-tui", diff --git a/crates/mergify-queue/Cargo.toml b/crates/mergify-queue/Cargo.toml index c0e4310f..361f6430 100644 --- a/crates/mergify-queue/Cargo.toml +++ b/crates/mergify-queue/Cargo.toml @@ -14,7 +14,6 @@ mergify-core = { path = "../mergify-core" } mergify-tui = { path = "../mergify-tui" } anstyle = "1" chrono = { version = "0.4", default-features = false, features = ["clock"] } -indexmap = "2" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" url = "2" diff --git a/crates/mergify-queue/src/status.rs b/crates/mergify-queue/src/status.rs index 2b9662e4..276bba94 100644 --- a/crates/mergify-queue/src/status.rs +++ b/crates/mergify-queue/src/status.rs @@ -31,7 +31,6 @@ use std::io::Write; use anstyle::AnsiColor; use chrono::DateTime; use chrono::Utc; -use indexmap::IndexMap; use mergify_core::CliError; use mergify_core::CommandContext; use mergify_core::Output; @@ -432,14 +431,20 @@ fn visit<'a>( result.push(batch); } -/// Group batches by scope, preserving insertion order for the +/// Group batches by scope, preserving first-seen order for the /// scopes (matches Python dict iteration). A batch with no scopes /// is grouped under `"default"` to match the Python fallback. A /// batch with multiple scopes appears in every group it claims — /// the Python implementation does the same so users see each batch /// in every scope it affects. -fn group_by_scope<'a>(batches: &[&'a Batch]) -> IndexMap> { - let mut groups: IndexMap> = IndexMap::new(); +/// +/// Returns a `Vec<(scope, batches)>` rather than a hashmap because +/// (a) we need insertion-order iteration and (b) typical N is +/// 1–3 scopes per repo — the linear-lookup `entry_or_insert` +/// below is cheaper than the hashing overhead of `IndexMap` at +/// this scale. +fn group_by_scope<'a>(batches: &[&'a Batch]) -> Vec<(String, Vec<&'a Batch>)> { + let mut groups: Vec<(String, Vec<&Batch>)> = Vec::new(); for batch in batches { let scopes: Vec = if batch.scopes.is_empty() { vec!["default".to_string()] @@ -447,7 +452,10 @@ fn group_by_scope<'a>(batches: &[&'a Batch]) -> IndexMap> batch.scopes.clone() }; for scope in scopes { - groups.entry(scope).or_default().push(batch); + match groups.iter_mut().find(|(s, _)| *s == scope) { + Some((_, bs)) => bs.push(batch), + None => groups.push((scope, vec![batch])), + } } } groups @@ -521,13 +529,17 @@ mod tests { assert_eq!(sorted[0].id, "only"); } + fn has_scope(groups: &[(String, Vec<&Batch>)], scope: &str) -> bool { + groups.iter().any(|(s, _)| s == scope) + } + #[test] fn group_by_scope_default_when_empty_scopes() { let batches = [sample_batch("a", &[])]; let refs: Vec<&Batch> = batches.iter().collect(); let groups = group_by_scope(&refs); assert_eq!(groups.len(), 1); - assert!(groups.contains_key("default")); + assert!(has_scope(&groups, "default")); } #[test] @@ -540,8 +552,8 @@ mod tests { let refs: Vec<&Batch> = batches.iter().collect(); let groups = group_by_scope(&refs); assert_eq!(groups.len(), 2); - assert!(groups.contains_key("foo")); - assert!(groups.contains_key("bar")); + assert!(has_scope(&groups, "foo")); + assert!(has_scope(&groups, "bar")); } #[test]