pub const BEGIN: &str = "<!-- git-queue:begin -->";
pub const END: &str = "<!-- git-queue:end -->";
pub struct Entry {
pub branch: String,
pub pr: Option<PrRef>,
pub conflicted: bool,
pub conflicts: Vec<String>,
pub commits: Vec<(Option<String>, String)>,
pub indent: usize,
}
#[derive(Clone)]
pub struct PrRef {
pub number: u64,
pub url: String,
pub state: String, pub review: Option<String>,
}
pub const GATE_CONTEXT: &str = "git-queue/merge-order";
#[derive(Debug, PartialEq)]
pub struct GateStatus {
pub branch: String,
pub success: bool,
pub description: String,
pub target_url: Option<String>,
}
pub fn gate_plan(entries: &[Entry]) -> Vec<GateStatus> {
let mut bottom: Option<&PrRef> = None;
let mut plan = Vec::new();
for e in entries {
let Some(pr) = &e.pr else { continue };
if pr.state != "OPEN" {
continue;
}
match bottom {
None => {
plan.push(GateStatus {
branch: e.branch.clone(),
success: true,
description: "Ready — front of the queue, merge this PR first".to_string(),
target_url: None,
});
bottom = Some(pr);
}
Some(b) => plan.push(GateStatus {
branch: e.branch.clone(),
success: false,
description: format!("Do not merge — merge PR #{} first (queue order)", b.number),
target_url: (!b.url.is_empty()).then(|| b.url.clone()),
}),
}
}
plan
}
fn approval_emoji(review: &Option<String>) -> &'static str {
match review.as_deref() {
Some("APPROVED") => "✅",
Some("CHANGES_REQUESTED") => "♻️",
_ => "⏳", }
}
fn state_emoji(state: &str) -> &'static str {
match state {
"MERGED" => "🟣",
"CLOSED" => "⚫",
_ => "🟢", }
}
pub fn numbered_title(subject: &str, index: usize, total: usize) -> String {
format!("[{}/{}] {}", index + 1, total, strip_prefix(subject))
}
fn strip_prefix(subject: &str) -> &str {
let s = subject.trim_start();
if let Some(rest) = s.strip_prefix('[') {
if let Some(close) = rest.find(']') {
let inside = &rest[..close];
if inside.contains('/') && inside.chars().all(|c| c.is_ascii_digit() || c == '/') {
return rest[close + 1..].trim_start();
}
}
}
s
}
pub fn nav_block(line: &[Entry], current: &str, base: &str, queue_name: &str) -> String {
let total = line.len();
let mut lines = vec![
format!(
"### 📚 {queue_name} PR · {} of {}",
position_of(line, current),
total
),
String::new(),
"Part of a queue. The PRs merge in FIFO order — the numbered order below, #1 \
first. Merging one supersedes the PRs after it until the author runs \
`git queue sync` (rebases the rest onto the merged base) and `git queue submit` \
(retargets their PRs)."
.to_string(),
String::new(),
];
for (i, e) in line.iter().enumerate() {
let is_current = e.branch == current;
let target = if i == 0 {
base
} else {
line[i - 1].branch.as_str()
};
let status = match &e.pr {
Some(p) if p.state == "OPEN" => {
format!("{}{} ", approval_emoji(&p.review), state_emoji(&p.state))
}
Some(p) => format!("{} ", state_emoji(&p.state)),
None => String::new(),
};
let label = match &e.pr {
Some(p) if !p.url.is_empty() => format!("[#{} `{}`]({})", p.number, e.branch, p.url),
Some(p) => format!("#{} `{}`", p.number, e.branch),
None => format!("`{}` _(not submitted)_", e.branch),
};
let arrow = format!(" → `{target}`");
let line_str = if is_current {
format!("{status}**{label}{arrow}** 👈 **this PR**")
} else {
format!("{status}{label}{arrow}")
};
lines.push(line_str);
}
lines.push(String::new());
lines.push(
"<sub>✅ approved · ♻️ changes requested · ⏳ review pending | \
🟣 merged · 🟢 open · ⚫ closed — status as of the last \
`git queue submit`.</sub>"
.to_string(),
);
lines.push("<sub>🥞 Managed by git-queue — do not edit this list by hand.</sub>".to_string());
lines.join("\n")
}
fn position_of(line: &[Entry], current: &str) -> usize {
line.iter()
.position(|e| e.branch == current)
.map_or(0, |i| i + 1)
}
pub fn compose_body(queue_description: &str, branch_description: &str, nav: &str) -> String {
let mut body = format!("{BEGIN}\n{nav}");
let qd = queue_description.trim();
if !qd.is_empty() {
body.push_str(&format!("\n\n# About this queue\n\n{qd}"));
}
let bd = strip_block(branch_description);
let bd = bd.trim();
let bd = bd.strip_prefix("---").map(str::trim_start).unwrap_or(bd);
if !bd.is_empty() {
body.push_str(&format!("\n\n# About this branch\n\n{bd}"));
}
body.push_str(&format!("\n{END}"));
body
}
pub fn strip_block(body: &str) -> String {
match (body.find(BEGIN), body.find(END)) {
(Some(start), Some(end)) if end >= start => {
let after = end + END.len();
let mut result = String::new();
result.push_str(&body[..start]);
result.push_str(&body[after..]);
result
}
_ => body.to_string(),
}
}
pub fn status_tree(
entries: &[Entry],
current: &str,
base: &str,
base_is_trunk: bool,
color: bool,
link_base: Option<&str>,
) -> String {
let paint = |code: &str, s: &str| -> String {
if color {
format!("\u{1b}[{code}m{s}\u{1b}[0m")
} else {
s.to_string()
}
};
let mut out = String::new();
for e in entries.iter() {
let is_current = e.branch == current;
let margin = if is_current {
paint("1;32", "❯ ")
} else {
" ".to_string()
};
let pad = " ".repeat(e.indent);
let node = if is_current { "◉" } else { "◯" };
let name = paint("1", &e.branch);
let link = |n: u64| -> String {
match link_base {
Some(base_url) => {
format!("\u{1b}]8;;{base_url}/pull/{n}\u{1b}\\#{n}\u{1b}]8;;\u{1b}\\")
}
None => format!("#{n}"),
}
};
let pr = match &e.pr {
Some(p) if p.state == "?" => format!(" {}", link(p.number)),
Some(p) => {
let state = match p.state.as_str() {
"OPEN" => paint("32", "OPEN"),
"MERGED" => paint("35", "MERGED"),
"CLOSED" => paint("31", "CLOSED"),
other => other.to_string(),
};
format!(" {} [{state}]", link(p.number))
}
None => String::new(),
};
let warn = if e.conflicted {
paint("33", " ⚠ conflicts")
} else {
String::new()
};
out.push_str(&format!("{margin}{pad}{node} {name}{pr}{warn}\n"));
for path in &e.conflicts {
out.push_str(&format!(
" {pad}{}\n",
paint("33", &format!("⚠ {path}"))
));
}
for (id, subject) in &e.commits {
let abbrev = match id {
Some(id) => {
let short: String = id.chars().take(10).collect();
paint("36", &format!("{short:<10}"))
}
None => paint("2", "(no id) "),
};
out.push_str(&format!(" {pad}{abbrev} {subject}\n"));
}
}
out.push_str(" ┴\n");
let label = if base_is_trunk { "trunk" } else { "base" };
out.push_str(&format!(" {base} ({label})\n"));
out
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum DiffLine {
Added,
Removed,
Hunk,
FileHeader,
Binary,
Context,
}
pub fn classify_diff_line(line: &str) -> DiffLine {
if line.starts_with("@@") {
DiffLine::Hunk
} else if line.starts_with("diff --git")
|| line.starts_with("index ")
|| line.starts_with("--- ")
|| line.starts_with("+++ ")
|| line.starts_with("new file")
|| line.starts_with("deleted file")
|| line.starts_with("old mode")
|| line.starts_with("new mode")
|| line.starts_with("rename ")
|| line.starts_with("copy ")
|| line.starts_with("similarity ")
|| line.starts_with("dissimilarity ")
{
DiffLine::FileHeader
} else if line.starts_with("Binary files") || line.starts_with("GIT binary patch") {
DiffLine::Binary
} else if line.starts_with('+') {
DiffLine::Added
} else if line.starts_with('-') {
DiffLine::Removed
} else {
DiffLine::Context
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classifies_diff_lines_headers_before_content() {
use DiffLine::*;
assert_eq!(classify_diff_line("@@ -1,3 +1,4 @@ fn main"), Hunk);
assert_eq!(classify_diff_line("diff --git a/x b/x"), FileHeader);
assert_eq!(classify_diff_line("index 0000..1111 100644"), FileHeader);
assert_eq!(classify_diff_line("--- a/x"), FileHeader);
assert_eq!(classify_diff_line("+++ b/x"), FileHeader);
assert_eq!(classify_diff_line("new file mode 100644"), FileHeader);
assert_eq!(classify_diff_line("+added line"), Added);
assert_eq!(classify_diff_line("-removed line"), Removed);
assert_eq!(classify_diff_line(" context"), Context);
assert_eq!(classify_diff_line("Binary files a/i.png and b/i.png differ"), Binary);
}
#[test]
fn strips_prior_number_prefix() {
assert_eq!(numbered_title("[2/9] Add widget", 0, 3), "[1/3] Add widget");
assert_eq!(numbered_title("Add widget", 2, 3), "[3/3] Add widget");
assert_eq!(numbered_title("[wip] thing", 0, 1), "[1/1] [wip] thing");
}
#[test]
fn compose_builds_sections_and_stays_idempotent() {
let branch_desc = format!("{BEGIN}\nold\n{END}\n\n---\n\nHello");
let composed = compose_body("The plan.", &branch_desc, "new-nav");
assert!(composed.starts_with(BEGIN));
assert!(composed.ends_with(END));
assert!(composed.contains("new-nav"));
assert!(!composed.contains("old"), "old nav must be stripped");
assert!(composed.contains("# About this queue\n\nThe plan."));
assert!(composed.contains("# About this branch\n\nHello"));
assert_eq!(composed.matches(BEGIN).count(), 1);
}
#[test]
fn compose_omits_empty_sections() {
let composed = compose_body("", "", "nav");
assert_eq!(composed, format!("{BEGIN}\nnav\n{END}"));
let q_only = compose_body("Q.", "", "nav");
assert!(q_only.contains("About this queue") && !q_only.contains("About this branch"));
}
fn entry(branch: &str, number: u64, url: &str, state: &str, review: Option<&str>) -> Entry {
Entry {
branch: branch.to_string(),
pr: Some(PrRef {
number,
url: url.to_string(),
state: state.to_string(),
review: review.map(|s| s.to_string()),
}),
conflicted: false,
conflicts: Vec::new(),
commits: Vec::new(),
indent: 0,
}
}
#[test]
fn gate_plan_marks_bottom_ready_and_blocks_the_rest() {
let line = vec![
entry("api", 10, "https://x/pull/10", "OPEN", None),
entry("service", 11, "https://x/pull/11", "OPEN", None),
entry("ui", 12, "https://x/pull/12", "OPEN", None),
];
let plan = gate_plan(&line);
assert_eq!(plan.len(), 3);
assert!(plan[0].success);
assert_eq!(plan[0].branch, "api");
assert_eq!(plan[0].target_url, None);
for s in &plan[1..] {
assert!(!s.success);
assert!(s.description.contains("#10"), "{}", s.description);
assert_eq!(s.target_url.as_deref(), Some("https://x/pull/10"));
}
}
#[test]
fn gate_plan_skips_merged_and_promotes_next_open_pr() {
let line = vec![
entry("api", 10, "https://x/pull/10", "MERGED", None),
entry("service", 11, "https://x/pull/11", "OPEN", None),
entry("ui", 12, "https://x/pull/12", "OPEN", None),
];
let plan = gate_plan(&line);
assert_eq!(plan.len(), 2);
assert!(plan[0].success);
assert_eq!(plan[0].branch, "service");
assert!(!plan[1].success);
assert!(
plan[1].description.contains("#11"),
"{}",
plan[1].description
);
}
#[test]
fn gate_plan_ignores_closed_prs_and_unsubmitted_branches() {
let mut line = vec![
entry("api", 10, "https://x/pull/10", "CLOSED", None),
entry("service", 11, "https://x/pull/11", "OPEN", None),
];
line.push(Entry {
branch: "ui".to_string(),
pr: None,
conflicted: false,
conflicts: Vec::new(),
commits: Vec::new(),
indent: 0,
});
let plan = gate_plan(&line);
assert_eq!(plan.len(), 1);
assert!(plan[0].success);
assert_eq!(plan[0].branch, "service");
}
#[test]
fn gate_plan_is_empty_when_no_pr_is_open() {
let line = vec![
entry("api", 10, "https://x/pull/10", "MERGED", None),
entry("service", 11, "https://x/pull/11", "CLOSED", None),
];
assert!(gate_plan(&line).is_empty());
assert!(gate_plan(&[]).is_empty());
}
#[test]
fn gate_plan_descriptions_fit_github_status_limit() {
let line = vec![
entry(
"api",
4_294_967_295,
"https://x/pull/4294967295",
"OPEN",
None,
),
entry("ui", 12, "https://x/pull/12", "OPEN", None),
];
for s in gate_plan(&line) {
assert!(s.description.len() <= 140, "{}", s.description);
}
}
#[test]
fn nav_block_links_marks_current_and_shows_status() {
let line = vec![
entry("api", 10, "https://x/pull/10", "MERGED", Some("APPROVED")),
entry(
"service",
11,
"https://x/pull/11",
"OPEN",
Some("CHANGES_REQUESTED"),
),
entry("ui", 12, "https://x/pull/12", "OPEN", None),
];
let nav = nav_block(&line, "service", "main", "payments");
assert!(nav.contains("📚 payments PR"), "{nav}");
assert!(
nav.contains("🟣 [#10 `api`](https://x/pull/10) → `main`"),
"{nav}"
);
assert!(
nav.contains("♻️🟢 **[#11 `service`](https://x/pull/11) → `api`**"),
"{nav}"
);
assert!(nav.contains("⏳🟢 [#12 `ui`]"), "{nav}");
assert!(nav.contains("👈 **this PR**"));
assert!(nav.contains("2 of 3"));
assert!(nav.contains("FIFO"), "merge order described as FIFO");
assert!(
!nav.contains("bottom-first"),
"confusing bottom-first wording removed"
);
}
}