#[derive(Debug, Serialize)] pub struct MrDetail { pub id: i64, pub iid: i64, pub title: String, pub description: Option, pub state: String, pub draft: bool, pub author_username: String, pub source_branch: String, pub target_branch: String, pub created_at: i64, pub updated_at: i64, pub merged_at: Option, pub closed_at: Option, pub web_url: Option, pub project_path: String, pub labels: Vec, pub assignees: Vec, pub reviewers: Vec, pub discussions: Vec, } #[derive(Debug, Serialize)] pub struct MrDiscussionDetail { pub notes: Vec, pub individual_note: bool, } #[derive(Debug, Serialize)] pub struct MrNoteDetail { pub author_username: String, pub body: String, pub created_at: i64, pub is_system: bool, pub position: Option, } #[derive(Debug, Clone, Serialize)] pub struct DiffNotePosition { pub old_path: Option, pub new_path: Option, pub old_line: Option, pub new_line: Option, pub position_type: Option, } pub fn run_show_mr(config: &Config, iid: i64, project_filter: Option<&str>) -> Result { let db_path = get_db_path(config.storage.db_path.as_deref()); let conn = create_connection(&db_path)?; let mr = find_mr(&conn, iid, project_filter)?; let labels = get_mr_labels(&conn, mr.id)?; let assignees = get_mr_assignees(&conn, mr.id)?; let reviewers = get_mr_reviewers(&conn, mr.id)?; let discussions = get_mr_discussions(&conn, mr.id)?; Ok(MrDetail { id: mr.id, iid: mr.iid, title: mr.title, description: mr.description, state: mr.state, draft: mr.draft, author_username: mr.author_username, source_branch: mr.source_branch, target_branch: mr.target_branch, created_at: mr.created_at, updated_at: mr.updated_at, merged_at: mr.merged_at, closed_at: mr.closed_at, web_url: mr.web_url, project_path: mr.project_path, labels, assignees, reviewers, discussions, }) } struct MrRow { id: i64, iid: i64, title: String, description: Option, state: String, draft: bool, author_username: String, source_branch: String, target_branch: String, created_at: i64, updated_at: i64, merged_at: Option, closed_at: Option, web_url: Option, project_path: String, } fn find_mr(conn: &Connection, iid: i64, project_filter: Option<&str>) -> Result { let (sql, params): (&str, Vec>) = match project_filter { Some(project) => { let project_id = resolve_project(conn, project)?; ( "SELECT m.id, m.iid, m.title, m.description, m.state, m.draft, m.author_username, m.source_branch, m.target_branch, m.created_at, m.updated_at, m.merged_at, m.closed_at, m.web_url, p.path_with_namespace FROM merge_requests m JOIN projects p ON m.project_id = p.id WHERE m.iid = ? AND m.project_id = ?", vec![Box::new(iid), Box::new(project_id)], ) } None => ( "SELECT m.id, m.iid, m.title, m.description, m.state, m.draft, m.author_username, m.source_branch, m.target_branch, m.created_at, m.updated_at, m.merged_at, m.closed_at, m.web_url, p.path_with_namespace FROM merge_requests m JOIN projects p ON m.project_id = p.id WHERE m.iid = ?", vec![Box::new(iid)], ), }; let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect(); let mut stmt = conn.prepare(sql)?; let mrs: Vec = stmt .query_map(param_refs.as_slice(), |row| { let draft_val: i64 = row.get(5)?; Ok(MrRow { id: row.get(0)?, iid: row.get(1)?, title: row.get(2)?, description: row.get(3)?, state: row.get(4)?, draft: draft_val == 1, author_username: row.get(6)?, source_branch: row.get(7)?, target_branch: row.get(8)?, created_at: row.get(9)?, updated_at: row.get(10)?, merged_at: row.get(11)?, closed_at: row.get(12)?, web_url: row.get(13)?, project_path: row.get(14)?, }) })? .collect::, _>>()?; match mrs.len() { 0 => Err(LoreError::NotFound(format!("MR !{} not found", iid))), 1 => Ok(mrs.into_iter().next().unwrap()), _ => { let projects: Vec = mrs.iter().map(|m| m.project_path.clone()).collect(); Err(LoreError::Ambiguous(format!( "MR !{} exists in multiple projects: {}. Use --project to specify.", iid, projects.join(", ") ))) } } } fn get_mr_labels(conn: &Connection, mr_id: i64) -> Result> { let mut stmt = conn.prepare( "SELECT l.name FROM labels l JOIN mr_labels ml ON l.id = ml.label_id WHERE ml.merge_request_id = ? ORDER BY l.name", )?; let labels: Vec = stmt .query_map([mr_id], |row| row.get(0))? .collect::, _>>()?; Ok(labels) } fn get_mr_assignees(conn: &Connection, mr_id: i64) -> Result> { let mut stmt = conn.prepare( "SELECT username FROM mr_assignees WHERE merge_request_id = ? ORDER BY username", )?; let assignees: Vec = stmt .query_map([mr_id], |row| row.get(0))? .collect::, _>>()?; Ok(assignees) } fn get_mr_reviewers(conn: &Connection, mr_id: i64) -> Result> { let mut stmt = conn.prepare( "SELECT username FROM mr_reviewers WHERE merge_request_id = ? ORDER BY username", )?; let reviewers: Vec = stmt .query_map([mr_id], |row| row.get(0))? .collect::, _>>()?; Ok(reviewers) } fn get_mr_discussions(conn: &Connection, mr_id: i64) -> Result> { let mut disc_stmt = conn.prepare( "SELECT id, individual_note FROM discussions WHERE merge_request_id = ? ORDER BY first_note_at", )?; let disc_rows: Vec<(i64, bool)> = disc_stmt .query_map([mr_id], |row| { let individual: i64 = row.get(1)?; Ok((row.get(0)?, individual == 1)) })? .collect::, _>>()?; let mut note_stmt = conn.prepare( "SELECT author_username, body, created_at, is_system, position_old_path, position_new_path, position_old_line, position_new_line, position_type FROM notes WHERE discussion_id = ? ORDER BY position", )?; let mut discussions = Vec::new(); for (disc_id, individual_note) in disc_rows { let notes: Vec = note_stmt .query_map([disc_id], |row| { let is_system: i64 = row.get(3)?; let old_path: Option = row.get(4)?; let new_path: Option = row.get(5)?; let old_line: Option = row.get(6)?; let new_line: Option = row.get(7)?; let position_type: Option = row.get(8)?; let position = if old_path.is_some() || new_path.is_some() || old_line.is_some() || new_line.is_some() { Some(DiffNotePosition { old_path, new_path, old_line, new_line, position_type, }) } else { None }; Ok(MrNoteDetail { author_username: row.get(0)?, body: row.get(1)?, created_at: row.get(2)?, is_system: is_system == 1, position, }) })? .collect::, _>>()?; let has_user_notes = notes.iter().any(|n| !n.is_system); if has_user_notes || notes.is_empty() { discussions.push(MrDiscussionDetail { notes, individual_note, }); } } Ok(discussions) }