diff --git a/src/cli/commands/list.rs b/src/cli/commands/list.rs index 456c479..ef4e2b7 100644 --- a/src/cli/commands/list.rs +++ b/src/cli/commands/list.rs @@ -232,8 +232,11 @@ fn query_issues(conn: &Connection, filters: &ListFilters) -> Result let mut params: Vec> = Vec::new(); if let Some(project) = filters.project { - where_clauses.push("p.path_with_namespace LIKE ?"); - params.push(Box::new(format!("%{project}%"))); + // Exact match or suffix match after '/' to avoid partial matches + // e.g. "foo" matches "group/foo" but NOT "group/foobar" + where_clauses.push("(p.path_with_namespace = ? OR p.path_with_namespace LIKE ?)"); + params.push(Box::new(project.to_string())); + params.push(Box::new(format!("%/{project}"))); } if let Some(state) = filters.state @@ -337,11 +340,11 @@ fn query_issues(conn: &Connection, filters: &ListFilters) -> Result i.updated_at, i.web_url, p.path_with_namespace, - (SELECT GROUP_CONCAT(l.name, ',') + (SELECT GROUP_CONCAT(l.name, X'1F') FROM issue_labels il JOIN labels l ON il.label_id = l.id WHERE il.issue_id = i.id) AS labels_csv, - (SELECT GROUP_CONCAT(ia.username, ',') + (SELECT GROUP_CONCAT(ia.username, X'1F') FROM issue_assignees ia WHERE ia.issue_id = i.id) AS assignees_csv, COALESCE(d.total, 0) AS discussion_count, @@ -369,12 +372,12 @@ fn query_issues(conn: &Connection, filters: &ListFilters) -> Result .query_map(param_refs.as_slice(), |row| { let labels_csv: Option = row.get(8)?; let labels = labels_csv - .map(|s| s.split(',').map(String::from).collect()) + .map(|s| s.split('\x1F').map(String::from).collect()) .unwrap_or_default(); let assignees_csv: Option = row.get(9)?; let assignees = assignees_csv - .map(|s| s.split(',').map(String::from).collect()) + .map(|s| s.split('\x1F').map(String::from).collect()) .unwrap_or_default(); Ok(IssueListRow { @@ -416,8 +419,11 @@ fn query_mrs(conn: &Connection, filters: &MrListFilters) -> Result let mut params: Vec> = Vec::new(); if let Some(project) = filters.project { - where_clauses.push("p.path_with_namespace LIKE ?"); - params.push(Box::new(format!("%{project}%"))); + // Exact match or suffix match after '/' to avoid partial matches + // e.g. "foo" matches "group/foo" but NOT "group/foobar" + where_clauses.push("(p.path_with_namespace = ? OR p.path_with_namespace LIKE ?)"); + params.push(Box::new(project.to_string())); + params.push(Box::new(format!("%/{project}"))); } if let Some(state) = filters.state @@ -536,14 +542,14 @@ fn query_mrs(conn: &Connection, filters: &MrListFilters) -> Result m.updated_at, m.web_url, p.path_with_namespace, - (SELECT GROUP_CONCAT(l.name, ',') + (SELECT GROUP_CONCAT(l.name, X'1F') FROM mr_labels ml JOIN labels l ON ml.label_id = l.id WHERE ml.merge_request_id = m.id) AS labels_csv, - (SELECT GROUP_CONCAT(ma.username, ',') + (SELECT GROUP_CONCAT(ma.username, X'1F') FROM mr_assignees ma WHERE ma.merge_request_id = m.id) AS assignees_csv, - (SELECT GROUP_CONCAT(mr.username, ',') + (SELECT GROUP_CONCAT(mr.username, X'1F') FROM mr_reviewers mr WHERE mr.merge_request_id = m.id) AS reviewers_csv, COALESCE(d.total, 0) AS discussion_count, @@ -571,17 +577,17 @@ fn query_mrs(conn: &Connection, filters: &MrListFilters) -> Result .query_map(param_refs.as_slice(), |row| { let labels_csv: Option = row.get(11)?; let labels = labels_csv - .map(|s| s.split(',').map(String::from).collect()) + .map(|s| s.split('\x1F').map(String::from).collect()) .unwrap_or_default(); let assignees_csv: Option = row.get(12)?; let assignees = assignees_csv - .map(|s| s.split(',').map(String::from).collect()) + .map(|s| s.split('\x1F').map(String::from).collect()) .unwrap_or_default(); let reviewers_csv: Option = row.get(13)?; let reviewers = reviewers_csv - .map(|s| s.split(',').map(String::from).collect()) + .map(|s| s.split('\x1F').map(String::from).collect()) .unwrap_or_default(); let draft_int: i64 = row.get(3)?; @@ -615,6 +621,10 @@ fn format_relative_time(ms_epoch: i64) -> String { let now = now_ms(); let diff = now - ms_epoch; + if diff < 0 { + return "in the future".to_string(); + } + match diff { d if d < 60_000 => "just now".to_string(), d if d < 3_600_000 => format!("{} min ago", d / 60_000), diff --git a/src/cli/commands/show.rs b/src/cli/commands/show.rs index 9ba3597..48b03ca 100644 --- a/src/cli/commands/show.rs +++ b/src/cli/commands/show.rs @@ -150,8 +150,12 @@ fn find_issue(conn: &Connection, iid: i64, project_filter: Option<&str>) -> Resu i.created_at, i.updated_at, i.web_url, p.path_with_namespace FROM issues i JOIN projects p ON i.project_id = p.id - WHERE i.iid = ? AND p.path_with_namespace LIKE ?", - vec![Box::new(iid), Box::new(format!("%{}%", project))], + WHERE i.iid = ? AND (p.path_with_namespace = ? OR p.path_with_namespace LIKE ?)", + vec![ + Box::new(iid), + Box::new(project.to_string()), + Box::new(format!("%/{}", project)), + ], ), None => ( "SELECT i.id, i.iid, i.title, i.description, i.state, i.author_username, @@ -336,8 +340,12 @@ fn find_mr(conn: &Connection, iid: i64, project_filter: Option<&str>) -> Result< m.web_url, p.path_with_namespace FROM merge_requests m JOIN projects p ON m.project_id = p.id - WHERE m.iid = ? AND p.path_with_namespace LIKE ?", - vec![Box::new(iid), Box::new(format!("%{}%", project))], + WHERE m.iid = ? AND (p.path_with_namespace = ? OR p.path_with_namespace LIKE ?)", + vec![ + Box::new(iid), + Box::new(project.to_string()), + Box::new(format!("%/{}", project)), + ], ), None => ( "SELECT m.id, m.iid, m.title, m.description, m.state, m.draft,