├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs └── src ├── args.rs ├── diffcmp.rs ├── lib.rs └── main.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ansi_term" 7 | version = "0.12.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 10 | dependencies = [ 11 | "winapi", 12 | ] 13 | 14 | [[package]] 15 | name = "anstream" 16 | version = "0.6.5" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" 19 | dependencies = [ 20 | "anstyle", 21 | "anstyle-parse", 22 | "anstyle-query", 23 | "anstyle-wincon", 24 | "colorchoice", 25 | "utf8parse", 26 | ] 27 | 28 | [[package]] 29 | name = "anstyle" 30 | version = "1.0.4" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" 33 | 34 | [[package]] 35 | name = "anstyle-parse" 36 | version = "0.2.3" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" 39 | dependencies = [ 40 | "utf8parse", 41 | ] 42 | 43 | [[package]] 44 | name = "anstyle-query" 45 | version = "1.0.2" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" 48 | dependencies = [ 49 | "windows-sys", 50 | ] 51 | 52 | [[package]] 53 | name = "anstyle-wincon" 54 | version = "3.0.2" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" 57 | dependencies = [ 58 | "anstyle", 59 | "windows-sys", 60 | ] 61 | 62 | [[package]] 63 | name = "atty" 64 | version = "0.2.14" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 67 | dependencies = [ 68 | "hermit-abi", 69 | "libc", 70 | "winapi", 71 | ] 72 | 73 | [[package]] 74 | name = "autocfg" 75 | version = "1.1.0" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 78 | 79 | [[package]] 80 | name = "cfg-if" 81 | version = "1.0.0" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 84 | 85 | [[package]] 86 | name = "clap" 87 | version = "4.4.12" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "dcfab8ba68f3668e89f6ff60f5b205cea56aa7b769451a59f34b8682f51c056d" 90 | dependencies = [ 91 | "clap_builder", 92 | "clap_derive", 93 | ] 94 | 95 | [[package]] 96 | name = "clap_builder" 97 | version = "4.4.12" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9" 100 | dependencies = [ 101 | "anstream", 102 | "anstyle", 103 | "clap_lex", 104 | "strsim", 105 | ] 106 | 107 | [[package]] 108 | name = "clap_derive" 109 | version = "4.4.7" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" 112 | dependencies = [ 113 | "heck", 114 | "proc-macro2", 115 | "quote", 116 | "syn", 117 | ] 118 | 119 | [[package]] 120 | name = "clap_lex" 121 | version = "0.6.0" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" 124 | 125 | [[package]] 126 | name = "clap_mangen" 127 | version = "0.2.16" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "10b5db60b3310cdb376fbeb8826e875a38080d0c61bdec0a91a3da8338948736" 130 | dependencies = [ 131 | "clap", 132 | "roff", 133 | ] 134 | 135 | [[package]] 136 | name = "colorchoice" 137 | version = "1.0.0" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 140 | 141 | [[package]] 142 | name = "crossbeam-deque" 143 | version = "0.8.4" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "fca89a0e215bab21874660c67903c5f143333cab1da83d041c7ded6053774751" 146 | dependencies = [ 147 | "cfg-if", 148 | "crossbeam-epoch", 149 | "crossbeam-utils", 150 | ] 151 | 152 | [[package]] 153 | name = "crossbeam-epoch" 154 | version = "0.9.17" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "0e3681d554572a651dda4186cd47240627c3d0114d45a95f6ad27f2f22e7548d" 157 | dependencies = [ 158 | "autocfg", 159 | "cfg-if", 160 | "crossbeam-utils", 161 | ] 162 | 163 | [[package]] 164 | name = "crossbeam-utils" 165 | version = "0.8.18" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" 168 | dependencies = [ 169 | "cfg-if", 170 | ] 171 | 172 | [[package]] 173 | name = "diffdir" 174 | version = "0.4.4" 175 | dependencies = [ 176 | "ansi_term", 177 | "atty", 178 | "clap", 179 | "clap_mangen", 180 | "glob", 181 | "md5", 182 | "rayon", 183 | "walkdir", 184 | ] 185 | 186 | [[package]] 187 | name = "either" 188 | version = "1.9.0" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" 191 | 192 | [[package]] 193 | name = "glob" 194 | version = "0.3.1" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 197 | 198 | [[package]] 199 | name = "heck" 200 | version = "0.4.1" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 203 | 204 | [[package]] 205 | name = "hermit-abi" 206 | version = "0.1.19" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 209 | dependencies = [ 210 | "libc", 211 | ] 212 | 213 | [[package]] 214 | name = "libc" 215 | version = "0.2.151" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" 218 | 219 | [[package]] 220 | name = "md5" 221 | version = "0.7.0" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" 224 | 225 | [[package]] 226 | name = "proc-macro2" 227 | version = "1.0.75" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "907a61bd0f64c2f29cd1cf1dc34d05176426a3f504a78010f08416ddb7b13708" 230 | dependencies = [ 231 | "unicode-ident", 232 | ] 233 | 234 | [[package]] 235 | name = "quote" 236 | version = "1.0.35" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 239 | dependencies = [ 240 | "proc-macro2", 241 | ] 242 | 243 | [[package]] 244 | name = "rayon" 245 | version = "1.8.0" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" 248 | dependencies = [ 249 | "either", 250 | "rayon-core", 251 | ] 252 | 253 | [[package]] 254 | name = "rayon-core" 255 | version = "1.12.0" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" 258 | dependencies = [ 259 | "crossbeam-deque", 260 | "crossbeam-utils", 261 | ] 262 | 263 | [[package]] 264 | name = "roff" 265 | version = "0.2.1" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" 268 | 269 | [[package]] 270 | name = "same-file" 271 | version = "1.0.6" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 274 | dependencies = [ 275 | "winapi-util", 276 | ] 277 | 278 | [[package]] 279 | name = "strsim" 280 | version = "0.10.0" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 283 | 284 | [[package]] 285 | name = "syn" 286 | version = "2.0.47" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "1726efe18f42ae774cc644f330953a5e7b3c3003d3edcecf18850fe9d4dd9afb" 289 | dependencies = [ 290 | "proc-macro2", 291 | "quote", 292 | "unicode-ident", 293 | ] 294 | 295 | [[package]] 296 | name = "unicode-ident" 297 | version = "1.0.12" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 300 | 301 | [[package]] 302 | name = "utf8parse" 303 | version = "0.2.1" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 306 | 307 | [[package]] 308 | name = "walkdir" 309 | version = "2.4.0" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" 312 | dependencies = [ 313 | "same-file", 314 | "winapi-util", 315 | ] 316 | 317 | [[package]] 318 | name = "winapi" 319 | version = "0.3.9" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 322 | dependencies = [ 323 | "winapi-i686-pc-windows-gnu", 324 | "winapi-x86_64-pc-windows-gnu", 325 | ] 326 | 327 | [[package]] 328 | name = "winapi-i686-pc-windows-gnu" 329 | version = "0.4.0" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 332 | 333 | [[package]] 334 | name = "winapi-util" 335 | version = "0.1.6" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" 338 | dependencies = [ 339 | "winapi", 340 | ] 341 | 342 | [[package]] 343 | name = "winapi-x86_64-pc-windows-gnu" 344 | version = "0.4.0" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 347 | 348 | [[package]] 349 | name = "windows-sys" 350 | version = "0.52.0" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 353 | dependencies = [ 354 | "windows-targets", 355 | ] 356 | 357 | [[package]] 358 | name = "windows-targets" 359 | version = "0.52.0" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" 362 | dependencies = [ 363 | "windows_aarch64_gnullvm", 364 | "windows_aarch64_msvc", 365 | "windows_i686_gnu", 366 | "windows_i686_msvc", 367 | "windows_x86_64_gnu", 368 | "windows_x86_64_gnullvm", 369 | "windows_x86_64_msvc", 370 | ] 371 | 372 | [[package]] 373 | name = "windows_aarch64_gnullvm" 374 | version = "0.52.0" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" 377 | 378 | [[package]] 379 | name = "windows_aarch64_msvc" 380 | version = "0.52.0" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" 383 | 384 | [[package]] 385 | name = "windows_i686_gnu" 386 | version = "0.52.0" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" 389 | 390 | [[package]] 391 | name = "windows_i686_msvc" 392 | version = "0.52.0" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" 395 | 396 | [[package]] 397 | name = "windows_x86_64_gnu" 398 | version = "0.52.0" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" 401 | 402 | [[package]] 403 | name = "windows_x86_64_gnullvm" 404 | version = "0.52.0" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" 407 | 408 | [[package]] 409 | name = "windows_x86_64_msvc" 410 | version = "0.52.0" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" 413 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "diffdir" 3 | version = "0.4.4" 4 | edition = "2021" 5 | authors = ["Vahe Danielyan "] 6 | license = "MIT" 7 | description = "deep compare two directories for differences" 8 | repository = "https://github.com/VaheDanielyan/diffdir/" 9 | documentation = "https://docs.rs/crate/ddiff/" 10 | categories = ["command-line-interface", "command-line-utilities"] 11 | keywords = ["diff", "compare", "cli", "input", "terminal"] 12 | readme = "README.md" 13 | exclude = ["target", "Cargo.lock", "/.github"] 14 | 15 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 16 | 17 | [dependencies] 18 | ansi_term = "0.12.1" 19 | atty = "0.2.14" 20 | clap = { version = "4.4.12", features = ["cargo", "derive"] } 21 | glob = "0.3.1" 22 | md5 = "0.7.0" 23 | rayon = "1.8.0" 24 | walkdir = "2.4.0" 25 | 26 | [build-dependencies] 27 | clap = { version = "4.4.12", features = ["cargo", "derive"] } 28 | clap_mangen = "0.2.16" 29 | glob = "0.3.1" 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Vahe Danielyan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![build status](https://github.com/VaheDanielyan/dirdiff.rs/actions/workflows/rust.yml/badge.svg) [![crates-io](https://img.shields.io/crates/v/diffdir?link=https%3A%2F%2Fcrates.io%2Fcrates%2Fdiffdir)](https://crates.io/crates/diffdir) ![stability-beta](https://img.shields.io/badge/stability-beta-33bbff.svg) 2 | # Diffdir 3 | 4 | A command line tool to compare two directories. 5 | 6 | Uses hashes to compares files with the same name. Also lists the unique files for both directories. 7 | 8 | ## Installation 9 | 10 | If you don't have rust installed, go ahead and [Install Rust](https://www.rust-lang.org/tools/install) 11 | 12 | Clone this repository 13 | 14 | ```sh 15 | git clone git@github.com:VaheDanielyan/diffdir.git 16 | ``` 17 | 18 | Build and install 19 | 20 | ```sh 21 | cargo install --path . 22 | ``` 23 | 24 | ## Usage 25 | 26 | ```sh 27 | Usage: diffdir [OPTIONS] 28 | 29 | Arguments: 30 | 31 | 32 | 33 | Options: 34 | --ignore ... 35 | --ignore-file 36 | --quiet Surpress output 37 | --no-colors will not format into ansi string and / or include colors 38 | -h, --help Print help 39 | -V, --version Print version 40 | ``` 41 | 42 | ## Output 43 | 44 | In Addition to the standard text output the program will return **42** if there are any differences between the directories and **0** in case of them being identical. This can be handy when calling this tool from other programs. 45 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use clap_mangen::Man; 2 | use std::env; 3 | 4 | include!("src/args.rs"); // Adjust the path to where your Cli struct is defined 5 | 6 | fn main() { 7 | let app = Args::command(); 8 | let out_dir = std::path::PathBuf::from(env::var("OUT_DIR").unwrap()); 9 | let mut file = File::create(out_dir.join("dirdiff.1")).expect("Could not create man page file"); 10 | Man::new(app).render(&mut file).expect("Could not render man page"); 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use glob::Pattern; 4 | use std::fs::File; 5 | use std::io::{BufReader, Read}; 6 | use clap::*; 7 | 8 | #[derive(Parser)] 9 | #[clap(author, version, about = "A cli tool to compare two directories", long_about)] 10 | pub struct Args { 11 | #[clap(value_parser, value_name = "dir a")] 12 | pub dir_a: PathBuf, 13 | 14 | #[clap(value_parser, value_name = "dir b")] 15 | pub dir_b: PathBuf, 16 | 17 | #[clap(long = "ignore", value_parser, use_value_delimiter(true), value_delimiter = ' ', num_args=1..)] 18 | pub ignore_patterns: Option>, 19 | 20 | #[clap(long = "ignore-file", value_parser)] 21 | pub ignore_file: Option, 22 | 23 | /// Surpress output 24 | #[clap(long = "quiet", value_parser)] 25 | pub quiet: bool, 26 | 27 | /// will not format into ansi string and / or include colors 28 | #[clap(long = "no-colors", value_parser)] 29 | pub no_colors: bool, 30 | } 31 | 32 | impl Args { 33 | pub fn verify(&self) -> Result<(), String> { 34 | let mut errors : Vec = Vec::new(); 35 | if !self.dir_a.exists() { 36 | errors.push("argument error: Dir A doesn't exist".to_string()); 37 | } 38 | if !self.dir_a.is_dir() { 39 | errors.push("argument error: A is not a directory".to_string()); 40 | } 41 | if !self.dir_b.exists() { 42 | errors.push("argument error: Dir B doesn't exist".to_string()); 43 | } 44 | if !self.dir_b.is_dir() { 45 | errors.push("argument error: B is not a directory".to_string()); 46 | } 47 | if let Some(ignore_file) = &self.ignore_file { 48 | if !ignore_file.exists() { 49 | errors.push("argument error: Ignore file doesn't exist".to_string()); 50 | } 51 | } 52 | if errors.is_empty() { 53 | return Ok(()); 54 | } 55 | Err(errors.join("\n")) 56 | } 57 | pub fn parse_ignore_file(path: PathBuf) -> Vec { 58 | let file = File::open(path).expect("Error opening ignore file"); 59 | let mut reader = BufReader::new(file); 60 | 61 | let mut patterns = Vec::new(); 62 | let mut file_contents: String = String::new(); 63 | _ = reader.read_to_string(&mut file_contents); 64 | for line in file_contents.lines() { 65 | if line.trim().is_empty() || line.starts_with('#') { 66 | continue; 67 | } 68 | match Pattern::new(&line) { 69 | Ok(pattern) => patterns.push(pattern), 70 | Err(e) => eprintln!("Invalid pattern in ignore file, line - '{}': {}", line, e), 71 | } 72 | } 73 | patterns 74 | } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /src/diffcmp.rs: -------------------------------------------------------------------------------- 1 | use std::path::{PathBuf, Path}; 2 | use std::collections::HashMap; 3 | use std::fs::File; 4 | use std::io::Read; 5 | 6 | use walkdir::WalkDir; 7 | use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; 8 | use glob::Pattern; 9 | use ansi_term::{Style, Colour::*}; 10 | use md5; 11 | 12 | #[derive(Debug, Clone)] 13 | pub enum Hash { 14 | Valid { hash: String }, 15 | Invalid { error: String }, 16 | } 17 | 18 | impl Hash { 19 | pub fn new(path: &Path) -> Hash { 20 | let mut file = match File::open(path) { 21 | Ok(file) => file, 22 | Err(e) => return Hash::Invalid { error: e.to_string() } 23 | }; 24 | 25 | let mut buffer = Vec::new(); 26 | match file.read_to_end(&mut buffer) { 27 | Err(e) => return Hash::Invalid { error: e.to_string() }, 28 | _ => {} 29 | } 30 | let digest = md5::compute(&buffer); 31 | 32 | Hash::Valid { hash: format!("{:?}", digest)} 33 | } 34 | } 35 | 36 | #[derive(Debug)] 37 | struct FileInfo { 38 | hash: Hash, 39 | } 40 | 41 | impl FileInfo { 42 | pub fn get_hash(&self) -> Hash { 43 | self.hash.clone() 44 | } 45 | } 46 | 47 | pub struct DirCmp { 48 | path_a : PathBuf, 49 | path_b : PathBuf, 50 | ignore_patterns : Option>, 51 | } 52 | 53 | impl DirCmp { 54 | pub fn new(path_a: &PathBuf, path_b: &PathBuf, ignore_patterns : &Option>) -> DirCmp { 55 | DirCmp { path_a: path_a.to_owned(), path_b: path_b.to_owned(), ignore_patterns: ignore_patterns.to_owned() } 56 | } 57 | pub fn compare_directories(&self) -> CmpResult { 58 | let path_a_clone = self.path_a.clone(); let path_b_clone = self.path_b.clone(); 59 | 60 | let ignore_patterns_1 = self.ignore_patterns.clone(); 61 | let ignore_patterns_2 = self.ignore_patterns.clone(); 62 | 63 | let thread_a = std::thread::spawn(move || { 64 | return DirCmp::process_directory(&path_a_clone, &ignore_patterns_1); 65 | }); 66 | 67 | let thread_b = std::thread::spawn(move || { 68 | return DirCmp::process_directory(&path_b_clone, &ignore_patterns_2); 69 | }); 70 | 71 | let map1 : HashMap = thread_a.join().unwrap().ok().unwrap(); 72 | let map2 : HashMap = thread_b.join().unwrap().ok().unwrap(); 73 | let mut result : CmpResult = CmpResult::new(&self.path_a, &self.path_b); 74 | 75 | for item in &map1 { 76 | if map2.contains_key(item.0) { 77 | let item2 = map2.get(item.0).unwrap(); 78 | let hash1 = match item.1.get_hash() { 79 | Hash::Valid { hash } => hash, 80 | Hash::Invalid { error } => error, 81 | }; 82 | let hash2 = match item2.get_hash() { 83 | Hash::Valid { hash } => hash, 84 | Hash::Invalid { error } => error, 85 | }; 86 | if hash1 != hash2 { 87 | result.differs.push(item.0.clone()); 88 | } 89 | } 90 | else { 91 | result.only_in_a.push(item.0.clone()); 92 | } 93 | } 94 | for item in &map2 { 95 | if !map1.contains_key(item.0) { 96 | result.only_in_b.push(item.0.clone()); 97 | } 98 | } 99 | result 100 | 101 | } 102 | fn process_directory(path: &PathBuf, ignore_patterns : &Option>) -> Result, String> { 103 | let files : Vec = WalkDir::new(path) 104 | .into_iter() 105 | .filter_map(|f| f.ok()) 106 | .filter(|f| { 107 | let mut ignore = false; 108 | let file_name = f.file_name().to_str().unwrap(); 109 | if let Some(patters) = ignore_patterns { 110 | ignore = patters.iter() 111 | .any(|patt| { 112 | patt.matches(file_name) 113 | }); 114 | } 115 | f.file_type().is_file() && !ignore 116 | }) 117 | .map(|f| f.path().to_owned()) 118 | .collect(); 119 | let result_map : HashMap = files 120 | .par_iter() 121 | .map(|f| { 122 | let file_hash = Hash::new(f); 123 | (f.strip_prefix(path).unwrap().to_owned(), FileInfo { hash: file_hash }) 124 | }) 125 | .collect(); 126 | Ok(result_map) 127 | } 128 | } 129 | 130 | pub struct CmpResult { 131 | pub dir_a : PathBuf, 132 | pub dir_b : PathBuf, 133 | pub only_in_a : Vec, 134 | pub only_in_b: Vec, 135 | pub differs : Vec, 136 | } 137 | 138 | impl CmpResult { 139 | pub fn new(dir_a : &PathBuf, dir_b : &PathBuf) -> CmpResult { 140 | CmpResult { dir_a: dir_a.to_owned(), 141 | dir_b: dir_b.to_owned(), 142 | only_in_a: Vec::new(), 143 | only_in_b: Vec::new(), 144 | differs: Vec::new() } 145 | } 146 | pub fn are_different(&self) -> bool { 147 | if self.only_in_a.is_empty() && self.only_in_b.is_empty() && self.differs.is_empty() { 148 | return false; 149 | } 150 | true 151 | } 152 | pub fn format_text(&self, ansi: bool) -> Vec { 153 | let bold = Style::new().bold(); 154 | let bold_underline = bold.underline(); 155 | let mut result : Vec = Vec::new(); 156 | let mut result_plain: Vec = Vec::new(); 157 | 158 | println!(); 159 | if !self.are_different() { 160 | let message = format!("The directories appear to be the same\n"); 161 | let styled_message = bold. 162 | paint(&message); 163 | result.push(styled_message.to_string()); 164 | result_plain.push(message); 165 | } 166 | 167 | if !self.only_in_a.is_empty() { 168 | let message = format!("Files that appear only in {}\n", self.dir_a.to_str().unwrap()); 169 | let styled_message = bold_underline.fg(Yellow) 170 | .paint(&message); 171 | result.push(styled_message.to_string()); 172 | result_plain.push(message); 173 | for item in &self.only_in_a { 174 | let file_message = format!("{}\n", item.to_str().unwrap()); 175 | result.push(file_message.clone()); 176 | result_plain.push(file_message); 177 | } 178 | result.push("\n".to_string()); 179 | result_plain.push("\n".to_string()); 180 | } 181 | 182 | if !self.only_in_b.is_empty() { 183 | let message = format!("Files that appear only in {}\n", self.dir_b.to_str().unwrap()); 184 | let styled_message = bold_underline.fg(Yellow) 185 | .paint(&message); 186 | result.push(styled_message.to_string()); 187 | result_plain.push(message); 188 | for item in &self.only_in_b { 189 | let file_message = format!("{}\n", item.to_str().unwrap()); 190 | result.push(file_message.clone()); 191 | result_plain.push(file_message); 192 | } 193 | result.push("\n".to_string()); 194 | result_plain.push("\n".to_string()); 195 | } 196 | 197 | if !self.differs.is_empty() { 198 | let message = format!("Files that differ\n"); 199 | let styled_message = bold_underline.fg(Red) 200 | .paint(&message); 201 | result.push(styled_message.to_string()); 202 | result_plain.push(message); 203 | for item in &self.differs { 204 | let file_message = format!("{}\n", item.to_str().unwrap()); 205 | result.push(file_message.clone()); 206 | result_plain.push(file_message); 207 | } 208 | } 209 | if ansi { result } else { result_plain } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] 2 | 3 | pub mod args; 4 | pub mod diffcmp; 5 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate diffdir; 2 | use diffdir::diffcmp::{DirCmp, CmpResult}; 3 | use diffdir::args::Args; 4 | 5 | use core::panic; 6 | use atty::Stream; 7 | use clap::Parser; 8 | 9 | fn main() { 10 | let args = Args::parse(); 11 | 12 | match args.verify() { 13 | Err(message) => panic!("{}", message), 14 | Ok(()) => {} 15 | }; 16 | 17 | let ignore_file_patterns = match args.ignore_file { 18 | Some(file) => Some(Args::parse_ignore_file(file)), 19 | _ => None 20 | }; 21 | 22 | let merged_patterns = match (ignore_file_patterns, args.ignore_patterns) { 23 | (Some(mut vec1), Some(vec2)) => { 24 | vec1.extend(vec2); 25 | Some(vec1) 26 | }, 27 | (Some(vec), None) | (None, Some(vec)) => Some(vec), 28 | (None, None) => None, 29 | }; 30 | 31 | let dir_comparator = DirCmp::new(&args.dir_a, &args.dir_b, &merged_patterns); 32 | let result: CmpResult = dir_comparator.compare_directories(); 33 | if !args.quiet { 34 | let text = if atty::is(Stream::Stdout) { 35 | result.format_text(!args.no_colors) 36 | } else { 37 | result.format_text(false) 38 | }; 39 | 40 | for item in text { 41 | print!("{}", item); 42 | } 43 | } 44 | if result.are_different() { std::process::exit(42) } 45 | } 46 | --------------------------------------------------------------------------------