refactor(structure): reorganize codebase into domain-focused modules
This commit is contained in:
580
src/cli/commands/show/render.rs
Normal file
580
src/cli/commands/show/render.rs
Normal file
@@ -0,0 +1,580 @@
|
||||
fn format_date(ms: i64) -> String {
|
||||
render::format_date(ms)
|
||||
}
|
||||
|
||||
fn wrap_text(text: &str, width: usize, indent: &str) -> String {
|
||||
render::wrap_indent(text, width, indent)
|
||||
}
|
||||
|
||||
pub fn print_show_issue(issue: &IssueDetail) {
|
||||
// Title line
|
||||
println!(
|
||||
" Issue #{}: {}",
|
||||
issue.iid,
|
||||
Theme::bold().render(&issue.title),
|
||||
);
|
||||
|
||||
// Details section
|
||||
println!("{}", render::section_divider("Details"));
|
||||
|
||||
println!(
|
||||
" Ref {}",
|
||||
Theme::muted().render(&issue.references_full)
|
||||
);
|
||||
println!(
|
||||
" Project {}",
|
||||
Theme::info().render(&issue.project_path)
|
||||
);
|
||||
|
||||
let (icon, state_style) = if issue.state == "opened" {
|
||||
(Icons::issue_opened(), Theme::success())
|
||||
} else {
|
||||
(Icons::issue_closed(), Theme::dim())
|
||||
};
|
||||
println!(
|
||||
" State {}",
|
||||
state_style.render(&format!("{icon} {}", issue.state))
|
||||
);
|
||||
|
||||
if let Some(status) = &issue.status_name {
|
||||
println!(
|
||||
" Status {}",
|
||||
render::style_with_hex(status, issue.status_color.as_deref())
|
||||
);
|
||||
}
|
||||
|
||||
if issue.confidential {
|
||||
println!(" {}", Theme::error().bold().render("CONFIDENTIAL"));
|
||||
}
|
||||
|
||||
println!(" Author @{}", issue.author_username);
|
||||
|
||||
if !issue.assignees.is_empty() {
|
||||
let label = if issue.assignees.len() > 1 {
|
||||
"Assignees"
|
||||
} else {
|
||||
"Assignee"
|
||||
};
|
||||
println!(
|
||||
" {}{} {}",
|
||||
label,
|
||||
" ".repeat(12 - label.len()),
|
||||
issue
|
||||
.assignees
|
||||
.iter()
|
||||
.map(|a| format!("@{a}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
);
|
||||
}
|
||||
|
||||
println!(
|
||||
" Created {} ({})",
|
||||
format_date(issue.created_at),
|
||||
render::format_relative_time_compact(issue.created_at),
|
||||
);
|
||||
println!(
|
||||
" Updated {} ({})",
|
||||
format_date(issue.updated_at),
|
||||
render::format_relative_time_compact(issue.updated_at),
|
||||
);
|
||||
|
||||
if let Some(closed_at) = &issue.closed_at {
|
||||
println!(" Closed {closed_at}");
|
||||
}
|
||||
|
||||
if let Some(due) = &issue.due_date {
|
||||
println!(" Due {due}");
|
||||
}
|
||||
|
||||
if let Some(ms) = &issue.milestone {
|
||||
println!(" Milestone {ms}");
|
||||
}
|
||||
|
||||
if !issue.labels.is_empty() {
|
||||
println!(
|
||||
" Labels {}",
|
||||
render::format_labels_bare(&issue.labels, issue.labels.len())
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(url) = &issue.web_url {
|
||||
println!(" URL {}", Theme::muted().render(url));
|
||||
}
|
||||
|
||||
// Development section
|
||||
if !issue.closing_merge_requests.is_empty() {
|
||||
println!("{}", render::section_divider("Development"));
|
||||
for mr in &issue.closing_merge_requests {
|
||||
let (mr_icon, mr_style) = match mr.state.as_str() {
|
||||
"merged" => (Icons::mr_merged(), Theme::accent()),
|
||||
"opened" => (Icons::mr_opened(), Theme::success()),
|
||||
"closed" => (Icons::mr_closed(), Theme::error()),
|
||||
_ => (Icons::mr_opened(), Theme::dim()),
|
||||
};
|
||||
println!(
|
||||
" {} !{} {} {}",
|
||||
mr_style.render(mr_icon),
|
||||
mr.iid,
|
||||
mr.title,
|
||||
mr_style.render(&mr.state),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Description section
|
||||
println!("{}", render::section_divider("Description"));
|
||||
if let Some(desc) = &issue.description {
|
||||
let wrapped = wrap_text(desc, 72, " ");
|
||||
println!(" {wrapped}");
|
||||
} else {
|
||||
println!(" {}", Theme::muted().render("(no description)"));
|
||||
}
|
||||
|
||||
// Discussions section
|
||||
let user_discussions: Vec<&DiscussionDetail> = issue
|
||||
.discussions
|
||||
.iter()
|
||||
.filter(|d| d.notes.iter().any(|n| !n.is_system))
|
||||
.collect();
|
||||
|
||||
if user_discussions.is_empty() {
|
||||
println!("\n {}", Theme::muted().render("No discussions"));
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
render::section_divider(&format!("Discussions ({})", user_discussions.len()))
|
||||
);
|
||||
|
||||
for discussion in user_discussions {
|
||||
let user_notes: Vec<&NoteDetail> =
|
||||
discussion.notes.iter().filter(|n| !n.is_system).collect();
|
||||
|
||||
if let Some(first_note) = user_notes.first() {
|
||||
println!(
|
||||
" {} {}",
|
||||
Theme::info().render(&format!("@{}", first_note.author_username)),
|
||||
format_date(first_note.created_at),
|
||||
);
|
||||
let wrapped = wrap_text(&first_note.body, 68, " ");
|
||||
println!(" {wrapped}");
|
||||
println!();
|
||||
|
||||
for reply in user_notes.iter().skip(1) {
|
||||
println!(
|
||||
" {} {}",
|
||||
Theme::info().render(&format!("@{}", reply.author_username)),
|
||||
format_date(reply.created_at),
|
||||
);
|
||||
let wrapped = wrap_text(&reply.body, 66, " ");
|
||||
println!(" {wrapped}");
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_show_mr(mr: &MrDetail) {
|
||||
// Title line
|
||||
let draft_prefix = if mr.draft {
|
||||
format!("{} ", Icons::mr_draft())
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
println!(
|
||||
" MR !{}: {}{}",
|
||||
mr.iid,
|
||||
draft_prefix,
|
||||
Theme::bold().render(&mr.title),
|
||||
);
|
||||
|
||||
// Details section
|
||||
println!("{}", render::section_divider("Details"));
|
||||
|
||||
println!(" Project {}", Theme::info().render(&mr.project_path));
|
||||
|
||||
let (icon, state_style) = match mr.state.as_str() {
|
||||
"opened" => (Icons::mr_opened(), Theme::success()),
|
||||
"merged" => (Icons::mr_merged(), Theme::accent()),
|
||||
"closed" => (Icons::mr_closed(), Theme::error()),
|
||||
_ => (Icons::mr_opened(), Theme::dim()),
|
||||
};
|
||||
println!(
|
||||
" State {}",
|
||||
state_style.render(&format!("{icon} {}", mr.state))
|
||||
);
|
||||
|
||||
println!(
|
||||
" Branches {} -> {}",
|
||||
Theme::info().render(&mr.source_branch),
|
||||
Theme::warning().render(&mr.target_branch)
|
||||
);
|
||||
|
||||
println!(" Author @{}", mr.author_username);
|
||||
|
||||
if !mr.assignees.is_empty() {
|
||||
println!(
|
||||
" Assignees {}",
|
||||
mr.assignees
|
||||
.iter()
|
||||
.map(|a| format!("@{a}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
);
|
||||
}
|
||||
|
||||
if !mr.reviewers.is_empty() {
|
||||
println!(
|
||||
" Reviewers {}",
|
||||
mr.reviewers
|
||||
.iter()
|
||||
.map(|r| format!("@{r}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
);
|
||||
}
|
||||
|
||||
println!(
|
||||
" Created {} ({})",
|
||||
format_date(mr.created_at),
|
||||
render::format_relative_time_compact(mr.created_at),
|
||||
);
|
||||
println!(
|
||||
" Updated {} ({})",
|
||||
format_date(mr.updated_at),
|
||||
render::format_relative_time_compact(mr.updated_at),
|
||||
);
|
||||
|
||||
if let Some(merged_at) = mr.merged_at {
|
||||
println!(
|
||||
" Merged {} ({})",
|
||||
format_date(merged_at),
|
||||
render::format_relative_time_compact(merged_at),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(closed_at) = mr.closed_at {
|
||||
println!(
|
||||
" Closed {} ({})",
|
||||
format_date(closed_at),
|
||||
render::format_relative_time_compact(closed_at),
|
||||
);
|
||||
}
|
||||
|
||||
if !mr.labels.is_empty() {
|
||||
println!(
|
||||
" Labels {}",
|
||||
render::format_labels_bare(&mr.labels, mr.labels.len())
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(url) = &mr.web_url {
|
||||
println!(" URL {}", Theme::muted().render(url));
|
||||
}
|
||||
|
||||
// Description section
|
||||
println!("{}", render::section_divider("Description"));
|
||||
if let Some(desc) = &mr.description {
|
||||
let wrapped = wrap_text(desc, 72, " ");
|
||||
println!(" {wrapped}");
|
||||
} else {
|
||||
println!(" {}", Theme::muted().render("(no description)"));
|
||||
}
|
||||
|
||||
// Discussions section
|
||||
let user_discussions: Vec<&MrDiscussionDetail> = mr
|
||||
.discussions
|
||||
.iter()
|
||||
.filter(|d| d.notes.iter().any(|n| !n.is_system))
|
||||
.collect();
|
||||
|
||||
if user_discussions.is_empty() {
|
||||
println!("\n {}", Theme::muted().render("No discussions"));
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
render::section_divider(&format!("Discussions ({})", user_discussions.len()))
|
||||
);
|
||||
|
||||
for discussion in user_discussions {
|
||||
let user_notes: Vec<&MrNoteDetail> =
|
||||
discussion.notes.iter().filter(|n| !n.is_system).collect();
|
||||
|
||||
if let Some(first_note) = user_notes.first() {
|
||||
if let Some(pos) = &first_note.position {
|
||||
print_diff_position(pos);
|
||||
}
|
||||
|
||||
println!(
|
||||
" {} {}",
|
||||
Theme::info().render(&format!("@{}", first_note.author_username)),
|
||||
format_date(first_note.created_at),
|
||||
);
|
||||
let wrapped = wrap_text(&first_note.body, 68, " ");
|
||||
println!(" {wrapped}");
|
||||
println!();
|
||||
|
||||
for reply in user_notes.iter().skip(1) {
|
||||
println!(
|
||||
" {} {}",
|
||||
Theme::info().render(&format!("@{}", reply.author_username)),
|
||||
format_date(reply.created_at),
|
||||
);
|
||||
let wrapped = wrap_text(&reply.body, 66, " ");
|
||||
println!(" {wrapped}");
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_diff_position(pos: &DiffNotePosition) {
|
||||
let file = pos.new_path.as_ref().or(pos.old_path.as_ref());
|
||||
|
||||
if let Some(file_path) = file {
|
||||
let line_str = match (pos.old_line, pos.new_line) {
|
||||
(Some(old), Some(new)) if old == new => format!(":{}", new),
|
||||
(Some(old), Some(new)) => format!(":{}→{}", old, new),
|
||||
(None, Some(new)) => format!(":+{}", new),
|
||||
(Some(old), None) => format!(":-{}", old),
|
||||
(None, None) => String::new(),
|
||||
};
|
||||
|
||||
println!(
|
||||
" {} {}{}",
|
||||
Theme::dim().render("\u{1f4cd}"),
|
||||
Theme::warning().render(file_path),
|
||||
Theme::dim().render(&line_str)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct IssueDetailJson {
|
||||
pub id: i64,
|
||||
pub iid: i64,
|
||||
pub title: String,
|
||||
pub description: Option<String>,
|
||||
pub state: String,
|
||||
pub author_username: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub closed_at: Option<String>,
|
||||
pub confidential: bool,
|
||||
pub web_url: Option<String>,
|
||||
pub project_path: String,
|
||||
pub references_full: String,
|
||||
pub labels: Vec<String>,
|
||||
pub assignees: Vec<String>,
|
||||
pub due_date: Option<String>,
|
||||
pub milestone: Option<String>,
|
||||
pub user_notes_count: i64,
|
||||
pub merge_requests_count: usize,
|
||||
pub closing_merge_requests: Vec<ClosingMrRefJson>,
|
||||
pub discussions: Vec<DiscussionDetailJson>,
|
||||
pub status_name: Option<String>,
|
||||
#[serde(skip_serializing)]
|
||||
pub status_category: Option<String>,
|
||||
pub status_color: Option<String>,
|
||||
pub status_icon_name: Option<String>,
|
||||
pub status_synced_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ClosingMrRefJson {
|
||||
pub iid: i64,
|
||||
pub title: String,
|
||||
pub state: String,
|
||||
pub web_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct DiscussionDetailJson {
|
||||
pub notes: Vec<NoteDetailJson>,
|
||||
pub individual_note: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct NoteDetailJson {
|
||||
pub author_username: String,
|
||||
pub body: String,
|
||||
pub created_at: String,
|
||||
pub is_system: bool,
|
||||
}
|
||||
|
||||
impl From<&IssueDetail> for IssueDetailJson {
|
||||
fn from(issue: &IssueDetail) -> Self {
|
||||
Self {
|
||||
id: issue.id,
|
||||
iid: issue.iid,
|
||||
title: issue.title.clone(),
|
||||
description: issue.description.clone(),
|
||||
state: issue.state.clone(),
|
||||
author_username: issue.author_username.clone(),
|
||||
created_at: ms_to_iso(issue.created_at),
|
||||
updated_at: ms_to_iso(issue.updated_at),
|
||||
closed_at: issue.closed_at.clone(),
|
||||
confidential: issue.confidential,
|
||||
web_url: issue.web_url.clone(),
|
||||
project_path: issue.project_path.clone(),
|
||||
references_full: issue.references_full.clone(),
|
||||
labels: issue.labels.clone(),
|
||||
assignees: issue.assignees.clone(),
|
||||
due_date: issue.due_date.clone(),
|
||||
milestone: issue.milestone.clone(),
|
||||
user_notes_count: issue.user_notes_count,
|
||||
merge_requests_count: issue.merge_requests_count,
|
||||
closing_merge_requests: issue
|
||||
.closing_merge_requests
|
||||
.iter()
|
||||
.map(|mr| ClosingMrRefJson {
|
||||
iid: mr.iid,
|
||||
title: mr.title.clone(),
|
||||
state: mr.state.clone(),
|
||||
web_url: mr.web_url.clone(),
|
||||
})
|
||||
.collect(),
|
||||
discussions: issue.discussions.iter().map(|d| d.into()).collect(),
|
||||
status_name: issue.status_name.clone(),
|
||||
status_category: issue.status_category.clone(),
|
||||
status_color: issue.status_color.clone(),
|
||||
status_icon_name: issue.status_icon_name.clone(),
|
||||
status_synced_at: issue.status_synced_at.map(ms_to_iso),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&DiscussionDetail> for DiscussionDetailJson {
|
||||
fn from(disc: &DiscussionDetail) -> Self {
|
||||
Self {
|
||||
notes: disc.notes.iter().map(|n| n.into()).collect(),
|
||||
individual_note: disc.individual_note,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&NoteDetail> for NoteDetailJson {
|
||||
fn from(note: &NoteDetail) -> Self {
|
||||
Self {
|
||||
author_username: note.author_username.clone(),
|
||||
body: note.body.clone(),
|
||||
created_at: ms_to_iso(note.created_at),
|
||||
is_system: note.is_system,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct MrDetailJson {
|
||||
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: String,
|
||||
pub updated_at: String,
|
||||
pub merged_at: Option<String>,
|
||||
pub closed_at: Option<String>,
|
||||
pub web_url: Option<String>,
|
||||
pub project_path: String,
|
||||
pub labels: Vec<String>,
|
||||
pub assignees: Vec<String>,
|
||||
pub reviewers: Vec<String>,
|
||||
pub discussions: Vec<MrDiscussionDetailJson>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct MrDiscussionDetailJson {
|
||||
pub notes: Vec<MrNoteDetailJson>,
|
||||
pub individual_note: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct MrNoteDetailJson {
|
||||
pub author_username: String,
|
||||
pub body: String,
|
||||
pub created_at: String,
|
||||
pub is_system: bool,
|
||||
pub position: Option<DiffNotePosition>,
|
||||
}
|
||||
|
||||
impl From<&MrDetail> for MrDetailJson {
|
||||
fn from(mr: &MrDetail) -> Self {
|
||||
Self {
|
||||
id: mr.id,
|
||||
iid: mr.iid,
|
||||
title: mr.title.clone(),
|
||||
description: mr.description.clone(),
|
||||
state: mr.state.clone(),
|
||||
draft: mr.draft,
|
||||
author_username: mr.author_username.clone(),
|
||||
source_branch: mr.source_branch.clone(),
|
||||
target_branch: mr.target_branch.clone(),
|
||||
created_at: ms_to_iso(mr.created_at),
|
||||
updated_at: ms_to_iso(mr.updated_at),
|
||||
merged_at: mr.merged_at.map(ms_to_iso),
|
||||
closed_at: mr.closed_at.map(ms_to_iso),
|
||||
web_url: mr.web_url.clone(),
|
||||
project_path: mr.project_path.clone(),
|
||||
labels: mr.labels.clone(),
|
||||
assignees: mr.assignees.clone(),
|
||||
reviewers: mr.reviewers.clone(),
|
||||
discussions: mr.discussions.iter().map(|d| d.into()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&MrDiscussionDetail> for MrDiscussionDetailJson {
|
||||
fn from(disc: &MrDiscussionDetail) -> Self {
|
||||
Self {
|
||||
notes: disc.notes.iter().map(|n| n.into()).collect(),
|
||||
individual_note: disc.individual_note,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&MrNoteDetail> for MrNoteDetailJson {
|
||||
fn from(note: &MrNoteDetail) -> Self {
|
||||
Self {
|
||||
author_username: note.author_username.clone(),
|
||||
body: note.body.clone(),
|
||||
created_at: ms_to_iso(note.created_at),
|
||||
is_system: note.is_system,
|
||||
position: note.position.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_show_issue_json(issue: &IssueDetail, elapsed_ms: u64) {
|
||||
let json_result = IssueDetailJson::from(issue);
|
||||
let meta = RobotMeta { elapsed_ms };
|
||||
let output = serde_json::json!({
|
||||
"ok": true,
|
||||
"data": json_result,
|
||||
"meta": meta,
|
||||
});
|
||||
match serde_json::to_string(&output) {
|
||||
Ok(json) => println!("{json}"),
|
||||
Err(e) => eprintln!("Error serializing to JSON: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_show_mr_json(mr: &MrDetail, elapsed_ms: u64) {
|
||||
let json_result = MrDetailJson::from(mr);
|
||||
let meta = RobotMeta { elapsed_ms };
|
||||
let output = serde_json::json!({
|
||||
"ok": true,
|
||||
"data": json_result,
|
||||
"meta": meta,
|
||||
});
|
||||
match serde_json::to_string(&output) {
|
||||
Ok(json) => println!("{json}"),
|
||||
Err(e) => eprintln!("Error serializing to JSON: {e}"),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user