284 lines
8.8 KiB
Rust
284 lines
8.8 KiB
Rust
#[derive(Debug, Serialize)]
|
|
pub struct MrDetail {
|
|
pub id: i64,
|
|
pub iid: i64,
|
|
pub title: String,
|
|
pub description: Option<String>,
|
|
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<i64>,
|
|
pub closed_at: Option<i64>,
|
|
pub web_url: Option<String>,
|
|
pub project_path: String,
|
|
pub labels: Vec<String>,
|
|
pub assignees: Vec<String>,
|
|
pub reviewers: Vec<String>,
|
|
pub discussions: Vec<MrDiscussionDetail>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct MrDiscussionDetail {
|
|
pub notes: Vec<MrNoteDetail>,
|
|
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<DiffNotePosition>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct DiffNotePosition {
|
|
pub old_path: Option<String>,
|
|
pub new_path: Option<String>,
|
|
pub old_line: Option<i64>,
|
|
pub new_line: Option<i64>,
|
|
pub position_type: Option<String>,
|
|
}
|
|
pub fn run_show_mr(config: &Config, iid: i64, project_filter: Option<&str>) -> Result<MrDetail> {
|
|
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<String>,
|
|
state: String,
|
|
draft: bool,
|
|
author_username: String,
|
|
source_branch: String,
|
|
target_branch: String,
|
|
created_at: i64,
|
|
updated_at: i64,
|
|
merged_at: Option<i64>,
|
|
closed_at: Option<i64>,
|
|
web_url: Option<String>,
|
|
project_path: String,
|
|
}
|
|
|
|
fn find_mr(conn: &Connection, iid: i64, project_filter: Option<&str>) -> Result<MrRow> {
|
|
let (sql, params): (&str, Vec<Box<dyn rusqlite::ToSql>>) = 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<MrRow> = 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::<std::result::Result<Vec<_>, _>>()?;
|
|
|
|
match mrs.len() {
|
|
0 => Err(LoreError::NotFound(format!("MR !{} not found", iid))),
|
|
1 => Ok(mrs.into_iter().next().unwrap()),
|
|
_ => {
|
|
let projects: Vec<String> = 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<Vec<String>> {
|
|
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<String> = stmt
|
|
.query_map([mr_id], |row| row.get(0))?
|
|
.collect::<std::result::Result<Vec<_>, _>>()?;
|
|
|
|
Ok(labels)
|
|
}
|
|
|
|
fn get_mr_assignees(conn: &Connection, mr_id: i64) -> Result<Vec<String>> {
|
|
let mut stmt = conn.prepare(
|
|
"SELECT username FROM mr_assignees
|
|
WHERE merge_request_id = ?
|
|
ORDER BY username",
|
|
)?;
|
|
|
|
let assignees: Vec<String> = stmt
|
|
.query_map([mr_id], |row| row.get(0))?
|
|
.collect::<std::result::Result<Vec<_>, _>>()?;
|
|
|
|
Ok(assignees)
|
|
}
|
|
|
|
fn get_mr_reviewers(conn: &Connection, mr_id: i64) -> Result<Vec<String>> {
|
|
let mut stmt = conn.prepare(
|
|
"SELECT username FROM mr_reviewers
|
|
WHERE merge_request_id = ?
|
|
ORDER BY username",
|
|
)?;
|
|
|
|
let reviewers: Vec<String> = stmt
|
|
.query_map([mr_id], |row| row.get(0))?
|
|
.collect::<std::result::Result<Vec<_>, _>>()?;
|
|
|
|
Ok(reviewers)
|
|
}
|
|
|
|
fn get_mr_discussions(conn: &Connection, mr_id: i64) -> Result<Vec<MrDiscussionDetail>> {
|
|
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::<std::result::Result<Vec<_>, _>>()?;
|
|
|
|
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<MrNoteDetail> = note_stmt
|
|
.query_map([disc_id], |row| {
|
|
let is_system: i64 = row.get(3)?;
|
|
let old_path: Option<String> = row.get(4)?;
|
|
let new_path: Option<String> = row.get(5)?;
|
|
let old_line: Option<i64> = row.get(6)?;
|
|
let new_line: Option<i64> = row.get(7)?;
|
|
let position_type: Option<String> = 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::<std::result::Result<Vec<_>, _>>()?;
|
|
|
|
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)
|
|
}
|
|
|