Compare commits
3 Commits
34680f0087
...
597095a283
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
597095a283 | ||
|
|
d0e88abe85 | ||
|
|
cb6894798e |
@@ -97,11 +97,14 @@ pub fn run_me(config: &Config, args: &MeArgs, robot_mode: bool) -> Result<()> {
|
|||||||
let single_project = project_ids.len() == 1;
|
let single_project = project_ids.len() == 1;
|
||||||
|
|
||||||
// 5. Parse --since (default 30d for activity feed, AC-2.3)
|
// 5. Parse --since (default 30d for activity feed, AC-2.3)
|
||||||
let since_ms = args
|
let since_ms = match args.since.as_deref() {
|
||||||
.since
|
Some(raw) => parse_since(raw).ok_or_else(|| {
|
||||||
.as_deref()
|
LoreError::Other(format!(
|
||||||
.and_then(parse_since)
|
"Invalid --since value '{raw}'. Expected: 7d, 2w, 3m, YYYY-MM-DD, or Unix-ms timestamp."
|
||||||
.unwrap_or_else(|| crate::core::time::now_ms() - DEFAULT_ACTIVITY_SINCE_DAYS * MS_PER_DAY);
|
))
|
||||||
|
})?,
|
||||||
|
None => crate::core::time::now_ms() - DEFAULT_ACTIVITY_SINCE_DAYS * MS_PER_DAY,
|
||||||
|
};
|
||||||
|
|
||||||
// 6. Determine which sections to query
|
// 6. Determine which sections to query
|
||||||
let show_all = args.show_all_sections();
|
let show_all = args.show_all_sections();
|
||||||
@@ -184,7 +187,7 @@ pub fn run_me(config: &Config, args: &MeArgs, robot_mode: bool) -> Result<()> {
|
|||||||
|
|
||||||
if robot_mode {
|
if robot_mode {
|
||||||
let fields = args.fields.as_deref();
|
let fields = args.fields.as_deref();
|
||||||
render_robot::print_me_json(&dashboard, elapsed_ms, fields);
|
render_robot::print_me_json(&dashboard, elapsed_ms, fields)?;
|
||||||
} else if show_all {
|
} else if show_all {
|
||||||
render_human::print_me_dashboard(&dashboard, single_project);
|
render_human::print_me_dashboard(&dashboard, single_project);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ use crate::core::error::Result;
|
|||||||
|
|
||||||
use super::types::{ActivityEventType, AttentionState, MeActivityEvent, MeIssue, MeMr};
|
use super::types::{ActivityEventType, AttentionState, MeActivityEvent, MeIssue, MeMr};
|
||||||
|
|
||||||
|
/// Stale threshold: items with no activity for 30 days are marked "stale".
|
||||||
|
const STALE_THRESHOLD_MS: i64 = 30 * 24 * 3600 * 1000;
|
||||||
|
|
||||||
// ─── Open Issues (AC-5.1, Task #7) ─────────────────────────────────────────
|
// ─── Open Issues (AC-5.1, Task #7) ─────────────────────────────────────────
|
||||||
|
|
||||||
/// Query open issues assigned to the user via issue_assignees.
|
/// Query open issues assigned to the user via issue_assignees.
|
||||||
@@ -23,62 +26,47 @@ pub fn query_open_issues(
|
|||||||
let project_clause = build_project_clause("i.project_id", project_ids);
|
let project_clause = build_project_clause("i.project_id", project_ids);
|
||||||
|
|
||||||
let sql = format!(
|
let sql = format!(
|
||||||
"WITH my_latest AS (
|
"WITH note_ts AS (
|
||||||
SELECT d.issue_id, MAX(n.created_at) AS ts
|
SELECT d.issue_id,
|
||||||
|
MAX(CASE WHEN n.author_username = ?1 THEN n.created_at END) AS my_ts,
|
||||||
|
MAX(CASE WHEN n.author_username != ?1 THEN n.created_at END) AS others_ts,
|
||||||
|
MAX(n.created_at) AS any_ts
|
||||||
FROM notes n
|
FROM notes n
|
||||||
JOIN discussions d ON n.discussion_id = d.id
|
JOIN discussions d ON n.discussion_id = d.id
|
||||||
WHERE n.author_username = ?1 AND n.is_system = 0
|
WHERE n.is_system = 0 AND d.issue_id IS NOT NULL
|
||||||
AND d.issue_id IS NOT NULL
|
|
||||||
GROUP BY d.issue_id
|
|
||||||
),
|
|
||||||
others_latest AS (
|
|
||||||
SELECT d.issue_id, MAX(n.created_at) AS ts
|
|
||||||
FROM notes n
|
|
||||||
JOIN discussions d ON n.discussion_id = d.id
|
|
||||||
WHERE n.author_username != ?1 AND n.is_system = 0
|
|
||||||
AND d.issue_id IS NOT NULL
|
|
||||||
GROUP BY d.issue_id
|
|
||||||
),
|
|
||||||
any_latest AS (
|
|
||||||
SELECT d.issue_id, MAX(n.created_at) AS ts
|
|
||||||
FROM notes n
|
|
||||||
JOIN discussions d ON n.discussion_id = d.id
|
|
||||||
WHERE n.is_system = 0
|
|
||||||
AND d.issue_id IS NOT NULL
|
|
||||||
GROUP BY d.issue_id
|
GROUP BY d.issue_id
|
||||||
)
|
)
|
||||||
SELECT i.iid, i.title, p.path_with_namespace, i.status_name, i.updated_at, i.web_url,
|
SELECT i.iid, i.title, p.path_with_namespace, i.status_name, i.updated_at, i.web_url,
|
||||||
CASE
|
CASE
|
||||||
WHEN ol.ts IS NOT NULL AND (ml.ts IS NULL OR ol.ts > ml.ts)
|
WHEN nt.others_ts IS NOT NULL AND (nt.my_ts IS NULL OR nt.others_ts > nt.my_ts)
|
||||||
THEN 'needs_attention'
|
THEN 'needs_attention'
|
||||||
WHEN al.ts IS NOT NULL AND al.ts < (strftime('%s', 'now') * 1000 - 30 * 24 * 3600 * 1000)
|
WHEN nt.any_ts IS NOT NULL AND nt.any_ts < (strftime('%s', 'now') * 1000 - {stale_ms})
|
||||||
THEN 'stale'
|
THEN 'stale'
|
||||||
WHEN ml.ts IS NOT NULL AND ml.ts >= COALESCE(ol.ts, 0)
|
WHEN nt.my_ts IS NOT NULL AND nt.my_ts >= COALESCE(nt.others_ts, 0)
|
||||||
THEN 'awaiting_response'
|
THEN 'awaiting_response'
|
||||||
ELSE 'not_started'
|
ELSE 'not_started'
|
||||||
END AS attention_state
|
END AS attention_state
|
||||||
FROM issues i
|
FROM issues i
|
||||||
JOIN issue_assignees ia ON ia.issue_id = i.id
|
JOIN issue_assignees ia ON ia.issue_id = i.id
|
||||||
JOIN projects p ON i.project_id = p.id
|
JOIN projects p ON i.project_id = p.id
|
||||||
LEFT JOIN my_latest ml ON ml.issue_id = i.id
|
LEFT JOIN note_ts nt ON nt.issue_id = i.id
|
||||||
LEFT JOIN others_latest ol ON ol.issue_id = i.id
|
|
||||||
LEFT JOIN any_latest al ON al.issue_id = i.id
|
|
||||||
WHERE ia.username = ?1
|
WHERE ia.username = ?1
|
||||||
AND i.state = 'opened'
|
AND i.state = 'opened'
|
||||||
{project_clause}
|
{project_clause}
|
||||||
ORDER BY
|
ORDER BY
|
||||||
CASE
|
CASE
|
||||||
WHEN ol.ts IS NOT NULL AND (ml.ts IS NULL OR ol.ts > ml.ts)
|
WHEN nt.others_ts IS NOT NULL AND (nt.my_ts IS NULL OR nt.others_ts > nt.my_ts)
|
||||||
THEN 0
|
THEN 0
|
||||||
WHEN al.ts IS NULL AND ml.ts IS NULL
|
WHEN nt.any_ts IS NULL AND nt.my_ts IS NULL
|
||||||
THEN 1
|
THEN 1
|
||||||
WHEN al.ts IS NOT NULL AND al.ts < (strftime('%s', 'now') * 1000 - 30 * 24 * 3600 * 1000)
|
WHEN nt.any_ts IS NOT NULL AND nt.any_ts < (strftime('%s', 'now') * 1000 - {stale_ms})
|
||||||
THEN 3
|
THEN 3
|
||||||
WHEN ml.ts IS NOT NULL AND ml.ts >= COALESCE(ol.ts, 0)
|
WHEN nt.my_ts IS NOT NULL AND nt.my_ts >= COALESCE(nt.others_ts, 0)
|
||||||
THEN 2
|
THEN 2
|
||||||
ELSE 1
|
ELSE 1
|
||||||
END,
|
END,
|
||||||
i.updated_at DESC"
|
i.updated_at DESC",
|
||||||
|
stale_ms = STALE_THRESHOLD_MS,
|
||||||
);
|
);
|
||||||
|
|
||||||
let params = build_params(username, project_ids);
|
let params = build_params(username, project_ids);
|
||||||
@@ -115,28 +103,14 @@ pub fn query_authored_mrs(
|
|||||||
let project_clause = build_project_clause("m.project_id", project_ids);
|
let project_clause = build_project_clause("m.project_id", project_ids);
|
||||||
|
|
||||||
let sql = format!(
|
let sql = format!(
|
||||||
"WITH my_latest AS (
|
"WITH note_ts AS (
|
||||||
SELECT d.merge_request_id, MAX(n.created_at) AS ts
|
SELECT d.merge_request_id,
|
||||||
|
MAX(CASE WHEN n.author_username = ?1 THEN n.created_at END) AS my_ts,
|
||||||
|
MAX(CASE WHEN n.author_username != ?1 THEN n.created_at END) AS others_ts,
|
||||||
|
MAX(n.created_at) AS any_ts
|
||||||
FROM notes n
|
FROM notes n
|
||||||
JOIN discussions d ON n.discussion_id = d.id
|
JOIN discussions d ON n.discussion_id = d.id
|
||||||
WHERE n.author_username = ?1 AND n.is_system = 0
|
WHERE n.is_system = 0 AND d.merge_request_id IS NOT NULL
|
||||||
AND d.merge_request_id IS NOT NULL
|
|
||||||
GROUP BY d.merge_request_id
|
|
||||||
),
|
|
||||||
others_latest AS (
|
|
||||||
SELECT d.merge_request_id, MAX(n.created_at) AS ts
|
|
||||||
FROM notes n
|
|
||||||
JOIN discussions d ON n.discussion_id = d.id
|
|
||||||
WHERE n.author_username != ?1 AND n.is_system = 0
|
|
||||||
AND d.merge_request_id IS NOT NULL
|
|
||||||
GROUP BY d.merge_request_id
|
|
||||||
),
|
|
||||||
any_latest AS (
|
|
||||||
SELECT d.merge_request_id, MAX(n.created_at) AS ts
|
|
||||||
FROM notes n
|
|
||||||
JOIN discussions d ON n.discussion_id = d.id
|
|
||||||
WHERE n.is_system = 0
|
|
||||||
AND d.merge_request_id IS NOT NULL
|
|
||||||
GROUP BY d.merge_request_id
|
GROUP BY d.merge_request_id
|
||||||
)
|
)
|
||||||
SELECT m.iid, m.title, p.path_with_namespace, m.draft, m.detailed_merge_status,
|
SELECT m.iid, m.title, p.path_with_namespace, m.draft, m.detailed_merge_status,
|
||||||
@@ -145,32 +119,31 @@ pub fn query_authored_mrs(
|
|||||||
WHEN m.draft = 1 AND NOT EXISTS (
|
WHEN m.draft = 1 AND NOT EXISTS (
|
||||||
SELECT 1 FROM mr_reviewers WHERE merge_request_id = m.id
|
SELECT 1 FROM mr_reviewers WHERE merge_request_id = m.id
|
||||||
) THEN 'not_ready'
|
) THEN 'not_ready'
|
||||||
WHEN ol.ts IS NOT NULL AND (ml.ts IS NULL OR ol.ts > ml.ts)
|
WHEN nt.others_ts IS NOT NULL AND (nt.my_ts IS NULL OR nt.others_ts > nt.my_ts)
|
||||||
THEN 'needs_attention'
|
THEN 'needs_attention'
|
||||||
WHEN al.ts IS NOT NULL AND al.ts < (strftime('%s', 'now') * 1000 - 30 * 24 * 3600 * 1000)
|
WHEN nt.any_ts IS NOT NULL AND nt.any_ts < (strftime('%s', 'now') * 1000 - {stale_ms})
|
||||||
THEN 'stale'
|
THEN 'stale'
|
||||||
WHEN ml.ts IS NOT NULL AND ml.ts >= COALESCE(ol.ts, 0)
|
WHEN nt.my_ts IS NOT NULL AND nt.my_ts >= COALESCE(nt.others_ts, 0)
|
||||||
THEN 'awaiting_response'
|
THEN 'awaiting_response'
|
||||||
ELSE 'not_started'
|
ELSE 'not_started'
|
||||||
END AS attention_state
|
END AS attention_state
|
||||||
FROM merge_requests m
|
FROM merge_requests m
|
||||||
JOIN projects p ON m.project_id = p.id
|
JOIN projects p ON m.project_id = p.id
|
||||||
LEFT JOIN my_latest ml ON ml.merge_request_id = m.id
|
LEFT JOIN note_ts nt ON nt.merge_request_id = m.id
|
||||||
LEFT JOIN others_latest ol ON ol.merge_request_id = m.id
|
|
||||||
LEFT JOIN any_latest al ON al.merge_request_id = m.id
|
|
||||||
WHERE m.author_username = ?1
|
WHERE m.author_username = ?1
|
||||||
AND m.state = 'opened'
|
AND m.state = 'opened'
|
||||||
{project_clause}
|
{project_clause}
|
||||||
ORDER BY
|
ORDER BY
|
||||||
CASE
|
CASE
|
||||||
WHEN m.draft = 1 AND NOT EXISTS (SELECT 1 FROM mr_reviewers WHERE merge_request_id = m.id) THEN 4
|
WHEN m.draft = 1 AND NOT EXISTS (SELECT 1 FROM mr_reviewers WHERE merge_request_id = m.id) THEN 4
|
||||||
WHEN ol.ts IS NOT NULL AND (ml.ts IS NULL OR ol.ts > ml.ts) THEN 0
|
WHEN nt.others_ts IS NOT NULL AND (nt.my_ts IS NULL OR nt.others_ts > nt.my_ts) THEN 0
|
||||||
WHEN al.ts IS NULL AND ml.ts IS NULL THEN 1
|
WHEN nt.any_ts IS NULL AND nt.my_ts IS NULL THEN 1
|
||||||
WHEN al.ts IS NOT NULL AND al.ts < (strftime('%s', 'now') * 1000 - 30 * 24 * 3600 * 1000) THEN 3
|
WHEN nt.any_ts IS NOT NULL AND nt.any_ts < (strftime('%s', 'now') * 1000 - {stale_ms}) THEN 3
|
||||||
WHEN ml.ts IS NOT NULL AND ml.ts >= COALESCE(ol.ts, 0) THEN 2
|
WHEN nt.my_ts IS NOT NULL AND nt.my_ts >= COALESCE(nt.others_ts, 0) THEN 2
|
||||||
ELSE 1
|
ELSE 1
|
||||||
END,
|
END,
|
||||||
m.updated_at DESC"
|
m.updated_at DESC",
|
||||||
|
stale_ms = STALE_THRESHOLD_MS,
|
||||||
);
|
);
|
||||||
|
|
||||||
let params = build_params(username, project_ids);
|
let params = build_params(username, project_ids);
|
||||||
@@ -209,63 +182,45 @@ pub fn query_reviewing_mrs(
|
|||||||
let project_clause = build_project_clause("m.project_id", project_ids);
|
let project_clause = build_project_clause("m.project_id", project_ids);
|
||||||
|
|
||||||
let sql = format!(
|
let sql = format!(
|
||||||
"WITH my_latest AS (
|
"WITH note_ts AS (
|
||||||
SELECT d.merge_request_id, MAX(n.created_at) AS ts
|
SELECT d.merge_request_id,
|
||||||
|
MAX(CASE WHEN n.author_username = ?1 THEN n.created_at END) AS my_ts,
|
||||||
|
MAX(CASE WHEN n.author_username != ?1 THEN n.created_at END) AS others_ts,
|
||||||
|
MAX(n.created_at) AS any_ts
|
||||||
FROM notes n
|
FROM notes n
|
||||||
JOIN discussions d ON n.discussion_id = d.id
|
JOIN discussions d ON n.discussion_id = d.id
|
||||||
WHERE n.author_username = ?1 AND n.is_system = 0
|
WHERE n.is_system = 0 AND d.merge_request_id IS NOT NULL
|
||||||
AND d.merge_request_id IS NOT NULL
|
|
||||||
GROUP BY d.merge_request_id
|
|
||||||
),
|
|
||||||
others_latest AS (
|
|
||||||
SELECT d.merge_request_id, MAX(n.created_at) AS ts
|
|
||||||
FROM notes n
|
|
||||||
JOIN discussions d ON n.discussion_id = d.id
|
|
||||||
WHERE n.author_username != ?1 AND n.is_system = 0
|
|
||||||
AND d.merge_request_id IS NOT NULL
|
|
||||||
GROUP BY d.merge_request_id
|
|
||||||
),
|
|
||||||
any_latest AS (
|
|
||||||
SELECT d.merge_request_id, MAX(n.created_at) AS ts
|
|
||||||
FROM notes n
|
|
||||||
JOIN discussions d ON n.discussion_id = d.id
|
|
||||||
WHERE n.is_system = 0
|
|
||||||
AND d.merge_request_id IS NOT NULL
|
|
||||||
GROUP BY d.merge_request_id
|
GROUP BY d.merge_request_id
|
||||||
)
|
)
|
||||||
SELECT m.iid, m.title, p.path_with_namespace, m.draft, m.detailed_merge_status,
|
SELECT m.iid, m.title, p.path_with_namespace, m.draft, m.detailed_merge_status,
|
||||||
m.author_username, m.updated_at, m.web_url,
|
m.author_username, m.updated_at, m.web_url,
|
||||||
CASE
|
CASE
|
||||||
WHEN m.draft = 1 AND NOT EXISTS (
|
-- not_ready is impossible here: JOIN mr_reviewers guarantees a reviewer exists
|
||||||
SELECT 1 FROM mr_reviewers WHERE merge_request_id = m.id
|
WHEN nt.others_ts IS NOT NULL AND (nt.my_ts IS NULL OR nt.others_ts > nt.my_ts)
|
||||||
) THEN 'not_ready'
|
|
||||||
WHEN ol.ts IS NOT NULL AND (ml.ts IS NULL OR ol.ts > ml.ts)
|
|
||||||
THEN 'needs_attention'
|
THEN 'needs_attention'
|
||||||
WHEN al.ts IS NOT NULL AND al.ts < (strftime('%s', 'now') * 1000 - 30 * 24 * 3600 * 1000)
|
WHEN nt.any_ts IS NOT NULL AND nt.any_ts < (strftime('%s', 'now') * 1000 - {stale_ms})
|
||||||
THEN 'stale'
|
THEN 'stale'
|
||||||
WHEN ml.ts IS NOT NULL AND ml.ts >= COALESCE(ol.ts, 0)
|
WHEN nt.my_ts IS NOT NULL AND nt.my_ts >= COALESCE(nt.others_ts, 0)
|
||||||
THEN 'awaiting_response'
|
THEN 'awaiting_response'
|
||||||
ELSE 'not_started'
|
ELSE 'not_started'
|
||||||
END AS attention_state
|
END AS attention_state
|
||||||
FROM merge_requests m
|
FROM merge_requests m
|
||||||
JOIN mr_reviewers r ON r.merge_request_id = m.id
|
JOIN mr_reviewers r ON r.merge_request_id = m.id
|
||||||
JOIN projects p ON m.project_id = p.id
|
JOIN projects p ON m.project_id = p.id
|
||||||
LEFT JOIN my_latest ml ON ml.merge_request_id = m.id
|
LEFT JOIN note_ts nt ON nt.merge_request_id = m.id
|
||||||
LEFT JOIN others_latest ol ON ol.merge_request_id = m.id
|
|
||||||
LEFT JOIN any_latest al ON al.merge_request_id = m.id
|
|
||||||
WHERE r.username = ?1
|
WHERE r.username = ?1
|
||||||
AND m.state = 'opened'
|
AND m.state = 'opened'
|
||||||
{project_clause}
|
{project_clause}
|
||||||
ORDER BY
|
ORDER BY
|
||||||
CASE
|
CASE
|
||||||
WHEN m.draft = 1 AND NOT EXISTS (SELECT 1 FROM mr_reviewers WHERE merge_request_id = m.id) THEN 4
|
WHEN nt.others_ts IS NOT NULL AND (nt.my_ts IS NULL OR nt.others_ts > nt.my_ts) THEN 0
|
||||||
WHEN ol.ts IS NOT NULL AND (ml.ts IS NULL OR ol.ts > ml.ts) THEN 0
|
WHEN nt.any_ts IS NULL AND nt.my_ts IS NULL THEN 1
|
||||||
WHEN al.ts IS NULL AND ml.ts IS NULL THEN 1
|
WHEN nt.any_ts IS NOT NULL AND nt.any_ts < (strftime('%s', 'now') * 1000 - {stale_ms}) THEN 3
|
||||||
WHEN al.ts IS NOT NULL AND al.ts < (strftime('%s', 'now') * 1000 - 30 * 24 * 3600 * 1000) THEN 3
|
WHEN nt.my_ts IS NOT NULL AND nt.my_ts >= COALESCE(nt.others_ts, 0) THEN 2
|
||||||
WHEN ml.ts IS NOT NULL AND ml.ts >= COALESCE(ol.ts, 0) THEN 2
|
|
||||||
ELSE 1
|
ELSE 1
|
||||||
END,
|
END,
|
||||||
m.updated_at DESC"
|
m.updated_at DESC",
|
||||||
|
stale_ms = STALE_THRESHOLD_MS,
|
||||||
);
|
);
|
||||||
|
|
||||||
let params = build_params(username, project_ids);
|
let params = build_params(username, project_ids);
|
||||||
@@ -552,7 +507,7 @@ fn build_params(username: &str, project_ids: &[i64]) -> Vec<Box<dyn rusqlite::ty
|
|||||||
params
|
params
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Populate labels for issues (avoids N+1 when there are few issues).
|
/// Populate labels for issues via cached per-item queries.
|
||||||
fn populate_issue_labels(conn: &Connection, issues: &mut [MeIssue]) -> Result<()> {
|
fn populate_issue_labels(conn: &Connection, issues: &mut [MeIssue]) -> Result<()> {
|
||||||
if issues.is_empty() {
|
if issues.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -576,7 +531,7 @@ fn populate_issue_labels(conn: &Connection, issues: &mut [MeIssue]) -> Result<()
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Populate labels for MRs (avoids N+1 when there are few MRs).
|
/// Populate labels for MRs via cached per-item queries.
|
||||||
fn populate_mr_labels(conn: &Connection, mrs: &mut [MeMr]) -> Result<()> {
|
fn populate_mr_labels(conn: &Connection, mrs: &mut [MeMr]) -> Result<()> {
|
||||||
if mrs.is_empty() {
|
if mrs.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ fn print_attention_legend() {
|
|||||||
(AttentionState::NotStarted, "not started"),
|
(AttentionState::NotStarted, "not started"),
|
||||||
(AttentionState::AwaitingResponse, "awaiting response"),
|
(AttentionState::AwaitingResponse, "awaiting response"),
|
||||||
(AttentionState::Stale, "stale (30d+)"),
|
(AttentionState::Stale, "stale (30d+)"),
|
||||||
|
(AttentionState::NotReady, "draft (not ready)"),
|
||||||
];
|
];
|
||||||
|
|
||||||
let legend: Vec<String> = states
|
let legend: Vec<String> = states
|
||||||
@@ -369,7 +370,7 @@ fn format_entity_ref(entity_type: &str, iid: i64) -> String {
|
|||||||
let s = format!("#{iid}");
|
let s = format!("#{iid}");
|
||||||
Theme::issue_ref().render(&s)
|
Theme::issue_ref().render(&s)
|
||||||
}
|
}
|
||||||
"merge_request" => {
|
"mr" => {
|
||||||
let s = format!("!{iid}");
|
let s = format!("!{iid}");
|
||||||
Theme::mr_ref().render(&s)
|
Theme::mr_ref().render(&s)
|
||||||
}
|
}
|
||||||
@@ -440,7 +441,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn format_entity_ref_mr() {
|
fn format_entity_ref_mr() {
|
||||||
let result = format_entity_ref("merge_request", 99);
|
let result = format_entity_ref("mr", 99);
|
||||||
assert!(result.contains("99"), "got: {result}");
|
assert!(result.contains("99"), "got: {result}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,20 +10,19 @@ use super::types::{
|
|||||||
// ─── Robot JSON Output (Task #18) ────────────────────────────────────────────
|
// ─── Robot JSON Output (Task #18) ────────────────────────────────────────────
|
||||||
|
|
||||||
/// Print the full me dashboard as robot-mode JSON.
|
/// Print the full me dashboard as robot-mode JSON.
|
||||||
pub fn print_me_json(dashboard: &MeDashboard, elapsed_ms: u64, fields: Option<&[String]>) {
|
pub fn print_me_json(
|
||||||
|
dashboard: &MeDashboard,
|
||||||
|
elapsed_ms: u64,
|
||||||
|
fields: Option<&[String]>,
|
||||||
|
) -> crate::core::error::Result<()> {
|
||||||
let envelope = MeJsonEnvelope {
|
let envelope = MeJsonEnvelope {
|
||||||
ok: true,
|
ok: true,
|
||||||
data: MeDataJson::from_dashboard(dashboard),
|
data: MeDataJson::from_dashboard(dashboard),
|
||||||
meta: RobotMeta { elapsed_ms },
|
meta: RobotMeta { elapsed_ms },
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut value = match serde_json::to_value(&envelope) {
|
let mut value = serde_json::to_value(&envelope)
|
||||||
Ok(v) => v,
|
.map_err(|e| crate::core::error::LoreError::Other(format!("JSON serialization: {e}")))?;
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Error serializing me JSON: {e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Apply --fields filtering (Task #19)
|
// Apply --fields filtering (Task #19)
|
||||||
if let Some(f) = fields {
|
if let Some(f) = fields {
|
||||||
@@ -38,10 +37,10 @@ pub fn print_me_json(dashboard: &MeDashboard, elapsed_ms: u64, fields: Option<&[
|
|||||||
crate::cli::robot::filter_fields(&mut value, "activity", &activity_expanded);
|
crate::cli::robot::filter_fields(&mut value, "activity", &activity_expanded);
|
||||||
}
|
}
|
||||||
|
|
||||||
match serde_json::to_string(&value) {
|
let json = serde_json::to_string(&value)
|
||||||
Ok(json) => println!("{json}"),
|
.map_err(|e| crate::core::error::LoreError::Other(format!("JSON serialization: {e}")))?;
|
||||||
Err(e) => eprintln!("Error serializing to JSON: {e}"),
|
println!("{json}");
|
||||||
}
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── JSON Envelope ───────────────────────────────────────────────────────────
|
// ─── JSON Envelope ───────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -168,7 +168,6 @@ fn test_ingest_issue_by_iid_upserts_and_marks_dirty() {
|
|||||||
|
|
||||||
let result = ingest_issue_by_iid(&conn, &config, 1, &issue).unwrap();
|
let result = ingest_issue_by_iid(&conn, &config, 1, &issue).unwrap();
|
||||||
|
|
||||||
assert!(!result.skipped_stale);
|
|
||||||
assert!(!result.skipped_stale);
|
assert!(!result.skipped_stale);
|
||||||
assert!(!result.dirty_source_keys.is_empty());
|
assert!(!result.dirty_source_keys.is_empty());
|
||||||
|
|
||||||
@@ -200,7 +199,6 @@ fn test_toctou_skips_stale_issue() {
|
|||||||
// Second ingest with same timestamp should be skipped
|
// Second ingest with same timestamp should be skipped
|
||||||
let r2 = ingest_issue_by_iid(&conn, &config, 1, &issue).unwrap();
|
let r2 = ingest_issue_by_iid(&conn, &config, 1, &issue).unwrap();
|
||||||
assert!(r2.skipped_stale);
|
assert!(r2.skipped_stale);
|
||||||
assert!(r2.skipped_stale);
|
|
||||||
assert!(r2.dirty_source_keys.is_empty());
|
assert!(r2.dirty_source_keys.is_empty());
|
||||||
|
|
||||||
// No new dirty mark
|
// No new dirty mark
|
||||||
@@ -224,7 +222,6 @@ fn test_toctou_allows_newer_issue() {
|
|||||||
let result = ingest_issue_by_iid(&conn, &config, 1, &issue_t2).unwrap();
|
let result = ingest_issue_by_iid(&conn, &config, 1, &issue_t2).unwrap();
|
||||||
|
|
||||||
assert!(!result.skipped_stale);
|
assert!(!result.skipped_stale);
|
||||||
assert!(!result.skipped_stale);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -272,7 +269,6 @@ fn test_ingest_mr_by_iid_upserts_and_marks_dirty() {
|
|||||||
|
|
||||||
let result = ingest_mr_by_iid(&conn, &config, 1, &mr).unwrap();
|
let result = ingest_mr_by_iid(&conn, &config, 1, &mr).unwrap();
|
||||||
|
|
||||||
assert!(!result.skipped_stale);
|
|
||||||
assert!(!result.skipped_stale);
|
assert!(!result.skipped_stale);
|
||||||
assert!(!result.dirty_source_keys.is_empty());
|
assert!(!result.dirty_source_keys.is_empty());
|
||||||
|
|
||||||
@@ -299,7 +295,6 @@ fn test_toctou_skips_stale_mr() {
|
|||||||
|
|
||||||
let r2 = ingest_mr_by_iid(&conn, &config, 1, &mr).unwrap();
|
let r2 = ingest_mr_by_iid(&conn, &config, 1, &mr).unwrap();
|
||||||
assert!(r2.skipped_stale);
|
assert!(r2.skipped_stale);
|
||||||
assert!(r2.skipped_stale);
|
|
||||||
assert!(r2.dirty_source_keys.is_empty());
|
assert!(r2.dirty_source_keys.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,7 +312,6 @@ fn test_toctou_allows_newer_mr() {
|
|||||||
let result = ingest_mr_by_iid(&conn, &config, 1, &mr_t2).unwrap();
|
let result = ingest_mr_by_iid(&conn, &config, 1, &mr_t2).unwrap();
|
||||||
|
|
||||||
assert!(!result.skipped_stale);
|
assert!(!result.skipped_stale);
|
||||||
assert!(!result.skipped_stale);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user