feat(cli): Add MR support to list/show/count/ingest commands
Extends all data commands to support merge requests alongside issues, with consistent patterns and JSON output for robot mode. List command (gi list mrs): - MR-specific columns: branches, draft status, reviewers - Filters: --state (opened|merged|closed|locked|all), --draft, --no-draft, --reviewer, --target-branch, --source-branch - Discussion count with unresolved indicator (e.g., "5/2!") - JSON output includes full MR metadata Show command (gi show mr <iid>): - MR details with branches, assignees, reviewers, merge status - DiffNote positions showing file:line for code review comments - Full description and discussion bodies (no truncation in JSON) - --json flag for structured output with ISO timestamps Count command (gi count mrs): - MR counting with optional --type filter for discussions/notes - JSON output with breakdown by state Ingest command (gi ingest --type mrs): - Full MR sync with discussion prefetch - Progress output shows MR-specific metrics (diffnotes count) - JSON summary with comprehensive sync statistics All commands respect global --robot mode for auto-JSON output. The pattern "gi list mrs --json | jq '.mrs[] | .iid'" now works for scripted MR processing. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
use console::style;
|
||||
use rusqlite::Connection;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::Config;
|
||||
use crate::core::db::create_connection;
|
||||
@@ -12,7 +13,16 @@ use crate::core::paths::get_db_path;
|
||||
pub struct CountResult {
|
||||
pub entity: String,
|
||||
pub count: i64,
|
||||
pub system_count: Option<i64>, // For notes only
|
||||
pub system_count: Option<i64>, // For notes only
|
||||
pub state_breakdown: Option<StateBreakdown>, // For issues/MRs
|
||||
}
|
||||
|
||||
/// State breakdown for issues or MRs.
|
||||
pub struct StateBreakdown {
|
||||
pub opened: i64,
|
||||
pub closed: i64,
|
||||
pub merged: Option<i64>, // MRs only
|
||||
pub locked: Option<i64>, // MRs only
|
||||
}
|
||||
|
||||
/// Run the count command.
|
||||
@@ -24,30 +34,83 @@ pub fn run_count(config: &Config, entity: &str, type_filter: Option<&str>) -> Re
|
||||
"issues" => count_issues(&conn),
|
||||
"discussions" => count_discussions(&conn, type_filter),
|
||||
"notes" => count_notes(&conn, type_filter),
|
||||
"mrs" => {
|
||||
// Placeholder for CP2
|
||||
Ok(CountResult {
|
||||
entity: "Merge Requests".to_string(),
|
||||
count: 0,
|
||||
system_count: None,
|
||||
})
|
||||
}
|
||||
"mrs" => count_mrs(&conn),
|
||||
_ => Ok(CountResult {
|
||||
entity: entity.to_string(),
|
||||
count: 0,
|
||||
system_count: None,
|
||||
state_breakdown: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Count issues.
|
||||
/// Count issues with state breakdown.
|
||||
fn count_issues(conn: &Connection) -> Result<CountResult> {
|
||||
let count: i64 = conn.query_row("SELECT COUNT(*) FROM issues", [], |row| row.get(0))?;
|
||||
|
||||
let opened: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM issues WHERE state = 'opened'",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
|
||||
let closed: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM issues WHERE state = 'closed'",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
|
||||
Ok(CountResult {
|
||||
entity: "Issues".to_string(),
|
||||
count,
|
||||
system_count: None,
|
||||
state_breakdown: Some(StateBreakdown {
|
||||
opened,
|
||||
closed,
|
||||
merged: None,
|
||||
locked: None,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
/// Count merge requests with state breakdown.
|
||||
fn count_mrs(conn: &Connection) -> Result<CountResult> {
|
||||
let count: i64 = conn.query_row("SELECT COUNT(*) FROM merge_requests", [], |row| row.get(0))?;
|
||||
|
||||
let opened: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM merge_requests WHERE state = 'opened'",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
|
||||
let merged: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM merge_requests WHERE state = 'merged'",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
|
||||
let closed: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM merge_requests WHERE state = 'closed'",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
|
||||
let locked: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM merge_requests WHERE state = 'locked'",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
|
||||
Ok(CountResult {
|
||||
entity: "Merge Requests".to_string(),
|
||||
count,
|
||||
system_count: None,
|
||||
state_breakdown: Some(StateBreakdown {
|
||||
opened,
|
||||
closed,
|
||||
merged: Some(merged),
|
||||
locked: Some(locked),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -81,6 +144,7 @@ fn count_discussions(conn: &Connection, type_filter: Option<&str>) -> Result<Cou
|
||||
entity: entity_name.to_string(),
|
||||
count,
|
||||
system_count: None,
|
||||
state_breakdown: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -126,6 +190,7 @@ fn count_notes(conn: &Connection, type_filter: Option<&str>) -> Result<CountResu
|
||||
entity: entity_name.to_string(),
|
||||
count: non_system,
|
||||
system_count: Some(system_count),
|
||||
state_breakdown: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -145,6 +210,55 @@ fn format_number(n: i64) -> String {
|
||||
result
|
||||
}
|
||||
|
||||
/// JSON output structure for count command.
|
||||
#[derive(Serialize)]
|
||||
struct CountJsonOutput {
|
||||
ok: bool,
|
||||
data: CountJsonData,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CountJsonData {
|
||||
entity: String,
|
||||
count: i64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
system_excluded: Option<i64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
breakdown: Option<CountJsonBreakdown>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CountJsonBreakdown {
|
||||
opened: i64,
|
||||
closed: i64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
merged: Option<i64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
locked: Option<i64>,
|
||||
}
|
||||
|
||||
/// Print count result as JSON (robot mode).
|
||||
pub fn print_count_json(result: &CountResult) {
|
||||
let breakdown = result.state_breakdown.as_ref().map(|b| CountJsonBreakdown {
|
||||
opened: b.opened,
|
||||
closed: b.closed,
|
||||
merged: b.merged,
|
||||
locked: b.locked.filter(|&l| l > 0),
|
||||
});
|
||||
|
||||
let output = CountJsonOutput {
|
||||
ok: true,
|
||||
data: CountJsonData {
|
||||
entity: result.entity.to_lowercase().replace(' ', "_"),
|
||||
count: result.count,
|
||||
system_excluded: result.system_count,
|
||||
breakdown,
|
||||
},
|
||||
};
|
||||
|
||||
println!("{}", serde_json::to_string(&output).unwrap());
|
||||
}
|
||||
|
||||
/// Print count result.
|
||||
pub fn print_count(result: &CountResult) {
|
||||
let count_str = format_number(result.count);
|
||||
@@ -153,7 +267,7 @@ pub fn print_count(result: &CountResult) {
|
||||
println!(
|
||||
"{}: {} {}",
|
||||
style(&result.entity).cyan(),
|
||||
style(count_str).bold(),
|
||||
style(&count_str).bold(),
|
||||
style(format!(
|
||||
"(excluding {} system)",
|
||||
format_number(system_count)
|
||||
@@ -164,9 +278,23 @@ pub fn print_count(result: &CountResult) {
|
||||
println!(
|
||||
"{}: {}",
|
||||
style(&result.entity).cyan(),
|
||||
style(count_str).bold()
|
||||
style(&count_str).bold()
|
||||
);
|
||||
}
|
||||
|
||||
// Print state breakdown if available
|
||||
if let Some(breakdown) = &result.state_breakdown {
|
||||
println!(" opened: {}", format_number(breakdown.opened));
|
||||
if let Some(merged) = breakdown.merged {
|
||||
println!(" merged: {}", format_number(merged));
|
||||
}
|
||||
println!(" closed: {}", format_number(breakdown.closed));
|
||||
if let Some(locked) = breakdown.locked
|
||||
&& locked > 0
|
||||
{
|
||||
println!(" locked: {}", format_number(locked));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user