diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..d752df2 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,32 @@ +name: Test + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + name: Run Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock') }} + + - name: Run tests + run: cargo test --all-features --verbose diff --git a/Cargo.lock b/Cargo.lock index 610ee4f..039a191 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,15 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -112,12 +121,69 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +[[package]] +name = "bstr" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "bumpalo" version = "3.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" +[[package]] +name = "camino" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab" + +[[package]] +name = "cargo-config2" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "833e8fc1f15f35c43fade3d199cc249539e5b7dbc4c5c9c8e55223a0ae3190c8" +dependencies = [ + "serde", + "serde_derive", + "toml_edit", + "windows-sys", +] + +[[package]] +name = "cargo-llvm-cov" +version = "0.6.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33091bb8baaf21eb24807559ece8ee6d4a37ef42509958d863b66f53557fc73" +dependencies = [ + "anyhow", + "camino", + "cargo-config2", + "duct", + "fs-err", + "glob", + "is_executable", + "lcov2cobertura", + "lexopt", + "opener", + "regex", + "rustc-demangle", + "ruzstd", + "serde", + "serde_derive", + "serde_json", + "shell-escape", + "tar", + "termcolor", + "walkdir", +] + [[package]] name = "cc" version = "1.2.26" @@ -231,12 +297,52 @@ dependencies = [ "syn", ] +[[package]] +name = "duct" +version = "0.13.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ab5718d1224b63252cd0c6f74f6480f9ffeb117438a2e0f5cf6d9a4798929c" +dependencies = [ + "libc", + "once_cell", + "os_pipe", + "shared_child", +] + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys", +] + [[package]] name = "flate2" version = "1.1.2" @@ -262,6 +368,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-err" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d7be93788013f265201256d58f04936a8079ad5dc898743aa20525f503b683" +dependencies = [ + "autocfg", +] + [[package]] name = "getrandom" version = "0.3.3" @@ -289,6 +404,12 @@ dependencies = [ "url", ] +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + [[package]] name = "hashbrown" version = "0.15.4" @@ -442,6 +563,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "is_executable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a1b5bad6f9072935961dfbf1cced2f3d129963d091b6f69f007fe04e758ae2" +dependencies = [ + "winapi", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -474,6 +604,24 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lcov2cobertura" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaa0cf456e88a45378a5737f228c0800175d94be6856908dc4718b3a91c7c9f8" +dependencies = [ + "anyhow", + "quick-xml 0.37.5", + "regex", + "rustc-demangle", +] + +[[package]] +name = "lexopt" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa0e2a1fcbe2f6be6c42e342259976206b383122fc152e872795338b5a3f3a7" + [[package]] name = "libc" version = "0.2.172" @@ -494,6 +642,17 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.9.1", + "libc", + "redox_syscall", +] + [[package]] name = "libssh2-sys" version = "0.3.1" @@ -526,6 +685,12 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + [[package]] name = "litemap" version = "0.8.0" @@ -553,6 +718,15 @@ dependencies = [ "adler2", ] +[[package]] +name = "normpath" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8911957c4b1549ac0dc74e30db9c8b0e66ddcd6d7acc33098f4c63a64a6d7ed" +dependencies = [ + "windows-sys", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -602,6 +776,17 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "opener" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0812e5e4df08da354c851a3376fead46db31c2214f849d3de356d774d057681" +dependencies = [ + "bstr", + "normpath", + "windows-sys", +] + [[package]] name = "openssl-probe" version = "0.1.6" @@ -620,6 +805,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "os_pipe" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db335f4760b14ead6290116f2427bf33a14d4f0617d49f78a246de10c1831224" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -640,7 +835,7 @@ checksum = "eac26e981c03a6e53e0aee43c113e3202f5581d5360dae7bd2c70e800dd0451d" dependencies = [ "base64", "indexmap", - "quick-xml", + "quick-xml 0.32.0", "serde", "time", ] @@ -678,6 +873,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.40" @@ -693,18 +897,75 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +[[package]] +name = "redox_syscall" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rustc-demangle" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" + +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "rustversion" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +[[package]] +name = "ruzstd" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640bec8aad418d7d03c72ea2de10d5c646a598f9883c7babc160d91e3c1b26c" + [[package]] name = "ryu" version = "1.0.20" @@ -752,6 +1013,31 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "shared_child" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e297bd52991bbe0686c086957bee142f13df85d1e79b0b21630a99d374ae9dc" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "shell-escape" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f" + [[package]] name = "shlex" version = "1.3.0" @@ -820,6 +1106,39 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -881,6 +1200,28 @@ dependencies = [ "zerovec", ] +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "unicode-ident" version = "1.0.18" @@ -998,12 +1339,30 @@ name = "wer" version = "1.2.0" dependencies = [ "anyhow", + "cargo-llvm-cov", "chrono", "clap", "git2", "syntect", + "tempfile", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.9" @@ -1013,6 +1372,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.61.2" @@ -1145,6 +1510,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" @@ -1160,6 +1534,16 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "xattr" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/Cargo.toml b/Cargo.toml index 435fcc8..c780079 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,7 @@ git2 = "0.18" anyhow = "1.0" chrono = { version = "0.4", features = ["serde"] } syntect = "5.1" + +[dev-dependencies] +tempfile = "3.3" +cargo-llvm-cov = "0.6" \ No newline at end of file diff --git a/README.md b/README.md index 1ed6494..f1279ad 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ +[![Crates.io](https://img.shields.io/crates/v/wer.svg)](https://crates.io/crates/wer) +[![Tests](https://github.com/matsjfunke/wer/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/matsjfunke/wer/actions/workflows/test.yml) +[![Crates.io](https://img.shields.io/crates/d/wer.svg)](https://crates.io/crates/wer) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.md) + # ⁉️ wer ⁉️ `wer` (German "who") is a command-line tool for answering that everyday question: @@ -74,7 +79,7 @@ cargo install --path . ### ✨ Smart Path Resolution -`wer` automatically finds files and directories by name - no need to remember exact paths! +`wer` automatically finds files and directories by name and intelligently handles different path types: ```bash # Just type the filename - wer finds it automatically @@ -84,6 +89,10 @@ wer Cargo.toml # Finds ./Cargo.toml # Works with directories too wer src/ # Works from anywhere in the repository +# Relative paths work across repositories +wer ../other-project/file.rs # Finds the git repo in ../other-project/ +wer ./subdir/file.py # Within current repository + # For absolute paths, use full paths to skip search wer ~/Documents/file.txt # Uses absolute path directly wer /full/path/to/file # No search, direct access @@ -97,6 +106,16 @@ wer config.toml # → a1b2c3d Jane Doe - 05 Jun 2025: Add test config ``` +**Path Types Supported:** + +| Path Type | Example | Behavior | +| ---------------------------- | -------------------------- | -------------------------------------------- | +| **Filename** | `main.rs` | Searches recursively in current directory | +| **Relative in current repo** | `./src/main.rs` | Checks path directly in current repository | +| **Relative outside repo** | `../other-project/file.rs` | Resolves path and finds appropriate git repo | +| **Absolute path** | `/full/path/to/file` | Uses path directly | +| **Home directory** | `~/Documents/file.txt` | Expands tilde and uses directly | + ### 🎮 Basic Usage ```bash @@ -108,6 +127,10 @@ wer Cargo.toml wer src/ # → 61fcdda Mats Julius Funke - 07 Jun 2025: Added new module +# Check files in other repositories using relative paths +wer ../other-project/README.md +# → a1b2c3d Jane Doe - 05 Jun 2025: Update documentation + # Check current directory wer # → 61fcdda Mats Julius Funke - 07 Jun 2025: Latest changes diff --git a/src/cli.rs b/src/cli.rs index 852ef8b..b1e7169 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -9,12 +9,20 @@ use clap::Parser; long_about = r#"Find out who last edited any file or directory in a Git repository SMART PATH RESOLUTION: - wer automatically finds files and directories by name: - • Type just the filename: "wer main.rs" finds src/main.rs - • Type directory name: "wer src/" works from anywhere + wer automatically finds files and directories by name and intelligently discovers git repositories: + • Type just the filename: "wer main.rs" finds src/main.rs in current repo + • Type directory name: "wer src/" works from anywhere in current repo + • Relative paths: "wer ../other-project/file.rs" finds git repo in ../other-project/ + • Current directory paths: "wer ./subdir/file.py" stays within current repo • Absolute paths: "wer ~/file.txt" or "wer /full/path" used directly • Search ignores common directories (.git, node_modules, target, etc.) +CROSS-REPOSITORY SUPPORT: + wer can work with files in different git repositories by resolving relative paths: + • "../other-repo/file.rs" → discovers git repo at ../other-repo/ + • "../../parent-project/src/" → finds git repo at ../../parent-project/ + • Each file is processed within its own git repository context + MULTIPLE MATCHES BEHAVIOR: When multiple files/directories with the same name are found: • Regular mode: Shows results for all matches, each prefixed with its path @@ -30,13 +38,14 @@ MODES: Only works with files, not directories EXAMPLES: - wer Cargo.toml Find and show who last edited Cargo.toml - wer main.rs Find src/main.rs automatically - wer src/ Show who last touched the src/ directory - wer -b git.rs Find and show blame for src/git.rs - wer -d . Show only the date of last change - wer -l 3 src/ Show last 3 contributors to src/ directory - wer -b -m file.py Show blame with commit messages"# + wer Cargo.toml Find and show who last edited Cargo.toml + wer main.rs Find src/main.rs automatically + wer src/ Show who last touched the src/ directory + wer ../other-project/README.md Show git info from different repository + wer -b git.rs Find and show blame for src/git.rs + wer -d . Show only the date of last change + wer -l 3 src/ Show last 3 contributors to src/ directory + wer -b -m ../docs/file.py Show blame with commit messages from ../docs/ repo"# )] #[command(arg(clap::Arg::new("version") .short('v') @@ -44,10 +53,13 @@ EXAMPLES: .action(clap::ArgAction::Version) .help("Print version")))] pub struct Cli { - /// File or directory path (searches automatically if not found in current directory) + /// File or directory path (searches automatically and works across repositories) /// - /// Can be just a filename (main.rs), directory name (src/), or full path. - /// For absolute paths, use ~/file.txt or /full/path to skip search. + /// Supports multiple path types: + /// • Filename only: "main.rs" (searches recursively in current repo) + /// • Relative paths: "../other-repo/file.rs" (discovers appropriate git repo) + /// • Directory paths: "./src/" or "../project/docs/" + /// • Absolute paths: "~/file.txt" or "/full/path" (used directly) pub path: Option, /// Show git blame with syntax highlighting (files only) diff --git a/src/git.rs b/src/git.rs index 045a14d..1581879 100644 --- a/src/git.rs +++ b/src/git.rs @@ -161,7 +161,11 @@ fn validate_git_path(path: &str, must_be_file: bool) -> Result<(Repository, Path let full_path = if Path::new(path).is_absolute() { PathBuf::from(path) } else { - std::env::current_dir()?.join(path) + // For relative paths, resolve them against current working directory + std::env::current_dir()? + .join(path) + .canonicalize() + .map_err(|_| anyhow!("Cannot resolve path '{}'. Check if it exists.", path))? }; // Check if the path exists @@ -177,7 +181,7 @@ fn validate_git_path(path: &str, must_be_file: bool) -> Result<(Repository, Path )); } - // Find the git repository + // Find the git repository for this specific path let search_path = if full_path.is_file() { full_path.parent().unwrap_or(&full_path).to_path_buf() } else { @@ -185,16 +189,22 @@ fn validate_git_path(path: &str, must_be_file: bool) -> Result<(Repository, Path }; let repo = Repository::discover(search_path) - .map_err(|_| anyhow!("Not a git repository (or any of the parent directories)"))?; + .map_err(|_| anyhow!("Path '{}' is not in a git repository", path))?; - // Convert path to relative path from repo root + // Convert path to relative path from the discovered repo root let repo_workdir = repo .workdir() .ok_or_else(|| anyhow!("Repository has no working directory"))?; let relative_path = full_path .strip_prefix(repo_workdir) - .map_err(|_| anyhow!("Path '{}' is not within the repository", path))? + .map_err(|_| { + anyhow!( + "Path '{}' is not within the repository at '{}'", + path, + repo_workdir.display() + ) + })? .to_path_buf(); Ok((repo, full_path, relative_path)) diff --git a/src/search.rs b/src/search.rs index 02a35f2..6a4ff8c 100644 --- a/src/search.rs +++ b/src/search.rs @@ -2,23 +2,46 @@ use anyhow::{Result, anyhow}; use std::fs; use std::path::{Path, PathBuf}; -/// Search for a file or directory by name starting from the current directory -/// Returns all matches found, or the original path if it's absolute or starts with ~/ +/// Search for a file or directory by name starting from current directory +/// Handles different path types: +/// - ~/path: home directory paths (returned as-is) +/// - /absolute/path: absolute paths (returned as-is) +/// - ../relative/path or ./path or subdir/path: relative paths (checked directly) +/// - filename: bare filename (searched recursively in current directory) pub fn find_all_matches(input: &str) -> Result> { + find_all_matches_from(input, None) +} + +/// Internal function that can optionally specify a base directory (for testing) +fn find_all_matches_from(input: &str, base_dir: Option<&Path>) -> Result> { // If it's already an absolute path or starts with ~/, return as-is if input.starts_with('/') || input.starts_with("~/") { return Ok(vec![input.to_string()]); } - // If it exists as a relative path from current directory, use it - if Path::new(input).exists() { + let current_dir = match base_dir { + Some(dir) => dir.to_path_buf(), + None => std::env::current_dir()?, + }; + + // If it contains path separators, treat it as a relative path and check if it exists + if input.contains('/') { + let path = current_dir.join(input); + if path.exists() { + return Ok(vec![input.to_string()]); + } else { + return Err(anyhow!("Path '{}' not found", input)); + } + } + + // If it exists as a file/directory in current directory, use it + let direct_path = current_dir.join(input); + if direct_path.exists() { return Ok(vec![input.to_string()]); } - // Otherwise, search for it recursively starting from current directory - let current_dir = std::env::current_dir()?; + // Otherwise, it's just a filename - search for it recursively let mut matches = Vec::new(); - search_recursive(¤t_dir, input, &mut matches)?; if matches.is_empty() { @@ -28,7 +51,7 @@ pub fn find_all_matches(input: &str) -> Result> { )); } - // Convert all matches to relative paths in one go + // Convert all matches to relative paths Ok(matches .into_iter() .map(|path| { @@ -79,3 +102,106 @@ fn is_ignored_directory(name: &str) -> bool { | "__pycache__" ) || name.starts_with('.') } + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::{self, File}; + use std::io::Write; + use tempfile::tempdir; + + #[test] + fn test_find_all_matches_absolute_path() { + // function should recognize an absolute path and return it directly. + let path = "/some/absolute/path"; + let result = find_all_matches(path).unwrap(); + assert_eq!(result, vec![path.to_string()]); + } + + #[test] + fn test_find_all_matches_home_tilde() { + // function should recognize a home path and return it directly. + let input = "~/myfile.txt"; + let result = find_all_matches(input).unwrap(); + assert_eq!(result, vec![input.to_string()]); + } + + #[test] + fn test_find_all_matches_existing_relative_file() { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("testfile.txt"); + File::create(&file_path).unwrap(); + + let rel_path = "testfile.txt"; + let result = find_all_matches_from(rel_path, Some(dir.path())).unwrap(); + assert_eq!(result, vec![rel_path.to_string()]); + } + + #[test] + fn test_find_all_matches_recursive_search() { + let dir = tempdir().unwrap(); + let nested_dir = dir.path().join("nested"); + fs::create_dir(&nested_dir).unwrap(); + + let target_file_name = "targetfile.txt"; + let target_path = nested_dir.join(target_file_name); + let mut file = File::create(&target_path).unwrap(); + file.write_all(b"test content").unwrap(); + file.sync_all().unwrap(); + drop(file); // Explicitly close the file + + let matches = find_all_matches_from(target_file_name, Some(dir.path())).unwrap(); + assert_eq!( + matches[0], "nested/targetfile.txt", + "Expected match to be 'nested/targetfile.txt', found: '{}'", + matches[0] + ); + } + + #[test] + fn test_find_all_matches_relative_path() { + let dir = tempdir().unwrap(); + let nested_dir = dir.path().join("subdir"); + fs::create_dir(&nested_dir).unwrap(); + let file_path = nested_dir.join("testfile.txt"); + File::create(&file_path).unwrap(); + + // Test relative path with directory separator + let result = find_all_matches_from("subdir/testfile.txt", Some(dir.path())).unwrap(); + assert_eq!(result, vec!["subdir/testfile.txt".to_string()]); + } + + #[test] + fn test_find_all_matches_not_found() { + let dir = tempdir().unwrap(); + + let result = find_all_matches_from("nonexistentfile.txt", Some(dir.path())); + assert!(result.is_err()); + + // Check that the error matches the expected "file not found" message + if let Err(e) = result { + let error_string = e.to_string(); + assert!( + error_string.contains("No file or directory named 'nonexistentfile.txt'"), + "Error message was: {}", + error_string + ); + } + } + + #[test] + fn test_is_ignored_directory() { + assert!(is_ignored_directory(".git")); + assert!(is_ignored_directory(".svn")); + assert!(is_ignored_directory("node_modules")); + assert!(is_ignored_directory("target")); + assert!(is_ignored_directory("build")); + assert!(is_ignored_directory("dist")); + assert!(is_ignored_directory("__pycache__")); + assert!(is_ignored_directory(".hidden")); + assert!(is_ignored_directory(".hg")); + assert!(is_ignored_directory(".vscode")); + assert!(is_ignored_directory(".idea")); + assert!(!is_ignored_directory("some_dir")); + } +} diff --git a/src/utils.rs b/src/utils.rs index 5432186..a8e7145 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -13,3 +13,22 @@ pub fn format_timestamp_day_month_year(timestamp: i64) -> String { dt.format("%d %b %Y").to_string() } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_format_timestamp_day_month() { + let timestamp = 1749456964; + let result = format_timestamp_day_month(timestamp); + assert_eq!(result, "09 Jun"); + } + + #[test] + fn test_format_timestamp_day_month_year() { + let timestamp = 1749456964; + let result = format_timestamp_day_month_year(timestamp); + assert_eq!(result, "09 Jun 2025"); + } +}