use std::collections::HashMap;
use std::path::Path;
use std::process::Command;
use serde::Deserialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MrState {
#[default]
Opened,
Merged,
Closed,
}
impl MrState {
fn rank(self) -> u8 {
match self {
Self::Opened => 3,
Self::Merged => 2,
Self::Closed => 1,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct MrRef {
pub iid: u64,
#[serde(alias = "source_branch")]
pub source_branch: String,
#[serde(default)]
pub title: String,
#[serde(default)]
pub state: MrState,
}
impl MrRef {
pub fn label(&self) -> String {
match self.state {
MrState::Opened => format!("!{}", self.iid),
MrState::Merged => format!("!{}✓", self.iid),
MrState::Closed => format!("!{}×", self.iid),
}
}
}
pub fn index_by_source_branch(cwd: &Path) -> HashMap<String, MrRef> {
let Ok(output) = Command::new("glab")
.args(["mr", "list", "--all", "-F", "json", "--per-page", "100"])
.current_dir(cwd)
.output()
else {
return HashMap::new();
};
if !output.status.success() {
return HashMap::new();
}
let raw = String::from_utf8_lossy(&output.stdout);
parse_mr_list(&raw)
}
pub fn parse_mr_list(raw: &str) -> HashMap<String, MrRef> {
let Ok(list) = serde_json::from_str::<Vec<MrRef>>(raw.trim()) else {
return HashMap::new();
};
let mut map: HashMap<String, MrRef> = HashMap::new();
for mr in list {
let branch = mr.source_branch.clone();
let replace = map
.get(&branch)
.map(|existing| mr.state.rank() > existing.state.rank())
.unwrap_or(true);
if replace {
map.insert(branch, mr);
}
}
map
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_glab_json() {
let raw = r#"[
{"iid": 12, "source_branch": "feat/login", "title": "Add login", "state": "opened"},
{"iid": 3, "source_branch": "fix-typo", "title": "typo", "state": "merged"}
]"#;
let map = parse_mr_list(raw);
assert_eq!(map["feat/login"].label(), "!12");
assert_eq!(map["fix-typo"].label(), "!3✓");
assert_eq!(map["fix-typo"].state, MrState::Merged);
}
#[test]
fn prefers_opened_over_closed_same_branch() {
let raw = r#"[
{"iid": 1, "source_branch": "feat/x", "state": "closed"},
{"iid": 2, "source_branch": "feat/x", "state": "opened"}
]"#;
let map = parse_mr_list(raw);
assert_eq!(map["feat/x"].iid, 2);
assert_eq!(map["feat/x"].label(), "!2");
}
#[test]
fn closed_label() {
let raw = r#"[{"iid": 9, "source_branch": "old", "state": "closed"}]"#;
assert_eq!(parse_mr_list(raw)["old"].label(), "!9×");
}
#[test]
fn bad_json_yields_empty() {
assert!(parse_mr_list("not-json").is_empty());
}
}