feat(log): add --grep parameter for commit message filtering#328
feat(log): add --grep parameter for commit message filtering#328lunygithub wants to merge 1 commit intoweb3infra-foundation:mainfrom
Conversation
|
Thanks for the PR! The 1. Integrate
|
There was a problem hiding this comment.
Pull request overview
This PR adds a --grep option to the log command to filter the displayed commit history by commit message substring (case-sensitive), aligning Libra’s CLI with common Git log workflows.
Changes:
- Add
--greptoLogArgsand apply message substring filtering inexecute_safe. - Add tests for
--grepargument parsing (currently duplicated across unit + integration test files).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
src/command/log.rs |
Adds --grep argument and applies commit-message filtering before rendering log output; adds parsing-focused tests. |
tests/command/log_test.rs |
Adds parsing-focused tests for the new --grep argument in the command integration test suite. |
| // default sort with signature time | ||
| reachable_commits.sort_by_key(|b| std::cmp::Reverse(b.committer.timestamp)); | ||
|
|
||
| // Apply grep filtering | ||
| if let Some(pattern) = &args.grep { | ||
| if !pattern.is_empty() { | ||
| reachable_commits = reachable_commits | ||
| .into_iter() | ||
| .filter(|commit| commit.message.contains(pattern)) | ||
| .collect(); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
--grep filtering is applied after sorting reachable_commits, which means you still sort commits that will be discarded. For large histories this adds avoidable overhead when --grep is used. Consider filtering first (or using retain) and then sorting the remaining commits, keeping the same output order semantics.
| // default sort with signature time | |
| reachable_commits.sort_by_key(|b| std::cmp::Reverse(b.committer.timestamp)); | |
| // Apply grep filtering | |
| if let Some(pattern) = &args.grep { | |
| if !pattern.is_empty() { | |
| reachable_commits = reachable_commits | |
| .into_iter() | |
| .filter(|commit| commit.message.contains(pattern)) | |
| .collect(); | |
| } | |
| } | |
| // Apply grep filtering before sorting to avoid sorting commits that will be discarded. | |
| if let Some(pattern) = &args.grep { | |
| if !pattern.is_empty() { | |
| reachable_commits | |
| .retain(|commit| commit.message.contains(pattern)); | |
| } | |
| } | |
| // default sort with signature time | |
| reachable_commits.sort_by_key(|b| std::cmp::Reverse(b.committer.timestamp)); |
| let args = LogArgs::parse_from(["libra", "log", "--grep", "fix"]); | ||
| assert_eq!(args.grep, Some("fix".to_string())); | ||
|
|
||
| let args = LogArgs::parse_from(["libra", "log"]); | ||
| assert_eq!(args.grep, None); | ||
| } | ||
|
|
||
| // Test grep combined with other arguments | ||
| #[test] | ||
| fn test_grep_with_other_args() { | ||
| let args = | ||
| LogArgs::parse_from(["libra", "log", "--grep", "feature", "--oneline", "-n", "5"]); | ||
| assert_eq!(args.grep, Some("feature".to_string())); | ||
| assert!(args.oneline); | ||
| assert_eq!(args.number, Some(5)); | ||
| } | ||
|
|
||
| // Test case-sensitive matching | ||
| #[test] | ||
| fn test_grep_case_sensitive() { | ||
| let args = LogArgs::parse_from(["libra", "log", "--grep", "FIX"]); | ||
| assert_eq!(args.grep, Some("FIX".to_string())); | ||
| } | ||
|
|
||
| // Test empty string grep | ||
| #[test] | ||
| fn test_grep_empty_string() { | ||
| let args = LogArgs::parse_from(["libra", "log", "--grep", ""]); | ||
| assert_eq!(args.grep, Some("".to_string())); | ||
| } | ||
|
|
||
| // Test graph with grep combination | ||
| #[test] | ||
| fn test_graph_with_grep() { | ||
| let args = LogArgs::parse_from(["libra", "log", "--graph", "--grep", "fix"]); | ||
| assert!(args.graph); | ||
| assert_eq!(args.grep, Some("fix".to_string())); |
There was a problem hiding this comment.
These LogArgs::parse_from(["libra", "log", ...]) assertions don’t actually parse the real CLI shape: the literal "log" is consumed as a positional pathspec value (since pathspec: Vec<String> accepts arbitrary trailing args). That makes the test pass while silently setting pathspec, and it won’t catch regressions in subcommand wiring. Prefer parsing without the extra "log" (e.g. ["libra", "--grep", "fix"]) and/or assert args.pathspec.is_empty() here.
| let args = LogArgs::parse_from(["libra", "log", "--grep", "fix"]); | |
| assert_eq!(args.grep, Some("fix".to_string())); | |
| let args = LogArgs::parse_from(["libra", "log"]); | |
| assert_eq!(args.grep, None); | |
| } | |
| // Test grep combined with other arguments | |
| #[test] | |
| fn test_grep_with_other_args() { | |
| let args = | |
| LogArgs::parse_from(["libra", "log", "--grep", "feature", "--oneline", "-n", "5"]); | |
| assert_eq!(args.grep, Some("feature".to_string())); | |
| assert!(args.oneline); | |
| assert_eq!(args.number, Some(5)); | |
| } | |
| // Test case-sensitive matching | |
| #[test] | |
| fn test_grep_case_sensitive() { | |
| let args = LogArgs::parse_from(["libra", "log", "--grep", "FIX"]); | |
| assert_eq!(args.grep, Some("FIX".to_string())); | |
| } | |
| // Test empty string grep | |
| #[test] | |
| fn test_grep_empty_string() { | |
| let args = LogArgs::parse_from(["libra", "log", "--grep", ""]); | |
| assert_eq!(args.grep, Some("".to_string())); | |
| } | |
| // Test graph with grep combination | |
| #[test] | |
| fn test_graph_with_grep() { | |
| let args = LogArgs::parse_from(["libra", "log", "--graph", "--grep", "fix"]); | |
| assert!(args.graph); | |
| assert_eq!(args.grep, Some("fix".to_string())); | |
| let args = LogArgs::parse_from(["libra", "--grep", "fix"]); | |
| assert_eq!(args.grep, Some("fix".to_string())); | |
| assert!(args.pathspec.is_empty()); | |
| let args = LogArgs::parse_from(["libra"]); | |
| assert_eq!(args.grep, None); | |
| assert!(args.pathspec.is_empty()); | |
| } | |
| // Test grep combined with other arguments | |
| #[test] | |
| fn test_grep_with_other_args() { | |
| let args = | |
| LogArgs::parse_from(["libra", "--grep", "feature", "--oneline", "-n", "5"]); | |
| assert_eq!(args.grep, Some("feature".to_string())); | |
| assert!(args.oneline); | |
| assert_eq!(args.number, Some(5)); | |
| assert!(args.pathspec.is_empty()); | |
| } | |
| // Test case-sensitive matching | |
| #[test] | |
| fn test_grep_case_sensitive() { | |
| let args = LogArgs::parse_from(["libra", "--grep", "FIX"]); | |
| assert_eq!(args.grep, Some("FIX".to_string())); | |
| assert!(args.pathspec.is_empty()); | |
| } | |
| // Test empty string grep | |
| #[test] | |
| fn test_grep_empty_string() { | |
| let args = LogArgs::parse_from(["libra", "--grep", ""]); | |
| assert_eq!(args.grep, Some("".to_string())); | |
| assert!(args.pathspec.is_empty()); | |
| } | |
| // Test graph with grep combination | |
| #[test] | |
| fn test_graph_with_grep() { | |
| let args = LogArgs::parse_from(["libra", "--graph", "--grep", "fix"]); | |
| assert!(args.graph); | |
| assert_eq!(args.grep, Some("fix".to_string())); | |
| assert!(args.pathspec.is_empty()); |
| // Test grep parameter parsing | ||
| #[test] | ||
| fn test_log_args_grep() { | ||
| let args = LogArgs::parse_from(["libra", "log", "--grep", "fix"]); | ||
| assert_eq!(args.grep, Some("fix".to_string())); | ||
|
|
||
| let args = LogArgs::parse_from(["libra", "log"]); | ||
| assert_eq!(args.grep, None); | ||
| } |
There was a problem hiding this comment.
The added tests here only validate clap parsing; they don’t verify that log --grep <pattern> actually filters output (including the “no matches => no output” behavior and --grep "" bypass). Since this PR changes execute_safe output behavior, please add an integration test that runs the libra log binary (you already have run_log_cmd) and asserts the filtered commit list.
| fn test_log_args_grep() { | ||
| let args = LogArgs::parse_from(["libra", "log", "--grep", "fix"]); | ||
| assert_eq!(args.grep, Some("fix".to_string())); | ||
|
|
||
| let args = LogArgs::parse_from(["libra", "log"]); | ||
| assert_eq!(args.grep, None); | ||
| } |
There was a problem hiding this comment.
Same issue as the integration test file: LogArgs::parse_from(["libra", "log", ...]) treats the "log" token as a positional pathspec entry, not a subcommand name. That makes these tests misleading and can mask bugs. Please parse LogArgs without the extra "log" token and consider asserting pathspec is empty.
| // Test grep parameter parsing | ||
| #[test] | ||
| fn test_log_args_grep() { | ||
| let args = LogArgs::parse_from(["libra", "log", "--grep", "fix"]); | ||
| assert_eq!(args.grep, Some("fix".to_string())); | ||
|
|
||
| let args = LogArgs::parse_from(["libra", "log"]); | ||
| assert_eq!(args.grep, None); | ||
| } |
There was a problem hiding this comment.
These new parsing-only tests are duplicated in both src/command/log.rs and tests/command/log_test.rs, but neither validates the actual --grep filtering behavior in execute_safe. Prefer keeping a single parsing test (if needed) and add/keep integration tests that execute libra log --grep ... and assert stdout matches expected commits.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ed1f48fdb9
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| reachable_commits = reachable_commits | ||
| .into_iter() | ||
| .filter(|commit| commit.message.contains(pattern)) | ||
| .collect(); |
There was a problem hiding this comment.
Preserve global hash uniqueness when filtering by --grep
execute_safe filters reachable_commits by --grep before default_abbrev is computed, so abbreviation length is now derived from only the matched subset instead of full reachable history. In repositories where a 7-character prefix is unique only within the subset, --oneline/--abbrev-commit output can become ambiguous when users reuse that hash in later commands. Keep abbreviation-length calculation based on the unfiltered reachable set (or retain a pre-filter copy) and apply grep only to the displayed iteration set.
Useful? React with 👍 / 👎.
| fn test_log_args_grep() { | ||
| let args = LogArgs::parse_from(["libra", "log", "--grep", "fix"]); | ||
| assert_eq!(args.grep, Some("fix".to_string())); |
There was a problem hiding this comment.
Add end-to-end tests for --grep filtering behavior
The added tests only verify Clap argument parsing and never execute log to confirm that commit selection is actually filtered by message content, case sensitivity, empty pattern handling, or interactions with flags like -n/--graph. That leaves the new runtime filtering path in execute_safe effectively untested, so logic regressions could ship while this suite remains green. Add at least one integration test that creates commits with different messages and asserts CLI output for matching and non-matching --grep queries.
Useful? React with 👍 / 👎.
功能描述
为
log命令添加--grep参数,支持按提交信息过滤提交历史。实现功能
--oneline、-n、--stat等)--grep ""时跳过过滤,显示所有提交使用示例
libra log --grep "fix" # 显示包含 "fix" 的提交
libra log --grep "feature" --oneline # 单行显示包含 "feature" 的提交
libra log --graph --grep "fix" # 图形化显示包含 "fix" 的提交
libra log --grep "" # 空字符串显示所有提交
测试验证
所有测试已通过:
test_log_args_grep ✅
test_grep_with_other_args ✅
test_grep_case_sensitive ✅
test_grep_empty_string ✅
test_graph_with_grep ✅
相关链接
原 Issue: [r2cn-测试任务] 为 log 命令添加 --grep 参数,支持按提交信息过滤提交历史 #47
原 PR: feat(log): add --grep parameter for commit message filtering #60
@genedna 请帮忙审核