From b5f427bbf6dd6e98fa0428131bf66597bafdb0bb Mon Sep 17 00:00:00 2001 From: codcod Date: Mon, 8 Dec 2025 21:32:07 +0100 Subject: [PATCH 1/2] feat: add repos review plugin --- Cargo.toml | 1 + justfile | 4 +- plugins/repos-review/Cargo.toml | 16 +++ plugins/repos-review/README.md | 61 +++++++++++ plugins/repos-review/src/main.rs | 175 +++++++++++++++++++++++++++++++ 5 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 plugins/repos-review/Cargo.toml create mode 100644 plugins/repos-review/README.md create mode 100644 plugins/repos-review/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 2ce487b..7a501c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ ".", "common/repos-github", "plugins/repos-health", + "plugins/repos-review", "plugins/repos-validate", ] diff --git a/justfile b/justfile index c9af484..ec39362 100644 --- a/justfile +++ b/justfile @@ -35,11 +35,13 @@ list-plugins: [group('devex')] link-plugins: sudo ln -sf $(pwd)/target/release/repos-health /usr/local/bin/repos-health - sudo ln -sf $(pwd)/target/release/repos-health /usr/local/bin/repos-health + sudo ln -sf $(pwd)/target/release/repos-validate /usr/local/bin/repos-validate + sudo ln -sf $(pwd)/target/release/repos-review /usr/local/bin/repos-review [group('devex')] unlink-plugins: sudo rm -f /usr/local/bin/repos-health sudo rm -f /usr/local/bin/repos-validate + sudo rm -f /usr/local/bin/repos-review # vim: set filetype=Makefile ts=4 sw=4 et: diff --git a/plugins/repos-review/Cargo.toml b/plugins/repos-review/Cargo.toml new file mode 100644 index 0000000..a4fe862 --- /dev/null +++ b/plugins/repos-review/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "repos-review" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "repos-review" +path = "src/main.rs" + +[dependencies] +anyhow = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +[dependencies.repos] +path = "../.." diff --git a/plugins/repos-review/README.md b/plugins/repos-review/README.md new file mode 100644 index 0000000..35416a1 --- /dev/null +++ b/plugins/repos-review/README.md @@ -0,0 +1,61 @@ +# repos-review + +Interactive repository review plugin for the `repos` CLI tool. + +## Overview + +`repos-review` allows you to interactively review changes made in repositories before creating a pull request. It uses `fzf` for repository selection with a live preview of `git status`, then displays both `git status` and `git diff` for detailed review. + +## Requirements + +- `fzf` - Fuzzy finder for interactive repository selection + - Install on macOS: `brew install fzf` + - Install on Linux: Use your package manager (e.g., `apt install fzf`, `yum install fzf`) + +## Usage + +```bash +repos review +``` + +The plugin will: + +1. Display a list of all repositories with an `fzf` interface +2. Show a preview of `git status` for each repository +3. After selection, display full `git status` and `git diff` +4. Wait for user input to either: + - Press **Enter** to go back to the repository list + - Press **Escape** or **Q** to exit + +## Features + +- **Interactive Selection**: Uses `fzf` with live preview of repository status +- **Color Output**: Syntax highlighting for better readability +- **Loop Mode**: Review multiple repositories in a single session +- **Simple Navigation**: Easy keyboard controls for efficient workflow + +## Example Workflow + +```bash +# Review changes across all repositories +repos review + +# Use with tag filters to review specific repositories +repos review --tags backend + +# Review repositories matching a pattern +repos review --pattern "api-*" +``` + +## Key Bindings + +- **↑/↓** or **Ctrl-N/Ctrl-P**: Navigate repository list +- **Enter**: Select repository for review +- **Escape** or **Q**: Exit after reviewing a repository +- **Enter** (in review): Return to repository list + +## Notes + +- The plugin respects the same filters as other `repos` commands (`--tags`, `--pattern`, etc.) +- Only repositories with a configured path are shown +- If `fzf` is not installed, the plugin will exit with an error message diff --git a/plugins/repos-review/src/main.rs b/plugins/repos-review/src/main.rs new file mode 100644 index 0000000..b4126f7 --- /dev/null +++ b/plugins/repos-review/src/main.rs @@ -0,0 +1,175 @@ +use anyhow::{Context, Result}; +use repos::Repository; +use std::env; +use std::io::{self, Read, Write}; +use std::path::PathBuf; +use std::process::{Command, Stdio}; + +fn main() -> Result<()> { + let _args: Vec = env::args().collect(); + + // Load context injected by core repos CLI + let repos = repos::load_plugin_context() + .context("Failed to load plugin context")? + .ok_or_else(|| anyhow::anyhow!("Plugin must be invoked via repos CLI"))?; + + // Check if fzf is available + if !is_fzf_available() { + eprintln!("Error: fzf must be installed."); + eprintln!("Install it via: brew install fzf (macOS) or your package manager"); + std::process::exit(1); + } + + // Main loop: select and review repositories + loop { + match select_repository(&repos)? { + Some(repo) => { + review_repository(&repo)?; + } + None => { + println!("No repo selected. Exiting."); + break; + } + } + } + + Ok(()) +} + +/// Check if fzf is installed and available in PATH +fn is_fzf_available() -> bool { + Command::new("which") + .arg("fzf") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .map(|status| status.success()) + .unwrap_or(false) +} + +/// Use fzf to select a repository interactively +fn select_repository(repos: &[Repository]) -> Result> { + // Build list of repository paths for fzf + let repo_list: Vec = repos + .iter() + .filter_map(|r| r.path.as_ref()) + .map(|p| p.to_string()) + .collect(); + + if repo_list.is_empty() { + return Ok(None); + } + + let input = repo_list.join("\n"); + + // Launch fzf with preview showing git status + let mut fzf = Command::new("fzf") + .args([ + "--color=fg:#4d4d4c,bg:#eeeeee,hl:#d7005f", + "--color=fg+:#4d4d4c,bg+:#e8e8e8,hl+:#d7005f", + "--color=info:#4271ae,prompt:#8959a8,pointer:#d7005f", + "--color=marker:#4271ae,spinner:#4271ae,header:#4271ae", + "--height=100%", + "--ansi", + "--preview", + "git -C {} status | head -20 | awk 'NF {print \"\\033[32m\" $0 \"\\033[0m\"}'", + "--preview-window=right:50%", + "--no-sort", + ]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .context("Failed to spawn fzf")?; + + // Write repo list to fzf's stdin + if let Some(mut stdin) = fzf.stdin.take() { + stdin + .write_all(input.as_bytes()) + .context("Failed to write to fzf stdin")?; + } + + // Get selected repository path from fzf + let output = fzf.wait_with_output().context("Failed to wait for fzf")?; + + if !output.status.success() { + return Ok(None); + } + + let selected_path = String::from_utf8(output.stdout) + .context("Invalid UTF-8 from fzf")? + .trim() + .to_string(); + + if selected_path.is_empty() { + return Ok(None); + } + + // Find the matching repository + let repo = repos + .iter() + .find(|r| r.path.as_deref() == Some(selected_path.as_str())) + .cloned(); + + Ok(repo) +} + +/// Review a repository by showing git status and git diff +fn review_repository(repo: &Repository) -> Result<()> { + let repo_path = repo + .path + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Repository has no path"))?; + + // Clear screen + print!("\x1B[2J\x1B[1;1H"); + io::stdout().flush()?; + + let path_buf = PathBuf::from(repo_path); + let repo_name = path_buf.file_name().unwrap_or_default().to_string_lossy(); + + println!("Reviewing changes in {}...\n", repo_name); + + // Show git status + let status = Command::new("git") + .arg("-C") + .arg(repo_path) + .arg("status") + .status() + .context("Failed to run git status")?; + + if !status.success() { + eprintln!("Warning: git status failed"); + } + + println!(); + + // Show git diff + let diff = Command::new("git") + .arg("-C") + .arg(repo_path) + .arg("diff") + .status() + .context("Failed to run git diff")?; + + if !diff.success() { + eprintln!("Warning: git diff failed"); + } + + // Prompt user + println!("\n\x1b[32mPress [Enter] to go back or [Escape/Q] to exit...\x1b[0m"); + + // Read single key + let mut buffer = [0u8; 1]; + io::stdin() + .read_exact(&mut buffer) + .context("Failed to read input")?; + + let key = buffer[0]; + + // Check for Escape (27) or Q/q (81/113) + if key == 27 || key == b'q' || key == b'Q' { + std::process::exit(0); + } + + Ok(()) +} From 5dad5f3488bae44298280f0eb0d10a707198ec60 Mon Sep 17 00:00:00 2001 From: codcod Date: Mon, 8 Dec 2025 21:36:09 +0100 Subject: [PATCH 2/2] feat: make message red for repos with changes --- plugins/repos-review/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/repos-review/src/main.rs b/plugins/repos-review/src/main.rs index b4126f7..856e4a7 100644 --- a/plugins/repos-review/src/main.rs +++ b/plugins/repos-review/src/main.rs @@ -72,7 +72,7 @@ fn select_repository(repos: &[Repository]) -> Result> { "--height=100%", "--ansi", "--preview", - "git -C {} status | head -20 | awk 'NF {print \"\\033[32m\" $0 \"\\033[0m\"}'", + r#"if git -C {} diff-index --quiet HEAD -- 2>/dev/null; then git -C {} status | head -20 | awk 'NF {print "\033[32m" $0 "\033[0m"}'; else git -C {} status | head -20 | awk 'NF {print "\033[31m" $0 "\033[0m"}'; fi"#, "--preview-window=right:50%", "--no-sort", ])