refactor(commands): Add IngestDisplay, resolve_project, and color-aware tables
Ingest: - Introduce IngestDisplay struct with show_progress/show_text booleans to decouple progress bars from text output. Replaces the robot_mode bool parameter with explicit display control, enabling sync to show progress without duplicating summary text (progress_only mode). - Use resolve_project() for --project filtering instead of LIKE queries, providing proper error messages for ambiguous or missing projects. List: - Add colored_cell() helper that checks console::colors_enabled() before applying comfy-table foreground colors, bridging the gap between the console and comfy-table crates for --color flag support. - Use resolve_project() for project filtering (exact ID match). - Improve since filter to return explicit errors instead of silently ignoring invalid values. - Improve format_relative_time for proper singular/plural forms. Search: - Validate --after/--updated-after with explicit error messages. - Handle optional title field (Option<String>) in HydratedRow. Show: - Use resolve_project() for project disambiguation. Sync: - Thread robot_mode via SyncOptions for IngestDisplay selection. - Use IngestDisplay::progress_only() in interactive sync mode. GenerateDocs: - Use resolve_project() for --project filtering. Co-Authored-By: Claude (us.anthropic.claude-opus-4-5-20251101-v1:0) <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,8 @@ use tracing::info;
|
|||||||
use crate::core::db::create_connection;
|
use crate::core::db::create_connection;
|
||||||
use crate::core::error::Result;
|
use crate::core::error::Result;
|
||||||
use crate::core::paths::get_db_path;
|
use crate::core::paths::get_db_path;
|
||||||
use crate::documents::{regenerate_dirty_documents, SourceType};
|
use crate::core::project::resolve_project;
|
||||||
|
use crate::documents::{SourceType, regenerate_dirty_documents};
|
||||||
use crate::Config;
|
use crate::Config;
|
||||||
|
|
||||||
const FULL_MODE_CHUNK_SIZE: i64 = 2000;
|
const FULL_MODE_CHUNK_SIZE: i64 = 2000;
|
||||||
@@ -81,18 +82,7 @@ fn seed_dirty(
|
|||||||
|
|
||||||
loop {
|
loop {
|
||||||
let inserted = if let Some(project) = project_filter {
|
let inserted = if let Some(project) = project_filter {
|
||||||
// Resolve project to ID for filtering
|
let project_id = resolve_project(conn, project)?;
|
||||||
let project_id: Option<i64> = conn
|
|
||||||
.query_row(
|
|
||||||
"SELECT id FROM projects WHERE path_with_namespace = ?1 COLLATE NOCASE",
|
|
||||||
[project],
|
|
||||||
|row| row.get(0),
|
|
||||||
)
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
let Some(pid) = project_id else {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
&format!(
|
&format!(
|
||||||
@@ -101,7 +91,7 @@ fn seed_dirty(
|
|||||||
FROM {table} WHERE id > ?3 AND project_id = ?4 ORDER BY id LIMIT ?5
|
FROM {table} WHERE id > ?3 AND project_id = ?4 ORDER BY id LIMIT ?5
|
||||||
ON CONFLICT(source_type, source_id) DO NOTHING"
|
ON CONFLICT(source_type, source_id) DO NOTHING"
|
||||||
),
|
),
|
||||||
rusqlite::params![type_str, now, last_id, pid, FULL_MODE_CHUNK_SIZE],
|
rusqlite::params![type_str, now, last_id, project_id, FULL_MODE_CHUNK_SIZE],
|
||||||
)?
|
)?
|
||||||
} else {
|
} else {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use crate::core::db::create_connection;
|
|||||||
use crate::core::error::{LoreError, Result};
|
use crate::core::error::{LoreError, Result};
|
||||||
use crate::core::lock::{AppLock, LockOptions};
|
use crate::core::lock::{AppLock, LockOptions};
|
||||||
use crate::core::paths::get_db_path;
|
use crate::core::paths::get_db_path;
|
||||||
|
use crate::core::project::resolve_project;
|
||||||
use crate::gitlab::GitLabClient;
|
use crate::gitlab::GitLabClient;
|
||||||
use crate::ingestion::{
|
use crate::ingestion::{
|
||||||
IngestMrProjectResult, IngestProjectResult, ProgressEvent, ingest_project_issues_with_progress,
|
IngestMrProjectResult, IngestProjectResult, ProgressEvent, ingest_project_issues_with_progress,
|
||||||
@@ -40,6 +41,36 @@ pub struct IngestResult {
|
|||||||
pub notes_upserted: usize,
|
pub notes_upserted: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Controls what interactive UI elements `run_ingest` displays.
|
||||||
|
///
|
||||||
|
/// Separates progress indicators (spinners, bars) from text output (headers,
|
||||||
|
/// per-project summaries) so callers like `sync` can show progress without
|
||||||
|
/// duplicating summary text.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct IngestDisplay {
|
||||||
|
/// Show animated spinners and progress bars.
|
||||||
|
pub show_progress: bool,
|
||||||
|
/// Show text headers ("Ingesting...") and per-project summary lines.
|
||||||
|
pub show_text: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IngestDisplay {
|
||||||
|
/// Interactive mode: everything visible.
|
||||||
|
pub fn interactive() -> Self {
|
||||||
|
Self { show_progress: true, show_text: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Robot/JSON mode: everything hidden.
|
||||||
|
pub fn silent() -> Self {
|
||||||
|
Self { show_progress: false, show_text: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Progress only (used by sync in interactive mode).
|
||||||
|
pub fn progress_only() -> Self {
|
||||||
|
Self { show_progress: true, show_text: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Run the ingest command.
|
/// Run the ingest command.
|
||||||
pub async fn run_ingest(
|
pub async fn run_ingest(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
@@ -47,7 +78,7 @@ pub async fn run_ingest(
|
|||||||
project_filter: Option<&str>,
|
project_filter: Option<&str>,
|
||||||
force: bool,
|
force: bool,
|
||||||
full: bool,
|
full: bool,
|
||||||
robot_mode: bool,
|
display: IngestDisplay,
|
||||||
) -> Result<IngestResult> {
|
) -> Result<IngestResult> {
|
||||||
// Validate resource type early
|
// Validate resource type early
|
||||||
if resource_type != "issues" && resource_type != "mrs" {
|
if resource_type != "issues" && resource_type != "mrs" {
|
||||||
@@ -86,7 +117,7 @@ pub async fn run_ingest(
|
|||||||
|
|
||||||
// If --full flag is set, reset sync cursors and discussion watermarks for a complete re-fetch
|
// If --full flag is set, reset sync cursors and discussion watermarks for a complete re-fetch
|
||||||
if full {
|
if full {
|
||||||
if !robot_mode {
|
if display.show_text {
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
style("Full sync: resetting cursors to fetch all data...").yellow()
|
style("Full sync: resetting cursors to fetch all data...").yellow()
|
||||||
@@ -139,7 +170,7 @@ pub async fn run_ingest(
|
|||||||
} else {
|
} else {
|
||||||
"merge requests"
|
"merge requests"
|
||||||
};
|
};
|
||||||
if !robot_mode {
|
if display.show_text {
|
||||||
println!("{}", style(format!("Ingesting {type_label}...")).blue());
|
println!("{}", style(format!("Ingesting {type_label}...")).blue());
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
@@ -147,7 +178,7 @@ pub async fn run_ingest(
|
|||||||
// Sync each project
|
// Sync each project
|
||||||
for (local_project_id, gitlab_project_id, path) in &projects {
|
for (local_project_id, gitlab_project_id, path) in &projects {
|
||||||
// Show spinner while fetching (only in interactive mode)
|
// Show spinner while fetching (only in interactive mode)
|
||||||
let spinner = if robot_mode {
|
let spinner = if !display.show_progress {
|
||||||
ProgressBar::hidden()
|
ProgressBar::hidden()
|
||||||
} else {
|
} else {
|
||||||
let s = ProgressBar::new_spinner();
|
let s = ProgressBar::new_spinner();
|
||||||
@@ -162,7 +193,7 @@ pub async fn run_ingest(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Progress bar for discussion sync (hidden until needed, or always hidden in robot mode)
|
// Progress bar for discussion sync (hidden until needed, or always hidden in robot mode)
|
||||||
let disc_bar = if robot_mode {
|
let disc_bar = if !display.show_progress {
|
||||||
ProgressBar::hidden()
|
ProgressBar::hidden()
|
||||||
} else {
|
} else {
|
||||||
let b = ProgressBar::new(0);
|
let b = ProgressBar::new(0);
|
||||||
@@ -178,7 +209,7 @@ pub async fn run_ingest(
|
|||||||
// Create progress callback (no-op in robot mode)
|
// Create progress callback (no-op in robot mode)
|
||||||
let spinner_clone = spinner.clone();
|
let spinner_clone = spinner.clone();
|
||||||
let disc_bar_clone = disc_bar.clone();
|
let disc_bar_clone = disc_bar.clone();
|
||||||
let progress_callback: crate::ingestion::ProgressCallback = if robot_mode {
|
let progress_callback: crate::ingestion::ProgressCallback = if !display.show_progress {
|
||||||
Box::new(|_| {})
|
Box::new(|_| {})
|
||||||
} else {
|
} else {
|
||||||
Box::new(move |event: ProgressEvent| match event {
|
Box::new(move |event: ProgressEvent| match event {
|
||||||
@@ -225,7 +256,7 @@ pub async fn run_ingest(
|
|||||||
disc_bar.finish_and_clear();
|
disc_bar.finish_and_clear();
|
||||||
|
|
||||||
// Print per-project summary (only in interactive mode)
|
// Print per-project summary (only in interactive mode)
|
||||||
if !robot_mode {
|
if display.show_text {
|
||||||
print_issue_project_summary(path, &result);
|
print_issue_project_summary(path, &result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +285,7 @@ pub async fn run_ingest(
|
|||||||
disc_bar.finish_and_clear();
|
disc_bar.finish_and_clear();
|
||||||
|
|
||||||
// Print per-project summary (only in interactive mode)
|
// Print per-project summary (only in interactive mode)
|
||||||
if !robot_mode {
|
if display.show_text {
|
||||||
print_mr_project_summary(path, &result);
|
print_mr_project_summary(path, &result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,16 +314,39 @@ fn get_projects_to_sync(
|
|||||||
configured_projects: &[crate::core::config::ProjectConfig],
|
configured_projects: &[crate::core::config::ProjectConfig],
|
||||||
filter: Option<&str>,
|
filter: Option<&str>,
|
||||||
) -> Result<Vec<(i64, i64, String)>> {
|
) -> Result<Vec<(i64, i64, String)>> {
|
||||||
let mut projects = Vec::new();
|
// If a filter is provided, resolve it to a specific project
|
||||||
|
if let Some(filter_str) = filter {
|
||||||
|
let project_id = resolve_project(conn, filter_str)?;
|
||||||
|
|
||||||
for project_config in configured_projects {
|
// Verify the resolved project is in our config
|
||||||
if let Some(filter_path) = filter
|
let row: Option<(i64, String)> = conn
|
||||||
&& !project_config.path.contains(filter_path)
|
.query_row(
|
||||||
{
|
"SELECT gitlab_project_id, path_with_namespace FROM projects WHERE id = ?1",
|
||||||
continue;
|
[project_id],
|
||||||
|
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||||
|
)
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
if let Some((gitlab_id, path)) = row {
|
||||||
|
// Confirm it's a configured project
|
||||||
|
if configured_projects.iter().any(|p| p.path == path) {
|
||||||
|
return Ok(vec![(project_id, gitlab_id, path)]);
|
||||||
|
}
|
||||||
|
return Err(LoreError::Other(format!(
|
||||||
|
"Project '{}' exists in database but is not in configuration",
|
||||||
|
path
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get project from database
|
return Err(LoreError::Other(format!(
|
||||||
|
"Project '{}' not found in database",
|
||||||
|
filter_str
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// No filter: return all configured projects
|
||||||
|
let mut projects = Vec::new();
|
||||||
|
for project_config in configured_projects {
|
||||||
let result: Option<(i64, i64)> = conn
|
let result: Option<(i64, i64)> = conn
|
||||||
.query_row(
|
.query_row(
|
||||||
"SELECT id, gitlab_project_id FROM projects WHERE path_with_namespace = ?",
|
"SELECT id, gitlab_project_id FROM projects WHERE path_with_namespace = ?",
|
||||||
|
|||||||
@@ -6,10 +6,21 @@ use serde::Serialize;
|
|||||||
|
|
||||||
use crate::Config;
|
use crate::Config;
|
||||||
use crate::core::db::create_connection;
|
use crate::core::db::create_connection;
|
||||||
use crate::core::error::Result;
|
use crate::core::error::{LoreError, Result};
|
||||||
use crate::core::paths::get_db_path;
|
use crate::core::paths::get_db_path;
|
||||||
|
use crate::core::project::resolve_project;
|
||||||
use crate::core::time::{ms_to_iso, now_ms, parse_since};
|
use crate::core::time::{ms_to_iso, now_ms, parse_since};
|
||||||
|
|
||||||
|
/// Apply foreground color to a Cell only if colors are enabled.
|
||||||
|
fn colored_cell(content: impl std::fmt::Display, color: Color) -> Cell {
|
||||||
|
let cell = Cell::new(content);
|
||||||
|
if console::colors_enabled() {
|
||||||
|
cell.fg(color)
|
||||||
|
} else {
|
||||||
|
cell
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Issue row for display.
|
/// Issue row for display.
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct IssueListRow {
|
pub struct IssueListRow {
|
||||||
@@ -232,11 +243,9 @@ fn query_issues(conn: &Connection, filters: &ListFilters) -> Result<ListResult>
|
|||||||
let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
|
let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
|
||||||
|
|
||||||
if let Some(project) = filters.project {
|
if let Some(project) = filters.project {
|
||||||
// Exact match or suffix match after '/' to avoid partial matches
|
let project_id = resolve_project(conn, project)?;
|
||||||
// e.g. "foo" matches "group/foo" but NOT "group/foobar"
|
where_clauses.push("i.project_id = ?");
|
||||||
where_clauses.push("(p.path_with_namespace = ? OR p.path_with_namespace LIKE ?)");
|
params.push(Box::new(project_id));
|
||||||
params.push(Box::new(project.to_string()));
|
|
||||||
params.push(Box::new(format!("%/{project}")));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(state) = filters.state
|
if let Some(state) = filters.state
|
||||||
@@ -264,9 +273,13 @@ fn query_issues(conn: &Connection, filters: &ListFilters) -> Result<ListResult>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle since filter
|
// Handle since filter
|
||||||
if let Some(since_str) = filters.since
|
if let Some(since_str) = filters.since {
|
||||||
&& let Some(cutoff_ms) = parse_since(since_str)
|
let cutoff_ms = parse_since(since_str).ok_or_else(|| {
|
||||||
{
|
LoreError::Other(format!(
|
||||||
|
"Invalid --since value '{}'. Use relative (7d, 2w, 1m) or absolute (YYYY-MM-DD) format.",
|
||||||
|
since_str
|
||||||
|
))
|
||||||
|
})?;
|
||||||
where_clauses.push("i.updated_at >= ?");
|
where_clauses.push("i.updated_at >= ?");
|
||||||
params.push(Box::new(cutoff_ms));
|
params.push(Box::new(cutoff_ms));
|
||||||
}
|
}
|
||||||
@@ -419,11 +432,9 @@ fn query_mrs(conn: &Connection, filters: &MrListFilters) -> Result<MrListResult>
|
|||||||
let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
|
let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
|
||||||
|
|
||||||
if let Some(project) = filters.project {
|
if let Some(project) = filters.project {
|
||||||
// Exact match or suffix match after '/' to avoid partial matches
|
let project_id = resolve_project(conn, project)?;
|
||||||
// e.g. "foo" matches "group/foo" but NOT "group/foobar"
|
where_clauses.push("m.project_id = ?");
|
||||||
where_clauses.push("(p.path_with_namespace = ? OR p.path_with_namespace LIKE ?)");
|
params.push(Box::new(project_id));
|
||||||
params.push(Box::new(project.to_string()));
|
|
||||||
params.push(Box::new(format!("%/{project}")));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(state) = filters.state
|
if let Some(state) = filters.state
|
||||||
@@ -461,9 +472,13 @@ fn query_mrs(conn: &Connection, filters: &MrListFilters) -> Result<MrListResult>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle since filter
|
// Handle since filter
|
||||||
if let Some(since_str) = filters.since
|
if let Some(since_str) = filters.since {
|
||||||
&& let Some(cutoff_ms) = parse_since(since_str)
|
let cutoff_ms = parse_since(since_str).ok_or_else(|| {
|
||||||
{
|
LoreError::Other(format!(
|
||||||
|
"Invalid --since value '{}'. Use relative (7d, 2w, 1m) or absolute (YYYY-MM-DD) format.",
|
||||||
|
since_str
|
||||||
|
))
|
||||||
|
})?;
|
||||||
where_clauses.push("m.updated_at >= ?");
|
where_clauses.push("m.updated_at >= ?");
|
||||||
params.push(Box::new(cutoff_ms));
|
params.push(Box::new(cutoff_ms));
|
||||||
}
|
}
|
||||||
@@ -628,10 +643,22 @@ fn format_relative_time(ms_epoch: i64) -> String {
|
|||||||
match diff {
|
match diff {
|
||||||
d if d < 60_000 => "just now".to_string(),
|
d if d < 60_000 => "just now".to_string(),
|
||||||
d if d < 3_600_000 => format!("{} min ago", d / 60_000),
|
d if d < 3_600_000 => format!("{} min ago", d / 60_000),
|
||||||
d if d < 86_400_000 => format!("{} hours ago", d / 3_600_000),
|
d if d < 86_400_000 => {
|
||||||
d if d < 604_800_000 => format!("{} days ago", d / 86_400_000),
|
let n = d / 3_600_000;
|
||||||
d if d < 2_592_000_000 => format!("{} weeks ago", d / 604_800_000),
|
format!("{n} {} ago", if n == 1 { "hour" } else { "hours" })
|
||||||
_ => format!("{} months ago", diff / 2_592_000_000),
|
}
|
||||||
|
d if d < 604_800_000 => {
|
||||||
|
let n = d / 86_400_000;
|
||||||
|
format!("{n} {} ago", if n == 1 { "day" } else { "days" })
|
||||||
|
}
|
||||||
|
d if d < 2_592_000_000 => {
|
||||||
|
let n = d / 604_800_000;
|
||||||
|
format!("{n} {} ago", if n == 1 { "week" } else { "weeks" })
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let n = diff / 2_592_000_000;
|
||||||
|
format!("{n} {} ago", if n == 1 { "month" } else { "months" })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -735,19 +762,19 @@ pub fn print_list_issues(result: &ListResult) {
|
|||||||
let discussions = format_discussions(issue.discussion_count, issue.unresolved_count);
|
let discussions = format_discussions(issue.discussion_count, issue.unresolved_count);
|
||||||
|
|
||||||
let state_cell = if issue.state == "opened" {
|
let state_cell = if issue.state == "opened" {
|
||||||
Cell::new(&issue.state).fg(Color::Green)
|
colored_cell(&issue.state, Color::Green)
|
||||||
} else {
|
} else {
|
||||||
Cell::new(&issue.state).fg(Color::DarkGrey)
|
colored_cell(&issue.state, Color::DarkGrey)
|
||||||
};
|
};
|
||||||
|
|
||||||
table.add_row(vec![
|
table.add_row(vec![
|
||||||
Cell::new(format!("#{}", issue.iid)).fg(Color::Cyan),
|
colored_cell(format!("#{}", issue.iid), Color::Cyan),
|
||||||
Cell::new(title),
|
Cell::new(title),
|
||||||
state_cell,
|
state_cell,
|
||||||
Cell::new(assignee).fg(Color::Magenta),
|
colored_cell(assignee, Color::Magenta),
|
||||||
Cell::new(labels).fg(Color::Yellow),
|
colored_cell(labels, Color::Yellow),
|
||||||
Cell::new(discussions),
|
Cell::new(discussions),
|
||||||
Cell::new(relative_time).fg(Color::DarkGrey),
|
colored_cell(relative_time, Color::DarkGrey),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -807,7 +834,6 @@ pub fn print_list_mrs(result: &MrListResult) {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
for mr in &result.mrs {
|
for mr in &result.mrs {
|
||||||
// Add [DRAFT] prefix for draft MRs
|
|
||||||
let title = if mr.draft {
|
let title = if mr.draft {
|
||||||
format!("[DRAFT] {}", truncate_with_ellipsis(&mr.title, 38))
|
format!("[DRAFT] {}", truncate_with_ellipsis(&mr.title, 38))
|
||||||
} else {
|
} else {
|
||||||
@@ -819,25 +845,24 @@ pub fn print_list_mrs(result: &MrListResult) {
|
|||||||
let discussions = format_discussions(mr.discussion_count, mr.unresolved_count);
|
let discussions = format_discussions(mr.discussion_count, mr.unresolved_count);
|
||||||
|
|
||||||
let state_cell = match mr.state.as_str() {
|
let state_cell = match mr.state.as_str() {
|
||||||
"opened" => Cell::new(&mr.state).fg(Color::Green),
|
"opened" => colored_cell(&mr.state, Color::Green),
|
||||||
"merged" => Cell::new(&mr.state).fg(Color::Magenta),
|
"merged" => colored_cell(&mr.state, Color::Magenta),
|
||||||
"closed" => Cell::new(&mr.state).fg(Color::Red),
|
"closed" => colored_cell(&mr.state, Color::Red),
|
||||||
"locked" => Cell::new(&mr.state).fg(Color::Yellow),
|
"locked" => colored_cell(&mr.state, Color::Yellow),
|
||||||
_ => Cell::new(&mr.state).fg(Color::DarkGrey),
|
_ => colored_cell(&mr.state, Color::DarkGrey),
|
||||||
};
|
};
|
||||||
|
|
||||||
table.add_row(vec![
|
table.add_row(vec![
|
||||||
Cell::new(format!("!{}", mr.iid)).fg(Color::Cyan),
|
colored_cell(format!("!{}", mr.iid), Color::Cyan),
|
||||||
Cell::new(title),
|
Cell::new(title),
|
||||||
state_cell,
|
state_cell,
|
||||||
Cell::new(format!(
|
colored_cell(
|
||||||
"@{}",
|
format!("@{}", truncate_with_ellipsis(&mr.author_username, 12)),
|
||||||
truncate_with_ellipsis(&mr.author_username, 12)
|
Color::Magenta,
|
||||||
))
|
),
|
||||||
.fg(Color::Magenta),
|
colored_cell(branches, Color::Blue),
|
||||||
Cell::new(branches).fg(Color::Blue),
|
|
||||||
Cell::new(discussions),
|
Cell::new(discussions),
|
||||||
Cell::new(relative_time).fg(Color::DarkGrey),
|
colored_cell(relative_time, Color::DarkGrey),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ pub use stats::{print_stats, print_stats_json, run_stats};
|
|||||||
pub use search::{
|
pub use search::{
|
||||||
print_search_results, print_search_results_json, run_search, SearchCliFilters, SearchResponse,
|
print_search_results, print_search_results_json, run_search, SearchCliFilters, SearchResponse,
|
||||||
};
|
};
|
||||||
pub use ingest::{print_ingest_summary, print_ingest_summary_json, run_ingest};
|
pub use ingest::{IngestDisplay, print_ingest_summary, print_ingest_summary_json, run_ingest};
|
||||||
pub use init::{InitInputs, InitOptions, InitResult, run_init};
|
pub use init::{InitInputs, InitOptions, InitResult, run_init};
|
||||||
pub use list::{
|
pub use list::{
|
||||||
ListFilters, MrListFilters, open_issue_in_browser, open_mr_in_browser, print_list_issues,
|
ListFilters, MrListFilters, open_issue_in_browser, open_mr_in_browser, print_list_issues,
|
||||||
|
|||||||
@@ -104,8 +104,30 @@ pub fn run_search(
|
|||||||
.map(|p| resolve_project(&conn, p))
|
.map(|p| resolve_project(&conn, p))
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
|
|
||||||
let after = cli_filters.after.as_deref().and_then(parse_since);
|
let after = cli_filters
|
||||||
let updated_after = cli_filters.updated_after.as_deref().and_then(parse_since);
|
.after
|
||||||
|
.as_deref()
|
||||||
|
.map(|s| {
|
||||||
|
parse_since(s).ok_or_else(|| {
|
||||||
|
LoreError::Other(format!(
|
||||||
|
"Invalid --after value '{}'. Use relative (7d, 2w, 1m) or absolute (YYYY-MM-DD) format.",
|
||||||
|
s
|
||||||
|
))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
|
let updated_after = cli_filters
|
||||||
|
.updated_after
|
||||||
|
.as_deref()
|
||||||
|
.map(|s| {
|
||||||
|
parse_since(s).ok_or_else(|| {
|
||||||
|
LoreError::Other(format!(
|
||||||
|
"Invalid --updated-after value '{}'. Use relative (7d, 2w, 1m) or absolute (YYYY-MM-DD) format.",
|
||||||
|
s
|
||||||
|
))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
let path = cli_filters.path.as_deref().map(|p| {
|
let path = cli_filters.path.as_deref().map(|p| {
|
||||||
if p.ends_with('/') {
|
if p.ends_with('/') {
|
||||||
@@ -192,7 +214,7 @@ pub fn run_search(
|
|||||||
results.push(SearchResultDisplay {
|
results.push(SearchResultDisplay {
|
||||||
document_id: row.document_id,
|
document_id: row.document_id,
|
||||||
source_type: row.source_type.clone(),
|
source_type: row.source_type.clone(),
|
||||||
title: row.title.clone(),
|
title: row.title.clone().unwrap_or_default(),
|
||||||
url: row.url.clone(),
|
url: row.url.clone(),
|
||||||
author: row.author.clone(),
|
author: row.author.clone(),
|
||||||
created_at: row.created_at.map(ms_to_iso),
|
created_at: row.created_at.map(ms_to_iso),
|
||||||
@@ -219,7 +241,7 @@ pub fn run_search(
|
|||||||
struct HydratedRow {
|
struct HydratedRow {
|
||||||
document_id: i64,
|
document_id: i64,
|
||||||
source_type: String,
|
source_type: String,
|
||||||
title: String,
|
title: Option<String>,
|
||||||
url: Option<String>,
|
url: Option<String>,
|
||||||
author: Option<String>,
|
author: Option<String>,
|
||||||
created_at: Option<i64>,
|
created_at: Option<i64>,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use crate::Config;
|
|||||||
use crate::core::db::create_connection;
|
use crate::core::db::create_connection;
|
||||||
use crate::core::error::{LoreError, Result};
|
use crate::core::error::{LoreError, Result};
|
||||||
use crate::core::paths::get_db_path;
|
use crate::core::paths::get_db_path;
|
||||||
|
use crate::core::project::resolve_project;
|
||||||
use crate::core::time::ms_to_iso;
|
use crate::core::time::ms_to_iso;
|
||||||
|
|
||||||
/// Merge request metadata for display.
|
/// Merge request metadata for display.
|
||||||
@@ -145,18 +146,20 @@ struct IssueRow {
|
|||||||
/// Find issue by iid, optionally filtered by project.
|
/// Find issue by iid, optionally filtered by project.
|
||||||
fn find_issue(conn: &Connection, iid: i64, project_filter: Option<&str>) -> Result<IssueRow> {
|
fn find_issue(conn: &Connection, iid: i64, project_filter: Option<&str>) -> Result<IssueRow> {
|
||||||
let (sql, params): (&str, Vec<Box<dyn rusqlite::ToSql>>) = match project_filter {
|
let (sql, params): (&str, Vec<Box<dyn rusqlite::ToSql>>) = match project_filter {
|
||||||
Some(project) => (
|
Some(project) => {
|
||||||
"SELECT i.id, i.iid, i.title, i.description, i.state, i.author_username,
|
let project_id = resolve_project(conn, project)?;
|
||||||
i.created_at, i.updated_at, i.web_url, p.path_with_namespace
|
(
|
||||||
FROM issues i
|
"SELECT i.id, i.iid, i.title, i.description, i.state, i.author_username,
|
||||||
JOIN projects p ON i.project_id = p.id
|
i.created_at, i.updated_at, i.web_url, p.path_with_namespace
|
||||||
WHERE i.iid = ? AND (p.path_with_namespace = ? OR p.path_with_namespace LIKE ?)",
|
FROM issues i
|
||||||
vec![
|
JOIN projects p ON i.project_id = p.id
|
||||||
Box::new(iid),
|
WHERE i.iid = ? AND i.project_id = ?",
|
||||||
Box::new(project.to_string()),
|
vec![
|
||||||
Box::new(format!("%/{}", project)),
|
Box::new(iid),
|
||||||
],
|
Box::new(project_id),
|
||||||
),
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
None => (
|
None => (
|
||||||
"SELECT i.id, i.iid, i.title, i.description, i.state, i.author_username,
|
"SELECT i.id, i.iid, i.title, i.description, i.state, i.author_username,
|
||||||
i.created_at, i.updated_at, i.web_url, p.path_with_namespace
|
i.created_at, i.updated_at, i.web_url, p.path_with_namespace
|
||||||
@@ -333,20 +336,22 @@ struct MrRow {
|
|||||||
/// Find MR by iid, optionally filtered by project.
|
/// Find MR by iid, optionally filtered by project.
|
||||||
fn find_mr(conn: &Connection, iid: i64, project_filter: Option<&str>) -> Result<MrRow> {
|
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 {
|
let (sql, params): (&str, Vec<Box<dyn rusqlite::ToSql>>) = match project_filter {
|
||||||
Some(project) => (
|
Some(project) => {
|
||||||
"SELECT m.id, m.iid, m.title, m.description, m.state, m.draft,
|
let project_id = resolve_project(conn, project)?;
|
||||||
m.author_username, m.source_branch, m.target_branch,
|
(
|
||||||
m.created_at, m.updated_at, m.merged_at, m.closed_at,
|
"SELECT m.id, m.iid, m.title, m.description, m.state, m.draft,
|
||||||
m.web_url, p.path_with_namespace
|
m.author_username, m.source_branch, m.target_branch,
|
||||||
FROM merge_requests m
|
m.created_at, m.updated_at, m.merged_at, m.closed_at,
|
||||||
JOIN projects p ON m.project_id = p.id
|
m.web_url, p.path_with_namespace
|
||||||
WHERE m.iid = ? AND (p.path_with_namespace = ? OR p.path_with_namespace LIKE ?)",
|
FROM merge_requests m
|
||||||
vec![
|
JOIN projects p ON m.project_id = p.id
|
||||||
Box::new(iid),
|
WHERE m.iid = ? AND m.project_id = ?",
|
||||||
Box::new(project.to_string()),
|
vec![
|
||||||
Box::new(format!("%/{}", project)),
|
Box::new(iid),
|
||||||
],
|
Box::new(project_id),
|
||||||
),
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
None => (
|
None => (
|
||||||
"SELECT m.id, m.iid, m.title, m.description, m.state, m.draft,
|
"SELECT m.id, m.iid, m.title, m.description, m.state, m.draft,
|
||||||
m.author_username, m.source_branch, m.target_branch,
|
m.author_username, m.source_branch, m.target_branch,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use crate::core::error::Result;
|
|||||||
|
|
||||||
use super::embed::run_embed;
|
use super::embed::run_embed;
|
||||||
use super::generate_docs::run_generate_docs;
|
use super::generate_docs::run_generate_docs;
|
||||||
use super::ingest::run_ingest;
|
use super::ingest::{IngestDisplay, run_ingest};
|
||||||
|
|
||||||
/// Options for the sync command.
|
/// Options for the sync command.
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
@@ -18,6 +18,7 @@ pub struct SyncOptions {
|
|||||||
pub force: bool,
|
pub force: bool,
|
||||||
pub no_embed: bool,
|
pub no_embed: bool,
|
||||||
pub no_docs: bool,
|
pub no_docs: bool,
|
||||||
|
pub robot_mode: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Result of the sync command.
|
/// Result of the sync command.
|
||||||
@@ -34,15 +35,21 @@ pub struct SyncResult {
|
|||||||
pub async fn run_sync(config: &Config, options: SyncOptions) -> Result<SyncResult> {
|
pub async fn run_sync(config: &Config, options: SyncOptions) -> Result<SyncResult> {
|
||||||
let mut result = SyncResult::default();
|
let mut result = SyncResult::default();
|
||||||
|
|
||||||
|
let ingest_display = if options.robot_mode {
|
||||||
|
IngestDisplay::silent()
|
||||||
|
} else {
|
||||||
|
IngestDisplay::progress_only()
|
||||||
|
};
|
||||||
|
|
||||||
// Stage 1: Ingest issues
|
// Stage 1: Ingest issues
|
||||||
info!("Sync stage 1/4: ingesting issues");
|
info!("Sync stage 1/4: ingesting issues");
|
||||||
let issues_result = run_ingest(config, "issues", None, options.force, options.full, true).await?;
|
let issues_result = run_ingest(config, "issues", None, options.force, options.full, ingest_display).await?;
|
||||||
result.issues_updated = issues_result.issues_upserted;
|
result.issues_updated = issues_result.issues_upserted;
|
||||||
result.discussions_fetched += issues_result.discussions_fetched;
|
result.discussions_fetched += issues_result.discussions_fetched;
|
||||||
|
|
||||||
// Stage 2: Ingest MRs
|
// Stage 2: Ingest MRs
|
||||||
info!("Sync stage 2/4: ingesting merge requests");
|
info!("Sync stage 2/4: ingesting merge requests");
|
||||||
let mrs_result = run_ingest(config, "mrs", None, options.force, options.full, true).await?;
|
let mrs_result = run_ingest(config, "mrs", None, options.force, options.full, ingest_display).await?;
|
||||||
result.mrs_updated = mrs_result.mrs_upserted;
|
result.mrs_updated = mrs_result.mrs_upserted;
|
||||||
result.discussions_fetched += mrs_result.discussions_fetched;
|
result.discussions_fetched += mrs_result.discussions_fetched;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user