From e722031e05523ffece2676e7bb932c1715d6f3f5 Mon Sep 17 00:00:00 2001 From: xronocode Date: Sat, 25 Apr 2026 06:20:42 +0500 Subject: [PATCH] feat: add discover-driven filters for yarn lint, dig, git cherry-pick, git remote Add discover-driven coverage for commands that are still absent from origin/develop. New TOML filters: - yarn-lint.toml: strip progress noise and blank lines from yarn lint output - dig.toml: keep DNS answer sections and query metadata while stripping decorative headers New git handlers: - git cherry-pick: compact successful output while preserving full conflict output - git remote: compact listings, deduplicate -v output, and summarize mutating actions Discover updates: - classify yarn, dig, git cherry-pick, git remote, and existing ssh TOML coverage - update registry tests and built-in TOML filter counts --- src/cmds/git/git.rs | 149 +++++++++++++++++++++++++++++++++++++ src/core/toml_filter.rs | 8 +- src/discover/registry.rs | 68 ++++++++++++++++- src/discover/rules.rs | 29 +++++++- src/filters/dig.toml | 45 +++++++++++ src/filters/yarn-lint.toml | 44 +++++++++++ src/main.rs | 27 +++++++ 7 files changed, 363 insertions(+), 7 deletions(-) create mode 100644 src/filters/dig.toml create mode 100644 src/filters/yarn-lint.toml diff --git a/src/cmds/git/git.rs b/src/cmds/git/git.rs index 35a56da52..164ca04c9 100644 --- a/src/cmds/git/git.rs +++ b/src/cmds/git/git.rs @@ -23,6 +23,8 @@ pub enum GitCommand { Fetch, Stash { subcommand: Option }, Worktree, + CherryPick, + Remote, } /// Create a git Command with global options (e.g. -C, -c, --git-dir, --work-tree) @@ -68,6 +70,8 @@ pub fn run( run_stash(subcommand.as_deref(), args, verbose, global_args) } GitCommand::Worktree => run_worktree(args, verbose, global_args), + GitCommand::CherryPick => run_cherry_pick(args, verbose, global_args), + GitCommand::Remote => run_remote(args, verbose, global_args), } } @@ -1771,6 +1775,151 @@ fn filter_worktree_list(output: &str) -> String { } /// Runs an unsupported git subcommand by passing it through directly +fn run_cherry_pick(args: &[String], verbose: u8, global_args: &[String]) -> Result { + let timer = tracking::TimedExecution::start(); + + if verbose > 0 { + eprintln!("git cherry-pick"); + } + + let mut cmd = git_cmd(global_args); + cmd.arg("cherry-pick"); + for arg in args { + cmd.arg(arg); + } + let result = exec_capture(&mut cmd).context("Failed to run git cherry-pick")?; + + let combined = result.combined(); + let is_conflict = !result.success(); + + if is_conflict { + let stderr = result.stderr.trim(); + let stdout = result.stdout.trim(); + if !stderr.is_empty() { + eprintln!("{}", stderr); + } + if !stdout.is_empty() { + println!("{}", stdout); + } + + timer.track( + &format!("git cherry-pick {}", args.join(" ")), + &format!("rtk git cherry-pick {} (conflict)", args.join(" ")), + &combined, + &combined, + ); + return Ok(result.exit_code); + } + + let output = result.stdout.trim(); + let compact = if output.lines().count() <= 3 { + output.to_string() + } else { + output + .lines() + .filter(|l| { + !l.starts_with("Author:") + && !l.starts_with("Date:") + && !l.trim().is_empty() + }) + .take(5) + .collect::>() + .join("\n") + }; + + println!("{}", compact); + + timer.track( + &format!("git cherry-pick {}", args.join(" ")), + &format!("rtk git cherry-pick {}", args.join(" ")), + &combined, + &compact, + ); + + Ok(0) +} + +fn run_remote(args: &[String], verbose: u8, global_args: &[String]) -> Result { + let timer = tracking::TimedExecution::start(); + + if verbose > 0 { + eprintln!("git remote"); + } + + let is_verbose = args.iter().any(|a| a == "-v" || a == "--verbose"); + let has_action = args + .iter() + .any(|a| a == "add" || a == "remove" || a == "rename" || a == "set-url" || a == "get-url" || a == "prune"); + + let mut cmd = git_cmd(global_args); + cmd.arg("remote"); + for arg in args { + cmd.arg(arg); + } + let result = exec_capture(&mut cmd).context("Failed to run git remote")?; + let combined = result.combined(); + + if !result.success() { + eprintln!("{}", result.stderr.trim()); + timer.track( + &format!("git remote {}", args.join(" ")), + &format!("rtk git remote {} (error)", args.join(" ")), + &combined, + &combined, + ); + return Ok(result.exit_code); + } + + let output = result.stdout.trim(); + + if has_action { + println!("{}", if output.is_empty() { "ok" } else { output }); + timer.track( + &format!("git remote {}", args.join(" ")), + &format!("rtk git remote {}", args.join(" ")), + &combined, + if output.is_empty() { "ok" } else { output }, + ); + return Ok(0); + } + + let compact = if is_verbose { + let mut lines: Vec<&str> = output.lines().collect(); + lines.dedup_by(|a, b| { + let a_name = a.split_whitespace().next().unwrap_or(""); + let b_name = b.split_whitespace().next().unwrap_or(""); + a_name == b_name + }); + lines + .into_iter() + .take(20) + .collect::>() + .join("\n") + } else { + output + .lines() + .take(20) + .collect::>() + .join("\n") + }; + + let display = if compact.is_empty() { + "(no remotes)".to_string() + } else { + compact + }; + println!("{}", display); + + timer.track( + &format!("git remote {}", args.join(" ")), + &format!("rtk git remote {}", args.join(" ")), + &combined, + &display, + ); + + Ok(0) +} + pub fn run_passthrough(args: &[OsString], global_args: &[String], verbose: u8) -> Result { let timer = tracking::TimedExecution::start(); diff --git a/src/core/toml_filter.rs b/src/core/toml_filter.rs index 06060d22d..753d19df8 100644 --- a/src/core/toml_filter.rs +++ b/src/core/toml_filter.rs @@ -1621,8 +1621,8 @@ match_command = "^make\\b" let filters = make_filters(BUILTIN_TOML); assert_eq!( filters.len(), - 59, - "Expected exactly 59 built-in filters, got {}. \ + 61, + "Expected exactly 61 built-in filters, got {}. \ Update this count when adding/removing filters in src/filters/.", filters.len() ); @@ -1682,8 +1682,8 @@ expected = "output line 1\noutput line 2" // All 59 existing filters still present + 1 new = 60 assert_eq!( filters.len(), - 60, - "Expected 60 filters after concat (59 built-in + 1 new)" + 62, + "Expected 62 filters after concat (61 built-in + 1 new)" ); // New filter is discoverable diff --git a/src/discover/registry.rs b/src/discover/registry.rs index bb7b11f2a..e5f9d19a5 100644 --- a/src/discover/registry.rs +++ b/src/discover/registry.rs @@ -1093,8 +1093,20 @@ mod tests { fn test_registry_covers_all_git_subcommands() { // Verify that every GitCommand subcommand has a matching pattern for subcmd in [ - "status", "log", "diff", "show", "add", "commit", "push", "pull", "branch", "fetch", - "stash", "worktree", + "status", + "log", + "diff", + "show", + "add", + "commit", + "push", + "pull", + "branch", + "fetch", + "stash", + "worktree", + "cherry-pick", + "remote", ] { let cmd = format!("git {subcmd}"); match classify_command(&cmd) { @@ -1958,6 +1970,58 @@ mod tests { )); } + #[test] + fn test_classify_yarn_lint() { + assert!(matches!( + classify_command("yarn lint"), + Classification::Supported { category, .. } if category == "Build" + )); + assert!(matches!( + classify_command("yarn lint src/"), + Classification::Supported { .. } + )); + } + + #[test] + fn test_classify_yarn_test() { + assert!(matches!( + classify_command("yarn test"), + Classification::Supported { category, .. } if category == "Build" + )); + } + + #[test] + fn test_classify_dig() { + assert!(matches!( + classify_command("dig example.com +short"), + Classification::Supported { category, .. } if category == "Network" + )); + } + + #[test] + fn test_classify_ssh() { + assert!(matches!( + classify_command("ssh user@host ls /var/log"), + Classification::Supported { category, .. } if category == "Network" + )); + } + + #[test] + fn test_classify_git_cherry_pick() { + assert!(matches!( + classify_command("git cherry-pick abc1234"), + Classification::Supported { category, .. } if category == "Git" + )); + } + + #[test] + fn test_classify_git_remote() { + assert!(matches!( + classify_command("git remote -v"), + Classification::Supported { category, .. } if category == "Git" + )); + } + #[test] fn test_classify_docker_build() { assert!(matches!( diff --git a/src/discover/rules.rs b/src/discover/rules.rs index df7c72d03..285da4d83 100644 --- a/src/discover/rules.rs +++ b/src/discover/rules.rs @@ -12,7 +12,7 @@ pub struct RtkRule { pub const RULES: &[RtkRule] = &[ RtkRule { - pattern: r"^(?:git|yadm)\s+(?:-[Cc]\s+\S+\s+)*(status|log|diff|show|add|commit|push|pull|branch|fetch|stash|worktree)", + pattern: r"^(?:git|yadm)\s+(?:-[Cc]\s+\S+\s+)*(status|log|diff|show|add|commit|push|pull|branch|fetch|stash|worktree|cherry-pick|remote)", rtk_cmd: "rtk git", rewrite_prefixes: &["git", "yadm"], category: "Git", @@ -878,6 +878,33 @@ pub const RULES: &[RtkRule] = &[ subcmd_savings: &[], subcmd_status: &[], }, + RtkRule { + pattern: r"^yarn\s+(lint|test|build|check)\b", + rtk_cmd: "rtk yarn", + rewrite_prefixes: &["yarn"], + category: "Build", + savings_pct: 65.0, + subcmd_savings: &[("lint", 65.0), ("test", 80.0)], + subcmd_status: &[], + }, + RtkRule { + pattern: r"^dig\s+", + rtk_cmd: "rtk dig", + rewrite_prefixes: &["dig"], + category: "Network", + savings_pct: 55.0, + subcmd_savings: &[], + subcmd_status: &[], + }, + RtkRule { + pattern: r"^ssh\s+", + rtk_cmd: "rtk ssh", + rewrite_prefixes: &["ssh"], + category: "Network", + savings_pct: 50.0, + subcmd_savings: &[], + subcmd_status: &[], + }, ]; pub const IGNORED_PREFIXES: &[&str] = &[ diff --git a/src/filters/dig.toml b/src/filters/dig.toml new file mode 100644 index 000000000..b35463d9b --- /dev/null +++ b/src/filters/dig.toml @@ -0,0 +1,45 @@ +[filters.dig] +description = "Compact dig output — keep ANSWER section and query metadata, strip decorative headers" +match_command = "^dig\\b" +strip_ansi = true +keep_lines_matching = [ + "^;;\\s*(ANSWER|AUTHORITY|ADDITIONAL)\\s+SECTION", + "^;;\\s*Query\\s+time", + "^;;\\s*SERVER:", + "^;;\\s*MSG SIZE", + "IN\\s+", + "^;", + "^[a-zA-Z0-9].*IN\\s+", +] +max_lines = 30 +on_empty = "dig: no answer" + +[[tests.dig]] +name = "keeps answer section and metadata" +input = """ +; <<>> DiG 9.10.6 <<>> example.com +;; global options: +cmd +;; Got answer: +;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 12345 +;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 + +;; QUESTION SECTION: +;example.com.\t\tIN\tA + +;; ANSWER SECTION: +example.com.\t5\tIN\tA\t93.184.216.34 + +;; Query time: 42 msec +;; SERVER: 8.8.8.8#53(8.8.8.8) +;; MSG SIZE rcvd: 56 +""" +expected = ";; ANSWER SECTION:\nexample.com.\t5\tIN\tA\t93.184.216.34\n;; Query time: 42 msec\n;; SERVER: 8.8.8.8#53(8.8.8.8)\n;; MSG SIZE rcvd: 56" + +[[tests.dig]] +name = "no answer shows fallback" +input = """ +; <<>> DiG 9.10.6 <<>> nonexistent.example +;; global options: +cmd +;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 54321 +""" +expected = "dig: no answer" diff --git a/src/filters/yarn-lint.toml b/src/filters/yarn-lint.toml new file mode 100644 index 000000000..78d4920b5 --- /dev/null +++ b/src/filters/yarn-lint.toml @@ -0,0 +1,44 @@ +[filters.yarn-lint] +description = "Compact yarn lint output — strip progress, blank lines, keep errors/warnings" +match_command = "^yarn\\s+lint\\b" +strip_ansi = true +strip_lines_matching = [ + "^\\s*$", + "^\\$ ", + "^yarn run", + "^Done in", + "^\\[\\d+/\\d+\\]", +] +max_lines = 50 +truncate_lines_at = 200 +on_empty = "yarn lint: ok" + +[[tests.yarn-lint]] +name = "strips progress noise, keeps errors" +input = """ +$ eslint src/ +1:1 error 'x' is not defined no-undef +2:5 warning Unexpected console statement no-console + +Done in 3.21s. +""" +expected = "1:1 error 'x' is not defined no-undef\n2:5 warning Unexpected console statement no-console" + +[[tests.yarn-lint]] +name = "empty output shows ok" +input = """ +$ eslint src/ +Done in 1.02s. +""" +expected = "yarn lint: ok" + +[[tests.yarn-lint]] +name = "blank lines stripped" +input = """ +src/main.ts + 3:10 error Missing semicolon semi + +src/util.ts + 1:1 warning Unexpected var no-var +""" +expected = "src/main.ts\n 3:10 error Missing semicolon semi\nsrc/util.ts\n 1:1 warning Unexpected var no-var" diff --git a/src/main.rs b/src/main.rs index 2565853bf..8ef4c0837 100644 --- a/src/main.rs +++ b/src/main.rs @@ -855,6 +855,19 @@ enum GitCommands { #[arg(trailing_var_arg = true, allow_hyphen_values = true)] args: Vec, }, + /// Cherry-pick commit → compact success or full conflict output + #[command(name = "cherry-pick")] + CherryPick { + /// Git cherry-pick arguments (commit refs, flags) + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, + }, + /// Compact remote listing (deduplicates -v output) + Remote { + /// Git remote arguments (add, remove, rename, -v, etc.) + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, + }, /// Passthrough: runs any unsupported git subcommand directly #[command(external_subcommand)] Other(Vec), @@ -1544,6 +1557,20 @@ fn run_cli() -> Result { cli.verbose, &global_args, )?, + GitCommands::CherryPick { args } => git::run( + git::GitCommand::CherryPick, + &args, + None, + cli.verbose, + &global_args, + )?, + GitCommands::Remote { args } => git::run( + git::GitCommand::Remote, + &args, + None, + cli.verbose, + &global_args, + )?, GitCommands::Other(args) => git::run_passthrough(&args, &global_args, cli.verbose)?, } }