├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── README.md ├── docs └── sample.png ├── rustfmt.toml └── src ├── bin └── pratdiff.rs ├── diff.rs ├── files.rs ├── lib.rs ├── printer.rs ├── style.rs └── testdata ├── new ├── add.txt ├── dangling ├── eof-newline.txt ├── modify.txt ├── move.txt └── new-only.txt └── old ├── add.txt ├── eof-newline.txt ├── modify.txt ├── move.txt └── old-only.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .*~ 2 | 3 | 4 | # Added by cargo 5 | 6 | /target 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.1.7] - 2025-05-21 11 | 12 | ### Changed 13 | 14 | - Update clap versions 15 | - Update dependency minimum versions 16 | 17 | ## [0.1.6] - 2025-05-21 18 | 19 | ### Changed 20 | 21 | - Move around code to make pratdiff easier to use as a library 22 | - Update dependency versions 23 | 24 | ## [0.1.5] - 2024-06-24 25 | 26 | ### Changed 27 | 28 | - Make directory walk report errors without halting the entire walk 29 | - Include symlink info in file names 30 | 31 | ## [0.1.4] - 2024-06-19 32 | 33 | ### Changed 34 | 35 | - Improve performance for the non-unique line case 36 | - Update dependency versions 37 | 38 | ## [0.1.2] - 2024-06-09 39 | 40 | ### Added 41 | 42 | - Added shell completions via `--completions` 43 | 44 | ## [0.1.1] - 2024-06-09 45 | 46 | Documentation fixes only 47 | 48 | ### Changed 49 | 50 | - Improve readme 51 | 52 | ### Fixed 53 | 54 | - Correct license 55 | 56 | ## [0.1.0] - 2024-06-09 57 | 58 | Initial release. 59 | 60 | ### Added 61 | 62 | - Color diff including token level coloration 63 | - Common prefix stripping for paths 64 | - Directory walking 65 | 66 | 67 | [Unreleased]: https://github.com/fowles/pratdiff/compare/0.1.7...main 68 | [0.1.7]: https://github.com/fowles/pratdiff/compare/0.1.6...0.1.7 69 | [0.1.6]: https://github.com/fowles/pratdiff/compare/0.1.5...0.1.6 70 | [0.1.5]: https://github.com/fowles/pratdiff/compare/0.1.4...0.1.5 71 | [0.1.4]: https://github.com/fowles/pratdiff/compare/0.1.2...0.1.4 72 | [0.1.2]: https://github.com/fowles/pratdiff/compare/0.1.1...0.1.2 73 | [0.1.1]: https://github.com/fowles/pratdiff/compare/0.1.0...0.1.1 74 | [0.1.0]: https://github.com/fowles/pratdiff/compare/33062819364ff9bf89dc1566e05e0b3e448ec094...0.1.0 75 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstream" 16 | version = "0.6.18" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 19 | dependencies = [ 20 | "anstyle", 21 | "anstyle-parse", 22 | "anstyle-query", 23 | "anstyle-wincon", 24 | "colorchoice", 25 | "is_terminal_polyfill", 26 | "utf8parse", 27 | ] 28 | 29 | [[package]] 30 | name = "anstyle" 31 | version = "1.0.10" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 34 | 35 | [[package]] 36 | name = "anstyle-parse" 37 | version = "0.2.6" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 40 | dependencies = [ 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-query" 46 | version = "1.1.2" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 49 | dependencies = [ 50 | "windows-sys", 51 | ] 52 | 53 | [[package]] 54 | name = "anstyle-wincon" 55 | version = "3.0.8" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" 58 | dependencies = [ 59 | "anstyle", 60 | "once_cell_polyfill", 61 | "windows-sys", 62 | ] 63 | 64 | [[package]] 65 | name = "bitflags" 66 | version = "2.9.1" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 69 | 70 | [[package]] 71 | name = "cfg-if" 72 | version = "1.0.0" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 75 | 76 | [[package]] 77 | name = "clap" 78 | version = "4.5.38" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" 81 | dependencies = [ 82 | "clap_builder", 83 | "clap_derive", 84 | ] 85 | 86 | [[package]] 87 | name = "clap_builder" 88 | version = "4.5.38" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" 91 | dependencies = [ 92 | "anstream", 93 | "anstyle", 94 | "clap_lex", 95 | "strsim", 96 | ] 97 | 98 | [[package]] 99 | name = "clap_complete" 100 | version = "4.5.50" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "c91d3baa3bcd889d60e6ef28874126a0b384fd225ab83aa6d8a801c519194ce1" 103 | dependencies = [ 104 | "clap", 105 | ] 106 | 107 | [[package]] 108 | name = "clap_complete_command" 109 | version = "0.6.1" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "da8e198c052315686d36371e8a3c5778b7852fc75cc313e4e11eeb7a644a1b62" 112 | dependencies = [ 113 | "clap", 114 | "clap_complete", 115 | "clap_complete_nushell", 116 | ] 117 | 118 | [[package]] 119 | name = "clap_complete_nushell" 120 | version = "4.5.5" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "c6a8b1593457dfc2fe539002b795710d022dc62a65bf15023f039f9760c7b18a" 123 | dependencies = [ 124 | "clap", 125 | "clap_complete", 126 | ] 127 | 128 | [[package]] 129 | name = "clap_derive" 130 | version = "4.5.32" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 133 | dependencies = [ 134 | "heck", 135 | "proc-macro2", 136 | "quote", 137 | "syn", 138 | ] 139 | 140 | [[package]] 141 | name = "clap_lex" 142 | version = "0.7.4" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 145 | 146 | [[package]] 147 | name = "colorchoice" 148 | version = "1.0.3" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 151 | 152 | [[package]] 153 | name = "common-path" 154 | version = "1.0.0" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "2382f75942f4b3be3690fe4f86365e9c853c1587d6ee58212cebf6e2a9ccd101" 157 | 158 | [[package]] 159 | name = "errno" 160 | version = "0.3.12" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" 163 | dependencies = [ 164 | "libc", 165 | "windows-sys", 166 | ] 167 | 168 | [[package]] 169 | name = "fastrand" 170 | version = "2.3.0" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 173 | 174 | [[package]] 175 | name = "getrandom" 176 | version = "0.3.3" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 179 | dependencies = [ 180 | "cfg-if", 181 | "libc", 182 | "r-efi", 183 | "wasi", 184 | ] 185 | 186 | [[package]] 187 | name = "heck" 188 | version = "0.5.0" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 191 | 192 | [[package]] 193 | name = "is_terminal_polyfill" 194 | version = "1.70.1" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 197 | 198 | [[package]] 199 | name = "libc" 200 | version = "0.2.172" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 203 | 204 | [[package]] 205 | name = "linux-raw-sys" 206 | version = "0.9.4" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 209 | 210 | [[package]] 211 | name = "memchr" 212 | version = "2.7.4" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 215 | 216 | [[package]] 217 | name = "once_cell" 218 | version = "1.21.3" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 221 | 222 | [[package]] 223 | name = "once_cell_polyfill" 224 | version = "1.70.0" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "2611b99ab098a31bdc8be48b4f1a285ca0ced28bd5b4f23e45efa8c63b09efa5" 227 | dependencies = [ 228 | "once_cell", 229 | ] 230 | 231 | [[package]] 232 | name = "owo-colors" 233 | version = "4.2.1" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "26995317201fa17f3656c36716aed4a7c81743a9634ac4c99c0eeda495db0cec" 236 | 237 | [[package]] 238 | name = "pratdiff" 239 | version = "0.1.7" 240 | dependencies = [ 241 | "anstream", 242 | "cfg-if", 243 | "clap", 244 | "clap_complete", 245 | "clap_complete_command", 246 | "common-path", 247 | "owo-colors", 248 | "regex", 249 | "tempfile", 250 | "walkdir", 251 | ] 252 | 253 | [[package]] 254 | name = "proc-macro2" 255 | version = "1.0.95" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 258 | dependencies = [ 259 | "unicode-ident", 260 | ] 261 | 262 | [[package]] 263 | name = "quote" 264 | version = "1.0.40" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 267 | dependencies = [ 268 | "proc-macro2", 269 | ] 270 | 271 | [[package]] 272 | name = "r-efi" 273 | version = "5.2.0" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 276 | 277 | [[package]] 278 | name = "regex" 279 | version = "1.11.1" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 282 | dependencies = [ 283 | "aho-corasick", 284 | "memchr", 285 | "regex-automata", 286 | "regex-syntax", 287 | ] 288 | 289 | [[package]] 290 | name = "regex-automata" 291 | version = "0.4.9" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 294 | dependencies = [ 295 | "aho-corasick", 296 | "memchr", 297 | "regex-syntax", 298 | ] 299 | 300 | [[package]] 301 | name = "regex-syntax" 302 | version = "0.8.5" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 305 | 306 | [[package]] 307 | name = "rustix" 308 | version = "1.0.7" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" 311 | dependencies = [ 312 | "bitflags", 313 | "errno", 314 | "libc", 315 | "linux-raw-sys", 316 | "windows-sys", 317 | ] 318 | 319 | [[package]] 320 | name = "same-file" 321 | version = "1.0.6" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 324 | dependencies = [ 325 | "winapi-util", 326 | ] 327 | 328 | [[package]] 329 | name = "strsim" 330 | version = "0.11.1" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 333 | 334 | [[package]] 335 | name = "syn" 336 | version = "2.0.101" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 339 | dependencies = [ 340 | "proc-macro2", 341 | "quote", 342 | "unicode-ident", 343 | ] 344 | 345 | [[package]] 346 | name = "tempfile" 347 | version = "3.20.0" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" 350 | dependencies = [ 351 | "fastrand", 352 | "getrandom", 353 | "once_cell", 354 | "rustix", 355 | "windows-sys", 356 | ] 357 | 358 | [[package]] 359 | name = "unicode-ident" 360 | version = "1.0.18" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 363 | 364 | [[package]] 365 | name = "utf8parse" 366 | version = "0.2.2" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 369 | 370 | [[package]] 371 | name = "walkdir" 372 | version = "2.5.0" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 375 | dependencies = [ 376 | "same-file", 377 | "winapi-util", 378 | ] 379 | 380 | [[package]] 381 | name = "wasi" 382 | version = "0.14.2+wasi-0.2.4" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 385 | dependencies = [ 386 | "wit-bindgen-rt", 387 | ] 388 | 389 | [[package]] 390 | name = "winapi-util" 391 | version = "0.1.9" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 394 | dependencies = [ 395 | "windows-sys", 396 | ] 397 | 398 | [[package]] 399 | name = "windows-sys" 400 | version = "0.59.0" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 403 | dependencies = [ 404 | "windows-targets", 405 | ] 406 | 407 | [[package]] 408 | name = "windows-targets" 409 | version = "0.52.6" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 412 | dependencies = [ 413 | "windows_aarch64_gnullvm", 414 | "windows_aarch64_msvc", 415 | "windows_i686_gnu", 416 | "windows_i686_gnullvm", 417 | "windows_i686_msvc", 418 | "windows_x86_64_gnu", 419 | "windows_x86_64_gnullvm", 420 | "windows_x86_64_msvc", 421 | ] 422 | 423 | [[package]] 424 | name = "windows_aarch64_gnullvm" 425 | version = "0.52.6" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 428 | 429 | [[package]] 430 | name = "windows_aarch64_msvc" 431 | version = "0.52.6" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 434 | 435 | [[package]] 436 | name = "windows_i686_gnu" 437 | version = "0.52.6" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 440 | 441 | [[package]] 442 | name = "windows_i686_gnullvm" 443 | version = "0.52.6" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 446 | 447 | [[package]] 448 | name = "windows_i686_msvc" 449 | version = "0.52.6" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 452 | 453 | [[package]] 454 | name = "windows_x86_64_gnu" 455 | version = "0.52.6" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 458 | 459 | [[package]] 460 | name = "windows_x86_64_gnullvm" 461 | version = "0.52.6" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 464 | 465 | [[package]] 466 | name = "windows_x86_64_msvc" 467 | version = "0.52.6" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 470 | 471 | [[package]] 472 | name = "wit-bindgen-rt" 473 | version = "0.39.0" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 476 | dependencies = [ 477 | "bitflags", 478 | ] 479 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pratdiff" 3 | description = "A colorfull diff tool based on the patience diff algorithm" 4 | repository = "https://github.com/fowles/pratdiff" 5 | version = "0.1.7" 6 | edition = "2021" 7 | license = "Apache-2.0" 8 | keywords = ["diff", "patience"] 9 | categories = ["command-line-utilities", "development-tools", "filesystem"] 10 | exclude = [ 11 | "src/testdata", 12 | "docs", 13 | ] 14 | 15 | [dependencies] 16 | anstream = "0.6.18" 17 | cfg-if = "1.0.0" 18 | clap = { version = "4.5.38", features = ["derive", "help", "usage", "suggestions", "color"] } 19 | clap_complete = "4.5.50" 20 | clap_complete_command = "0.6.1" 21 | common-path = "1.0.0" 22 | owo-colors = "4.2.1" 23 | regex = "1.11.1" 24 | walkdir = "2.5.0" 25 | 26 | [dev-dependencies] 27 | tempfile = "3.20.0" 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2024 Matt Kulukundis 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A diff tool that provides line and token level colorization. 2 | 3 | ![Sample Code Diff](https://raw.githubusercontent.com/fowles/pratdiff/main/docs/sample.png) 4 | 5 | Based on the [Patience Diff 6 | algorithm](https://bramcohen.livejournal.com/73318.html) described by Bram 7 | Cohen and then expanded upon by James Coglan in two blogs posts 8 | ([algorithm](https://blog.jcoglan.com/2017/09/19/the-patience-diff-algorithm/) 9 | and 10 | [implementation](https://blog.jcoglan.com/2017/09/28/implementing-patience-diff/)). 11 | 12 | # FAQ 13 | 14 | ## How do I install `pratdiff`? 15 | 16 | Use `cargo install pratdiff`. You probably want to get `cargo` from 17 | [`rustup`](https://www.rust-lang.org/tools/install) or 18 | [`brew`](https://brew.sh/). 19 | 20 | ## How do I enable autocompletions? 21 | 22 | The `--completions` flag takes a shell and outputs a completion script. 23 | 24 | ```bash 25 | eval "$(pratdiff --completions=bash)" 26 | ``` 27 | 28 | ```fish 29 | pratdiff --completions=fish | source 30 | ``` 31 | 32 | ## Why did you bother doing this? 33 | 34 | Cause I wanted a learning project and this seemed like a reasonable one. 35 | 36 | ## Why did you name it `pratdiff`? 37 | 38 | I wanted to insert an "r" into `patdiff`, and I kind of like "prat" as an oddly 39 | out of date insult. 40 | 41 | ## Did you learn anything interesting? 42 | 43 | The way that token level diffing uses the same algorithm as the line level 44 | diffing is pretty cool in my mind. I didn't think going into it that I would 45 | structure it that way and it all kinda just fell out. 46 | 47 | Also, I learned that the tiny extension on patience diff I made to use 48 | non-unique lines if unique ones fail is a known algorithm called "histogram 49 | diff". 50 | -------------------------------------------------------------------------------- /docs/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fowles/pratdiff/dfa395c21bd501b8676d61b9a42b009d92aaa3d2/docs/sample.png -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 2 | tab_spaces = 2 3 | 4 | struct_lit_width = 40 5 | struct_variant_width = 40 6 | chain_width = 60 7 | use_small_heuristics = "Max" 8 | -------------------------------------------------------------------------------- /src/bin/pratdiff.rs: -------------------------------------------------------------------------------- 1 | use clap::{ColorChoice, CommandFactory, Parser}; 2 | use clap_complete_command::Shell; 3 | use common_path::common_path; 4 | use std::error::Error; 5 | use std::path::PathBuf; 6 | 7 | #[derive(Parser, Debug)] 8 | #[command(version)] 9 | #[command(about = "Diff files using patience algorithm")] 10 | struct Args { 11 | /// Path to old file, directory tree, or `-` for stdin. 12 | #[clap(name = "OLD_FILE", required_unless_present = "shell")] 13 | lhs: Option, 14 | 15 | /// Path to new file, directory tree, or `-` for stdin. 16 | #[clap(name = "NEW_FILE", required_unless_present = "shell")] 17 | rhs: Option, 18 | 19 | /// Display NUM lines of unchanged context before and after changes 20 | #[clap(short, long, value_name = "NUM", default_value_t = 3)] 21 | context: usize, 22 | 23 | /// Print full paths instead of stripping a common prefix 24 | #[clap(short, long)] 25 | verbose_paths: bool, 26 | 27 | #[clap(long, default_value_t = ColorChoice::Auto)] 28 | color: ColorChoice, 29 | 30 | /// The shell to generate the completions for 31 | #[arg(long = "completions", value_name = "SHELL", value_enum)] 32 | shell: Option, 33 | } 34 | 35 | fn main() -> Result<(), Box> { 36 | let args = Args::parse(); 37 | if let Some(shell) = args.shell { 38 | shell.generate(&mut Args::command(), &mut std::io::stdout()); 39 | return Ok(()); 40 | } 41 | 42 | match args.color { 43 | ColorChoice::Auto => anstream::ColorChoice::Auto, 44 | ColorChoice::Always => anstream::ColorChoice::Always, 45 | ColorChoice::Never => anstream::ColorChoice::Never, 46 | } 47 | .write_global(); 48 | 49 | let lhs = args.lhs.unwrap(); 50 | let rhs = args.rhs.unwrap(); 51 | let common_prefix = if args.verbose_paths { 52 | PathBuf::new() 53 | } else { 54 | common_path(&lhs, &rhs).unwrap_or_default() 55 | }; 56 | 57 | let mut stdout = anstream::stdout(); 58 | let mut p = 59 | pratdiff::Printer::default(&mut stdout, args.context, common_prefix); 60 | pratdiff::diff_files(&mut p, &lhs, &rhs) 61 | } 62 | -------------------------------------------------------------------------------- /src/diff.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Range; 2 | 3 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 4 | pub enum Side { 5 | Lhs, 6 | Rhs, 7 | } 8 | 9 | #[derive(Clone, Debug, Eq, PartialEq)] 10 | pub enum DiffItem { 11 | Match { lhs: Range, rhs: Range }, 12 | Mutation { lhs: Range, rhs: Range }, 13 | } 14 | 15 | use DiffItem::*; 16 | 17 | impl DiffItem { 18 | pub fn lhs(&self) -> Range { 19 | match self { 20 | Match { lhs, .. } => lhs.clone(), 21 | Mutation { lhs, .. } => lhs.clone(), 22 | } 23 | } 24 | 25 | pub fn rhs(&self) -> Range { 26 | match self { 27 | Match { rhs, .. } => rhs.clone(), 28 | Mutation { rhs, .. } => rhs.clone(), 29 | } 30 | } 31 | 32 | pub fn side(&self, side: Side) -> Range { 33 | match side { 34 | Side::Lhs => self.lhs(), 35 | Side::Rhs => self.rhs(), 36 | } 37 | } 38 | } 39 | 40 | #[derive(Clone, Debug, Eq, PartialEq)] 41 | pub struct Hunk { 42 | pub diffs: Vec, 43 | } 44 | 45 | impl Hunk { 46 | pub fn build(context: usize, diffs: &[DiffItem]) -> Vec { 47 | let mut res = vec![Hunk { diffs: Vec::new() }]; 48 | 49 | for d in diffs { 50 | res.last_mut().unwrap().diffs.push(d.clone()); 51 | 52 | if matches!(d, Match { lhs, .. } if lhs.len() > 2 * context) { 53 | res.push(Hunk { diffs: vec![d.clone()] }); 54 | } 55 | } 56 | 57 | res 58 | .into_iter() 59 | .filter_map(|mut hunk| { 60 | if hunk.diffs.len() <= 1 && matches!(hunk.diffs[0], Match { .. }) { 61 | return None; 62 | } 63 | 64 | if context == 0 { 65 | hunk.diffs.retain(|d| matches!(d, Mutation { .. })); 66 | return Some(hunk); 67 | } 68 | 69 | if let Some(Match { lhs, rhs }) = hunk.diffs.first_mut() { 70 | if lhs.len() > context { 71 | lhs.start = lhs.end - context; 72 | rhs.start = rhs.end - context; 73 | } 74 | } 75 | if let Some(Match { lhs, rhs }) = hunk.diffs.last_mut() { 76 | if lhs.len() > context { 77 | lhs.end = lhs.start + context; 78 | rhs.end = rhs.start + context; 79 | } 80 | } 81 | Some(hunk) 82 | }) 83 | .collect() 84 | } 85 | 86 | pub fn side(&self, side: Side) -> Range { 87 | Range { 88 | start: self.diffs.first().map_or(0, |d| d.side(side).start), 89 | end: self.diffs.last().map_or(0, |d| d.side(side).end), 90 | } 91 | } 92 | 93 | pub fn lhs(&self) -> Range { 94 | self.side(Side::Lhs) 95 | } 96 | 97 | pub fn rhs(&self) -> Range { 98 | self.side(Side::Rhs) 99 | } 100 | } 101 | 102 | #[derive(Clone, Debug, Default, Eq, PartialEq)] 103 | pub struct Diffs { 104 | pub(crate) vec: Vec, 105 | } 106 | 107 | impl Diffs { 108 | pub(crate) fn add_match(&mut self, len: usize) { 109 | if len == 0 { 110 | return; 111 | } 112 | if let Some(Match { lhs, rhs }) = self.vec.last_mut() { 113 | lhs.end += len; 114 | rhs.end += len; 115 | } else { 116 | self.vec.push(Match { 117 | lhs: Range { 118 | start: self.lhs_pos(), 119 | end: self.lhs_pos() + len, 120 | }, 121 | rhs: Range { 122 | start: self.rhs_pos(), 123 | end: self.rhs_pos() + len, 124 | }, 125 | }); 126 | } 127 | } 128 | 129 | pub(crate) fn add_mutation(&mut self, lhs: usize, rhs: usize) { 130 | if lhs == 0 && rhs == 0 { 131 | return; 132 | } 133 | if let Some(Mutation { lhs: l, rhs: r }) = self.vec.last_mut() { 134 | l.end += lhs; 135 | r.end += rhs; 136 | } else { 137 | self.vec.push(Mutation { 138 | lhs: Range { 139 | start: self.lhs_pos(), 140 | end: self.lhs_pos() + lhs, 141 | }, 142 | rhs: Range { 143 | start: self.rhs_pos(), 144 | end: self.rhs_pos() + rhs, 145 | }, 146 | }); 147 | } 148 | } 149 | 150 | fn lhs_pos(&self) -> usize { 151 | self.vec.last().map_or(0, |d| d.lhs().end) 152 | } 153 | 154 | fn rhs_pos(&self) -> usize { 155 | self.vec.last().map_or(0, |d| d.rhs().end) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/files.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | use std::error::Error; 3 | use std::fs::File; 4 | use std::io::Read; 5 | use std::path::Path; 6 | use walkdir::{DirEntry, WalkDir}; 7 | 8 | use crate::printer::Printer; 9 | 10 | pub fn diff_files( 11 | p: &mut Printer, 12 | lhs: &Path, 13 | rhs: &Path, 14 | ) -> Result<(), Box> { 15 | match (lhs.metadata()?.is_dir(), rhs.metadata()?.is_dir()) { 16 | (false, false) => diff_file_candidates(p, Some(lhs), Some(rhs)), 17 | (true, true) => diff_directories(p, lhs, rhs), 18 | _ => Ok(p.print_directory_mismatch(lhs, rhs)?), 19 | } 20 | } 21 | 22 | fn diff_directories( 23 | p: &mut Printer, 24 | lhs: &Path, 25 | rhs: &Path, 26 | ) -> Result<(), Box> { 27 | walk_dirs(lhs, rhs, |l, r| { 28 | if let Err(e) = diff_entries(p, l, r) { 29 | p.print_error( 30 | l.as_ref().map(|l| l.path()), 31 | r.as_ref().map(|r| r.path()), 32 | e, 33 | )?; 34 | } 35 | Ok(()) 36 | }) 37 | } 38 | 39 | fn diff_entries( 40 | p: &mut Printer, 41 | lhs: &Option, 42 | rhs: &Option, 43 | ) -> Result<(), Box> { 44 | match (lhs, rhs) { 45 | (None, None) => Ok(()), 46 | (Some(lhs), None) => { 47 | if lhs.metadata()?.is_dir() { 48 | Ok(()) 49 | } else { 50 | diff_file_candidates(p, Some(lhs.path()), None) 51 | } 52 | } 53 | (None, Some(rhs)) => { 54 | if rhs.metadata()?.is_dir() { 55 | Ok(()) 56 | } else { 57 | diff_file_candidates(p, None, Some(rhs.path())) 58 | } 59 | } 60 | (Some(lhs), Some(rhs)) => { 61 | cfg_if::cfg_if! { 62 | if #[cfg(unix)] { 63 | use walkdir::DirEntryExt; 64 | if lhs.ino() == rhs.ino() { 65 | return Ok(()) 66 | } 67 | } 68 | } 69 | 70 | match (lhs.metadata()?.is_dir(), rhs.metadata()?.is_dir()) { 71 | (false, false) => { 72 | diff_file_candidates(p, Some(lhs.path()), Some(rhs.path())) 73 | } 74 | (true, true) => Ok(()), 75 | _ => Ok(p.print_directory_mismatch(lhs.path(), rhs.path())?), 76 | } 77 | } 78 | } 79 | } 80 | 81 | fn diff_file_candidates( 82 | p: &mut Printer, 83 | lhs_path: Option<&Path>, 84 | rhs_path: Option<&Path>, 85 | ) -> Result<(), Box> { 86 | let lhs_raw = read(lhs_path)?; 87 | let rhs_raw = read(rhs_path)?; 88 | if lhs_raw == rhs_raw { 89 | return Ok(()); 90 | } 91 | 92 | let (Ok(l), Ok(r)) = (String::from_utf8(lhs_raw), String::from_utf8(rhs_raw)) 93 | else { 94 | p.print_binary_files_differ(lhs_path, rhs_path)?; 95 | return Ok(()); 96 | }; 97 | 98 | p.print_file_header(lhs_path, rhs_path)?; 99 | p.print_diff(&l, &r)?; 100 | Ok(()) 101 | } 102 | 103 | fn walk_dirs< 104 | Handler: FnMut(&Option, &Option) -> Result<(), Box>, 105 | >( 106 | lhs_root: &Path, 107 | rhs_root: &Path, 108 | mut handler: Handler, 109 | ) -> Result<(), Box> 110 | where 111 | { 112 | let mut lhs = WalkDir::new(lhs_root) 113 | .sort_by_file_name() 114 | .min_depth(1) 115 | .into_iter() 116 | .filter_map(|e| e.ok()); 117 | let mut rhs = WalkDir::new(rhs_root) 118 | .sort_by_file_name() 119 | .min_depth(1) 120 | .into_iter() 121 | .filter_map(|e| e.ok()); 122 | 123 | let compare = |l: &Option, r: &Option| match (&l, &r) { 124 | (None, None) => Ordering::Equal, 125 | (Some(_), None) => Ordering::Less, 126 | (None, Some(_)) => Ordering::Greater, 127 | (Some(lhs), Some(rhs)) => { 128 | let lhs_relative = lhs.path().strip_prefix(lhs_root).unwrap(); 129 | let rhs_relative = rhs.path().strip_prefix(rhs_root).unwrap(); 130 | lhs_relative.cmp(rhs_relative) 131 | } 132 | }; 133 | let mut lhs_next = lhs.next(); 134 | let mut rhs_next = rhs.next(); 135 | loop { 136 | match compare(&lhs_next, &rhs_next) { 137 | Ordering::Equal => { 138 | if let (None, None) = (&lhs_next, &rhs_next) { 139 | return Ok(()); 140 | } 141 | handler(&lhs_next, &rhs_next)?; 142 | lhs_next = lhs.next(); 143 | rhs_next = rhs.next(); 144 | } 145 | Ordering::Less => { 146 | handler(&lhs_next, &None)?; 147 | lhs_next = lhs.next(); 148 | } 149 | Ordering::Greater => { 150 | handler(&None, &rhs_next)?; 151 | rhs_next = rhs.next(); 152 | } 153 | } 154 | } 155 | } 156 | 157 | fn open(path: &Path) -> Result, Box> { 158 | if path == Path::new("-") { 159 | Ok(Box::new(std::io::stdin())) 160 | } else { 161 | Ok(Box::new(File::open(path)?)) 162 | } 163 | } 164 | 165 | fn read(path: Option<&Path>) -> Result, Box> { 166 | let mut buffer = Vec::new(); 167 | if let Some(path) = path { 168 | let mut file = open(path)?; 169 | file.read_to_end(&mut buffer)?; 170 | } 171 | Ok(buffer) 172 | } 173 | 174 | #[cfg(test)] 175 | mod tests { 176 | fn filename(entry: &Option) -> Option { 177 | entry 178 | .as_ref() 179 | .and_then(|e| e.path().file_name()) 180 | .and_then(|f| f.to_str()) 181 | .map(|s| s.to_owned()) 182 | } 183 | 184 | use super::*; 185 | 186 | #[test] 187 | fn directories() -> Result<(), Box> { 188 | let old = tempfile::tempdir()?; 189 | File::create(old.path().join("1"))?; 190 | File::create(old.path().join("2"))?; 191 | File::create(old.path().join("3"))?; 192 | let new = tempfile::tempdir()?; 193 | File::create(new.path().join("1"))?; 194 | File::create(new.path().join("3"))?; 195 | File::create(new.path().join("4"))?; 196 | 197 | let mut v = Vec::<(Option, Option)>::new(); 198 | walk_dirs(old.path(), new.path(), |lhs, rhs| { 199 | v.push((filename(&lhs), filename(&rhs))); 200 | Ok(()) 201 | })?; 202 | assert_eq!( 203 | v, 204 | &[ 205 | (Some("1".to_owned()), Some("1".to_owned())), 206 | (Some("2".to_owned()), None), 207 | (Some("3".to_owned()), Some("3".to_owned())), 208 | (None, Some("4".to_owned())), 209 | ] 210 | ); 211 | Ok(()) 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::iter::zip; 3 | 4 | mod diff; 5 | mod files; 6 | mod printer; 7 | mod style; 8 | 9 | use diff::Diffs; 10 | use diff::DiffItem; 11 | 12 | pub use style::Styles; 13 | pub use files::diff_files; 14 | pub use printer::Printer; 15 | 16 | pub fn diff(lhs: &[&str], rhs: &[&str]) -> Vec { 17 | let mut d = Diffs::default(); 18 | accumulate_partitions(&mut d, lhs, rhs); 19 | d.vec 20 | } 21 | 22 | pub fn tokenize_lines<'a>(lines: &[&'a str]) -> Vec<&'a str> { 23 | let re = regex::Regex::new(r"\w+|\s+").unwrap(); 24 | let mut v = Vec::new(); 25 | for &line in lines { 26 | let mut last_pos = 0; 27 | for m in re.find_iter(line) { 28 | if m.start() > last_pos { 29 | v.push(&line[last_pos..m.start()]); 30 | } 31 | last_pos = m.end(); 32 | v.push(m.as_str()); 33 | } 34 | if last_pos < line.len() { 35 | v.push(&line[last_pos..]); 36 | } 37 | v.push("\n"); 38 | } 39 | v.pop(); 40 | v 41 | } 42 | 43 | // Patience diff algorithm 44 | // 45 | // 1. Match the first lines of both if they're identical, then match the second, 46 | // third, etc. until a pair doesn't match. 47 | // 2. Match the last lines of both if they're identical, then match the next to 48 | // last, second to last, etc. until a pair doesn't match. 49 | // 3. Find all lines which occur exactly once on both sides, then do longest 50 | // common subsequence on those lines, matching them up. 51 | // 4. Do steps 1-2 on each section between matched lines. 52 | fn accumulate_diffs(diffs: &mut Diffs, lhs: &[&str], rhs: &[&str]) { 53 | let leading = leading_match_len(lhs, rhs); 54 | diffs.add_match(leading); 55 | if leading == lhs.len() && leading == rhs.len() { 56 | return; 57 | } 58 | 59 | let trailing = 60 | trailing_match_len(&lhs[leading..lhs.len()], &rhs[leading..rhs.len()]); 61 | accumulate_partitions( 62 | diffs, 63 | &lhs[leading..lhs.len() - trailing], 64 | &rhs[leading..rhs.len() - trailing], 65 | ); 66 | diffs.add_match(trailing); 67 | } 68 | 69 | fn leading_match_len(lhs: &[&str], rhs: &[&str]) -> usize { 70 | zip(lhs, rhs).take_while(|(&l, &r)| l == r).count() 71 | } 72 | 73 | fn trailing_match_len(lhs: &[&str], rhs: &[&str]) -> usize { 74 | zip(lhs.iter().rev(), rhs.iter().rev()) 75 | .take_while(|(&l, &r)| l == r) 76 | .count() 77 | } 78 | 79 | fn accumulate_partitions(diffs: &mut Diffs, lhs: &[&str], rhs: &[&str]) { 80 | let matched = match_lines(lhs, rhs); 81 | if matched.is_empty() { 82 | diffs.add_mutation(lhs.len(), rhs.len()); 83 | return; 84 | } 85 | let matched = longest_common_subseq(&matched); 86 | 87 | let mut lhs_pos: usize = 0; 88 | let mut rhs_pos: usize = 0; 89 | for (lhs_next, rhs_next) in matched { 90 | accumulate_diffs(diffs, &lhs[lhs_pos..lhs_next], &rhs[rhs_pos..rhs_next]); 91 | diffs.add_match(1); 92 | lhs_pos = lhs_next + 1; 93 | rhs_pos = rhs_next + 1; 94 | } 95 | accumulate_diffs(diffs, &lhs[lhs_pos..lhs.len()], &rhs[rhs_pos..rhs.len()]); 96 | } 97 | 98 | fn match_lines(lhs: &[&str], rhs: &[&str]) -> Vec<(usize, usize)> { 99 | let mut m: HashMap<&str, (Vec, Vec)> = HashMap::new(); 100 | for (i, l) in lhs.iter().enumerate() { 101 | m.entry(l).or_default().0.push(i); 102 | } 103 | for (i, r) in rhs.iter().enumerate() { 104 | m.entry(r).or_default().1.push(i); 105 | } 106 | 107 | let mut min = usize::MAX; 108 | m.retain(|_, (l, r)| { 109 | if l.len() == r.len() { 110 | min = min.min(l.len()); 111 | true 112 | } else { 113 | false 114 | } 115 | }); 116 | 117 | let mut v: Vec<(usize, usize)> = m 118 | .into_values() 119 | .filter(|(l, _)| l.len() == min) 120 | .flat_map(|(l, r)| zip(l, r)) 121 | .collect(); 122 | v.sort(); 123 | v 124 | } 125 | 126 | fn longest_common_subseq(pairings: &[(usize, usize)]) -> Vec<(usize, usize)> { 127 | type PairingStack = Vec>; 128 | fn find_push_pos(stacks: &PairingStack, p: &(usize, usize)) -> usize { 129 | for (pos, stack) in stacks.iter().enumerate() { 130 | if p.1 < stack.last().unwrap().0 .1 { 131 | return pos; 132 | } 133 | } 134 | stacks.len() 135 | } 136 | 137 | let mut stacks = PairingStack::new(); 138 | for p in pairings { 139 | let push_pos = find_push_pos(&stacks, p); 140 | if push_pos == stacks.len() { 141 | stacks.push(vec![]); 142 | } 143 | let prev = if push_pos == 0 { 0 } else { stacks[push_pos - 1].len() - 1 }; 144 | stacks[push_pos].push((*p, prev)); 145 | } 146 | 147 | let mut r = vec![]; 148 | let mut prev = stacks.last().unwrap().len() - 1; 149 | for stack in stacks.iter().rev() { 150 | r.push(stack[prev].0); 151 | prev = stack[prev].1; 152 | } 153 | r.reverse(); 154 | r 155 | } 156 | 157 | #[cfg(test)] 158 | mod tests { 159 | use super::*; 160 | use std::ops::Range; 161 | use diff::DiffItem; 162 | use diff::DiffItem::*; 163 | use diff::Hunk; 164 | 165 | fn diff_lines(lhs: &str, rhs: &str) -> Vec { 166 | let lhs_lines: Vec<_> = lhs.lines().collect(); 167 | let rhs_lines: Vec<_> = rhs.lines().collect(); 168 | diff(&lhs_lines, &rhs_lines) 169 | } 170 | 171 | #[test] 172 | fn diff_empty() { 173 | assert_eq!(diff(&[], &[]), &[]); 174 | } 175 | 176 | #[test] 177 | fn diff_eq() { 178 | assert_eq!( 179 | diff(&["a", "b", "c"], &["a", "b", "c"]), 180 | &[Match { 181 | lhs: Range { start: 0, end: 3 }, 182 | rhs: Range { start: 0, end: 3 }, 183 | }] 184 | ); 185 | } 186 | 187 | #[test] 188 | fn diff_ne() { 189 | assert_eq!( 190 | diff(&["a", "b", "c"], &["a", "c"]), 191 | &[ 192 | Match { 193 | lhs: Range { start: 0, end: 1 }, 194 | rhs: Range { start: 0, end: 1 }, 195 | }, 196 | Mutation { 197 | lhs: Range { start: 1, end: 2 }, 198 | rhs: Range { start: 1, end: 1 }, 199 | }, 200 | Match { 201 | lhs: Range { start: 2, end: 3 }, 202 | rhs: Range { start: 1, end: 2 }, 203 | }, 204 | ] 205 | ); 206 | assert_eq!( 207 | diff(&["z", "a", "b", "c"], &["a", "c"]), 208 | &[ 209 | Mutation { 210 | lhs: Range { start: 0, end: 1 }, 211 | rhs: Range { start: 0, end: 0 }, 212 | }, 213 | Match { 214 | lhs: Range { start: 1, end: 2 }, 215 | rhs: Range { start: 0, end: 1 }, 216 | }, 217 | Mutation { 218 | lhs: Range { start: 2, end: 3 }, 219 | rhs: Range { start: 1, end: 1 }, 220 | }, 221 | Match { 222 | lhs: Range { start: 3, end: 4 }, 223 | rhs: Range { start: 1, end: 2 }, 224 | }, 225 | ] 226 | ); 227 | assert_eq!( 228 | diff(&["z", "a", "e", "b", "c"], &["a", "e", "c"]), 229 | &[ 230 | Mutation { 231 | lhs: Range { start: 0, end: 1 }, 232 | rhs: Range { start: 0, end: 0 }, 233 | }, 234 | Match { 235 | lhs: Range { start: 1, end: 3 }, 236 | rhs: Range { start: 0, end: 2 }, 237 | }, 238 | Mutation { 239 | lhs: Range { start: 3, end: 4 }, 240 | rhs: Range { start: 2, end: 2 }, 241 | }, 242 | Match { 243 | lhs: Range { start: 4, end: 5 }, 244 | rhs: Range { start: 2, end: 3 }, 245 | }, 246 | ] 247 | ); 248 | } 249 | 250 | #[test] 251 | fn diff_only_non_unique() { 252 | assert_eq!( 253 | diff(&["a", "b", "b", "c"], &["b", "b"]), 254 | &[ 255 | Mutation { 256 | lhs: Range { start: 0, end: 1 }, 257 | rhs: Range { start: 0, end: 0 }, 258 | }, 259 | Match { 260 | lhs: Range { start: 1, end: 3 }, 261 | rhs: Range { start: 0, end: 2 }, 262 | }, 263 | Mutation { 264 | lhs: Range { start: 3, end: 4 }, 265 | rhs: Range { start: 2, end: 2 }, 266 | }, 267 | ] 268 | ); 269 | } 270 | 271 | #[test] 272 | fn match_lines_arity1() { 273 | assert_eq!( 274 | match_lines(&["a", "b", "c", "d", "e", "d"], &["a", "c", "d", "e"]), 275 | vec![(0, 0), (2, 1), (4, 3)], 276 | ); 277 | } 278 | 279 | #[test] 280 | fn match_lines_arity2() { 281 | assert_eq!( 282 | match_lines(&["a", "b", "b", "c"], &["b", "b"]), 283 | vec![(1, 0), (2, 1)], 284 | ); 285 | } 286 | 287 | #[test] 288 | fn longest_common_subseq_basic() { 289 | // From https://blog.jcoglan.com/2017/09/19/the-patience-diff-algorithm/ 290 | assert_eq!( 291 | longest_common_subseq(&[ 292 | (0, 9), 293 | (1, 4), 294 | (2, 6), 295 | (3, 12), 296 | (4, 8), 297 | (5, 7), 298 | (6, 1), 299 | (7, 5), 300 | (8, 10), 301 | (9, 11), 302 | (10, 3), 303 | (11, 2), 304 | (12, 13), 305 | ]), 306 | &[(1, 4), (2, 6), (5, 7), (8, 10), (9, 11), (12, 13),] 307 | ); 308 | } 309 | 310 | #[test] 311 | fn lead_trail_overlap() { 312 | assert_eq!( 313 | diff(&["a", "b", "d", "b", "c"], &["a", "b", "c"]), 314 | &[ 315 | Match { 316 | lhs: Range { start: 0, end: 2 }, 317 | rhs: Range { start: 0, end: 2 }, 318 | }, 319 | Mutation { 320 | lhs: Range { start: 2, end: 4 }, 321 | rhs: Range { start: 2, end: 2 }, 322 | }, 323 | Match { 324 | lhs: Range { start: 4, end: 5 }, 325 | rhs: Range { start: 2, end: 3 }, 326 | }, 327 | ] 328 | ); 329 | } 330 | 331 | #[test] 332 | fn lead_move_txt() { 333 | assert_eq!( 334 | diff_lines( 335 | include_str!("testdata/old/move.txt"), 336 | include_str!("testdata/new/move.txt"), 337 | ), 338 | &[ 339 | Mutation { 340 | lhs: Range { start: 0, end: 8 }, 341 | rhs: Range { start: 0, end: 0 }, 342 | }, 343 | Match { 344 | lhs: Range { start: 8, end: 16 }, 345 | rhs: Range { start: 0, end: 8 }, 346 | }, 347 | Mutation { 348 | lhs: Range { start: 16, end: 16 }, 349 | rhs: Range { start: 8, end: 16 }, 350 | }, 351 | ] 352 | ); 353 | } 354 | 355 | fn hunk_positions(hunks: &[Hunk]) -> Vec<((usize, usize), (usize, usize))> { 356 | hunks 357 | .iter() 358 | .map(|h| { 359 | let (l, r) = (h.lhs(), h.rhs()); 360 | ((l.start + 1, l.len()), (r.start + 1, r.len())) 361 | }) 362 | .collect::>() 363 | } 364 | 365 | #[test] 366 | fn build_hunks() { 367 | let diff = diff_lines( 368 | include_str!("testdata/old/move.txt"), 369 | include_str!("testdata/new/move.txt"), 370 | ); 371 | assert_eq!( 372 | hunk_positions(&Hunk::build(3, &diff)), 373 | &[((1, 11), (1, 3)), ((14, 3), (6, 11))] 374 | ); 375 | assert_eq!( 376 | hunk_positions(&Hunk::build(0, &diff)), 377 | &[((1, 8), (1, 0)), ((17, 0), (9, 8))] 378 | ); 379 | } 380 | 381 | #[test] 382 | fn tokenize() { 383 | assert_eq!( 384 | tokenize_lines(&["void func1() {", " x += 1"]), 385 | &[ 386 | "void", " ", "func1", "()", " ", "{", "\n", " ", "x", " ", "+=", " ", 387 | "1" 388 | ], 389 | ); 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /src/printer.rs: -------------------------------------------------------------------------------- 1 | use crate::style::Styles; 2 | use diff::DiffItem::*; 3 | use diff::DiffItem; 4 | use diff::Hunk; 5 | use diff::Side; 6 | use crate::{diff, tokenize_lines}; 7 | use owo_colors::OwoColorize; 8 | use owo_colors::Style; 9 | use std::error::Error; 10 | use std::io::Result; 11 | use std::io::Write; 12 | use std::path::{Path, PathBuf}; 13 | 14 | pub struct Printer<'a> { 15 | pub styles: Styles, 16 | writer: &'a mut dyn Write, 17 | context: usize, 18 | common_prefix: PathBuf, 19 | } 20 | 21 | impl<'a> Printer<'a> { 22 | pub fn default( 23 | writer: &'a mut dyn Write, 24 | context: usize, 25 | common_prefix: PathBuf, 26 | ) -> Printer<'a> { 27 | Printer { 28 | styles: Styles::simple(), 29 | writer, 30 | context, 31 | common_prefix, 32 | } 33 | } 34 | 35 | fn display_name(&self, p: Option<&Path>) -> String { 36 | let Some(p) = p else { 37 | return "/dev/null".into(); 38 | }; 39 | let stripped = p.strip_prefix(&self.common_prefix).unwrap_or(p); 40 | if let Ok(link) = std::fs::read_link(p) { 41 | let stripped_link = 42 | link.strip_prefix(&self.common_prefix).unwrap_or(&link); 43 | return format!("{} -> {}", stripped.display(), stripped_link.display()); 44 | } 45 | stripped.display().to_string() 46 | } 47 | 48 | pub fn print_error( 49 | &mut self, 50 | lhs: Option<&Path>, 51 | rhs: Option<&Path>, 52 | err: Box, 53 | ) -> Result<()> { 54 | writeln!( 55 | self.writer, 56 | "Error diffing {} and {}:\n{}", 57 | self.display_name(lhs).style(self.styles.old), 58 | self.display_name(rhs).style(self.styles.new), 59 | err 60 | ) 61 | } 62 | 63 | pub fn print_directory_mismatch( 64 | &mut self, 65 | lhs: &Path, 66 | rhs: &Path, 67 | ) -> Result<()> { 68 | fn ft(p: &Path) -> &str { 69 | if p.metadata().unwrap().is_dir() { 70 | "directory" 71 | } else { 72 | "file" 73 | } 74 | } 75 | writeln!( 76 | self.writer, 77 | "File/directory mistmatch:\n {} is a {}\n {} is a {}", 78 | self.display_name(Some(lhs)).style(self.styles.old), 79 | ft(lhs), 80 | self.display_name(Some(rhs)).style(self.styles.new), 81 | ft(rhs), 82 | ) 83 | } 84 | 85 | pub fn print_binary_files_differ( 86 | &mut self, 87 | lhs: Option<&Path>, 88 | rhs: Option<&Path>, 89 | ) -> Result<()> { 90 | writeln!( 91 | self.writer, 92 | "Binary files {} and {} differ", 93 | self.display_name(lhs).style(self.styles.old), 94 | self.display_name(rhs).style(self.styles.new), 95 | )?; 96 | Ok(()) 97 | } 98 | 99 | pub fn print_file_header( 100 | &mut self, 101 | lhs: Option<&Path>, 102 | rhs: Option<&Path>, 103 | ) -> Result<()> { 104 | writeln!( 105 | self.writer, 106 | "{} {}", 107 | "---".style(self.styles.old), 108 | self.display_name(lhs).style(self.styles.header), 109 | )?; 110 | writeln!( 111 | self.writer, 112 | "{} {}", 113 | "+++".style(self.styles.new), 114 | self.display_name(rhs).style(self.styles.header) 115 | )?; 116 | Ok(()) 117 | } 118 | 119 | pub fn print_diff(&mut self, lhs_all: &str, rhs_all: &str) -> Result<()> { 120 | let lhs: Vec<_> = lhs_all.lines().collect(); 121 | let rhs: Vec<_> = rhs_all.lines().collect(); 122 | let diffs = diff(&lhs, &rhs); 123 | let hunks = Hunk::build(self.context, &diffs); 124 | 125 | for h in hunks { 126 | self.print_hunk_header(&h)?; 127 | self.print_hunk_body(&lhs, &rhs, &h.diffs)?; 128 | } 129 | Ok(()) 130 | } 131 | 132 | fn print_hunk_header(&mut self, h: &Hunk) -> Result<()> { 133 | let (l, r) = (h.lhs(), h.rhs()); 134 | writeln!( 135 | self.writer, 136 | "{}", 137 | format!( 138 | "@@ -{},{} +{},{} @@", 139 | l.start + 1, 140 | l.len(), 141 | r.start + 1, 142 | r.len() 143 | ) 144 | .style(self.styles.separator) 145 | )?; 146 | Ok(()) 147 | } 148 | 149 | fn print_hunk_body( 150 | &mut self, 151 | lhs_lines: &[&str], 152 | rhs_lines: &[&str], 153 | diffs: &[DiffItem], 154 | ) -> Result<()> { 155 | for d in diffs { 156 | match &d { 157 | Mutation { lhs, rhs } => { 158 | if rhs.is_empty() { 159 | self.print_lines(&lhs_lines[lhs.clone()], "-", self.styles.old)?; 160 | } else if lhs.is_empty() { 161 | self.print_lines(&rhs_lines[rhs.clone()], "+", self.styles.new)?; 162 | } else { 163 | self.print_mutation( 164 | &lhs_lines[lhs.clone()], 165 | &rhs_lines[rhs.clone()], 166 | )?; 167 | } 168 | } 169 | Match { lhs, .. } => { 170 | self.print_lines(&lhs_lines[lhs.clone()], " ", self.styles.both)?; 171 | } 172 | } 173 | } 174 | Ok(()) 175 | } 176 | 177 | fn print_lines( 178 | &mut self, 179 | lines: &[&str], 180 | prefix: &str, 181 | style: Style, 182 | ) -> Result<()> { 183 | for line in lines { 184 | writeln!(self.writer, "{}{}", prefix.style(style), line.style(style))?; 185 | } 186 | Ok(()) 187 | } 188 | 189 | fn print_mutation( 190 | &mut self, 191 | lhs_lines: &[&str], 192 | rhs_lines: &[&str], 193 | ) -> Result<()> { 194 | let lhs_tokens = tokenize_lines(lhs_lines); 195 | let rhs_tokens = tokenize_lines(rhs_lines); 196 | let diffs = diff(&lhs_tokens, &rhs_tokens); 197 | self.print_mutation_side( 198 | &lhs_tokens, 199 | &diffs, 200 | "-", 201 | Side::Lhs, 202 | self.styles.old, 203 | self.styles.old_dim, 204 | )?; 205 | self.print_mutation_side( 206 | &rhs_tokens, 207 | &diffs, 208 | "+", 209 | Side::Rhs, 210 | self.styles.new, 211 | self.styles.new_dim, 212 | )?; 213 | Ok(()) 214 | } 215 | 216 | fn print_mutation_side( 217 | &mut self, 218 | tokens: &[&str], 219 | diffs: &[DiffItem], 220 | prefix: &str, 221 | side: Side, 222 | mutation: Style, 223 | matching: Style, 224 | ) -> Result<()> { 225 | write!(self.writer, "{}", prefix.style(mutation))?; 226 | for d in diffs { 227 | let style = if matches!(d, Match { .. }) { matching } else { mutation }; 228 | for &t in &tokens[d.side(side)] { 229 | write!(self.writer, "{}", t.style(style))?; 230 | if t == "\n" { 231 | write!(self.writer, "{}", prefix.style(mutation))?; 232 | } 233 | } 234 | } 235 | writeln!(self.writer)?; 236 | Ok(()) 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/style.rs: -------------------------------------------------------------------------------- 1 | use owo_colors::Style; 2 | 3 | /// Defaults to using the terminal default colors. 4 | #[derive(Default)] 5 | pub struct Styles { 6 | pub(crate) header: Style, 7 | pub(crate) separator: Style, 8 | pub(crate) both: Style, 9 | pub(crate) old: Style, 10 | pub(crate) old_dim: Style, 11 | pub(crate) new: Style, 12 | pub(crate) new_dim: Style, 13 | } 14 | 15 | impl Styles { 16 | /// A simple set of color choices reasonable for most colorized terminal output. 17 | pub fn simple() -> Styles { 18 | Styles { 19 | header: Style::new().bold().white(), 20 | separator: Style::new().cyan(), 21 | both: Style::new().default_color(), 22 | old: Style::new().red(), 23 | new: Style::new().green(), 24 | old_dim: Style::new().dimmed(), 25 | new_dim: Style::new().default_color(), 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/testdata/new/add.txt: -------------------------------------------------------------------------------- 1 | void func1() { 2 | x += 1 3 | } 4 | 5 | void functhreehalves() { 6 | x += 1.5 7 | } 8 | 9 | void func2() { 10 | x += 2 11 | } 12 | -------------------------------------------------------------------------------- /src/testdata/new/dangling: -------------------------------------------------------------------------------- 1 | symlink -------------------------------------------------------------------------------- /src/testdata/new/eof-newline.txt: -------------------------------------------------------------------------------- 1 | void func1() { 2 | x += 1 3 | } 4 | 5 | void func2() { 6 | x += 2 7 | } 8 | 9 | -------------------------------------------------------------------------------- /src/testdata/new/modify.txt: -------------------------------------------------------------------------------- 1 | void func_better() { y += 1 2 | } 3 | 4 | void func_worse() { 5 | x += 2 6 | } 7 | -------------------------------------------------------------------------------- /src/testdata/new/move.txt: -------------------------------------------------------------------------------- 1 | int Chunk_bounds_check(Chunk *chunk, size_t start, size_t n) 2 | { 3 | if (chunk == NULL) return 0; 4 | if (start == -1) return 0; 5 | if (n == -1) return 0; 6 | 7 | return start <= chunk->length && n <= chunk->length - start; 8 | } 9 | 10 | void Chunk_copy(Chunk *src, size_t src_start, Chunk *dst, size_t dst_start, size_t n) 11 | { 12 | if (!Chunk_bounds_check(src, src_start, n)) return; 13 | if (!Chunk_bounds_check(dst, dst_start, n)) return; 14 | 15 | memcpy(dst->data + dst_start, src->data + src_start, n); 16 | } 17 | -------------------------------------------------------------------------------- /src/testdata/new/new-only.txt: -------------------------------------------------------------------------------- 1 | new only 2 | -------------------------------------------------------------------------------- /src/testdata/old/add.txt: -------------------------------------------------------------------------------- 1 | void func1() { 2 | x += 1 3 | } 4 | 5 | void func2() { 6 | x += 2 7 | } 8 | -------------------------------------------------------------------------------- /src/testdata/old/eof-newline.txt: -------------------------------------------------------------------------------- 1 | void func1() { 2 | x += 1 3 | } 4 | 5 | void func2() { 6 | x += 2 7 | } 8 | -------------------------------------------------------------------------------- /src/testdata/old/modify.txt: -------------------------------------------------------------------------------- 1 | void func1() { 2 | x += 1 3 | } 4 | 5 | void func2() { 6 | x += 2 7 | } 8 | -------------------------------------------------------------------------------- /src/testdata/old/move.txt: -------------------------------------------------------------------------------- 1 | void Chunk_copy(Chunk *src, size_t src_start, Chunk *dst, size_t dst_start, size_t n) 2 | { 3 | if (!Chunk_bounds_check(src, src_start, n)) return; 4 | if (!Chunk_bounds_check(dst, dst_start, n)) return; 5 | 6 | memcpy(dst->data + dst_start, src->data + src_start, n); 7 | } 8 | 9 | int Chunk_bounds_check(Chunk *chunk, size_t start, size_t n) 10 | { 11 | if (chunk == NULL) return 0; 12 | if (start == -1) return 0; 13 | if (n == -1) return 0; 14 | 15 | return start <= chunk->length && n <= chunk->length - start; 16 | } 17 | -------------------------------------------------------------------------------- /src/testdata/old/old-only.txt: -------------------------------------------------------------------------------- 1 | old only 2 | --------------------------------------------------------------------------------