feat(me): add --full flag to show untruncated note content
By default, the `me` command truncates note/comment bodies to 200 characters in both the activity feed and since-last-check inbox. This is sensible for overview displays but loses context when you need to see the full comment text. The new `--full` flag disables truncation, returning complete note bodies. This affects: - Activity feed: Note summaries now show full content - Since-last-check inbox: Both regular comments and @mentions show full bodies Implementation: - CLI: Added `--full` boolean arg under "Output" help heading - queries.rs: `query_activity()` and `query_since_last_check()` now accept a `full_body: bool` parameter that controls SQL body selection (`n.body` vs `SUBSTR(n.body, 1, 200)`) - mod.rs: Wired `args.full` through to both query functions - Tests: Added `activity_full_body_flag` and `since_last_check_full_body_flag` tests that verify: - 300-char bodies truncated to 200 without --full - 300-char bodies preserved in full with --full Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -476,7 +476,7 @@ fn activity_note_on_assigned_issue() {
|
||||
let t = now_ms() - 1000;
|
||||
insert_note_at(&conn, 200, disc_id, 1, "bob", false, "a comment", t);
|
||||
|
||||
let results = query_activity(&conn, "alice", &[], 0).unwrap();
|
||||
let results = query_activity(&conn, "alice", &[], 0, false).unwrap();
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].event_type, ActivityEventType::Note);
|
||||
assert_eq!(results[0].entity_iid, 42);
|
||||
@@ -495,7 +495,7 @@ fn activity_note_on_authored_mr() {
|
||||
let t = now_ms() - 1000;
|
||||
insert_note_at(&conn, 200, disc_id, 1, "bob", false, "nice work", t);
|
||||
|
||||
let results = query_activity(&conn, "alice", &[], 0).unwrap();
|
||||
let results = query_activity(&conn, "alice", &[], 0, false).unwrap();
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].event_type, ActivityEventType::Note);
|
||||
assert_eq!(results[0].entity_type, "mr");
|
||||
@@ -512,7 +512,7 @@ fn activity_state_event_on_my_issue() {
|
||||
let t = now_ms() - 1000;
|
||||
insert_state_event(&conn, 300, 1, Some(10), None, "closed", "bob", t);
|
||||
|
||||
let results = query_activity(&conn, "alice", &[], 0).unwrap();
|
||||
let results = query_activity(&conn, "alice", &[], 0, false).unwrap();
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].event_type, ActivityEventType::StatusChange);
|
||||
assert_eq!(results[0].summary, "closed");
|
||||
@@ -528,7 +528,7 @@ fn activity_label_event_on_my_issue() {
|
||||
let t = now_ms() - 1000;
|
||||
insert_label_event(&conn, 400, 1, Some(10), None, "add", "bug", "bob", t);
|
||||
|
||||
let results = query_activity(&conn, "alice", &[], 0).unwrap();
|
||||
let results = query_activity(&conn, "alice", &[], 0, false).unwrap();
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].event_type, ActivityEventType::LabelChange);
|
||||
assert!(results[0].summary.contains("bug"));
|
||||
@@ -546,7 +546,7 @@ fn activity_excludes_unassociated_items() {
|
||||
let t = now_ms() - 1000;
|
||||
insert_note_at(&conn, 200, disc_id, 1, "bob", false, "a comment", t);
|
||||
|
||||
let results = query_activity(&conn, "alice", &[], 0).unwrap();
|
||||
let results = query_activity(&conn, "alice", &[], 0, false).unwrap();
|
||||
assert!(
|
||||
results.is_empty(),
|
||||
"should not see activity on unassigned issues"
|
||||
@@ -578,13 +578,39 @@ fn activity_since_filter() {
|
||||
|
||||
// since = 50 seconds ago, should only get the recent note
|
||||
let since = now_ms() - 50_000;
|
||||
let results = query_activity(&conn, "alice", &[], since).unwrap();
|
||||
let results = query_activity(&conn, "alice", &[], since, false).unwrap();
|
||||
assert_eq!(results.len(), 1);
|
||||
// Notes no longer duplicate body into body_preview (summary carries the content)
|
||||
assert_eq!(results[0].body_preview, None);
|
||||
assert_eq!(results[0].summary, "new comment");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn activity_full_body_flag() {
|
||||
let conn = setup_test_db();
|
||||
insert_project(&conn, 1, "group/repo");
|
||||
insert_issue(&conn, 10, 1, 42, "someone");
|
||||
insert_assignee(&conn, 10, "alice");
|
||||
|
||||
let disc_id = 100;
|
||||
insert_discussion(&conn, disc_id, 1, None, Some(10));
|
||||
let t = now_ms() - 1000;
|
||||
// Create a body longer than 200 characters
|
||||
let long_body = "a".repeat(300);
|
||||
insert_note_at(&conn, 200, disc_id, 1, "bob", false, &long_body, t);
|
||||
|
||||
// Without --full, summary should be truncated to 200 chars
|
||||
let results = query_activity(&conn, "alice", &[], 0, false).unwrap();
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].summary.len(), 200);
|
||||
|
||||
// With --full, summary should contain the full body
|
||||
let results = query_activity(&conn, "alice", &[], 0, true).unwrap();
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].summary.len(), 300);
|
||||
assert_eq!(results[0].summary, long_body);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn activity_project_filter() {
|
||||
let conn = setup_test_db();
|
||||
@@ -604,7 +630,7 @@ fn activity_project_filter() {
|
||||
insert_note_at(&conn, 201, disc_b, 2, "bob", false, "comment b", t);
|
||||
|
||||
// Filter to project 1 only
|
||||
let results = query_activity(&conn, "alice", &[1], 0).unwrap();
|
||||
let results = query_activity(&conn, "alice", &[1], 0, false).unwrap();
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].project_path, "group/repo-a");
|
||||
}
|
||||
@@ -623,7 +649,7 @@ fn activity_sorted_newest_first() {
|
||||
insert_note_at(&conn, 200, disc_id, 1, "bob", false, "first", t1);
|
||||
insert_note_at(&conn, 201, disc_id, 1, "charlie", false, "second", t2);
|
||||
|
||||
let results = query_activity(&conn, "alice", &[], 0).unwrap();
|
||||
let results = query_activity(&conn, "alice", &[], 0, false).unwrap();
|
||||
assert_eq!(results.len(), 2);
|
||||
assert!(
|
||||
results[0].timestamp >= results[1].timestamp,
|
||||
@@ -643,7 +669,7 @@ fn activity_is_own_flag() {
|
||||
let t = now_ms() - 1000;
|
||||
insert_note_at(&conn, 200, disc_id, 1, "alice", false, "my comment", t);
|
||||
|
||||
let results = query_activity(&conn, "alice", &[], 0).unwrap();
|
||||
let results = query_activity(&conn, "alice", &[], 0, false).unwrap();
|
||||
assert_eq!(results.len(), 1);
|
||||
assert!(results[0].is_own);
|
||||
}
|
||||
@@ -662,7 +688,7 @@ fn activity_assignment_system_note() {
|
||||
let t = now_ms() - 1000;
|
||||
insert_note_at(&conn, 200, disc_id, 1, "bob", true, "assigned to @alice", t);
|
||||
|
||||
let results = query_activity(&conn, "alice", &[], 0).unwrap();
|
||||
let results = query_activity(&conn, "alice", &[], 0, false).unwrap();
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].event_type, ActivityEventType::Assign);
|
||||
}
|
||||
@@ -679,7 +705,7 @@ fn activity_unassignment_system_note() {
|
||||
let t = now_ms() - 1000;
|
||||
insert_note_at(&conn, 200, disc_id, 1, "bob", true, "unassigned @alice", t);
|
||||
|
||||
let results = query_activity(&conn, "alice", &[], 0).unwrap();
|
||||
let results = query_activity(&conn, "alice", &[], 0, false).unwrap();
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].event_type, ActivityEventType::Unassign);
|
||||
}
|
||||
@@ -705,7 +731,7 @@ fn activity_review_request_system_note() {
|
||||
t,
|
||||
);
|
||||
|
||||
let results = query_activity(&conn, "alice", &[], 0).unwrap();
|
||||
let results = query_activity(&conn, "alice", &[], 0, false).unwrap();
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].event_type, ActivityEventType::ReviewRequest);
|
||||
}
|
||||
@@ -731,7 +757,7 @@ fn since_last_check_detects_mention_with_trailing_comma() {
|
||||
t,
|
||||
);
|
||||
|
||||
let groups = query_since_last_check(&conn, "alice", 0).unwrap();
|
||||
let groups = query_since_last_check(&conn, "alice", 0, false).unwrap();
|
||||
let total_events: usize = groups.iter().map(|g| g.events.len()).sum();
|
||||
assert_eq!(total_events, 1, "expected mention with comma to match");
|
||||
}
|
||||
@@ -755,7 +781,7 @@ fn since_last_check_ignores_email_like_text() {
|
||||
t,
|
||||
);
|
||||
|
||||
let groups = query_since_last_check(&conn, "alice", 0).unwrap();
|
||||
let groups = query_since_last_check(&conn, "alice", 0, false).unwrap();
|
||||
let total_events: usize = groups.iter().map(|g| g.events.len()).sum();
|
||||
assert_eq!(total_events, 0, "email text should not count as mention");
|
||||
}
|
||||
@@ -779,7 +805,7 @@ fn since_last_check_detects_mention_with_trailing_period() {
|
||||
t,
|
||||
);
|
||||
|
||||
let groups = query_since_last_check(&conn, "alice", 0).unwrap();
|
||||
let groups = query_since_last_check(&conn, "alice", 0, false).unwrap();
|
||||
let total_events: usize = groups.iter().map(|g| g.events.len()).sum();
|
||||
assert_eq!(total_events, 1, "expected mention with period to match");
|
||||
}
|
||||
@@ -803,7 +829,7 @@ fn since_last_check_detects_mention_inside_parentheses() {
|
||||
t,
|
||||
);
|
||||
|
||||
let groups = query_since_last_check(&conn, "alice", 0).unwrap();
|
||||
let groups = query_since_last_check(&conn, "alice", 0, false).unwrap();
|
||||
let total_events: usize = groups.iter().map(|g| g.events.len()).sum();
|
||||
assert_eq!(total_events, 1, "expected parenthesized mention to match");
|
||||
}
|
||||
@@ -827,7 +853,7 @@ fn since_last_check_ignores_domain_like_text() {
|
||||
t,
|
||||
);
|
||||
|
||||
let groups = query_since_last_check(&conn, "alice", 0).unwrap();
|
||||
let groups = query_since_last_check(&conn, "alice", 0, false).unwrap();
|
||||
let total_events: usize = groups.iter().map(|g| g.events.len()).sum();
|
||||
assert_eq!(
|
||||
total_events, 0,
|
||||
@@ -835,6 +861,33 @@ fn since_last_check_ignores_domain_like_text() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn since_last_check_full_body_flag() {
|
||||
let conn = setup_test_db();
|
||||
insert_project(&conn, 1, "group/repo");
|
||||
insert_issue(&conn, 10, 1, 42, "someone");
|
||||
insert_assignee(&conn, 10, "alice");
|
||||
let disc_id = 100;
|
||||
insert_discussion(&conn, disc_id, 1, None, Some(10));
|
||||
let t = now_ms() - 1000;
|
||||
// Create a body longer than 200 characters
|
||||
let long_body = "b".repeat(300);
|
||||
insert_note_at(&conn, 200, disc_id, 1, "bob", false, &long_body, t);
|
||||
|
||||
// Without --full, summary should be truncated to 200 chars
|
||||
let groups = query_since_last_check(&conn, "alice", 0, false).unwrap();
|
||||
assert_eq!(groups.len(), 1);
|
||||
assert_eq!(groups[0].events.len(), 1);
|
||||
assert_eq!(groups[0].events[0].summary.len(), 200);
|
||||
|
||||
// With --full, summary should contain the full body
|
||||
let groups = query_since_last_check(&conn, "alice", 0, true).unwrap();
|
||||
assert_eq!(groups.len(), 1);
|
||||
assert_eq!(groups[0].events.len(), 1);
|
||||
assert_eq!(groups[0].events[0].summary.len(), 300);
|
||||
assert_eq!(groups[0].events[0].summary, long_body);
|
||||
}
|
||||
|
||||
// ─── Helper Tests ──────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -147,7 +147,7 @@ pub fn run_me(config: &Config, args: &MeArgs, robot_mode: bool) -> Result<()> {
|
||||
};
|
||||
|
||||
let activity = if want_activity {
|
||||
query_activity(&conn, username, &project_ids, since_ms)?
|
||||
query_activity(&conn, username, &project_ids, since_ms, args.full)?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
@@ -158,7 +158,7 @@ pub fn run_me(config: &Config, args: &MeArgs, robot_mode: bool) -> Result<()> {
|
||||
// permanently skip events from other projects.
|
||||
let mut global_watermark: Option<i64> = None;
|
||||
let since_last_check = if let Some(prev_cursor) = cursor_ms {
|
||||
let groups = query_since_last_check(&conn, username, prev_cursor)?;
|
||||
let groups = query_since_last_check(&conn, username, prev_cursor, args.full)?;
|
||||
// Watermark from ALL groups (unfiltered) — this is the true high-water mark
|
||||
global_watermark = groups.iter().map(|g| g.latest_timestamp).max();
|
||||
// If --project was passed, filter groups by project for display only
|
||||
@@ -318,6 +318,7 @@ mod tests {
|
||||
all: false,
|
||||
user: user.map(String::from),
|
||||
fields: None,
|
||||
full: false,
|
||||
reset_cursor: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,11 +266,14 @@ pub fn query_reviewing_mrs(
|
||||
/// Query activity events on items currently associated with the user.
|
||||
/// Combines notes, state events, label events, milestone events, and
|
||||
/// assignment/reviewer system notes into a unified feed sorted newest-first.
|
||||
///
|
||||
/// When `full_body` is true, note content is returned in full without truncation.
|
||||
pub fn query_activity(
|
||||
conn: &Connection,
|
||||
username: &str,
|
||||
project_ids: &[i64],
|
||||
since_ms: i64,
|
||||
full_body: bool,
|
||||
) -> Result<Vec<MeActivityEvent>> {
|
||||
// Build project filter for activity sources.
|
||||
// Activity params: ?1=username, ?2=since_ms, ?3+=project_ids
|
||||
@@ -292,6 +295,13 @@ pub fn query_activity(
|
||||
WHERE rv.merge_request_id = {entity_mr_id} AND rv.username = ?1 AND mr3.state = 'opened')
|
||||
)";
|
||||
|
||||
// Body selection: truncate to 200 chars unless --full was passed
|
||||
let body_expr = if full_body {
|
||||
"n.body"
|
||||
} else {
|
||||
"SUBSTR(n.body, 1, 200)"
|
||||
};
|
||||
|
||||
// Source 1: Human comments on my items
|
||||
let notes_sql = format!(
|
||||
"SELECT n.created_at, 'note',
|
||||
@@ -300,7 +310,7 @@ pub fn query_activity(
|
||||
p.path_with_namespace,
|
||||
n.author_username,
|
||||
CASE WHEN n.author_username = ?1 THEN 1 ELSE 0 END,
|
||||
SUBSTR(n.body, 1, 200),
|
||||
{body_expr},
|
||||
NULL
|
||||
FROM notes n
|
||||
JOIN discussions d ON n.discussion_id = d.id
|
||||
@@ -492,10 +502,13 @@ struct RawSinceCheckRow {
|
||||
/// 1. Others' comments on my open items
|
||||
/// 2. @mentions on any item (not restricted to my items)
|
||||
/// 3. Assignment/review-request system notes mentioning me
|
||||
///
|
||||
/// When `full_body` is true, note content is returned in full without truncation.
|
||||
pub fn query_since_last_check(
|
||||
conn: &Connection,
|
||||
username: &str,
|
||||
cursor_ms: i64,
|
||||
full_body: bool,
|
||||
) -> Result<Vec<SinceCheckGroup>> {
|
||||
// Build the "my items" subquery fragments (reused from activity).
|
||||
let my_issue_check = "EXISTS (
|
||||
@@ -510,6 +523,13 @@ pub fn query_since_last_check(
|
||||
WHERE rv.merge_request_id = {entity_mr_id} AND rv.username = ?1 AND mr3.state = 'opened')
|
||||
)";
|
||||
|
||||
// Body selection: truncate to 200 chars unless --full was passed
|
||||
let body_expr = if full_body {
|
||||
"n.body"
|
||||
} else {
|
||||
"SUBSTR(n.body, 1, 200)"
|
||||
};
|
||||
|
||||
// Source 1: Others' comments on my open items
|
||||
let source1 = format!(
|
||||
"SELECT n.created_at, 'note',
|
||||
@@ -518,7 +538,7 @@ pub fn query_since_last_check(
|
||||
COALESCE(i.title, m.title),
|
||||
p.path_with_namespace,
|
||||
n.author_username,
|
||||
SUBSTR(n.body, 1, 200),
|
||||
{body_expr},
|
||||
NULL,
|
||||
0,
|
||||
NULL
|
||||
@@ -547,7 +567,7 @@ pub fn query_since_last_check(
|
||||
COALESCE(i.title, m.title),
|
||||
p.path_with_namespace,
|
||||
n.author_username,
|
||||
SUBSTR(n.body, 1, 200),
|
||||
{body_expr},
|
||||
NULL,
|
||||
1,
|
||||
n.body
|
||||
|
||||
@@ -1123,6 +1123,10 @@ pub struct MeArgs {
|
||||
#[arg(long, help_heading = "Output", value_delimiter = ',')]
|
||||
pub fields: Option<Vec<String>>,
|
||||
|
||||
/// Show full note content (no truncation)
|
||||
#[arg(long, help_heading = "Output")]
|
||||
pub full: bool,
|
||||
|
||||
/// Reset the since-last-check cursor (next run shows no new events)
|
||||
#[arg(long, help_heading = "Output")]
|
||||
pub reset_cursor: bool,
|
||||
|
||||
Reference in New Issue
Block a user