├── .circleci └── config.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── commands ├── add.rs ├── branch.rs ├── checkout.rs ├── commit.rs ├── diff.rs ├── init.rs ├── log.rs ├── mod.rs └── status.rs ├── database ├── blob.rs ├── commit.rs ├── mod.rs ├── object.rs ├── tree.rs └── tree_diff.rs ├── diff ├── mod.rs └── myers.rs ├── index.rs ├── lockfile.rs ├── main.rs ├── pager.rs ├── refs.rs ├── repository ├── migration.rs └── mod.rs ├── revision.rs ├── util.rs └── workspace.rs /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/rust:latest 6 | 7 | steps: 8 | - checkout 9 | 10 | - run: 11 | name: Version information 12 | command: rustc --version; cargo --version; rustup --version 13 | - run: 14 | name: Build 15 | command: cargo build 16 | - run: 17 | name: Run tests 18 | command: cargo test --all 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /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 = "adler" 7 | version = "1.0.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 10 | 11 | [[package]] 12 | name = "aho-corasick" 13 | version = "1.1.3" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 16 | dependencies = [ 17 | "memchr", 18 | ] 19 | 20 | [[package]] 21 | name = "android-tzdata" 22 | version = "0.1.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 25 | 26 | [[package]] 27 | name = "android_system_properties" 28 | version = "0.1.5" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 31 | dependencies = [ 32 | "libc", 33 | ] 34 | 35 | [[package]] 36 | name = "ansi_term" 37 | version = "0.12.1" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 40 | dependencies = [ 41 | "winapi", 42 | ] 43 | 44 | [[package]] 45 | name = "assert_cmd" 46 | version = "0.11.1" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "2dc477793bd82ec39799b6f6b3df64938532fdf2ab0d49ef817eac65856a5a1e" 49 | dependencies = [ 50 | "escargot", 51 | "predicates", 52 | "predicates-core", 53 | "predicates-tree", 54 | ] 55 | 56 | [[package]] 57 | name = "atty" 58 | version = "0.2.14" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 61 | dependencies = [ 62 | "hermit-abi 0.1.19", 63 | "libc", 64 | "winapi", 65 | ] 66 | 67 | [[package]] 68 | name = "autocfg" 69 | version = "0.1.8" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" 72 | dependencies = [ 73 | "autocfg 1.2.0", 74 | ] 75 | 76 | [[package]] 77 | name = "autocfg" 78 | version = "1.2.0" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" 81 | 82 | [[package]] 83 | name = "bitflags" 84 | version = "1.3.2" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 87 | 88 | [[package]] 89 | name = "bumpalo" 90 | version = "3.16.0" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 93 | 94 | [[package]] 95 | name = "cc" 96 | version = "1.0.95" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" 99 | 100 | [[package]] 101 | name = "cfg-if" 102 | version = "1.0.0" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 105 | 106 | [[package]] 107 | name = "chrono" 108 | version = "0.4.38" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" 111 | dependencies = [ 112 | "android-tzdata", 113 | "iana-time-zone", 114 | "js-sys", 115 | "num-traits", 116 | "wasm-bindgen", 117 | "windows-targets", 118 | ] 119 | 120 | [[package]] 121 | name = "clap" 122 | version = "2.34.0" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" 125 | dependencies = [ 126 | "ansi_term", 127 | "atty", 128 | "bitflags", 129 | "strsim", 130 | "textwrap", 131 | "unicode-width", 132 | "vec_map", 133 | ] 134 | 135 | [[package]] 136 | name = "cloudabi" 137 | version = "0.0.3" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" 140 | dependencies = [ 141 | "bitflags", 142 | ] 143 | 144 | [[package]] 145 | name = "colored" 146 | version = "1.9.4" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "5a5f741c91823341bebf717d4c71bda820630ce065443b58bd1b7451af008355" 149 | dependencies = [ 150 | "is-terminal", 151 | "lazy_static", 152 | "winapi", 153 | ] 154 | 155 | [[package]] 156 | name = "core-foundation-sys" 157 | version = "0.8.6" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" 160 | 161 | [[package]] 162 | name = "crc32fast" 163 | version = "1.4.0" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" 166 | dependencies = [ 167 | "cfg-if", 168 | ] 169 | 170 | [[package]] 171 | name = "difference" 172 | version = "2.0.0" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" 175 | 176 | [[package]] 177 | name = "errno" 178 | version = "0.2.8" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" 181 | dependencies = [ 182 | "errno-dragonfly", 183 | "libc", 184 | "winapi", 185 | ] 186 | 187 | [[package]] 188 | name = "errno-dragonfly" 189 | version = "0.1.2" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 192 | dependencies = [ 193 | "cc", 194 | "libc", 195 | ] 196 | 197 | [[package]] 198 | name = "escargot" 199 | version = "0.4.0" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "ceb9adbf9874d5d028b5e4c5739d22b71988252b25c9c98fe7cf9738bee84597" 202 | dependencies = [ 203 | "lazy_static", 204 | "log", 205 | "serde", 206 | "serde_json", 207 | ] 208 | 209 | [[package]] 210 | name = "filetime" 211 | version = "0.2.23" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" 214 | dependencies = [ 215 | "cfg-if", 216 | "libc", 217 | "redox_syscall", 218 | "windows-sys", 219 | ] 220 | 221 | [[package]] 222 | name = "flate2" 223 | version = "1.0.29" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "4556222738635b7a3417ae6130d8f52201e45a0c4d1a907f0826383adb5f85e7" 226 | dependencies = [ 227 | "crc32fast", 228 | "libz-sys", 229 | "miniz_oxide", 230 | ] 231 | 232 | [[package]] 233 | name = "fuchsia-cprng" 234 | version = "0.1.1" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" 237 | 238 | [[package]] 239 | name = "gcc" 240 | version = "0.3.55" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" 243 | 244 | [[package]] 245 | name = "hermit-abi" 246 | version = "0.1.19" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 249 | dependencies = [ 250 | "libc", 251 | ] 252 | 253 | [[package]] 254 | name = "hermit-abi" 255 | version = "0.3.9" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 258 | 259 | [[package]] 260 | name = "iana-time-zone" 261 | version = "0.1.60" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" 264 | dependencies = [ 265 | "android_system_properties", 266 | "core-foundation-sys", 267 | "iana-time-zone-haiku", 268 | "js-sys", 269 | "wasm-bindgen", 270 | "windows-core", 271 | ] 272 | 273 | [[package]] 274 | name = "iana-time-zone-haiku" 275 | version = "0.1.2" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 278 | dependencies = [ 279 | "cc", 280 | ] 281 | 282 | [[package]] 283 | name = "is-terminal" 284 | version = "0.4.12" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" 287 | dependencies = [ 288 | "hermit-abi 0.3.9", 289 | "libc", 290 | "windows-sys", 291 | ] 292 | 293 | [[package]] 294 | name = "itoa" 295 | version = "1.0.11" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 298 | 299 | [[package]] 300 | name = "js-sys" 301 | version = "0.3.69" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" 304 | dependencies = [ 305 | "wasm-bindgen", 306 | ] 307 | 308 | [[package]] 309 | name = "lazy_static" 310 | version = "1.4.0" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 313 | 314 | [[package]] 315 | name = "libc" 316 | version = "0.2.153" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 319 | 320 | [[package]] 321 | name = "libz-sys" 322 | version = "1.1.16" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9" 325 | dependencies = [ 326 | "cc", 327 | "pkg-config", 328 | "vcpkg", 329 | ] 330 | 331 | [[package]] 332 | name = "log" 333 | version = "0.4.21" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 336 | 337 | [[package]] 338 | name = "memchr" 339 | version = "2.7.2" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" 342 | 343 | [[package]] 344 | name = "miniz_oxide" 345 | version = "0.7.2" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" 348 | dependencies = [ 349 | "adler", 350 | ] 351 | 352 | [[package]] 353 | name = "num-traits" 354 | version = "0.2.18" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" 357 | dependencies = [ 358 | "autocfg 1.2.0", 359 | ] 360 | 361 | [[package]] 362 | name = "once_cell" 363 | version = "1.19.0" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 366 | 367 | [[package]] 368 | name = "pkg-config" 369 | version = "0.3.30" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" 372 | 373 | [[package]] 374 | name = "predicates" 375 | version = "1.0.8" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "f49cfaf7fdaa3bfacc6fa3e7054e65148878354a5cfddcf661df4c851f8021df" 378 | dependencies = [ 379 | "difference", 380 | "predicates-core", 381 | ] 382 | 383 | [[package]] 384 | name = "predicates-core" 385 | version = "1.0.6" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" 388 | 389 | [[package]] 390 | name = "predicates-tree" 391 | version = "1.0.9" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" 394 | dependencies = [ 395 | "predicates-core", 396 | "termtree", 397 | ] 398 | 399 | [[package]] 400 | name = "proc-macro2" 401 | version = "1.0.81" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" 404 | dependencies = [ 405 | "unicode-ident", 406 | ] 407 | 408 | [[package]] 409 | name = "quote" 410 | version = "1.0.36" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 413 | dependencies = [ 414 | "proc-macro2", 415 | ] 416 | 417 | [[package]] 418 | name = "rand" 419 | version = "0.3.23" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" 422 | dependencies = [ 423 | "libc", 424 | "rand 0.4.6", 425 | ] 426 | 427 | [[package]] 428 | name = "rand" 429 | version = "0.4.6" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" 432 | dependencies = [ 433 | "fuchsia-cprng", 434 | "libc", 435 | "rand_core 0.3.1", 436 | "rdrand", 437 | "winapi", 438 | ] 439 | 440 | [[package]] 441 | name = "rand" 442 | version = "0.6.5" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" 445 | dependencies = [ 446 | "autocfg 0.1.8", 447 | "libc", 448 | "rand_chacha", 449 | "rand_core 0.4.2", 450 | "rand_hc", 451 | "rand_isaac", 452 | "rand_jitter", 453 | "rand_os", 454 | "rand_pcg", 455 | "rand_xorshift", 456 | "winapi", 457 | ] 458 | 459 | [[package]] 460 | name = "rand_chacha" 461 | version = "0.1.1" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" 464 | dependencies = [ 465 | "autocfg 0.1.8", 466 | "rand_core 0.3.1", 467 | ] 468 | 469 | [[package]] 470 | name = "rand_core" 471 | version = "0.3.1" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" 474 | dependencies = [ 475 | "rand_core 0.4.2", 476 | ] 477 | 478 | [[package]] 479 | name = "rand_core" 480 | version = "0.4.2" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" 483 | 484 | [[package]] 485 | name = "rand_hc" 486 | version = "0.1.0" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" 489 | dependencies = [ 490 | "rand_core 0.3.1", 491 | ] 492 | 493 | [[package]] 494 | name = "rand_isaac" 495 | version = "0.1.1" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" 498 | dependencies = [ 499 | "rand_core 0.3.1", 500 | ] 501 | 502 | [[package]] 503 | name = "rand_jitter" 504 | version = "0.1.4" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" 507 | dependencies = [ 508 | "libc", 509 | "rand_core 0.4.2", 510 | "winapi", 511 | ] 512 | 513 | [[package]] 514 | name = "rand_os" 515 | version = "0.1.3" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" 518 | dependencies = [ 519 | "cloudabi", 520 | "fuchsia-cprng", 521 | "libc", 522 | "rand_core 0.4.2", 523 | "rdrand", 524 | "winapi", 525 | ] 526 | 527 | [[package]] 528 | name = "rand_pcg" 529 | version = "0.1.2" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" 532 | dependencies = [ 533 | "autocfg 0.1.8", 534 | "rand_core 0.4.2", 535 | ] 536 | 537 | [[package]] 538 | name = "rand_xorshift" 539 | version = "0.1.1" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" 542 | dependencies = [ 543 | "rand_core 0.3.1", 544 | ] 545 | 546 | [[package]] 547 | name = "rdrand" 548 | version = "0.4.0" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" 551 | dependencies = [ 552 | "rand_core 0.3.1", 553 | ] 554 | 555 | [[package]] 556 | name = "redox_syscall" 557 | version = "0.4.1" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 560 | dependencies = [ 561 | "bitflags", 562 | ] 563 | 564 | [[package]] 565 | name = "regex" 566 | version = "1.10.4" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" 569 | dependencies = [ 570 | "aho-corasick", 571 | "memchr", 572 | "regex-automata", 573 | "regex-syntax", 574 | ] 575 | 576 | [[package]] 577 | name = "regex-automata" 578 | version = "0.4.6" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" 581 | dependencies = [ 582 | "aho-corasick", 583 | "memchr", 584 | "regex-syntax", 585 | ] 586 | 587 | [[package]] 588 | name = "regex-syntax" 589 | version = "0.8.3" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" 592 | 593 | [[package]] 594 | name = "rug" 595 | version = "0.1.0" 596 | dependencies = [ 597 | "assert_cmd", 598 | "chrono", 599 | "clap", 600 | "colored", 601 | "errno", 602 | "filetime", 603 | "flate2", 604 | "lazy_static", 605 | "libc", 606 | "rand 0.6.5", 607 | "regex", 608 | "rust-crypto", 609 | ] 610 | 611 | [[package]] 612 | name = "rust-crypto" 613 | version = "0.2.36" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "f76d05d3993fd5f4af9434e8e436db163a12a9d40e1a58a726f27a01dfd12a2a" 616 | dependencies = [ 617 | "gcc", 618 | "libc", 619 | "rand 0.3.23", 620 | "rustc-serialize", 621 | "time", 622 | ] 623 | 624 | [[package]] 625 | name = "rustc-serialize" 626 | version = "0.3.25" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "fe834bc780604f4674073badbad26d7219cadfb4a2275802db12cbae17498401" 629 | 630 | [[package]] 631 | name = "ryu" 632 | version = "1.0.17" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" 635 | 636 | [[package]] 637 | name = "serde" 638 | version = "1.0.199" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a" 641 | dependencies = [ 642 | "serde_derive", 643 | ] 644 | 645 | [[package]] 646 | name = "serde_derive" 647 | version = "1.0.199" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc" 650 | dependencies = [ 651 | "proc-macro2", 652 | "quote", 653 | "syn", 654 | ] 655 | 656 | [[package]] 657 | name = "serde_json" 658 | version = "1.0.116" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" 661 | dependencies = [ 662 | "itoa", 663 | "ryu", 664 | "serde", 665 | ] 666 | 667 | [[package]] 668 | name = "strsim" 669 | version = "0.8.0" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 672 | 673 | [[package]] 674 | name = "syn" 675 | version = "2.0.60" 676 | source = "registry+https://github.com/rust-lang/crates.io-index" 677 | checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" 678 | dependencies = [ 679 | "proc-macro2", 680 | "quote", 681 | "unicode-ident", 682 | ] 683 | 684 | [[package]] 685 | name = "termtree" 686 | version = "0.4.1" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" 689 | 690 | [[package]] 691 | name = "textwrap" 692 | version = "0.11.0" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 695 | dependencies = [ 696 | "unicode-width", 697 | ] 698 | 699 | [[package]] 700 | name = "time" 701 | version = "0.1.45" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" 704 | dependencies = [ 705 | "libc", 706 | "wasi", 707 | "winapi", 708 | ] 709 | 710 | [[package]] 711 | name = "unicode-ident" 712 | version = "1.0.12" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 715 | 716 | [[package]] 717 | name = "unicode-width" 718 | version = "0.1.12" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" 721 | 722 | [[package]] 723 | name = "vcpkg" 724 | version = "0.2.15" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 727 | 728 | [[package]] 729 | name = "vec_map" 730 | version = "0.8.2" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 733 | 734 | [[package]] 735 | name = "wasi" 736 | version = "0.10.0+wasi-snapshot-preview1" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 739 | 740 | [[package]] 741 | name = "wasm-bindgen" 742 | version = "0.2.92" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" 745 | dependencies = [ 746 | "cfg-if", 747 | "wasm-bindgen-macro", 748 | ] 749 | 750 | [[package]] 751 | name = "wasm-bindgen-backend" 752 | version = "0.2.92" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" 755 | dependencies = [ 756 | "bumpalo", 757 | "log", 758 | "once_cell", 759 | "proc-macro2", 760 | "quote", 761 | "syn", 762 | "wasm-bindgen-shared", 763 | ] 764 | 765 | [[package]] 766 | name = "wasm-bindgen-macro" 767 | version = "0.2.92" 768 | source = "registry+https://github.com/rust-lang/crates.io-index" 769 | checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" 770 | dependencies = [ 771 | "quote", 772 | "wasm-bindgen-macro-support", 773 | ] 774 | 775 | [[package]] 776 | name = "wasm-bindgen-macro-support" 777 | version = "0.2.92" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" 780 | dependencies = [ 781 | "proc-macro2", 782 | "quote", 783 | "syn", 784 | "wasm-bindgen-backend", 785 | "wasm-bindgen-shared", 786 | ] 787 | 788 | [[package]] 789 | name = "wasm-bindgen-shared" 790 | version = "0.2.92" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" 793 | 794 | [[package]] 795 | name = "winapi" 796 | version = "0.3.9" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 799 | dependencies = [ 800 | "winapi-i686-pc-windows-gnu", 801 | "winapi-x86_64-pc-windows-gnu", 802 | ] 803 | 804 | [[package]] 805 | name = "winapi-i686-pc-windows-gnu" 806 | version = "0.4.0" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 809 | 810 | [[package]] 811 | name = "winapi-x86_64-pc-windows-gnu" 812 | version = "0.4.0" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 815 | 816 | [[package]] 817 | name = "windows-core" 818 | version = "0.52.0" 819 | source = "registry+https://github.com/rust-lang/crates.io-index" 820 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 821 | dependencies = [ 822 | "windows-targets", 823 | ] 824 | 825 | [[package]] 826 | name = "windows-sys" 827 | version = "0.52.0" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 830 | dependencies = [ 831 | "windows-targets", 832 | ] 833 | 834 | [[package]] 835 | name = "windows-targets" 836 | version = "0.52.5" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 839 | dependencies = [ 840 | "windows_aarch64_gnullvm", 841 | "windows_aarch64_msvc", 842 | "windows_i686_gnu", 843 | "windows_i686_gnullvm", 844 | "windows_i686_msvc", 845 | "windows_x86_64_gnu", 846 | "windows_x86_64_gnullvm", 847 | "windows_x86_64_msvc", 848 | ] 849 | 850 | [[package]] 851 | name = "windows_aarch64_gnullvm" 852 | version = "0.52.5" 853 | source = "registry+https://github.com/rust-lang/crates.io-index" 854 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 855 | 856 | [[package]] 857 | name = "windows_aarch64_msvc" 858 | version = "0.52.5" 859 | source = "registry+https://github.com/rust-lang/crates.io-index" 860 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 861 | 862 | [[package]] 863 | name = "windows_i686_gnu" 864 | version = "0.52.5" 865 | source = "registry+https://github.com/rust-lang/crates.io-index" 866 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 867 | 868 | [[package]] 869 | name = "windows_i686_gnullvm" 870 | version = "0.52.5" 871 | source = "registry+https://github.com/rust-lang/crates.io-index" 872 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 873 | 874 | [[package]] 875 | name = "windows_i686_msvc" 876 | version = "0.52.5" 877 | source = "registry+https://github.com/rust-lang/crates.io-index" 878 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 879 | 880 | [[package]] 881 | name = "windows_x86_64_gnu" 882 | version = "0.52.5" 883 | source = "registry+https://github.com/rust-lang/crates.io-index" 884 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 885 | 886 | [[package]] 887 | name = "windows_x86_64_gnullvm" 888 | version = "0.52.5" 889 | source = "registry+https://github.com/rust-lang/crates.io-index" 890 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 891 | 892 | [[package]] 893 | name = "windows_x86_64_msvc" 894 | version = "0.52.5" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 897 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rug" 3 | version = "0.1.0" 4 | authors = ["Samrat Man Singh "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | rust-crypto = "^0.2" 11 | flate2 = { version = "1.0", features = ["zlib"], default-features = false } 12 | rand = "0.6" 13 | chrono = "0.4" 14 | lazy_static = "1.2.0" 15 | filetime = "0.2.6" 16 | colored = "1.8" 17 | libc = "0.2" 18 | errno = "0.2" 19 | regex="1" 20 | clap = "2.33.0" 21 | assert_cmd = "0.11" 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Samrat Man Singh 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## rug, a Git implementation in Rust 2 | 3 | [![CircleCI](https://circleci.com/gh/samrat/rug.svg?style=svg)](https://circleci.com/gh/samrat/rug) 4 | 5 | This is my implementation of *Jit*, from James Coglan's book 6 | [*Building Git*](https://shop.jcoglan.com/building-git/). 7 | 8 | 9 | ### Usage 10 | 11 | Build the `rug` binary and add it to your PATH: 12 | 13 | ```sh 14 | $ cargo build 15 | $ export PATH=/path/to/rug/target/debug:$PATH 16 | ``` 17 | 18 | Switch to the directory you want to track using `rug`: 19 | 20 | ``` 21 | $ mkdir /tmp/rug-test && cd /tmp/rug-test 22 | $ mkdir -p foo/bar 23 | 24 | $ echo "hello" > hello.txt 25 | $ echo "world" > foo/bar/world.txt 26 | ``` 27 | 28 | Finally, initialize a Git repo and create a commit: 29 | 30 | ``` 31 | $ rug init 32 | $ rug add . 33 | 34 | # Currently, this waits for your input. Type in your commit message 35 | and hit Ctrl+D 36 | $ rug commit 37 | ``` 38 | 39 | You should now be able to use Git to view the commit you just created: 40 | 41 | ``` 42 | git show 43 | ``` 44 | 45 | 46 | ### Other supported commands 47 | 48 | ``` 49 | rug status 50 | rug status --porcelain 51 | ``` 52 | 53 | ``` 54 | rug diff 55 | rug diff --cached 56 | ``` 57 | 58 | ``` 59 | rug branch foo HEAD~5 60 | ``` 61 | 62 | ### Gotchas with the `rug` repo 63 | 64 | I use `rug` as the version-control system for the `rug` 65 | source-code. However, because all commands are not implemented yet, 66 | I've been using `git` for eg. pushing to Github. 67 | 68 | This means sometimes you might have to hackily modify files in `.git` 69 | to bring it back into a state that `rug` finds acceptable. Here are 70 | some ways in which things can break: 71 | 72 | 1. Updating `master` after pulling from `origin` 73 | `rug pull` does not currently work. 74 | 75 | After running `git fetch origin`, copy the SHA from 76 | `.git/refs/remotes/origin/master` into `.git/refs/heads/master`: 77 | 78 | ```shell 79 | cp .git/refs/remotes/origin/master .git/refs/heads/master 80 | ``` 81 | 82 | 2. `rug` doesn't understand packed objects 83 | 84 | Copy the packed object outside `.git` and unpack it: 85 | 86 | ``` 87 | mkdir temp 88 | mv .git/objects/pack/pack-ab7ec7453bc7444032731b68f2c1fe06279bd017.pack temp/ 89 | git unpack-objects < temp/pack-ab7ec7453bc7444032731b68f2c1fe06279bd017.pack 90 | ``` 91 | -------------------------------------------------------------------------------- /src/commands/add.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Read, Write}; 2 | 3 | use crate::commands::CommandContext; 4 | use crate::database::blob::Blob; 5 | 6 | use crate::database::object::Object; 7 | 8 | use crate::repository::Repository; 9 | 10 | static INDEX_LOAD_OR_CREATE_FAILED: &'static str = "fatal: could not create/load .git/index\n"; 11 | 12 | fn locked_index_message(e: &std::io::Error) -> String { 13 | format!("fatal: {} 14 | 15 | Another jit process seems to be running in this repository. Please make sure all processes are terminated then try again. 16 | 17 | If it still fails, a jit process may have crashed in this repository earlier: remove the .git/index.lock file manually to continue.\n", 18 | e) 19 | } 20 | 21 | fn add_failed_message(e: &std::io::Error) -> String { 22 | format!( 23 | "{} 24 | 25 | fatal: adding files failed\n", 26 | e 27 | ) 28 | } 29 | 30 | fn add_to_index(repo: &mut Repository, pathname: &str) -> Result<(), String> { 31 | let data = match repo.workspace.read_file(&pathname) { 32 | Ok(data) => data, 33 | Err(ref err) if err.kind() == io::ErrorKind::PermissionDenied => { 34 | repo.index.release_lock().unwrap(); 35 | return Err(add_failed_message(&err)); 36 | } 37 | _ => { 38 | panic!("fatal: adding files failed"); 39 | } 40 | }; 41 | 42 | let stat = repo 43 | .workspace 44 | .stat_file(&pathname) 45 | .expect("could not stat file"); 46 | let blob = Blob::new(data.as_bytes()); 47 | repo.database.store(&blob).expect("storing blob failed"); 48 | 49 | repo.index.add(&pathname, &blob.get_oid(), &stat); 50 | 51 | Ok(()) 52 | } 53 | 54 | pub fn add_command(ctx: CommandContext) -> Result<(), String> 55 | where 56 | I: Read, 57 | O: Write, 58 | E: Write, 59 | { 60 | let working_dir = ctx.dir; 61 | let root_path = working_dir.as_path(); 62 | let mut repo = Repository::new(&root_path); 63 | let options = ctx.options.as_ref().unwrap(); 64 | let args: Vec<_> = if let Some(args) = options.values_of("args") { 65 | args.collect() 66 | } else { 67 | vec![] 68 | }; 69 | 70 | match repo.index.load_for_update() { 71 | Ok(_) => (), 72 | Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => { 73 | return Err(locked_index_message(e)); 74 | } 75 | Err(_) => { 76 | return Err(INDEX_LOAD_OR_CREATE_FAILED.to_string()); 77 | } 78 | } 79 | 80 | let mut paths = vec![]; 81 | for arg in args { 82 | let path = match working_dir.join(arg).canonicalize() { 83 | Ok(canon_path) => canon_path, 84 | Err(_) => { 85 | repo.index.release_lock().unwrap(); 86 | return Err(format!( 87 | "fatal: pathspec '{:}' did not match any files\n", 88 | arg 89 | )); 90 | } 91 | }; 92 | 93 | for pathname in repo.workspace.list_files(&path).unwrap() { 94 | paths.push(pathname); 95 | } 96 | } 97 | 98 | for pathname in paths { 99 | add_to_index(&mut repo, &pathname)?; 100 | } 101 | 102 | repo.index 103 | .write_updates() 104 | .expect("writing updates to index failed"); 105 | 106 | Ok(()) 107 | } 108 | 109 | #[cfg(test)] 110 | mod tests { 111 | use crate::commands::tests::*; 112 | 113 | #[test] 114 | fn add_regular_file_to_index() { 115 | let mut cmd_helper = CommandHelper::new(); 116 | cmd_helper.write_file("hello.txt", b"hello").unwrap(); 117 | cmd_helper.jit_cmd(&["init"]).unwrap(); 118 | cmd_helper.jit_cmd(&["add", "hello.txt"]).unwrap(); 119 | cmd_helper 120 | .assert_index(vec![(0o100644, "hello.txt".to_string())]) 121 | .unwrap(); 122 | } 123 | 124 | #[test] 125 | fn add_executable_file_to_index() { 126 | let mut cmd_helper = CommandHelper::new(); 127 | cmd_helper.write_file("hello.txt", b"hello").unwrap(); 128 | cmd_helper.make_executable("hello.txt").unwrap(); 129 | 130 | cmd_helper.jit_cmd(&["init"]).unwrap(); 131 | cmd_helper.jit_cmd(&["add", "hello.txt"]).unwrap(); 132 | cmd_helper 133 | .assert_index(vec![(0o100755, "hello.txt".to_string())]) 134 | .unwrap(); 135 | } 136 | 137 | #[test] 138 | fn add_multiple_files_to_index() { 139 | let mut cmd_helper = CommandHelper::new(); 140 | cmd_helper.write_file("hello.txt", b"hello").unwrap(); 141 | cmd_helper.write_file("world.txt", b"world").unwrap(); 142 | 143 | cmd_helper.jit_cmd(&["init"]).unwrap(); 144 | cmd_helper 145 | .jit_cmd(&["add", "hello.txt", "world.txt"]) 146 | .unwrap(); 147 | 148 | cmd_helper 149 | .assert_index(vec![ 150 | (0o100644, "hello.txt".to_string()), 151 | (0o100644, "world.txt".to_string()), 152 | ]) 153 | .unwrap(); 154 | } 155 | 156 | #[test] 157 | fn incrementally_add_files_to_index() { 158 | let mut cmd_helper = CommandHelper::new(); 159 | cmd_helper.write_file("hello.txt", b"hello").unwrap(); 160 | cmd_helper.write_file("world.txt", b"world").unwrap(); 161 | 162 | cmd_helper.jit_cmd(&["init"]).unwrap(); 163 | cmd_helper.jit_cmd(&["add", "hello.txt"]).unwrap(); 164 | 165 | cmd_helper 166 | .assert_index(vec![(0o100644, "hello.txt".to_string())]) 167 | .unwrap(); 168 | 169 | cmd_helper.jit_cmd(&["add", "world.txt"]).unwrap(); 170 | cmd_helper 171 | .assert_index(vec![ 172 | (0o100644, "hello.txt".to_string()), 173 | (0o100644, "world.txt".to_string()), 174 | ]) 175 | .unwrap(); 176 | } 177 | 178 | #[test] 179 | fn add_a_directory_to_index() { 180 | let mut cmd_helper = CommandHelper::new(); 181 | cmd_helper.write_file("a-dir/nested.txt", b"hello").unwrap(); 182 | cmd_helper.jit_cmd(&["init"]).unwrap(); 183 | 184 | cmd_helper.jit_cmd(&["add", "a-dir"]).unwrap(); 185 | cmd_helper 186 | .assert_index(vec![(0o100644, "a-dir/nested.txt".to_string())]) 187 | .unwrap(); 188 | } 189 | 190 | #[test] 191 | fn add_repository_root_to_index() { 192 | let mut cmd_helper = CommandHelper::new(); 193 | cmd_helper.write_file("a/b/c/hello.txt", b"hello").unwrap(); 194 | 195 | cmd_helper.jit_cmd(&["init"]).unwrap(); 196 | cmd_helper.jit_cmd(&["add", "."]).unwrap(); 197 | 198 | cmd_helper 199 | .assert_index(vec![(0o100644, "a/b/c/hello.txt".to_string())]) 200 | .unwrap(); 201 | } 202 | 203 | #[test] 204 | fn add_fails_for_non_existent_files() { 205 | let mut cmd_helper = CommandHelper::new(); 206 | 207 | cmd_helper.jit_cmd(&["init"]).unwrap(); 208 | assert!(cmd_helper.jit_cmd(&["add", "hello.txt"]).is_err()); 209 | } 210 | 211 | #[test] 212 | fn add_fails_for_unreadable_files() { 213 | let mut cmd_helper = CommandHelper::new(); 214 | cmd_helper.write_file("hello.txt", b"hello").unwrap(); 215 | cmd_helper.make_unreadable("hello.txt").unwrap(); 216 | 217 | cmd_helper.jit_cmd(&["init"]).unwrap(); 218 | assert!(cmd_helper.jit_cmd(&["add", "hello.txt"]).is_err()); 219 | } 220 | 221 | #[test] 222 | fn add_fails_if_index_is_locked() { 223 | let mut cmd_helper = CommandHelper::new(); 224 | cmd_helper.write_file("hello.txt", b"hello").unwrap(); 225 | cmd_helper.write_file(".git/index.lock", b"hello").unwrap(); 226 | 227 | cmd_helper.jit_cmd(&["init"]).unwrap(); 228 | assert!(cmd_helper.jit_cmd(&["add", "hello.txt"]).is_err()); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/commands/branch.rs: -------------------------------------------------------------------------------- 1 | use crate::commands::CommandContext; 2 | use crate::database::object::Object; 3 | use crate::database::{Database, ParsedObject}; 4 | use crate::pager::Pager; 5 | use crate::refs::Ref; 6 | use crate::repository::Repository; 7 | use crate::revision::Revision; 8 | use colored::*; 9 | use std::io::{Read, Write}; 10 | 11 | pub struct Branch<'a, I, O, E> 12 | where 13 | I: Read, 14 | O: Write, 15 | E: Write, 16 | { 17 | repo: Repository, 18 | ctx: CommandContext<'a, I, O, E>, 19 | } 20 | 21 | impl<'a, I, O, E> Branch<'a, I, O, E> 22 | where 23 | I: Read, 24 | O: Write, 25 | E: Write, 26 | { 27 | pub fn new(ctx: CommandContext<'a, I, O, E>) -> Branch<'a, I, O, E> { 28 | let working_dir = &ctx.dir; 29 | let root_path = working_dir.as_path(); 30 | let repo = Repository::new(&root_path); 31 | 32 | Branch { repo, ctx } 33 | } 34 | 35 | pub fn run(&mut self) -> Result<(), String> { 36 | let options = self.ctx.options.as_ref().unwrap().clone(); 37 | let args: Vec<_> = if let Some(args) = options.values_of("args") { 38 | args.collect() 39 | } else { 40 | vec![] 41 | }; 42 | 43 | if options.is_present("delete") || options.is_present("force_delete") { 44 | self.delete_branches(args)?; 45 | } else if args.is_empty() { 46 | self.list_branches()?; 47 | } else { 48 | let branch_name = args.get(0).expect("no branch name provided"); 49 | let start_point = args.get(1); 50 | self.create_branch(branch_name, start_point)?; 51 | } 52 | Ok(()) 53 | } 54 | 55 | fn list_branches(&mut self) -> Result<(), String> { 56 | let current = self.repo.refs.current_ref("HEAD"); 57 | let mut branches = self.repo.refs.list_branches(); 58 | branches.sort(); 59 | 60 | let max_width = branches 61 | .iter() 62 | .map(|b| self.repo.refs.ref_short_name(b).len()) 63 | .max() 64 | .unwrap_or(0); 65 | 66 | Pager::setup_pager(); 67 | 68 | for r#ref in branches { 69 | let info = self.format_ref(&r#ref, ¤t); 70 | let extended_info = self.extended_branch_info(&r#ref, max_width); 71 | println!("{}{}", info, extended_info); 72 | } 73 | 74 | Ok(()) 75 | } 76 | 77 | fn format_ref(&self, r#ref: &Ref, current: &Ref) -> String { 78 | if r#ref == current { 79 | format!("* {}", self.repo.refs.ref_short_name(r#ref).green()) 80 | } else { 81 | format!(" {}", self.repo.refs.ref_short_name(r#ref)) 82 | } 83 | } 84 | 85 | fn extended_branch_info(&mut self, r#ref: &Ref, max_width: usize) -> String { 86 | if self 87 | .ctx 88 | .options 89 | .as_ref() 90 | .map(|o| o.is_present("verbose")) 91 | .unwrap_or(false) 92 | { 93 | let oid = self 94 | .repo 95 | .refs 96 | .read_oid(r#ref) 97 | .expect("unable to resolve branch to oid"); 98 | let commit = if let ParsedObject::Commit(commit) = self.repo.database.load(&oid) { 99 | commit 100 | } else { 101 | panic!("branch ref was not pointing to commit"); 102 | }; 103 | let oid = commit.get_oid(); 104 | let short = Database::short_oid(&oid); 105 | let ref_short_name = self.repo.refs.ref_short_name(r#ref); 106 | format!( 107 | "{:width$}{} {}", 108 | " ", 109 | short, 110 | commit.title_line(), 111 | width = (max_width - ref_short_name.len() + 1) 112 | ) 113 | } else { 114 | "".to_string() 115 | } 116 | } 117 | 118 | fn create_branch( 119 | &mut self, 120 | branch_name: &str, 121 | start_point: Option<&&str>, 122 | ) -> Result<(), String> { 123 | let start_point = if start_point.is_none() { 124 | self.repo.refs.read_head().expect("empty HEAD") 125 | } else { 126 | match Revision::new(&mut self.repo, start_point.unwrap()).resolve() { 127 | Ok(rev) => rev, 128 | Err(errors) => { 129 | let mut v = vec![]; 130 | for error in errors { 131 | v.push(format!("error: {}", error.message)); 132 | for h in error.hint { 133 | v.push(format!("hint: {}", h)); 134 | } 135 | } 136 | 137 | v.push("\n".to_string()); 138 | 139 | return Err(v.join("\n")); 140 | } 141 | } 142 | }; 143 | 144 | self.repo.refs.create_branch(branch_name, &start_point)?; 145 | 146 | Ok(()) 147 | } 148 | 149 | fn delete_branches(&mut self, branch_names: Vec<&str>) -> Result<(), String> { 150 | for branch in branch_names { 151 | self.delete_branch(branch)?; 152 | } 153 | Ok(()) 154 | } 155 | 156 | fn delete_branch(&mut self, branch_name: &str) -> Result<(), String> { 157 | let force = self 158 | .ctx 159 | .options 160 | .as_ref() 161 | .map(|o| o.is_present("force") || o.is_present("force_delete")) 162 | .unwrap_or(false); 163 | if !force { 164 | return Ok(()); 165 | } 166 | 167 | let oid = self.repo.refs.delete_branch(branch_name)?; 168 | let short = Database::short_oid(&oid); 169 | 170 | println!("Deleted branch {} (was {})", branch_name, short); 171 | Ok(()) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/commands/commit.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Write}; 2 | 3 | use chrono::prelude::*; 4 | 5 | use crate::commands::CommandContext; 6 | use crate::database::commit::{Author, Commit}; 7 | use crate::database::object::Object; 8 | use crate::database::tree::Tree; 9 | use crate::database::Entry; 10 | use crate::repository::Repository; 11 | 12 | pub fn commit_command(mut ctx: CommandContext) -> Result<(), String> 13 | where 14 | I: Read, 15 | O: Write, 16 | E: Write, 17 | { 18 | let working_dir = ctx.dir; 19 | let root_path = working_dir.as_path(); 20 | let mut repo = Repository::new(&root_path); 21 | 22 | repo.index.load().expect("loading .git/index failed"); 23 | let entries: Vec = repo 24 | .index 25 | .entries 26 | .iter() 27 | .map(|(_path, idx_entry)| Entry::from(idx_entry)) 28 | .collect(); 29 | let root = Tree::build(&entries); 30 | root.traverse(&|tree| { 31 | repo.database 32 | .store(tree) 33 | .expect("Traversing tree to write to database failed") 34 | }); 35 | 36 | let parent = repo.refs.read_head(); 37 | let author_name = ctx 38 | .env 39 | .get("GIT_AUTHOR_NAME") 40 | .expect("GIT_AUTHOR_NAME not set"); 41 | let author_email = ctx 42 | .env 43 | .get("GIT_AUTHOR_EMAIL") 44 | .expect("GIT_AUTHOR_EMAIL not set"); 45 | 46 | let author = Author { 47 | name: author_name.to_string(), 48 | email: author_email.to_string(), 49 | time: Utc::now().with_timezone(&FixedOffset::east(0)), 50 | }; 51 | 52 | let mut commit_message = String::new(); 53 | ctx.stdin 54 | .read_to_string(&mut commit_message) 55 | .expect("reading commit from STDIN failed"); 56 | 57 | let commit = Commit::new(&parent, root.get_oid(), author, commit_message); 58 | repo.database.store(&commit).expect("writing commit failed"); 59 | repo.refs 60 | .update_head(&commit.get_oid()) 61 | .expect("updating HEAD failed"); 62 | 63 | let commit_prefix = if parent.is_some() { 64 | "" 65 | } else { 66 | "(root-commit) " 67 | }; 68 | 69 | println!("[{}{}] {}", commit_prefix, commit.get_oid(), commit.message); 70 | 71 | Ok(()) 72 | } 73 | -------------------------------------------------------------------------------- /src/commands/diff.rs: -------------------------------------------------------------------------------- 1 | use crate::commands::CommandContext; 2 | use crate::database::blob::Blob; 3 | use crate::database::object::Object; 4 | use crate::database::{Database, ParsedObject}; 5 | use crate::diff; 6 | use crate::diff::myers::{Edit, EditType}; 7 | use crate::pager::Pager; 8 | use crate::repository::{ChangeType, Repository}; 9 | use colored::*; 10 | use std::io::{Read, Write}; 11 | use std::os::unix::fs::MetadataExt; 12 | 13 | const NULL_OID: &str = "0000000"; 14 | const NULL_PATH: &str = "/dev/null"; 15 | 16 | pub struct Diff<'a, I, O, E> 17 | where 18 | I: Read, 19 | O: Write, 20 | E: Write, 21 | { 22 | repo: Repository, 23 | ctx: CommandContext<'a, I, O, E>, 24 | } 25 | 26 | struct Target { 27 | path: String, 28 | oid: String, 29 | mode: Option, 30 | data: String, 31 | } 32 | 33 | impl<'a, I, O, E> Diff<'a, I, O, E> 34 | where 35 | I: Read, 36 | O: Write, 37 | E: Write, 38 | { 39 | pub fn new(ctx: CommandContext<'a, I, O, E>) -> Diff<'a, I, O, E> { 40 | let working_dir = &ctx.dir; 41 | let root_path = working_dir.as_path(); 42 | let repo = Repository::new(&root_path); 43 | 44 | Diff { ctx, repo } 45 | } 46 | 47 | pub fn run(&mut self) -> Result<(), String> { 48 | self.repo.index.load().map_err(|e| e.to_string())?; 49 | self.repo.initialize_status()?; 50 | 51 | Pager::setup_pager(); 52 | 53 | if self 54 | .ctx 55 | .options 56 | .as_ref() 57 | .map(|o| o.is_present("cached")) 58 | .unwrap_or(false) 59 | { 60 | self.diff_head_index() 61 | } else { 62 | self.diff_index_workspace() 63 | } 64 | } 65 | 66 | fn diff_head_index(&mut self) -> Result<(), String> { 67 | for (path, state) in &self.repo.index_changes.clone() { 68 | match state { 69 | ChangeType::Added => { 70 | let b = self.from_index(path); 71 | self.print_diff(self.from_nothing(path), b)?; 72 | } 73 | ChangeType::Modified => { 74 | let a = self.from_head(path); 75 | let b = self.from_index(path); 76 | self.print_diff(a, b)?; 77 | } 78 | ChangeType::Deleted => { 79 | let a = self.from_head(path); 80 | self.print_diff(a, self.from_nothing(path))?; 81 | } 82 | state => panic!("NYI: {:?}", state), 83 | } 84 | } 85 | 86 | Ok(()) 87 | } 88 | 89 | fn diff_index_workspace(&mut self) -> Result<(), String> { 90 | for (path, state) in &self.repo.workspace_changes.clone() { 91 | match state { 92 | ChangeType::Added => { 93 | self.print_diff(self.from_nothing(path), self.from_file(path))?; 94 | } 95 | ChangeType::Modified => { 96 | let a = self.from_index(path); 97 | self.print_diff(a, self.from_file(path))?; 98 | } 99 | ChangeType::Deleted => { 100 | let a = self.from_index(path); 101 | self.print_diff(a, self.from_nothing(path))?; 102 | } 103 | state => panic!("NYI: {:?}", state), 104 | } 105 | } 106 | Ok(()) 107 | } 108 | 109 | fn print_diff(&mut self, mut a: Target, mut b: Target) -> Result<(), String> { 110 | if a.oid == b.oid && a.mode == b.mode { 111 | return Ok(()); 112 | } 113 | 114 | a.path = format!("a/{}", a.path); 115 | b.path = format!("b/{}", b.path); 116 | 117 | println!("{}", format!("diff --git {} {}", a.path, b.path).bold()); 118 | 119 | self.print_diff_mode(&a, &b)?; 120 | self.print_diff_content(&a, &b) 121 | } 122 | 123 | fn print_diff_mode(&mut self, a: &Target, b: &Target) -> Result<(), String> { 124 | if a.mode == None { 125 | println!( 126 | "{}", 127 | format!("new file mode {:o}", b.mode.expect("missing mode")).bold() 128 | ); 129 | } else if b.mode == None { 130 | println!( 131 | "{}", 132 | format!("deleted file mode {:o}", a.mode.expect("missing mode")).bold() 133 | ); 134 | } else if a.mode != b.mode { 135 | println!( 136 | "{}", 137 | format!("old mode {:o}", a.mode.expect("missing mode")).bold() 138 | ); 139 | 140 | println!( 141 | "{}", 142 | format!("new mode {:o}", b.mode.expect("missing mode")).bold() 143 | ); 144 | } 145 | 146 | Ok(()) 147 | } 148 | 149 | fn print_diff_content(&mut self, a: &Target, b: &Target) -> Result<(), String> { 150 | if a.oid == b.oid { 151 | return Ok(()); 152 | } 153 | 154 | println!( 155 | "{}", 156 | format!( 157 | "index {}..{}{}", 158 | short(&a.oid), 159 | short(&b.oid), 160 | if a.mode == b.mode { 161 | format!(" {:o}", a.mode.expect("Missing mode")) 162 | } else { 163 | "".to_string() 164 | } 165 | ) 166 | .bold() 167 | ); 168 | println!("{}", format!("--- {}", a.path).bold()); 169 | println!("{}", format!("+++ {}", b.path).bold()); 170 | 171 | let hunks = diff::Diff::diff_hunks(&a.data, &b.data); 172 | for h in hunks { 173 | self.print_diff_hunk(h).map_err(|e| e.to_string())?; 174 | } 175 | 176 | Ok(()) 177 | } 178 | 179 | fn print_diff_edit(&mut self, edit: Edit) -> Result<(), String> { 180 | let edit_string = match &edit.edit_type { 181 | EditType::Ins => format!("{}", edit).green(), 182 | EditType::Del => format!("{}", edit).red(), 183 | EditType::Eql => format!("{}", edit).normal(), 184 | }; 185 | println!("{}", edit_string); 186 | 187 | Ok(()) 188 | } 189 | 190 | fn print_diff_hunk(&mut self, hunk: diff::Hunk) -> Result<(), String> { 191 | println!("{}", hunk.header().cyan()); 192 | 193 | for edit in hunk.edits { 194 | self.print_diff_edit(edit).map_err(|e| e.to_string())?; 195 | } 196 | 197 | Ok(()) 198 | } 199 | 200 | fn from_index(&mut self, path: &str) -> Target { 201 | let entry = self 202 | .repo 203 | .index 204 | .entry_for_path(path) 205 | .expect("Path not found in index"); 206 | let oid = entry.oid.clone(); 207 | let blob = match self.repo.database.load(&oid) { 208 | ParsedObject::Blob(blob) => blob, 209 | _ => panic!("path is not a blob"), 210 | }; 211 | 212 | Target { 213 | path: path.to_string(), 214 | oid, 215 | mode: Some(entry.mode), 216 | data: std::str::from_utf8(&blob.data) 217 | .expect("utf8 conversion failed") 218 | .to_string(), 219 | } 220 | } 221 | 222 | fn from_file(&self, path: &str) -> Target { 223 | let blob = Blob::new( 224 | self.repo 225 | .workspace 226 | .read_file(path) 227 | .expect("Failed to read file") 228 | .as_bytes(), 229 | ); 230 | let oid = blob.get_oid(); 231 | let mode = self.repo.stats.get(path).unwrap().mode(); 232 | Target { 233 | path: path.to_string(), 234 | oid, 235 | mode: Some(mode), 236 | data: std::str::from_utf8(&blob.data) 237 | .expect("utf8 conversion failed") 238 | .to_string(), 239 | } 240 | } 241 | 242 | fn from_nothing(&self, path: &str) -> Target { 243 | Target { 244 | path: path.to_string(), 245 | oid: NULL_OID.to_string(), 246 | mode: None, 247 | data: "".to_string(), 248 | } 249 | } 250 | 251 | fn from_head(&mut self, path: &str) -> Target { 252 | let entry = self 253 | .repo 254 | .head_tree 255 | .get(path) 256 | .expect("Path not found in HEAD"); 257 | let oid = entry.get_oid(); 258 | let mode = entry.mode(); 259 | let blob = match self.repo.database.load(&oid) { 260 | ParsedObject::Blob(blob) => blob, 261 | _ => panic!("path is not a blob"), 262 | }; 263 | 264 | Target { 265 | path: path.to_string(), 266 | oid, 267 | mode: Some(mode), 268 | data: std::str::from_utf8(&blob.data) 269 | .expect("utf8 conversion failed") 270 | .to_string(), 271 | } 272 | } 273 | } 274 | 275 | fn short(oid: &str) -> &str { 276 | Database::short_oid(oid) 277 | } 278 | -------------------------------------------------------------------------------- /src/commands/init.rs: -------------------------------------------------------------------------------- 1 | use crate::refs::Refs; 2 | use std::fs; 3 | use std::io::{Read, Write}; 4 | use std::path::Path; 5 | 6 | use crate::commands::CommandContext; 7 | 8 | const DEFAULT_BRANCH: &str = "master"; 9 | 10 | pub fn init_command(ctx: CommandContext) -> Result<(), String> 11 | where 12 | I: Read, 13 | O: Write, 14 | E: Write, 15 | { 16 | let working_dir = ctx.dir; 17 | let options = ctx.options.as_ref().unwrap(); 18 | let args: Vec<_> = if let Some(args) = options.values_of("args") { 19 | args.collect() 20 | } else { 21 | vec![] 22 | }; 23 | let root_path = if !args.is_empty() { 24 | Path::new(args[0]) 25 | } else { 26 | working_dir.as_path() 27 | }; 28 | let git_path = root_path.join(".git"); 29 | 30 | for d in ["objects", "refs/heads"].iter() { 31 | fs::create_dir_all(git_path.join(d)).expect("failed to create dir"); 32 | } 33 | 34 | let refs = Refs::new(&git_path); 35 | let path = Path::new("refs/heads").join(DEFAULT_BRANCH); 36 | refs.update_head(&format!( 37 | "ref: {}", 38 | path.to_str().expect("failed to convert path to str") 39 | )) 40 | .map_err(|e| e.to_string())?; 41 | 42 | println!("Initialized empty Jit repository in {:?}\n", git_path); 43 | Ok(()) 44 | } 45 | -------------------------------------------------------------------------------- /src/commands/log.rs: -------------------------------------------------------------------------------- 1 | use crate::commands::CommandContext; 2 | use crate::database::commit::Commit; 3 | use crate::database::object::Object; 4 | use crate::database::{Database, ParsedObject}; 5 | use crate::pager::Pager; 6 | use crate::refs::Ref; 7 | use crate::repository::Repository; 8 | use colored::*; 9 | use std::collections::HashMap; 10 | use std::io::{Read, Write}; 11 | 12 | #[derive(Clone, Copy)] 13 | enum FormatOption { 14 | Medium, 15 | OneLine, 16 | } 17 | 18 | #[derive(Clone, Copy)] 19 | enum DecorateOption { 20 | Auto, 21 | Short, 22 | Full, 23 | No, 24 | } 25 | 26 | struct Options { 27 | abbrev: bool, 28 | format: FormatOption, 29 | decorate: DecorateOption, 30 | } 31 | 32 | pub struct Log<'a, I, O, E> 33 | where 34 | I: Read, 35 | O: Write, 36 | E: Write, 37 | { 38 | current_oid: Option, 39 | repo: Repository, 40 | ctx: CommandContext<'a, I, O, E>, 41 | options: Options, 42 | reverse_refs: Option>>, 43 | current_ref: Option, 44 | } 45 | 46 | impl<'a, I, O, E> Log<'a, I, O, E> 47 | where 48 | I: Read, 49 | O: Write, 50 | E: Write, 51 | { 52 | pub fn new(ctx: CommandContext<'a, I, O, E>) -> Log<'a, I, O, E> { 53 | let working_dir = &ctx.dir; 54 | let root_path = working_dir.as_path(); 55 | let repo = Repository::new(&root_path); 56 | let current_oid = repo.refs.read_head(); 57 | let ctx_options = ctx.options.as_ref().unwrap().clone(); 58 | let options = Self::define_options(ctx_options); 59 | 60 | Log { 61 | ctx, 62 | repo, 63 | current_oid, 64 | options, 65 | reverse_refs: None, 66 | current_ref: None, 67 | } 68 | } 69 | 70 | fn define_options(options: clap::ArgMatches) -> Options { 71 | let mut abbrev = None; 72 | 73 | if options.is_present("abbrev-commit") { 74 | abbrev = Some(true); 75 | } 76 | if options.is_present("no-abbrev-commit") { 77 | abbrev = Some(false); 78 | } 79 | 80 | let mut format = FormatOption::Medium; 81 | if options.is_present("format") || options.is_present("pretty") { 82 | match options.value_of("format").unwrap() { 83 | "oneline" => { 84 | format = FormatOption::OneLine; 85 | } 86 | "medium" => { 87 | format = FormatOption::Medium; 88 | } 89 | _ => (), 90 | }; 91 | } 92 | 93 | if options.is_present("oneline") { 94 | format = FormatOption::OneLine; 95 | if abbrev == None { 96 | abbrev = Some(true); 97 | } 98 | } 99 | 100 | let mut decorate = DecorateOption::Short; 101 | 102 | if options.is_present("decorate") { 103 | decorate = match options.value_of("decorate").unwrap() { 104 | "full" => DecorateOption::Full, 105 | "short" => DecorateOption::Short, 106 | "no" => DecorateOption::No, 107 | _ => unimplemented!(), 108 | } 109 | } 110 | 111 | if options.is_present("no-decorate") { 112 | decorate = DecorateOption::No; 113 | } 114 | 115 | Options { 116 | abbrev: abbrev.unwrap_or(false), 117 | format, 118 | decorate, 119 | } 120 | } 121 | 122 | pub fn run(&mut self) -> Result<(), String> { 123 | Pager::setup_pager(); 124 | 125 | self.reverse_refs = Some(self.repo.refs.reverse_refs()); 126 | self.current_ref = Some(self.repo.refs.current_ref("HEAD")); 127 | 128 | // FIXME: Print commits as they are returned by the iterator 129 | // instead of collecting into a Vec. 130 | let mut commits = vec![]; 131 | for c in &mut self.into_iter() { 132 | commits.push(c); 133 | } 134 | 135 | commits 136 | .iter() 137 | .for_each(|commit| self.show_commit(commit).unwrap()); 138 | Ok(()) 139 | } 140 | 141 | fn show_commit(&self, commit: &Commit) -> Result<(), String> { 142 | match self.options.format { 143 | FormatOption::Medium => { 144 | self.show_commit_medium(commit)?; // , abbrev, decorate, reverse_refs, current_ref) 145 | } 146 | FormatOption::OneLine => { 147 | self.show_commit_oneline(commit)?; // , abbrev, decorate, reverse_refs, current_ref) 148 | } 149 | } 150 | 151 | Ok(()) 152 | } 153 | 154 | fn abbrev(&self, commit: &Commit) -> String { 155 | if self.options.abbrev { 156 | let oid = commit.get_oid(); 157 | Database::short_oid(&oid).to_string() 158 | } else { 159 | commit.get_oid() 160 | } 161 | } 162 | 163 | fn show_commit_medium(&self, commit: &Commit) -> Result<(), String> { 164 | let author = &commit.author; 165 | println!(); 166 | println!( 167 | "commit {} {}", 168 | self.abbrev(commit).yellow(), 169 | self.decorate(commit) 170 | ); 171 | println!("Author: {} <{}>", author.name, author.email); 172 | println!("Date: {}", author.readable_time()); 173 | println!(); 174 | 175 | for line in commit.message.lines() { 176 | println!(" {}", line); 177 | } 178 | Ok(()) 179 | } 180 | 181 | fn show_commit_oneline(&self, commit: &Commit) -> Result<(), String> { 182 | println!( 183 | "{} {} {}", 184 | self.abbrev(commit).yellow(), 185 | self.decorate(commit), 186 | commit.title_line() 187 | ); 188 | 189 | Ok(()) 190 | } 191 | 192 | fn decorate(&self, commit: &Commit) -> String { 193 | match self.options.decorate { 194 | DecorateOption::No | DecorateOption::Auto => return "".to_string(), // TODO: check isatty 195 | _ => (), 196 | } 197 | 198 | let refs = self.reverse_refs.as_ref().unwrap().get(&commit.get_oid()); 199 | if let Some(refs) = refs { 200 | let (head, refs): (Vec<&Ref>, Vec<&Ref>) = refs.into_iter().partition(|r#ref| { 201 | r#ref.is_head() && !self.current_ref.as_ref().unwrap().is_head() 202 | }); 203 | let names: Vec<_> = refs 204 | .iter() 205 | .map(|r#ref| self.decoration_name(head.get(0), r#ref)) 206 | .collect(); 207 | 208 | format!( 209 | " {}{}{}", 210 | "(".yellow(), 211 | names.join(&", ".yellow()), 212 | ")".yellow() 213 | ) 214 | } else { 215 | "".to_string() 216 | } 217 | } 218 | 219 | fn decoration_name(&self, head: Option<&&Ref>, r#ref: &Ref) -> String { 220 | let mut name = match self.options.decorate { 221 | DecorateOption::Short | DecorateOption::Auto => self.repo.refs.ref_short_name(r#ref), 222 | DecorateOption::Full => r#ref.path().to_string(), 223 | _ => unimplemented!(), 224 | }; 225 | 226 | name = name.bold().color(Self::ref_color(&r#ref)).to_string(); 227 | 228 | if let Some(head) = head { 229 | if r#ref == self.current_ref.as_ref().unwrap() { 230 | name = format!("{} -> {}", "HEAD", name) 231 | .color(Self::ref_color(head)) 232 | .to_string(); 233 | } 234 | } 235 | 236 | name 237 | } 238 | 239 | fn ref_color(r#ref: &Ref) -> &str { 240 | if r#ref.is_head() { 241 | "cyan" 242 | } else { 243 | "green" 244 | } 245 | } 246 | } 247 | 248 | impl<'a, I, O, E> Iterator for Log<'a, I, O, E> 249 | where 250 | I: Read, 251 | O: Write, 252 | E: Write, 253 | { 254 | type Item = Commit; 255 | 256 | fn next(&mut self) -> Option { 257 | if let Some(current_oid) = &self.current_oid { 258 | if let ParsedObject::Commit(commit) = self.repo.database.load(¤t_oid) { 259 | self.current_oid = commit.parent.clone(); 260 | Some(commit.clone()) 261 | } else { 262 | None 263 | } 264 | } else { 265 | None 266 | } 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | use clap::{App, Arg, ArgMatches, SubCommand}; 2 | use std::collections::HashMap; 3 | use std::io::{Read, Write}; 4 | use std::path::PathBuf; 5 | 6 | mod add; 7 | use add::add_command; 8 | mod init; 9 | use init::init_command; 10 | mod commit; 11 | use commit::commit_command; 12 | mod status; 13 | use status::Status; 14 | mod diff; 15 | use diff::Diff; 16 | mod branch; 17 | use branch::Branch; 18 | mod checkout; 19 | use checkout::Checkout; 20 | mod log; 21 | use log::Log; 22 | 23 | #[derive(Debug)] 24 | pub struct CommandContext<'a, I, O, E> 25 | where 26 | I: Read, 27 | O: Write, 28 | E: Write, 29 | { 30 | pub dir: PathBuf, 31 | pub env: &'a HashMap, 32 | pub options: Option>, 33 | pub stdin: I, 34 | pub stdout: O, 35 | pub stderr: E, 36 | } 37 | 38 | pub fn get_app() -> App<'static, 'static> { 39 | App::new("rug") 40 | .subcommand( 41 | SubCommand::with_name("init") 42 | .about("Create an empty Git repository or reinitialize an existing one") 43 | .arg(Arg::with_name("args").multiple(true)), 44 | ) 45 | .subcommand( 46 | SubCommand::with_name("status") 47 | .about("Show the working tree status") 48 | .arg(Arg::with_name("porcelain").long("porcelain")) 49 | .arg(Arg::with_name("args").multiple(true)), 50 | ) 51 | .subcommand( 52 | SubCommand::with_name("commit") 53 | .about("Record changes to the repository") 54 | .arg(Arg::with_name("args").multiple(true)), 55 | ) 56 | .subcommand( 57 | SubCommand::with_name("add") 58 | .about("Add file contents to the index") 59 | .arg(Arg::with_name("args").multiple(true)), 60 | ) 61 | .subcommand( 62 | SubCommand::with_name("diff") 63 | .about("Show changes between commits, commit and working tree, etc") 64 | .arg(Arg::with_name("cached").long("cached")) 65 | .arg(Arg::with_name("args").multiple(true)), 66 | ) 67 | .subcommand( 68 | SubCommand::with_name("branch") 69 | .about("List, create, or delete branches") 70 | .arg(Arg::with_name("verbose").short("v").long("verbose")) 71 | .arg(Arg::with_name("delete").short("d").long("delete")) 72 | .arg(Arg::with_name("force").long("force")) 73 | .arg(Arg::with_name("force_delete").short("D")) 74 | .arg(Arg::with_name("args").multiple(true)), 75 | ) 76 | .subcommand( 77 | SubCommand::with_name("checkout") 78 | .about("Switch branches or restore working tree files") 79 | .arg(Arg::with_name("args").multiple(true)), 80 | ) 81 | .subcommand( 82 | SubCommand::with_name("log") 83 | .about("Show commit logs") 84 | .arg(Arg::with_name("abbrev-commit").long("abbrev-commit")) 85 | .arg(Arg::with_name("no-abbrev-commit").long("no-abbrev-commit")) 86 | .arg( 87 | Arg::with_name("pretty") 88 | .long("pretty") 89 | .takes_value(true) 90 | .value_name("format"), 91 | ) 92 | .arg( 93 | Arg::with_name("format") 94 | .long("format") 95 | .takes_value(true) 96 | .value_name("format"), 97 | ) 98 | .arg(Arg::with_name("oneline").long("oneline")) 99 | .arg( 100 | Arg::with_name("decorate") 101 | .long("decorate") 102 | .takes_value(true) 103 | .value_name("format"), 104 | ) 105 | .arg(Arg::with_name("no-decorate").long("no-decorate")) 106 | .arg(Arg::with_name("args").multiple(true)), 107 | ) 108 | } 109 | 110 | pub fn execute<'a, I, O, E>( 111 | matches: ArgMatches<'a>, 112 | mut ctx: CommandContext<'a, I, O, E>, 113 | ) -> Result<(), String> 114 | where 115 | I: Read, 116 | O: Write, 117 | E: Write, 118 | { 119 | match matches.subcommand() { 120 | ("init", sub_matches) => { 121 | ctx.options = sub_matches.cloned(); 122 | init_command(ctx) 123 | } 124 | ("commit", sub_matches) => { 125 | ctx.options = sub_matches.cloned(); 126 | commit_command(ctx) 127 | } 128 | ("add", sub_matches) => { 129 | ctx.options = sub_matches.cloned(); 130 | add_command(ctx) 131 | } 132 | ("status", sub_matches) => { 133 | ctx.options = sub_matches.cloned(); 134 | let mut cmd = Status::new(ctx); 135 | cmd.run() 136 | } 137 | ("diff", sub_matches) => { 138 | ctx.options = sub_matches.cloned(); 139 | let mut cmd = Diff::new(ctx); 140 | cmd.run() 141 | } 142 | ("branch", sub_matches) => { 143 | ctx.options = sub_matches.cloned(); 144 | let mut cmd = Branch::new(ctx); 145 | cmd.run() 146 | } 147 | ("checkout", sub_matches) => { 148 | ctx.options = sub_matches.cloned(); 149 | let mut cmd = Checkout::new(ctx); 150 | cmd.run() 151 | } 152 | ("log", sub_matches) => { 153 | ctx.options = sub_matches.cloned(); 154 | let mut cmd = Log::new(ctx); 155 | cmd.run() 156 | } 157 | _ => Ok(()), 158 | } 159 | } 160 | 161 | #[cfg(test)] 162 | mod tests { 163 | use super::*; 164 | use crate::repository::Repository; 165 | use crate::util::*; 166 | use assert_cmd::prelude::*; 167 | use filetime::FileTime; 168 | use std::env; 169 | use std::fs::{self, File, OpenOptions}; 170 | use std::io::Cursor; 171 | use std::io::Write; 172 | use std::os::unix::fs::PermissionsExt; 173 | use std::path::Path; 174 | use std::process::{Command, Stdio}; 175 | use std::str; 176 | use std::time::{SystemTime, UNIX_EPOCH}; 177 | extern crate assert_cmd; 178 | 179 | pub fn gen_repo_path() -> PathBuf { 180 | let mut temp_dir = generate_temp_name(); 181 | temp_dir.push_str("_rug_test"); 182 | env::temp_dir() 183 | .canonicalize() 184 | .expect("canonicalization failed") 185 | .join(temp_dir) 186 | } 187 | 188 | pub fn repo(repo_path: &Path) -> Repository { 189 | Repository::new(&repo_path) 190 | } 191 | 192 | pub struct CommandHelper { 193 | repo_path: PathBuf, 194 | stdin: String, 195 | stdout: Cursor>, 196 | env: HashMap, 197 | } 198 | 199 | impl CommandHelper { 200 | pub fn new() -> CommandHelper { 201 | let repo_path = gen_repo_path(); 202 | fs::create_dir_all(&repo_path).unwrap(); 203 | CommandHelper { 204 | repo_path, 205 | stdin: String::new(), 206 | stdout: Cursor::new(vec![]), 207 | env: HashMap::new(), 208 | } 209 | } 210 | 211 | fn set_env(&mut self, key: &str, value: &str) { 212 | self.env.insert(key.to_string(), value.to_string()); 213 | } 214 | 215 | fn set_stdin(&mut self, s: &str) { 216 | self.stdin = s.to_string(); 217 | } 218 | 219 | pub fn jit_cmd(&mut self, args: &[&str]) -> Result<(String, String), String> { 220 | let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")) 221 | .unwrap() 222 | .args(args) 223 | .current_dir(&self.repo_path) 224 | .envs(&self.env) 225 | .stdout(Stdio::piped()) 226 | .stderr(Stdio::piped()) 227 | .stdin(Stdio::piped()) 228 | .spawn() 229 | .expect("Failed to spawn child process"); 230 | 231 | cmd.stdin 232 | .as_mut() 233 | .unwrap() 234 | .write_all(self.stdin.as_bytes()) 235 | .unwrap(); 236 | 237 | let output = cmd.wait_with_output().expect("failed to run executable"); 238 | 239 | let (stdout, stderr) = ( 240 | String::from_utf8_lossy(&output.stdout).to_string(), 241 | String::from_utf8_lossy(&output.stderr).to_string(), 242 | ); 243 | 244 | if output.status.success() { 245 | Ok((stdout, stderr)) 246 | } else { 247 | Err(stderr) 248 | } 249 | } 250 | 251 | pub fn commit(&mut self, msg: &str) { 252 | self.set_env("GIT_AUTHOR_NAME", "A. U. Thor"); 253 | self.set_env("GIT_AUTHOR_EMAIL", "author@example.com"); 254 | self.set_stdin(msg); 255 | self.jit_cmd(&["commit"]).unwrap(); 256 | } 257 | 258 | pub fn write_file(&self, file_name: &str, contents: &[u8]) -> Result<(), std::io::Error> { 259 | let path = Path::new(&self.repo_path).join(file_name); 260 | fs::create_dir_all(path.parent().unwrap())?; 261 | let mut file = OpenOptions::new() 262 | .read(true) 263 | .write(true) 264 | .create(true) 265 | .truncate(true) 266 | .open(&path)?; 267 | file.write_all(contents)?; 268 | 269 | Ok(()) 270 | } 271 | 272 | pub fn mkdir(&self, dir_name: &str) -> Result<(), std::io::Error> { 273 | fs::create_dir_all(self.repo_path.join(dir_name)) 274 | } 275 | 276 | pub fn touch(&self, file_name: &str) -> Result<(), std::io::Error> { 277 | let path = Path::new(&self.repo_path).join(file_name); 278 | let now = FileTime::from_unix_time( 279 | SystemTime::now() 280 | .duration_since(UNIX_EPOCH) 281 | .expect("time is broken") 282 | .as_secs() as i64, 283 | 0, 284 | ); 285 | filetime::set_file_times(path, now, now) 286 | } 287 | 288 | pub fn delete(&self, pathname: &str) -> Result<(), std::io::Error> { 289 | let path = Path::new(&self.repo_path).join(pathname); 290 | 291 | if path.is_dir() { 292 | fs::remove_dir_all(path) 293 | } else { 294 | fs::remove_file(path) 295 | } 296 | } 297 | 298 | pub fn make_executable(&self, file_name: &str) -> Result<(), std::io::Error> { 299 | let path = self.repo_path.join(file_name); 300 | let file = File::open(&path)?; 301 | let metadata = file.metadata()?; 302 | let mut permissions = metadata.permissions(); 303 | 304 | permissions.set_mode(0o744); 305 | fs::set_permissions(path, permissions)?; 306 | Ok(()) 307 | } 308 | 309 | pub fn make_unreadable(&self, file_name: &str) -> Result<(), std::io::Error> { 310 | let path = self.repo_path.join(file_name); 311 | let file = File::open(&path)?; 312 | let metadata = file.metadata()?; 313 | let mut permissions = metadata.permissions(); 314 | 315 | permissions.set_mode(0o044); 316 | fs::set_permissions(path, permissions)?; 317 | Ok(()) 318 | } 319 | 320 | pub fn assert_index(&self, expected: Vec<(u32, String)>) -> Result<(), std::io::Error> { 321 | let mut repo = repo(&self.repo_path); 322 | repo.index.load()?; 323 | 324 | let actual: Vec<(u32, String)> = repo 325 | .index 326 | .entries 327 | .iter() 328 | .map(|(_, entry)| (entry.mode, entry.path.clone())) 329 | .collect(); 330 | 331 | assert_eq!(expected, actual); 332 | 333 | Ok(()) 334 | } 335 | 336 | pub fn clear_stdout(&mut self) { 337 | self.stdout = Cursor::new(vec![]); 338 | } 339 | 340 | pub fn assert_status(&mut self, expected: &str) { 341 | if let Ok((stdout, _stderr)) = self.jit_cmd(&["status", "--porcelain"]) { 342 | assert_output(&stdout, expected) 343 | } else { 344 | assert!(false); 345 | } 346 | } 347 | 348 | pub fn assert_workspace(&self, expected_contents: HashMap<&str, &str>) { 349 | let mut files = HashMap::new(); 350 | for file in repo(&self.repo_path) 351 | .workspace 352 | .list_files(&self.repo_path) 353 | .unwrap() 354 | { 355 | let file_contents = repo(&self.repo_path).workspace.read_file(&file).unwrap(); 356 | files.insert(file, file_contents); 357 | } 358 | 359 | assert_maps_equal(expected_contents, files); 360 | } 361 | 362 | pub fn assert_noent(&self, filename: &str) { 363 | assert_eq!(false, Path::new(filename).exists()) 364 | } 365 | } 366 | 367 | impl Drop for CommandHelper { 368 | fn drop(&mut self) { 369 | fs::remove_dir_all(&self.repo_path); 370 | } 371 | } 372 | 373 | pub fn assert_output(stream: &str, expected: &str) { 374 | assert_eq!(stream, expected); 375 | } 376 | 377 | fn assert_maps_equal(a: HashMap<&str, &str>, b: HashMap) { 378 | assert_eq!(a.len(), b.len()); 379 | for (k, v) in a { 380 | if let Some(bv) = b.get(k) { 381 | assert_eq!(v, *bv); 382 | } 383 | } 384 | } 385 | } 386 | -------------------------------------------------------------------------------- /src/commands/status.rs: -------------------------------------------------------------------------------- 1 | use crate::commands::CommandContext; 2 | use crate::repository::{ChangeType, Repository}; 3 | use colored::*; 4 | use std::collections::HashMap; 5 | use std::io::{Read, Write}; 6 | 7 | static LABEL_WIDTH: usize = 12; 8 | 9 | lazy_static! { 10 | static ref SHORT_STATUS: HashMap = { 11 | let mut m = HashMap::new(); 12 | m.insert(ChangeType::Added, "A"); 13 | m.insert(ChangeType::Modified, "M"); 14 | m.insert(ChangeType::Deleted, "D"); 15 | m 16 | }; 17 | static ref LONG_STATUS: HashMap = { 18 | let mut m = HashMap::new(); 19 | m.insert(ChangeType::Added, "new file:"); 20 | m.insert(ChangeType::Modified, "modified:"); 21 | m.insert(ChangeType::Deleted, "deleted:"); 22 | m 23 | }; 24 | } 25 | 26 | pub struct Status<'a, I, O, E> 27 | where 28 | I: Read, 29 | O: Write, 30 | E: Write, 31 | { 32 | repo: Repository, 33 | ctx: CommandContext<'a, I, O, E>, 34 | } 35 | 36 | impl<'a, I, O, E> Status<'a, I, O, E> 37 | where 38 | I: Read, 39 | O: Write, 40 | E: Write, 41 | { 42 | pub fn new(ctx: CommandContext<'a, I, O, E>) -> Status<'a, I, O, E> 43 | where 44 | I: Read, 45 | O: Write, 46 | E: Write, 47 | { 48 | let working_dir = &ctx.dir; 49 | let root_path = working_dir.as_path(); 50 | let repo = Repository::new(&root_path); 51 | 52 | Status { repo, ctx } 53 | } 54 | 55 | fn status_for(&self, path: &str) -> String { 56 | let left = if let Some(index_change) = self.repo.index_changes.get(path) { 57 | SHORT_STATUS.get(index_change).unwrap_or(&" ") 58 | } else { 59 | " " 60 | }; 61 | let right = if let Some(workspace_change) = self.repo.workspace_changes.get(path) { 62 | SHORT_STATUS.get(workspace_change).unwrap_or(&" ") 63 | } else { 64 | " " 65 | }; 66 | format!("{}{}", left, right) 67 | } 68 | 69 | fn print_porcelain_format(&mut self) -> Result<(), String> { 70 | for file in &self.repo.changed { 71 | println!("{} {}", self.status_for(file), file); 72 | } 73 | 74 | for file in &self.repo.untracked { 75 | println!("?? {}", file); 76 | } 77 | 78 | Ok(()) 79 | } 80 | 81 | fn print_long_format(&mut self) -> Result<(), String> { 82 | self.print_index_changes("Changes to be committed", "green")?; 83 | self.print_workspace_changes("Changes not staged for commit", "red")?; 84 | self.print_untracked_files("Untracked files", "red")?; 85 | 86 | self.print_commit_status()?; 87 | 88 | Ok(()) 89 | } 90 | 91 | fn print_index_changes(&mut self, message: &str, style: &str) -> Result<(), String> { 92 | println!("{}", message); 93 | 94 | for (path, change_type) in &self.repo.index_changes { 95 | if let Some(status) = LONG_STATUS.get(change_type) { 96 | println!( 97 | "{}", 98 | format!("\t{:width$}{}", status, path, width = LABEL_WIDTH).color(style) 99 | ); 100 | } 101 | } 102 | 103 | println!(); 104 | Ok(()) 105 | } 106 | 107 | fn print_workspace_changes(&mut self, message: &str, style: &str) -> Result<(), String> { 108 | println!("{}", message); 109 | 110 | for (path, change_type) in &self.repo.workspace_changes { 111 | if let Some(status) = LONG_STATUS.get(change_type) { 112 | println!( 113 | "{}", 114 | format!("\t{:width$}{}", status, path, width = LABEL_WIDTH).color(style) 115 | ); 116 | } 117 | } 118 | 119 | println!(); 120 | Ok(()) 121 | } 122 | 123 | fn print_untracked_files(&mut self, message: &str, style: &str) -> Result<(), String> { 124 | println!("{}", message); 125 | 126 | for path in &self.repo.untracked { 127 | println!("{}", format!("\t{}", path).color(style)); 128 | } 129 | println!(); 130 | Ok(()) 131 | } 132 | 133 | pub fn print_results(&mut self) -> Result<(), String> { 134 | if self 135 | .ctx 136 | .options 137 | .as_ref() 138 | .map(|o| o.is_present("porcelain")) 139 | .unwrap_or(false) 140 | { 141 | self.print_porcelain_format()?; 142 | } else { 143 | self.print_long_format()?; 144 | } 145 | 146 | Ok(()) 147 | } 148 | 149 | fn print_commit_status(&mut self) -> Result<(), String> { 150 | if !self.repo.index_changes.is_empty() { 151 | return Ok(()); 152 | } 153 | 154 | if !self.repo.workspace_changes.is_empty() { 155 | println!("no changes added to commit"); 156 | } else if !self.repo.untracked.is_empty() { 157 | println!("nothing added to commit but untracked files present"); 158 | } else { 159 | println!("nothing to commit, working tree clean"); 160 | } 161 | 162 | Ok(()) 163 | } 164 | 165 | pub fn run(&mut self) -> Result<(), String> { 166 | self.repo 167 | .index 168 | .load_for_update() 169 | .expect("failed to load index"); 170 | 171 | self.repo.initialize_status()?; 172 | 173 | self.repo 174 | .index 175 | .write_updates() 176 | .expect("failed to write index"); 177 | 178 | self.print_results() 179 | .expect("printing status results failed"); 180 | 181 | Ok(()) 182 | } 183 | } 184 | 185 | #[cfg(test)] 186 | mod tests { 187 | use crate::commands::tests::*; 188 | use std::{thread, time}; 189 | 190 | #[test] 191 | fn list_untracked_files_in_name_order() { 192 | let mut cmd_helper = CommandHelper::new(); 193 | 194 | cmd_helper.jit_cmd(&["init"]).unwrap(); 195 | cmd_helper.write_file("file.txt", b"hello").unwrap(); 196 | cmd_helper.write_file("another.txt", b"hello").unwrap(); 197 | 198 | cmd_helper.clear_stdout(); 199 | cmd_helper.assert_status( 200 | "?? another.txt 201 | ?? file.txt\n", 202 | ); 203 | } 204 | 205 | #[test] 206 | fn list_files_as_untracked_if_not_in_index() { 207 | let mut cmd_helper = CommandHelper::new(); 208 | 209 | cmd_helper.write_file("committed.txt", b"").unwrap(); 210 | cmd_helper.jit_cmd(&["init"]).unwrap(); 211 | cmd_helper.jit_cmd(&["add", "."]).unwrap(); 212 | cmd_helper.commit("commit message"); 213 | 214 | cmd_helper.write_file("file.txt", b"").unwrap(); 215 | 216 | cmd_helper.clear_stdout(); 217 | cmd_helper.assert_status("?? file.txt\n"); 218 | } 219 | 220 | #[test] 221 | fn list_untracked_dir_not_contents() { 222 | let mut cmd_helper = CommandHelper::new(); 223 | cmd_helper.jit_cmd(&["init"]).unwrap(); 224 | cmd_helper.clear_stdout(); 225 | cmd_helper.write_file("file.txt", b"").unwrap(); 226 | cmd_helper.write_file("dir/another.txt", b"").unwrap(); 227 | cmd_helper.assert_status( 228 | "?? dir/ 229 | ?? file.txt\n", 230 | ); 231 | } 232 | 233 | #[test] 234 | fn list_untracked_files_inside_tracked_dir() { 235 | let mut cmd_helper = CommandHelper::new(); 236 | cmd_helper.write_file("a/b/inner.txt", b"").unwrap(); 237 | cmd_helper.jit_cmd(&["init"]).unwrap(); 238 | cmd_helper.jit_cmd(&["add", "."]).unwrap(); 239 | cmd_helper.commit("commit message"); 240 | 241 | cmd_helper.write_file("a/outer.txt", b"").unwrap(); 242 | cmd_helper.write_file("a/b/c/file.txt", b"").unwrap(); 243 | 244 | cmd_helper.clear_stdout(); 245 | cmd_helper.assert_status( 246 | "?? a/b/c/ 247 | ?? a/outer.txt\n", 248 | ); 249 | } 250 | 251 | #[test] 252 | fn does_not_list_empty_untracked_dirs() { 253 | let mut cmd_helper = CommandHelper::new(); 254 | cmd_helper.mkdir("outer").unwrap(); 255 | cmd_helper.jit_cmd(&["init"]).unwrap(); 256 | cmd_helper.clear_stdout(); 257 | cmd_helper.assert_status(""); 258 | } 259 | 260 | #[test] 261 | fn list_untracked_dirs_that_indirectly_contain_files() { 262 | let mut cmd_helper = CommandHelper::new(); 263 | cmd_helper.write_file("outer/inner/file.txt", b"").unwrap(); 264 | cmd_helper.jit_cmd(&["init"]).unwrap(); 265 | cmd_helper.clear_stdout(); 266 | cmd_helper.assert_status("?? outer/\n"); 267 | } 268 | 269 | fn create_and_commit(cmd_helper: &mut CommandHelper) { 270 | cmd_helper.write_file("1.txt", b"one").unwrap(); 271 | cmd_helper.write_file("a/2.txt", b"two").unwrap(); 272 | cmd_helper.write_file("a/b/3.txt", b"three").unwrap(); 273 | cmd_helper.jit_cmd(&["init"]).unwrap(); 274 | cmd_helper.jit_cmd(&["add", "."]).unwrap(); 275 | cmd_helper.commit("commit message"); 276 | } 277 | 278 | #[test] 279 | fn prints_nothing_when_no_files_changed() { 280 | let mut cmd_helper = CommandHelper::new(); 281 | create_and_commit(&mut cmd_helper); 282 | 283 | cmd_helper.clear_stdout(); 284 | cmd_helper.assert_status(""); 285 | } 286 | 287 | #[test] 288 | fn reports_files_with_changed_contents() { 289 | let mut cmd_helper = CommandHelper::new(); 290 | create_and_commit(&mut cmd_helper); 291 | 292 | cmd_helper.clear_stdout(); 293 | cmd_helper.write_file("1.txt", b"changed").unwrap(); 294 | cmd_helper.write_file("a/2.txt", b"modified").unwrap(); 295 | cmd_helper.assert_status( 296 | " M 1.txt 297 | M a/2.txt\n", 298 | ); 299 | } 300 | 301 | #[test] 302 | fn reports_files_with_changed_modes() { 303 | let mut cmd_helper = CommandHelper::new(); 304 | create_and_commit(&mut cmd_helper); 305 | 306 | cmd_helper.make_executable("a/2.txt").unwrap(); 307 | cmd_helper.clear_stdout(); 308 | cmd_helper.assert_status(" M a/2.txt\n"); 309 | } 310 | 311 | #[test] 312 | fn reports_modified_files_with_unchanged_size() { 313 | let mut cmd_helper = CommandHelper::new(); 314 | create_and_commit(&mut cmd_helper); 315 | 316 | // Sleep so that mtime is slightly different from what is in 317 | // index 318 | let ten_millis = time::Duration::from_millis(2); 319 | thread::sleep(ten_millis); 320 | 321 | cmd_helper.write_file("a/b/3.txt", b"hello").unwrap(); 322 | cmd_helper.clear_stdout(); 323 | cmd_helper.assert_status(" M a/b/3.txt\n"); 324 | } 325 | 326 | #[test] 327 | fn prints_nothing_if_file_is_touched() { 328 | let mut cmd_helper = CommandHelper::new(); 329 | create_and_commit(&mut cmd_helper); 330 | cmd_helper.touch("1.txt").unwrap(); 331 | 332 | cmd_helper.clear_stdout(); 333 | cmd_helper.assert_status(""); 334 | } 335 | 336 | #[test] 337 | fn reports_deleted_files() { 338 | let mut cmd_helper = CommandHelper::new(); 339 | create_and_commit(&mut cmd_helper); 340 | cmd_helper.delete("a/2.txt").unwrap(); 341 | 342 | cmd_helper.clear_stdout(); 343 | cmd_helper.assert_status(" D a/2.txt\n"); 344 | } 345 | 346 | #[test] 347 | fn reports_files_in_deleted_dir() { 348 | let mut cmd_helper = CommandHelper::new(); 349 | create_and_commit(&mut cmd_helper); 350 | cmd_helper.delete("a").unwrap(); 351 | 352 | cmd_helper.clear_stdout(); 353 | cmd_helper.assert_status( 354 | " D a/2.txt 355 | D a/b/3.txt\n", 356 | ); 357 | } 358 | 359 | #[test] 360 | fn reports_file_added_to_tracked_dir() { 361 | let mut cmd_helper = CommandHelper::new(); 362 | create_and_commit(&mut cmd_helper); 363 | cmd_helper.write_file("a/4.txt", b"four").unwrap(); 364 | cmd_helper.jit_cmd(&["add", "."]).unwrap(); 365 | cmd_helper.clear_stdout(); 366 | cmd_helper.assert_status("A a/4.txt\n"); 367 | } 368 | 369 | #[test] 370 | fn reports_file_added_to_untracked_dir() { 371 | let mut cmd_helper = CommandHelper::new(); 372 | create_and_commit(&mut cmd_helper); 373 | cmd_helper.write_file("d/e/5.txt", b"five").unwrap(); 374 | cmd_helper.jit_cmd(&["add", "."]).unwrap(); 375 | cmd_helper.clear_stdout(); 376 | cmd_helper.assert_status("A d/e/5.txt\n"); 377 | } 378 | 379 | #[test] 380 | fn reports_files_with_modes_modified_between_head_and_index() { 381 | let mut cmd_helper = CommandHelper::new(); 382 | create_and_commit(&mut cmd_helper); 383 | 384 | cmd_helper.make_executable("1.txt").unwrap(); 385 | cmd_helper.jit_cmd(&["add", "."]).unwrap(); 386 | 387 | cmd_helper.clear_stdout(); 388 | cmd_helper.assert_status("M 1.txt\n"); 389 | } 390 | 391 | #[test] 392 | fn reports_files_with_contents_modified_between_head_and_index() { 393 | let mut cmd_helper = CommandHelper::new(); 394 | create_and_commit(&mut cmd_helper); 395 | 396 | cmd_helper.write_file("a/b/3.txt", b"modified").unwrap(); 397 | cmd_helper.jit_cmd(&["add", "."]).unwrap(); 398 | 399 | cmd_helper.clear_stdout(); 400 | cmd_helper.assert_status("M a/b/3.txt\n"); 401 | } 402 | 403 | #[test] 404 | fn reports_files_deleted_in_index() { 405 | let mut cmd_helper = CommandHelper::new(); 406 | create_and_commit(&mut cmd_helper); 407 | cmd_helper.delete("1.txt").unwrap(); 408 | cmd_helper.delete(".git/index").unwrap(); 409 | cmd_helper.jit_cmd(&["add", "."]).unwrap(); 410 | 411 | cmd_helper.clear_stdout(); 412 | cmd_helper.assert_status("D 1.txt\n"); 413 | } 414 | 415 | #[test] 416 | fn reports_all_deleted_files_in_dir() { 417 | let mut cmd_helper = CommandHelper::new(); 418 | create_and_commit(&mut cmd_helper); 419 | cmd_helper.delete("a").unwrap(); 420 | cmd_helper.delete(".git/index").unwrap(); 421 | cmd_helper.jit_cmd(&["add", "."]).unwrap(); 422 | 423 | cmd_helper.clear_stdout(); 424 | cmd_helper.assert_status( 425 | "D a/2.txt 426 | D a/b/3.txt\n", 427 | ); 428 | } 429 | } 430 | -------------------------------------------------------------------------------- /src/database/blob.rs: -------------------------------------------------------------------------------- 1 | use crate::database::object::Object; 2 | use crate::database::ParsedObject; 3 | 4 | #[derive(Debug)] 5 | pub struct Blob { 6 | pub data: Vec, 7 | } 8 | 9 | impl Blob { 10 | pub fn new(data: &[u8]) -> Blob { 11 | Blob { 12 | data: data.to_vec(), 13 | } 14 | } 15 | } 16 | 17 | impl Object for Blob { 18 | fn r#type(&self) -> String { 19 | "blob".to_string() 20 | } 21 | 22 | fn to_string(&self) -> Vec { 23 | self.data.clone() 24 | } 25 | 26 | fn parse(s: &[u8]) -> ParsedObject { 27 | ParsedObject::Blob(Blob::new(s)) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/database/commit.rs: -------------------------------------------------------------------------------- 1 | use chrono::prelude::*; 2 | use std::collections::HashMap; 3 | use std::str; 4 | 5 | use crate::database::{Object, ParsedObject}; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct Author { 9 | pub name: String, 10 | pub email: String, 11 | pub time: DateTime, 12 | } 13 | 14 | impl Author { 15 | fn to_string(&self) -> String { 16 | format!( 17 | "{} <{}> {}", 18 | self.name, 19 | self.email, 20 | self.time.format("%s %z") 21 | ) 22 | } 23 | 24 | pub fn short_date(&self) -> String { 25 | self.time.format("%Y-%m-%d").to_string() 26 | } 27 | 28 | pub fn readable_time(&self) -> String { 29 | self.time.format("%a %b %-d %H:%M:%S %Y %Z").to_string() 30 | } 31 | 32 | pub fn parse(s: &str) -> Author { 33 | let split_author_str = s 34 | .split(&['<', '>'][..]) 35 | .map(|s| s.trim()) 36 | .collect::>(); 37 | 38 | let name = split_author_str[0].to_string(); 39 | let email = split_author_str[1].to_string(); 40 | let time = DateTime::parse_from_str(split_author_str[2], "%s %z") 41 | .expect("could not parse datetime"); 42 | 43 | Author { name, email, time } 44 | } 45 | } 46 | 47 | #[derive(Debug, Clone)] 48 | pub struct Commit { 49 | pub parent: Option, 50 | pub tree_oid: String, 51 | pub author: Author, 52 | pub message: String, 53 | } 54 | 55 | impl Commit { 56 | pub fn new( 57 | parent: &Option, 58 | tree_oid: String, 59 | author: Author, 60 | message: String, 61 | ) -> Commit { 62 | Commit { 63 | parent: parent.clone(), 64 | tree_oid, 65 | author, 66 | message, 67 | } 68 | } 69 | 70 | pub fn title_line(&self) -> String { 71 | self.message 72 | .lines() 73 | .next() 74 | .expect("could not get first line of commit") 75 | .to_string() 76 | } 77 | } 78 | 79 | impl Object for Commit { 80 | fn r#type(&self) -> String { 81 | "commit".to_string() 82 | } 83 | 84 | fn to_string(&self) -> Vec { 85 | let author_str = self.author.to_string(); 86 | let mut lines = String::new(); 87 | lines.push_str(&format!("tree {}\n", self.tree_oid)); 88 | if let Some(parent_oid) = &self.parent { 89 | lines.push_str(&format!("parent {}\n", parent_oid)); 90 | } 91 | lines.push_str(&format!("author {}\n", author_str)); 92 | lines.push_str(&format!("committer {}\n", author_str)); 93 | lines.push_str("\n"); 94 | lines.push_str(&self.message); 95 | 96 | lines.as_bytes().to_vec() 97 | } 98 | 99 | fn parse(s: &[u8]) -> ParsedObject { 100 | let mut s = str::from_utf8(s).expect("invalid utf-8"); 101 | let mut headers = HashMap::new(); 102 | // Parse headers 103 | loop { 104 | if let Some(newline) = s.find('\n') { 105 | let line = &s[..newline]; 106 | s = &s[newline + 1..]; 107 | 108 | // Headers and commit message is separated by empty 109 | // line 110 | if line == "" { 111 | break; 112 | } 113 | 114 | let v: Vec<&str> = line.splitn(2, ' ').collect(); 115 | headers.insert(v[0], v[1]); 116 | } else { 117 | panic!("no body in commit"); 118 | } 119 | } 120 | 121 | ParsedObject::Commit(Commit::new( 122 | &headers.get("parent").map(|s| s.to_string()), 123 | headers.get("tree").expect("no tree header").to_string(), 124 | Author::parse(headers.get("author").expect("no author found in commit")), 125 | s.to_string(), 126 | )) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/database/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fs::{self, OpenOptions}; 3 | use std::io::prelude::*; 4 | use std::path::{Path, PathBuf}; 5 | use std::str; 6 | 7 | use flate2::read::ZlibDecoder; 8 | use flate2::write::ZlibEncoder; 9 | use flate2::Compression; 10 | 11 | use crate::index; 12 | use crate::util::*; 13 | 14 | pub mod blob; 15 | pub mod commit; 16 | pub mod object; 17 | pub mod tree; 18 | pub mod tree_diff; 19 | 20 | use blob::Blob; 21 | use commit::Commit; 22 | use object::Object; 23 | use tree::{Tree, TREE_MODE}; 24 | 25 | #[derive(Debug)] 26 | pub enum ParsedObject { 27 | Commit(Commit), 28 | Blob(Blob), 29 | Tree(Tree), 30 | } 31 | 32 | impl ParsedObject { 33 | pub fn obj_type(&self) -> &str { 34 | match *self { 35 | ParsedObject::Commit(_) => "commit", 36 | ParsedObject::Blob(_) => "blob", 37 | ParsedObject::Tree(_) => "tree", 38 | } 39 | } 40 | 41 | pub fn get_oid(&self) -> String { 42 | match self { 43 | ParsedObject::Commit(obj) => obj.get_oid(), 44 | ParsedObject::Blob(obj) => obj.get_oid(), 45 | ParsedObject::Tree(obj) => obj.get_oid(), 46 | } 47 | } 48 | } 49 | 50 | #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] 51 | pub struct Entry { 52 | name: String, 53 | oid: String, 54 | mode: u32, 55 | } 56 | 57 | impl From<&index::Entry> for Entry { 58 | fn from(entry: &index::Entry) -> Entry { 59 | Entry { 60 | name: entry.path.clone(), 61 | oid: entry.oid.clone(), 62 | mode: entry.mode, 63 | } 64 | } 65 | } 66 | 67 | impl Entry { 68 | pub fn new(name: &str, oid: &str, mode: u32) -> Entry { 69 | Entry { 70 | name: name.to_string(), 71 | oid: oid.to_string(), 72 | mode, 73 | } 74 | } 75 | 76 | // if user is allowed to executable, set mode to Executable, 77 | // else Regular 78 | fn is_executable(&self) -> bool { 79 | (self.mode >> 6) & 0b1 == 1 80 | } 81 | 82 | fn mode(&self) -> u32 { 83 | if self.mode == TREE_MODE { 84 | return TREE_MODE; 85 | } 86 | if self.is_executable() { 87 | return 0o100755; 88 | } else { 89 | return 0o100644; 90 | } 91 | } 92 | } 93 | 94 | pub struct Database { 95 | path: PathBuf, 96 | objects: HashMap, 97 | } 98 | 99 | impl Database { 100 | pub fn new(path: &Path) -> Database { 101 | Database { 102 | path: path.to_path_buf(), 103 | objects: HashMap::new(), 104 | } 105 | } 106 | 107 | pub fn read_object(&self, oid: &str) -> Option { 108 | let mut contents = vec![]; 109 | let mut file = OpenOptions::new() 110 | .read(true) 111 | .create(false) 112 | .open(self.object_path(oid)) 113 | .unwrap_or_else(|_| panic!("failed to open file: {:?}", self.object_path(oid))); 114 | file.read_to_end(&mut contents) 115 | .expect("reading file failed"); 116 | 117 | let mut z = ZlibDecoder::new(&contents[..]); 118 | let mut v = vec![]; 119 | z.read_to_end(&mut v).unwrap(); 120 | let mut vs = &v[..]; 121 | 122 | let (obj_type, rest) = match vs 123 | .splitn(2, |c| *c as char == ' ') 124 | .collect::>() 125 | .as_slice() 126 | { 127 | &[type_bytes, rest] => ( 128 | str::from_utf8(type_bytes).expect("failed to parse type"), 129 | rest, 130 | ), 131 | _ => panic!("EOF while parsing type"), 132 | }; 133 | vs = rest; 134 | 135 | let (_size, rest) = match *vs 136 | .splitn(2, |c| *c as char == '\u{0}') 137 | .collect::>() 138 | .as_slice() 139 | { 140 | [size_bytes, rest] => ( 141 | str::from_utf8(size_bytes).expect("failed to parse size"), 142 | rest, 143 | ), 144 | _ => panic!("EOF while parsing size"), 145 | }; 146 | 147 | match obj_type { 148 | "commit" => Some(Commit::parse(&rest)), 149 | "blob" => Some(Blob::parse(&rest)), 150 | "tree" => Some(Tree::parse(&rest)), 151 | _ => unimplemented!(), 152 | } 153 | } 154 | 155 | pub fn load(&mut self, oid: &str) -> &ParsedObject { 156 | let o = self.read_object(oid); 157 | self.objects.insert(oid.to_string(), o.unwrap()); 158 | 159 | self.objects.get(oid).unwrap() 160 | } 161 | 162 | pub fn store(&self, obj: &T) -> Result<(), std::io::Error> 163 | where 164 | T: Object, 165 | { 166 | let oid = obj.get_oid(); 167 | let content = obj.get_content(); 168 | 169 | self.write_object(oid, content) 170 | } 171 | 172 | fn object_path(&self, oid: &str) -> PathBuf { 173 | let dir: &str = &oid[0..2]; 174 | let filename: &str = &oid[2..]; 175 | 176 | self.path.as_path().join(dir).join(filename) 177 | } 178 | 179 | fn write_object(&self, oid: String, content: Vec) -> Result<(), std::io::Error> { 180 | let object_path = self.object_path(&oid); 181 | 182 | // If object already exists, we are certain that the contents 183 | // have not changed. So there is no need to write it again. 184 | if object_path.exists() { 185 | return Ok(()); 186 | } 187 | 188 | let dir_path = object_path.parent().expect("invalid parent path"); 189 | fs::create_dir_all(dir_path)?; 190 | let mut temp_file_name = String::from("tmp_obj_"); 191 | temp_file_name.push_str(&generate_temp_name()); 192 | let temp_path = dir_path.join(temp_file_name); 193 | 194 | let mut file = OpenOptions::new() 195 | .read(true) 196 | .write(true) 197 | .create_new(true) 198 | .open(&temp_path)?; 199 | 200 | let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); 201 | e.write_all(&content)?; 202 | let compressed_bytes = e.finish()?; 203 | 204 | file.write_all(&compressed_bytes)?; 205 | fs::rename(temp_path, object_path)?; 206 | Ok(()) 207 | } 208 | 209 | pub fn short_oid(oid: &str) -> &str { 210 | &oid[0..6] 211 | } 212 | 213 | pub fn prefix_match(&self, name: &str) -> Vec { 214 | let object_path = self.object_path(name); 215 | let dirname = object_path 216 | .parent() 217 | .expect("Could not get parent from object_path"); 218 | 219 | let oids: Vec<_> = fs::read_dir(&dirname) 220 | .expect("read_dir call failed") 221 | .map(|f| { 222 | format!( 223 | "{}{}", 224 | dirname 225 | .file_name() 226 | .expect("could not get filename") 227 | .to_str() 228 | .expect("conversion from OsStr to str failed"), 229 | f.unwrap() 230 | .file_name() 231 | .to_str() 232 | .expect("conversion from OsStr to str failed") 233 | ) 234 | }) 235 | .filter(|o| o.starts_with(name)) 236 | .collect(); 237 | 238 | oids 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/database/object.rs: -------------------------------------------------------------------------------- 1 | use crate::database::ParsedObject; 2 | use crypto::digest::Digest; 3 | use crypto::sha1::Sha1; 4 | 5 | pub trait Object { 6 | fn r#type(&self) -> String; 7 | fn to_string(&self) -> Vec; 8 | 9 | fn parse(s: &[u8]) -> ParsedObject; 10 | 11 | fn get_oid(&self) -> String { 12 | let mut hasher = Sha1::new(); 13 | hasher.input(&self.get_content()); 14 | hasher.result_str() 15 | } 16 | 17 | fn get_content(&self) -> Vec { 18 | // TODO: need to do something to force ASCII encoding? 19 | let string = self.to_string(); 20 | let mut content: Vec = self.r#type().as_bytes().to_vec(); 21 | 22 | content.push(0x20); 23 | content.extend_from_slice(format!("{}", string.len()).as_bytes()); 24 | content.push(0x0); 25 | content.extend_from_slice(&string); 26 | 27 | content 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/database/tree.rs: -------------------------------------------------------------------------------- 1 | use crate::database::object::Object; 2 | use crate::database::{Entry, ParsedObject}; 3 | use crate::util::*; 4 | 5 | use std::collections::{BTreeMap}; 6 | use std::path::{Path}; 7 | use std::str; 8 | 9 | pub const TREE_MODE: u32 = 0o40000; 10 | 11 | #[derive(Clone, Debug, PartialEq, Eq)] 12 | pub enum TreeEntry { 13 | Entry(Entry), 14 | Tree(Tree), 15 | } 16 | 17 | impl TreeEntry { 18 | pub fn mode(&self) -> u32 { 19 | match self { 20 | TreeEntry::Entry(e) => e.mode(), 21 | _ => TREE_MODE, 22 | } 23 | } 24 | 25 | pub fn get_oid(&self) -> String { 26 | match self { 27 | TreeEntry::Entry(e) => e.oid.clone(), 28 | TreeEntry::Tree(t) => t.get_oid(), 29 | } 30 | } 31 | 32 | pub fn is_tree(&self) -> bool { 33 | match self { 34 | TreeEntry::Entry(e) => e.mode() == TREE_MODE, 35 | _ => false, 36 | } 37 | } 38 | } 39 | 40 | #[derive(Clone, Debug, PartialEq, Eq)] 41 | pub struct Tree { 42 | pub entries: BTreeMap, 43 | } 44 | 45 | impl Tree { 46 | pub fn new() -> Tree { 47 | Tree { 48 | entries: BTreeMap::new(), 49 | } 50 | } 51 | 52 | pub fn build(entries: &[Entry]) -> Tree { 53 | let mut sorted_entries = entries.to_vec(); 54 | sorted_entries.sort(); 55 | 56 | let mut root = Tree::new(); 57 | for entry in sorted_entries.iter() { 58 | let mut path: Vec = Path::new(&entry.name) 59 | .iter() 60 | .map(|c| c.to_str().unwrap().to_string()) 61 | .collect(); 62 | let name = path.pop().expect("file path has zero components"); 63 | root.add_entry(&path, name, entry.clone()); 64 | } 65 | 66 | root 67 | } 68 | 69 | pub fn add_entry(&mut self, path: &[String], name: String, entry: Entry) { 70 | if path.is_empty() { 71 | self.entries.insert(name, TreeEntry::Entry(entry)); 72 | } else if let Some(TreeEntry::Tree(tree)) = self.entries.get_mut(&path[0]) { 73 | tree.add_entry(&path[1..], name, entry); 74 | } else { 75 | let mut tree = Tree::new(); 76 | tree.add_entry(&path[1..], name, entry); 77 | self.entries.insert(path[0].clone(), TreeEntry::Tree(tree)); 78 | }; 79 | } 80 | 81 | pub fn traverse(&self, f: &F) 82 | where 83 | F: Fn(&Tree) -> (), 84 | { 85 | // Do a postorder traversal(visit all children first, then 86 | // process `self` 87 | for (_name, entry) in self.entries.clone() { 88 | if let TreeEntry::Tree(tree) = entry { 89 | tree.traverse(f); 90 | } 91 | } 92 | 93 | f(self); 94 | } 95 | } 96 | 97 | impl Object for Tree { 98 | fn r#type(&self) -> String { 99 | "tree".to_string() 100 | } 101 | 102 | fn to_string(&self) -> Vec { 103 | let mut tree_vec = Vec::new(); 104 | for (name, entry) in self.entries.iter() { 105 | let mut entry_vec: Vec = 106 | format!("{:o} {}\0", entry.mode(), name).as_bytes().to_vec(); 107 | entry_vec.extend_from_slice(&decode_hex(&entry.get_oid()).expect("invalid oid")); 108 | tree_vec.extend_from_slice(&entry_vec); 109 | } 110 | tree_vec 111 | } 112 | 113 | fn parse(v: &[u8]) -> ParsedObject { 114 | let mut entries: Vec = vec![]; 115 | 116 | let mut vs = v; 117 | 118 | while !vs.is_empty() { 119 | let (mode, rest): (u32, &[u8]) = match vs 120 | .splitn(2, |c| *c as char == ' ') 121 | .collect::>() 122 | .as_slice() 123 | { 124 | &[mode, rest] => ( 125 | u32::from_str_radix(str::from_utf8(mode).expect("invalid utf8"), 8) 126 | .expect("parsing mode failed"), 127 | rest, 128 | ), 129 | _ => panic!("EOF while parsing mode"), 130 | }; 131 | vs = rest; 132 | 133 | let (name, rest) = match *vs 134 | .splitn(2, |c| *c as char == '\u{0}') 135 | .collect::>() 136 | .as_slice() 137 | { 138 | [name_bytes, rest] => (str::from_utf8(name_bytes).expect("invalid utf8"), rest), 139 | _ => panic!("EOF while parsing name"), 140 | }; 141 | vs = rest; 142 | 143 | let (oid_bytes, rest) = vs.split_at(20); 144 | vs = rest; 145 | 146 | let oid = encode_hex(&oid_bytes); 147 | 148 | entries.push(Entry::new(name, &oid, mode)); 149 | } 150 | ParsedObject::Tree(Tree::build(&entries)) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/database/tree_diff.rs: -------------------------------------------------------------------------------- 1 | use crate::database::tree::TreeEntry; 2 | use crate::database::{Database, ParsedObject, Tree}; 3 | use std::collections::{BTreeMap, HashMap}; 4 | use std::path::{Path, PathBuf}; 5 | 6 | pub struct TreeDiff<'a> { 7 | database: &'a mut Database, 8 | pub changes: HashMap, Option)>, 9 | } 10 | 11 | impl<'a> TreeDiff<'a> { 12 | pub fn new(database: &mut Database) -> TreeDiff { 13 | TreeDiff { 14 | database, 15 | changes: HashMap::new(), 16 | } 17 | } 18 | 19 | pub fn compare_oids(&mut self, a: Option, b: Option, prefix: &Path) { 20 | if a == b { 21 | return; 22 | } 23 | 24 | let a_entries = if let Some(a_oid) = a { 25 | self.oid_to_tree(&a_oid).entries 26 | } else { 27 | BTreeMap::new() 28 | }; 29 | 30 | let b_entries = if let Some(b_oid) = b { 31 | self.oid_to_tree(&b_oid).entries 32 | } else { 33 | BTreeMap::new() 34 | }; 35 | 36 | self.detect_deletions(&a_entries, &b_entries, prefix); 37 | self.detect_additions(&a_entries, &b_entries, prefix); 38 | } 39 | 40 | fn detect_deletions( 41 | &mut self, 42 | a_entries: &BTreeMap, 43 | b_entries: &BTreeMap, 44 | prefix: &Path, 45 | ) { 46 | for (name, entry) in a_entries { 47 | let path = prefix.join(name); 48 | let other = b_entries.get(name); 49 | 50 | let tree_b = if let Some(b_entry) = other { 51 | if b_entry == entry { 52 | continue; 53 | } 54 | 55 | if b_entry.is_tree() { 56 | Some(b_entry.get_oid()) 57 | } else { 58 | None 59 | } 60 | } else { 61 | None 62 | }; 63 | 64 | let tree_a = if entry.is_tree() { 65 | Some(entry.get_oid()) 66 | } else { 67 | None 68 | }; 69 | 70 | self.compare_oids(tree_a, tree_b, &path); 71 | 72 | let blobs = match (!entry.is_tree(), other.map(|e| !e.is_tree()).unwrap_or(false)) { 73 | (true, true) => (Some(entry.clone()), other.cloned()), 74 | (true, false) => (Some(entry.clone()), None), 75 | (false, true) => (None, other.cloned()), 76 | (false, false) => continue, 77 | }; 78 | self.changes.insert(path, blobs); 79 | } 80 | } 81 | 82 | fn detect_additions( 83 | &mut self, 84 | a_entries: &BTreeMap, 85 | b_entries: &BTreeMap, 86 | prefix: &Path, 87 | ) { 88 | for (name, entry) in b_entries { 89 | let path = prefix.join(name); 90 | let other = a_entries.get(name); 91 | 92 | if other.is_some() { 93 | continue; 94 | } 95 | 96 | if entry.is_tree() { 97 | self.compare_oids(None, Some(entry.get_oid()), &path); 98 | } else { 99 | self.changes.insert(path, (None, Some(entry.clone()))); 100 | } 101 | } 102 | } 103 | 104 | fn oid_to_tree(&mut self, oid: &str) -> Tree { 105 | let tree_oid = match self.database.load(oid) { 106 | ParsedObject::Tree(tree) => return tree.clone(), 107 | ParsedObject::Commit(commit) => commit.tree_oid.clone(), 108 | _ => panic!("oid not a commit or tree"), 109 | }; 110 | 111 | match self.database.load(&tree_oid) { 112 | ParsedObject::Tree(tree) => tree.clone(), 113 | _ => panic!("oid not a tree"), 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/diff/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod myers; 2 | use myers::{Edit, EditType, Myers}; 3 | use std::fmt; 4 | 5 | pub struct Diff {} 6 | 7 | #[derive(Clone, Debug)] 8 | pub struct Line { 9 | number: usize, 10 | text: String, 11 | } 12 | 13 | impl Line { 14 | fn new(number: usize, text: &str) -> Line { 15 | Line { 16 | number, 17 | text: text.to_string(), 18 | } 19 | } 20 | } 21 | 22 | impl fmt::Display for Line { 23 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 24 | write!(f, "{}", self.text) 25 | } 26 | } 27 | 28 | fn lines(a: &str) -> Vec { 29 | let mut a_lines = vec![]; 30 | for (i, text) in a.split('\n').enumerate() { 31 | a_lines.push(Line::new(i + 1, text)); 32 | } 33 | 34 | a_lines 35 | } 36 | 37 | impl Diff { 38 | pub fn diff(a: &str, b: &str) -> Vec { 39 | let a_lines = lines(a); 40 | let b_lines = lines(b); 41 | 42 | Myers::new(a_lines, b_lines).diff() 43 | } 44 | 45 | pub fn diff_hunks(a: &str, b: &str) -> Vec { 46 | Hunk::filter(Self::diff(a, b)) 47 | } 48 | } 49 | 50 | fn get_edit(edits: &[Edit], offset: isize) -> Option<&Edit> { 51 | if offset < 0 || offset >= edits.len() as isize { 52 | None 53 | } else { 54 | Some(&edits[offset as usize]) 55 | } 56 | } 57 | 58 | const HUNK_CONTEXT: usize = 3; 59 | 60 | const EMPTY_EDIT: Edit = Edit { 61 | edit_type: EditType::Eql, 62 | a_line: None, 63 | b_line: None, 64 | }; 65 | 66 | pub struct Hunk { 67 | pub a_start: usize, 68 | pub b_start: usize, 69 | pub edits: Vec, 70 | } 71 | 72 | enum LineType { 73 | A, 74 | B, 75 | } 76 | 77 | impl Hunk { 78 | fn new(a_start: usize, b_start: usize, edits: Vec) -> Hunk { 79 | Hunk { 80 | a_start, 81 | b_start, 82 | edits, 83 | } 84 | } 85 | 86 | pub fn header(&self) -> String { 87 | let (a_start, a_lines) = self.offsets_for(LineType::A, self.a_start); 88 | let (b_start, b_lines) = self.offsets_for(LineType::B, self.b_start); 89 | 90 | format!("@@ -{},{} +{},{} @@", a_start, a_lines, b_start, b_lines) 91 | } 92 | 93 | fn offsets_for(&self, line_type: LineType, default: usize) -> (usize, usize) { 94 | let lines: Vec<_> = self 95 | .edits 96 | .iter() 97 | .map(|e| match line_type { 98 | LineType::A => &e.a_line, 99 | LineType::B => &e.b_line, 100 | }) 101 | .filter_map(|l| l.as_ref()) 102 | .collect(); 103 | let start = if lines.len() > 0 { 104 | lines[0].number 105 | } else { 106 | default 107 | }; 108 | 109 | (start, lines.len()) 110 | } 111 | 112 | pub fn filter(edits: Vec) -> Vec { 113 | let mut hunks = vec![]; 114 | let mut offset: isize = 0; 115 | 116 | let empty_line = Line::new(0, ""); 117 | 118 | loop { 119 | // Skip over Eql edits 120 | while let Some(edit) = get_edit(&edits, offset) { 121 | if edit.edit_type == EditType::Eql { 122 | offset += 1; 123 | } else { 124 | break; 125 | } 126 | } 127 | 128 | if offset >= (edits.len() as isize) { 129 | return hunks; 130 | } 131 | 132 | offset -= (HUNK_CONTEXT + 1) as isize; 133 | 134 | let a_start = if offset < 0 { 135 | 0 136 | } else { 137 | get_edit(&edits, offset) 138 | .unwrap_or(&EMPTY_EDIT) 139 | .a_line 140 | .clone() 141 | .unwrap_or(empty_line.clone()) 142 | .number 143 | }; 144 | 145 | let b_start = if offset < 0 { 146 | 0 147 | } else { 148 | get_edit(&edits, offset) 149 | .unwrap_or(&EMPTY_EDIT) 150 | .b_line 151 | .clone() 152 | .unwrap_or(empty_line.clone()) 153 | .number 154 | }; 155 | 156 | let (hunk, new_offset) = Self::build_hunk(a_start, b_start, &edits, offset); 157 | hunks.push(hunk); 158 | offset = new_offset; 159 | } 160 | } 161 | 162 | fn build_hunk( 163 | a_start: usize, 164 | b_start: usize, 165 | edits: &[Edit], 166 | mut offset: isize, 167 | ) -> (Hunk, isize) { 168 | let mut counter: isize = -1; 169 | 170 | let mut hunk = Hunk::new(a_start, b_start, vec![]); 171 | 172 | while counter != 0 { 173 | if offset >= 0 && counter > 0 { 174 | hunk.edits.push( 175 | get_edit(edits, offset) 176 | .expect("offset out of bounds") 177 | .clone(), 178 | ) 179 | } 180 | 181 | offset += 1; 182 | if offset >= edits.len() as isize { 183 | break; 184 | } 185 | 186 | if let Some(edit) = get_edit(edits, offset + HUNK_CONTEXT as isize) { 187 | match edit.edit_type { 188 | EditType::Ins | EditType::Del => { 189 | counter = (2 * HUNK_CONTEXT + 1) as isize; 190 | } 191 | _ => { 192 | counter -= 1; 193 | } 194 | } 195 | } else { 196 | counter -= 1; 197 | } 198 | } 199 | 200 | (hunk, offset) 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/diff/myers.rs: -------------------------------------------------------------------------------- 1 | use crate::diff::Line; 2 | use std::collections::BTreeMap; 3 | use std::convert::TryFrom; 4 | use std::fmt; 5 | 6 | #[derive(Clone, Debug, PartialEq, Eq)] 7 | pub enum EditType { 8 | Eql, 9 | Ins, 10 | Del, 11 | } 12 | 13 | impl EditType { 14 | fn to_string(&self) -> &str { 15 | match self { 16 | EditType::Eql => " ", 17 | EditType::Ins => "+", 18 | EditType::Del => "-", 19 | } 20 | } 21 | } 22 | 23 | #[derive(Debug, Clone)] 24 | pub struct Edit { 25 | pub edit_type: EditType, 26 | pub a_line: Option, 27 | pub b_line: Option, 28 | } 29 | 30 | impl Edit { 31 | fn new(edit_type: EditType, a_line: Option, b_line: Option) -> Edit { 32 | Edit { 33 | edit_type, 34 | a_line, 35 | b_line, 36 | } 37 | } 38 | 39 | } 40 | 41 | 42 | impl fmt::Display for Edit { 43 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 44 | let line = if let Some(a) = &self.a_line { 45 | a 46 | } else if let Some(b) = &self.b_line { 47 | b 48 | } else { 49 | panic!("both lines None") 50 | }; 51 | write!(f, "{}{}", self.edit_type.to_string(), line) 52 | } 53 | } 54 | 55 | pub struct Myers { 56 | a: Vec, 57 | b: Vec, 58 | } 59 | 60 | fn to_usize(i: isize) -> usize { 61 | usize::try_from(i).unwrap() 62 | } 63 | 64 | impl Myers { 65 | pub fn new(a: Vec, b: Vec) -> Myers { 66 | Myers { a, b } 67 | } 68 | 69 | pub fn diff(&self) -> Vec { 70 | let mut diff = vec![]; 71 | for (prev_x, prev_y, x, y) in self.backtrack().iter() { 72 | let a_line = if to_usize(*prev_x) >= self.a.len() { 73 | None 74 | } else { 75 | Some(self.a[to_usize(*prev_x)].clone()) 76 | }; 77 | 78 | let b_line = if to_usize(*prev_y) >= self.b.len() { 79 | None 80 | } else { 81 | Some(self.b[to_usize(*prev_y)].clone()) 82 | }; 83 | 84 | if x == prev_x { 85 | diff.push(Edit::new(EditType::Ins, None, b_line)); 86 | } else if y == prev_y { 87 | diff.push(Edit::new(EditType::Del, a_line, None)); 88 | } else { 89 | diff.push(Edit::new(EditType::Eql, a_line, b_line)); 90 | } 91 | } 92 | 93 | diff.reverse(); 94 | diff 95 | } 96 | 97 | fn shortest_edit(&self) -> Vec> { 98 | let n = self.a.len() as isize; 99 | let m = self.b.len() as isize; 100 | 101 | let max: isize = n + m; 102 | 103 | let mut v = BTreeMap::new(); 104 | v.insert(1, 0); 105 | let mut trace = vec![]; 106 | 107 | for d in 0..=max { 108 | trace.push(v.clone()); 109 | for k in (-d..=d).step_by(2) { 110 | let mut x: isize = 111 | if k == -d || (k != d && v.get(&(k - 1)).unwrap() < v.get(&(k + 1)).unwrap()) { 112 | // v[k+1] has the farthest x- position along line 113 | // k+1 114 | // move downward 115 | *v.get(&(k + 1)).unwrap() 116 | } else { 117 | // move rightward 118 | v.get(&(k - 1)).unwrap() + 1 119 | }; 120 | 121 | let mut y: isize = x - k; 122 | while x < n && y < m && self.a[to_usize(x)].text == self.b[to_usize(y)].text { 123 | x = x + 1; 124 | y = y + 1; 125 | } 126 | 127 | v.insert(k, x); 128 | if x >= n && y >= m { 129 | return trace; 130 | } 131 | } 132 | } 133 | vec![] 134 | } 135 | 136 | fn backtrack(&self) -> Vec<(isize, isize, isize, isize)> { 137 | let mut x = self.a.len() as isize; 138 | let mut y = self.b.len() as isize; 139 | let mut seq = vec![]; 140 | 141 | for (d, v) in self.shortest_edit().iter().enumerate().rev() { 142 | let d = d as isize; 143 | let k = x - y; 144 | 145 | let prev_k = 146 | if k == -d || (k != d && v.get(&(k - 1)).unwrap() < v.get(&(k + 1)).unwrap()) { 147 | k + 1 148 | } else { 149 | k - 1 150 | }; 151 | 152 | let prev_x = *v.get(&prev_k).unwrap(); 153 | let prev_y = prev_x - prev_k; 154 | 155 | while x > prev_x && y > prev_y { 156 | seq.push((x - 1, y - 1, x, y)); 157 | x = x - 1; 158 | y = y - 1; 159 | } 160 | 161 | if d > 0 { 162 | seq.push((prev_x, prev_y, x, y)); 163 | } 164 | 165 | x = prev_x; 166 | y = prev_y; 167 | } 168 | 169 | seq 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/index.rs: -------------------------------------------------------------------------------- 1 | use crypto::digest::Digest; 2 | use crypto::sha1::Sha1; 3 | use std::cmp; 4 | use std::collections::{BTreeMap, HashMap, HashSet}; 5 | use std::convert::TryInto; 6 | use std::fs::{self, File, OpenOptions}; 7 | use std::io::{self, ErrorKind, Read, Write}; 8 | use std::os::unix::fs::MetadataExt; 9 | use std::path::{Path, PathBuf}; 10 | use std::str; 11 | 12 | use crate::lockfile::Lockfile; 13 | use crate::util::*; 14 | 15 | const MAX_PATH_SIZE: u16 = 0xfff; 16 | const CHECKSUM_SIZE: u64 = 20; 17 | 18 | const HEADER_SIZE: usize = 12; // bytes 19 | const MIN_ENTRY_SIZE: usize = 64; 20 | 21 | #[derive(Debug, Clone)] 22 | pub struct Entry { 23 | ctime: i64, 24 | ctime_nsec: i64, 25 | mtime: i64, 26 | mtime_nsec: i64, 27 | dev: u64, 28 | ino: u64, 29 | uid: u32, 30 | gid: u32, 31 | size: u64, 32 | flags: u16, 33 | pub mode: u32, 34 | pub oid: String, 35 | pub path: String, 36 | } 37 | 38 | impl Entry { 39 | fn is_executable(mode: u32) -> bool { 40 | (mode >> 6) & 0b1 == 1 41 | } 42 | 43 | fn mode(mode: u32) -> u32 { 44 | if Entry::is_executable(mode) { 45 | 0o100755u32 46 | } else { 47 | 0o100644u32 48 | } 49 | } 50 | 51 | fn new(pathname: &str, oid: &str, metadata: &fs::Metadata) -> Entry { 52 | let path = pathname.to_string(); 53 | Entry { 54 | ctime: metadata.ctime(), 55 | ctime_nsec: metadata.ctime_nsec(), 56 | mtime: metadata.mtime(), 57 | mtime_nsec: metadata.mtime_nsec(), 58 | dev: metadata.dev(), 59 | ino: metadata.ino(), 60 | mode: Entry::mode(metadata.mode()), 61 | uid: metadata.uid(), 62 | gid: metadata.gid(), 63 | size: metadata.size(), 64 | oid: oid.to_string(), 65 | flags: cmp::min(path.len() as u16, MAX_PATH_SIZE), 66 | path, 67 | } 68 | } 69 | 70 | fn parse(bytes: &[u8]) -> Result { 71 | let mut metadata_ints: Vec = vec![]; 72 | for i in 0..10 { 73 | metadata_ints.push(u32::from_be_bytes( 74 | bytes[i * 4..i * 4 + 4].try_into().unwrap(), 75 | )); 76 | } 77 | 78 | let oid = encode_hex(&bytes[40..60]); 79 | let flags = u16::from_be_bytes(bytes[60..62].try_into().unwrap()); 80 | let path_bytes = bytes[62..].split(|b| b == &0u8).next().unwrap(); 81 | let path = str::from_utf8(path_bytes).unwrap().to_string(); 82 | 83 | Ok(Entry { 84 | ctime: i64::from(metadata_ints[0]), 85 | ctime_nsec: i64::from(metadata_ints[1]), 86 | mtime: i64::from(metadata_ints[2]), 87 | mtime_nsec: i64::from(metadata_ints[3]), 88 | dev: u64::from(metadata_ints[4]), 89 | ino: u64::from(metadata_ints[5]), 90 | mode: metadata_ints[6], 91 | uid: metadata_ints[7], 92 | gid: metadata_ints[8], 93 | size: u64::from(metadata_ints[9]), 94 | 95 | oid, 96 | flags, 97 | path, 98 | }) 99 | } 100 | 101 | fn to_bytes(&self) -> Vec { 102 | let mut bytes = Vec::new(); 103 | // 10 32-bit integers 104 | bytes.extend_from_slice(&(self.ctime as u32).to_be_bytes()); 105 | bytes.extend_from_slice(&(self.ctime_nsec as u32).to_be_bytes()); 106 | bytes.extend_from_slice(&(self.mtime as u32).to_be_bytes()); 107 | bytes.extend_from_slice(&(self.mtime_nsec as u32).to_be_bytes()); 108 | bytes.extend_from_slice(&(self.dev as u32).to_be_bytes()); 109 | bytes.extend_from_slice(&(self.ino as u32).to_be_bytes()); 110 | bytes.extend_from_slice(&(self.mode as u32).to_be_bytes()); 111 | bytes.extend_from_slice(&(self.uid as u32).to_be_bytes()); 112 | bytes.extend_from_slice(&(self.gid as u32).to_be_bytes()); 113 | bytes.extend_from_slice(&(self.size as u32).to_be_bytes()); 114 | 115 | // 20 bytes (40-char hex-string) 116 | bytes.extend_from_slice(&decode_hex(&self.oid).expect("invalid oid")); 117 | 118 | // 16-bit 119 | bytes.extend_from_slice(&self.flags.to_be_bytes()); 120 | 121 | bytes.extend_from_slice(self.path.as_bytes()); 122 | bytes.push(0x0); 123 | 124 | // add padding 125 | while bytes.len() % 8 != 0 { 126 | bytes.push(0x0) 127 | } 128 | 129 | bytes 130 | } 131 | 132 | fn parent_dirs(&self) -> Vec<&str> { 133 | let path = Path::new(&self.path); 134 | let mut parent_dirs: Vec<_> = path 135 | .ancestors() 136 | .map(|d| d.to_str().expect("invalid filename")) 137 | .collect(); 138 | parent_dirs.pop(); // drop root dir(always "") 139 | parent_dirs.reverse(); 140 | parent_dirs.pop(); // drop filename 141 | 142 | parent_dirs 143 | } 144 | 145 | pub fn stat_match(&self, stat: &fs::Metadata) -> bool { 146 | (self.mode == Entry::mode(stat.mode())) && (self.size == 0 || self.size == stat.size()) 147 | } 148 | 149 | pub fn times_match(&self, stat: &fs::Metadata) -> bool { 150 | self.ctime == stat.ctime() 151 | && self.ctime_nsec == stat.ctime_nsec() 152 | && self.mtime == stat.mtime() 153 | && self.mtime_nsec == stat.mtime_nsec() 154 | } 155 | 156 | pub fn update_stat(&mut self, stat: &fs::Metadata) { 157 | self.ctime = stat.ctime(); 158 | self.ctime_nsec = stat.ctime_nsec(); 159 | self.mtime = stat.mtime(); 160 | self.mtime_nsec = stat.mtime_nsec(); 161 | self.dev = stat.dev(); 162 | self.ino = stat.ino(); 163 | self.mode = Entry::mode(stat.mode()); 164 | self.uid = stat.uid(); 165 | self.gid = stat.gid(); 166 | self.size = stat.size(); 167 | } 168 | } 169 | 170 | pub struct Checksum 171 | where 172 | T: Read + Write, 173 | { 174 | file: T, 175 | digest: Sha1, 176 | } 177 | 178 | impl Checksum 179 | where 180 | T: Read + Write, 181 | { 182 | fn new(file: T) -> Checksum { 183 | Checksum { 184 | file, 185 | digest: Sha1::new(), 186 | } 187 | } 188 | 189 | fn read(&mut self, size: usize) -> Result, std::io::Error> { 190 | let mut buf = vec![0; size]; 191 | self.file.read_exact(&mut buf)?; 192 | self.digest.input(&buf); 193 | 194 | Ok(buf) 195 | } 196 | 197 | fn write(&mut self, data: &[u8]) -> Result<(), std::io::Error> { 198 | self.file.write_all(data)?; 199 | self.digest.input(data); 200 | 201 | Ok(()) 202 | } 203 | 204 | fn write_checksum(&mut self) -> Result<(), std::io::Error> { 205 | self.file 206 | .write_all(&decode_hex(&self.digest.result_str()).unwrap())?; 207 | 208 | Ok(()) 209 | } 210 | 211 | fn verify_checksum(&mut self) -> Result<(), std::io::Error> { 212 | let hash = self.digest.result_str(); 213 | 214 | let mut buf = vec![0; CHECKSUM_SIZE as usize]; 215 | self.file.read_exact(&mut buf)?; 216 | 217 | let sum = encode_hex(&buf); 218 | 219 | if sum != hash { 220 | return Err(io::Error::new( 221 | ErrorKind::Other, 222 | "Checksum does not match value stored on disk", 223 | )); 224 | } 225 | 226 | Ok(()) 227 | } 228 | } 229 | 230 | pub struct Index { 231 | pathname: PathBuf, 232 | pub entries: BTreeMap, 233 | parents: HashMap>, 234 | lockfile: Lockfile, 235 | hasher: Option, 236 | changed: bool, 237 | } 238 | 239 | impl Index { 240 | pub fn new(path: &Path) -> Index { 241 | Index { 242 | pathname: path.to_path_buf(), 243 | entries: BTreeMap::new(), 244 | parents: HashMap::new(), 245 | lockfile: Lockfile::new(path), 246 | hasher: None, 247 | changed: false, 248 | } 249 | } 250 | 251 | pub fn write_updates(&mut self) -> Result<(), std::io::Error> { 252 | if !self.changed { 253 | return self.lockfile.rollback(); 254 | } 255 | 256 | let lock = &mut self.lockfile; 257 | let mut writer: Checksum<&Lockfile> = Checksum::new(lock); 258 | 259 | let mut header_bytes: Vec = vec![]; 260 | header_bytes.extend_from_slice(b"DIRC"); 261 | header_bytes.extend_from_slice(&2u32.to_be_bytes()); // version no. 262 | header_bytes.extend_from_slice(&(self.entries.len() as u32).to_be_bytes()); 263 | writer.write(&header_bytes)?; 264 | for (_key, entry) in self.entries.iter() { 265 | writer.write(&entry.to_bytes())?; 266 | } 267 | writer.write_checksum()?; 268 | lock.commit()?; 269 | 270 | Ok(()) 271 | } 272 | 273 | /// Remove any entries whose name matches the name of one of the 274 | /// new entry's parent directories 275 | pub fn discard_conflicts(&mut self, entry: &Entry) { 276 | for parent in entry.parent_dirs() { 277 | self.remove_entry(parent); 278 | } 279 | 280 | let to_remove = { 281 | let mut children = vec![]; 282 | if let Some(children_set) = self.parents.get(&entry.path) { 283 | for child in children_set { 284 | children.push(child.clone()) 285 | } 286 | } 287 | children 288 | }; 289 | 290 | for child in to_remove { 291 | self.remove_entry(&child); 292 | } 293 | } 294 | 295 | pub fn remove(&mut self, pathname: &str) { 296 | if let Some(children) = self.parents.get(pathname).cloned() { 297 | for child in children { 298 | self.remove_entry(&child); 299 | } 300 | } 301 | self.remove_entry(pathname); 302 | self.changed = true; 303 | } 304 | 305 | fn remove_entry(&mut self, pathname: &str) { 306 | if let Some(entry) = self.entries.remove(pathname) { 307 | for dirname in entry.parent_dirs() { 308 | if let Some(ref mut children_set) = self.parents.get_mut(dirname) { 309 | children_set.remove(pathname); 310 | if children_set.is_empty() { 311 | self.parents.remove(dirname); 312 | } 313 | } 314 | } 315 | } 316 | } 317 | 318 | pub fn add(&mut self, pathname: &str, oid: &str, metadata: &fs::Metadata) { 319 | let entry = Entry::new(pathname, oid, metadata); 320 | self.discard_conflicts(&entry); 321 | self.store_entry(entry); 322 | self.changed = true; 323 | } 324 | 325 | pub fn store_entry(&mut self, entry: Entry) { 326 | self.entries.insert(entry.path.clone(), entry.clone()); 327 | 328 | for dirname in entry.parent_dirs() { 329 | if let Some(ref mut children_set) = self.parents.get_mut(dirname) { 330 | children_set.insert(entry.path.clone()); 331 | } else { 332 | let mut h = HashSet::new(); 333 | h.insert(entry.path.clone()); 334 | self.parents.insert(dirname.to_string(), h); 335 | } 336 | } 337 | } 338 | 339 | pub fn load_for_update(&mut self) -> Result<(), std::io::Error> { 340 | self.lockfile.hold_for_update()?; 341 | self.load()?; 342 | 343 | Ok(()) 344 | } 345 | 346 | fn clear(&mut self) { 347 | self.entries = BTreeMap::new(); 348 | self.hasher = None; 349 | self.parents = HashMap::new(); 350 | self.changed = false; 351 | } 352 | 353 | fn open_index_file(&self) -> Option { 354 | if self.pathname.exists() { 355 | OpenOptions::new() 356 | .read(true) 357 | .open(self.pathname.clone()) 358 | .ok() 359 | } else { 360 | None 361 | } 362 | } 363 | 364 | fn read_header(checksum: &mut Checksum) -> usize { 365 | let data = checksum 366 | .read(HEADER_SIZE) 367 | .expect("could not read checksum header"); 368 | let signature = str::from_utf8(&data[0..4]).expect("invalid signature"); 369 | let version = u32::from_be_bytes(data[4..8].try_into().unwrap()); 370 | let count = u32::from_be_bytes(data[8..12].try_into().unwrap()); 371 | 372 | if signature != "DIRC" { 373 | panic!("Signature: expected 'DIRC', but found {}", signature); 374 | } 375 | 376 | if version != 2 { 377 | panic!("Version: expected '2', but found {}", version); 378 | } 379 | 380 | count as usize 381 | } 382 | 383 | fn read_entries( 384 | &mut self, 385 | checksum: &mut Checksum, 386 | count: usize, 387 | ) -> Result<(), std::io::Error> { 388 | for _i in 0..count { 389 | let mut entry = checksum.read(MIN_ENTRY_SIZE)?; 390 | while entry.last().unwrap() != &0u8 { 391 | entry.extend_from_slice(&checksum.read(8)?); 392 | } 393 | 394 | self.store_entry(Entry::parse(&entry)?); 395 | } 396 | 397 | Ok(()) 398 | } 399 | 400 | pub fn load(&mut self) -> Result<(), std::io::Error> { 401 | self.clear(); 402 | if let Some(file) = self.open_index_file() { 403 | let mut reader = Checksum::new(file); 404 | let count = Index::read_header(&mut reader); 405 | self.read_entries(&mut reader, count)?; 406 | reader.verify_checksum()?; 407 | } 408 | 409 | Ok(()) 410 | } 411 | 412 | pub fn release_lock(&mut self) -> Result<(), std::io::Error> { 413 | self.lockfile.rollback() 414 | } 415 | 416 | pub fn is_tracked_file(&self, pathname: &str) -> bool { 417 | self.entries.contains_key(pathname) 418 | } 419 | 420 | pub fn is_tracked(&self, pathname: &str) -> bool { 421 | self.is_tracked_file(pathname) || self.parents.contains_key(pathname) 422 | } 423 | 424 | pub fn update_entry_stat(&mut self, entry: &mut Entry, stat: &fs::Metadata) { 425 | entry.update_stat(stat); 426 | self.changed = true; 427 | } 428 | 429 | pub fn entry_for_path(&self, path: &str) -> Option<&Entry> { 430 | self.entries.get(path) 431 | } 432 | } 433 | 434 | #[cfg(test)] 435 | mod tests { 436 | use super::*; 437 | use crate::database::blob::Blob; 438 | use crate::database::object::Object; 439 | use crate::repository::Repository; 440 | use rand::random; 441 | use std::process::Command; 442 | 443 | #[test] 444 | fn add_files_to_index() -> Result<(), std::io::Error> { 445 | // Add a file to an index and check that it's there 446 | let mut temp_dir = generate_temp_name(); 447 | temp_dir.push_str("_jit_test"); 448 | 449 | let root_path = Path::new("/tmp").join(temp_dir); 450 | let mut repo = Repository::new(&root_path); 451 | fs::create_dir(&root_path)?; 452 | 453 | let oid = encode_hex(&(0..20).map(|_n| random::()).collect::>()); 454 | 455 | let f1_filename = "alice.txt"; 456 | let f1_path = root_path.join(f1_filename); 457 | File::create(&f1_path)?.write(b"file 1")?; 458 | let stat = repo.workspace.stat_file(f1_filename)?; 459 | 460 | { 461 | repo.index.clear(); 462 | repo.index.add(f1_filename, &oid, &stat); 463 | 464 | let index_entry_paths: Vec<&String> = 465 | repo.index.entries.iter().map(|(path, _)| path).collect(); 466 | 467 | assert_eq!(vec![f1_filename], index_entry_paths); 468 | } 469 | 470 | // Replace file with directory 471 | { 472 | repo.index.clear(); 473 | repo.index.add("alice.txt", &oid, &stat); 474 | repo.index.add("alice.txt/nested.txt", &oid, &stat); 475 | repo.index.add("bob.txt", &oid, &stat); 476 | let index_entry_paths: Vec<&String> = 477 | repo.index.entries.iter().map(|(path, _)| path).collect(); 478 | 479 | assert_eq!(vec!["alice.txt/nested.txt", "bob.txt"], index_entry_paths); 480 | } 481 | 482 | // Replace directory with file 483 | { 484 | repo.index.clear(); 485 | repo.index.add("alice.txt", &oid, &stat); 486 | repo.index.add("nested/bob.txt", &oid, &stat); 487 | 488 | repo.index.add("nested", &oid, &stat); 489 | 490 | let index_entry_paths: Vec<&String> = 491 | repo.index.entries.iter().map(|(path, _)| path).collect(); 492 | 493 | assert_eq!(vec!["alice.txt", "nested"], index_entry_paths); 494 | } 495 | 496 | // Replace directory(with subdirectories) with file 497 | { 498 | repo.index.clear(); 499 | repo.index.add("alice.txt", &oid, &stat); 500 | repo.index.add("nested/bob.txt", &oid, &stat); 501 | repo.index.add("nested/inner/claire.txt", &oid, &stat); 502 | 503 | repo.index.add("nested", &oid, &stat); 504 | 505 | let index_entry_paths: Vec<&String> = 506 | repo.index.entries.iter().map(|(path, _)| path).collect(); 507 | 508 | assert_eq!(vec!["alice.txt", "nested"], index_entry_paths); 509 | } 510 | 511 | // Cleanup 512 | fs::remove_dir_all(&root_path)?; 513 | 514 | Ok(()) 515 | } 516 | 517 | #[test] 518 | fn emit_index_file_same_as_stock_git() -> Result<(), std::io::Error> { 519 | // Create index file, using "stock" git and our implementation and 520 | // check that they are byte-for-byte equal 521 | 522 | let mut temp_dir = generate_temp_name(); 523 | temp_dir.push_str("_jit_test"); 524 | 525 | let root_path = Path::new("/tmp").join(temp_dir); 526 | let mut repo = Repository::new(&root_path); 527 | fs::create_dir(&root_path)?; 528 | 529 | let git_path = root_path.join(".git"); 530 | fs::create_dir(&git_path)?; 531 | 532 | repo.index.load_for_update()?; 533 | 534 | // Create some files 535 | File::create(root_path.join("f1.txt"))?.write(b"file 1")?; 536 | File::create(root_path.join("f2.txt"))?.write(b"file 2")?; 537 | 538 | // Create an index out of those files 539 | for pathname in repo.workspace.list_files(&root_path)? { 540 | let data = repo.workspace.read_file(&pathname)?; 541 | let stat = repo.workspace.stat_file(&pathname)?; 542 | 543 | let blob = Blob::new(data.as_bytes()); 544 | repo.database.store(&blob)?; 545 | 546 | repo.index.add(&pathname, &blob.get_oid(), &stat); 547 | } 548 | 549 | repo.index.write_updates()?; 550 | 551 | // Store contents of our index file 552 | let mut our_index = File::open(&git_path.join("index"))?; 553 | let mut our_index_contents = Vec::new(); 554 | our_index.read_to_end(&mut our_index_contents)?; 555 | 556 | // Remove .git dir that we created 557 | fs::remove_dir_all(&git_path)?; 558 | 559 | // Create index using "stock" git 560 | let _git_init_output = Command::new("git") 561 | .current_dir(&root_path) 562 | .arg("init") 563 | .arg(".") 564 | .output(); 565 | let _git_output = Command::new("git") 566 | .current_dir(&root_path) 567 | .arg("add") 568 | .arg(".") 569 | .output(); 570 | 571 | let mut git_index = File::open(&git_path.join("index"))?; 572 | let mut git_index_contents = Vec::new(); 573 | git_index.read_to_end(&mut git_index_contents)?; 574 | 575 | assert_eq!(our_index_contents, git_index_contents); 576 | 577 | // Cleanup 578 | fs::remove_dir_all(&root_path)?; 579 | 580 | Ok(()) 581 | } 582 | } 583 | -------------------------------------------------------------------------------- /src/lockfile.rs: -------------------------------------------------------------------------------- 1 | use std::fs::{self, File, OpenOptions}; 2 | use std::io::prelude::*; 3 | use std::io::{self, ErrorKind}; 4 | use std::path::{Path, PathBuf}; 5 | 6 | #[derive(Debug)] 7 | pub struct Lockfile { 8 | file_path: PathBuf, 9 | lock_path: PathBuf, 10 | pub lock: Option, 11 | } 12 | 13 | impl Lockfile { 14 | pub fn new(path: &Path) -> Lockfile { 15 | Lockfile { 16 | file_path: path.to_path_buf(), 17 | lock_path: path.with_extension("lock"), 18 | lock: None, 19 | } 20 | } 21 | 22 | pub fn hold_for_update(&mut self) -> Result<(), std::io::Error> { 23 | if self.lock.is_none() { 24 | let open_file = OpenOptions::new() 25 | .read(true) 26 | .write(true) 27 | .create_new(true) 28 | .open(&self.lock_path)?; 29 | 30 | self.lock = Some(open_file); 31 | } 32 | 33 | Ok(()) 34 | } 35 | 36 | pub fn write(&mut self, contents: &str) -> Result<(), std::io::Error> { 37 | self.write_bytes(contents.as_bytes()) 38 | } 39 | 40 | pub fn write_bytes(&mut self, data: &[u8]) -> Result<(), std::io::Error> { 41 | self.raise_on_stale_lock()?; 42 | 43 | let mut lock = self.lock.as_ref().unwrap(); 44 | lock.write_all(data)?; 45 | 46 | Ok(()) 47 | } 48 | 49 | pub fn commit(&mut self) -> Result<(), std::io::Error> { 50 | self.raise_on_stale_lock()?; 51 | self.lock = None; 52 | fs::rename(&self.lock_path, &self.file_path)?; 53 | 54 | Ok(()) 55 | } 56 | 57 | pub fn rollback(&mut self) -> Result<(), std::io::Error> { 58 | self.raise_on_stale_lock()?; 59 | fs::remove_file(&self.lock_path)?; 60 | self.lock = None; 61 | 62 | Ok(()) 63 | } 64 | 65 | fn raise_on_stale_lock(&self) -> Result<(), std::io::Error> { 66 | if self.lock.is_none() { 67 | Err(io::Error::new( 68 | ErrorKind::Other, 69 | format!("Not holding lock on file: {:?}", self.lock_path), 70 | )) 71 | } else { 72 | Ok(()) 73 | } 74 | } 75 | } 76 | 77 | impl Read for Lockfile { 78 | fn read(&mut self, mut buf: &mut [u8]) -> Result { 79 | self.raise_on_stale_lock()?; 80 | 81 | let mut lock = self.lock.as_ref().unwrap(); 82 | lock.read(&mut buf) 83 | } 84 | } 85 | 86 | impl Write for Lockfile { 87 | fn write(&mut self, buf: &[u8]) -> Result { 88 | self.raise_on_stale_lock()?; 89 | 90 | let mut lock = self.lock.as_ref().unwrap(); 91 | lock.write(buf) 92 | } 93 | 94 | fn flush(&mut self) -> Result<(), io::Error> { 95 | let mut lock = self.lock.as_ref().unwrap(); 96 | lock.flush() 97 | } 98 | } 99 | 100 | impl<'a> Read for &'a Lockfile { 101 | fn read(&mut self, mut buf: &mut [u8]) -> Result { 102 | self.raise_on_stale_lock()?; 103 | 104 | let mut lock = self.lock.as_ref().unwrap(); 105 | lock.read(&mut buf) 106 | } 107 | } 108 | 109 | impl<'a> Write for &'a Lockfile { 110 | fn write(&mut self, buf: &[u8]) -> Result { 111 | self.raise_on_stale_lock()?; 112 | 113 | let mut lock = self.lock.as_ref().unwrap(); 114 | lock.write(buf) 115 | } 116 | 117 | fn flush(&mut self) -> Result<(), io::Error> { 118 | let mut lock = self.lock.as_ref().unwrap(); 119 | lock.flush() 120 | } 121 | } 122 | 123 | impl Drop for Lockfile { 124 | fn drop(&mut self) { 125 | if self.lock.is_some() { 126 | fs::remove_file(&self.lock_path).expect("Could not delete lockfile"); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate chrono; 2 | extern crate crypto; 3 | extern crate flate2; 4 | extern crate rand; 5 | #[macro_use] 6 | extern crate lazy_static; 7 | extern crate regex; 8 | extern crate clap; 9 | 10 | use std::collections::HashMap; 11 | use std::env; 12 | use std::io::{self, Write}; 13 | 14 | mod lockfile; 15 | 16 | mod database; 17 | mod index; 18 | mod refs; 19 | mod repository; 20 | mod util; 21 | mod workspace; 22 | mod diff; 23 | mod pager; 24 | mod revision; 25 | 26 | mod commands; 27 | use commands::{execute, get_app, CommandContext}; 28 | 29 | fn main() { 30 | let ctx = CommandContext { 31 | dir: env::current_dir().unwrap(), 32 | env: &env::vars().collect::>(), 33 | options: None, 34 | stdin: io::stdin(), 35 | stdout: io::stdout(), 36 | stderr: io::stderr(), 37 | }; 38 | 39 | let matches = get_app().get_matches(); 40 | 41 | match execute(matches, ctx) { 42 | Ok(_) => (), 43 | Err(msg) => { 44 | io::stderr().write_all(msg.as_bytes()).unwrap(); 45 | std::process::exit(128); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/pager.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::ffi::OsString; 3 | 4 | const PAGER_CMD: &str = "less"; 5 | 6 | lazy_static! { 7 | static ref PAGER_ENV: HashMap<&'static str, &'static str> = { 8 | let mut m = HashMap::new(); 9 | m.insert("LESS", "FRX"); 10 | m.insert("LV", "-c"); 11 | m 12 | }; 13 | } 14 | 15 | mod utils { 16 | use std::ffi::{CString, OsString}; 17 | use std::os::unix::ffi::OsStringExt; 18 | use std::ptr; 19 | 20 | use errno; 21 | use libc; 22 | 23 | fn split_string(s: &OsString) -> Vec { 24 | match s.clone().into_string() { 25 | Ok(cmd) => cmd.split_whitespace().map(OsString::from).collect(), 26 | Err(cmd) => vec![cmd], 27 | } 28 | } 29 | 30 | pub fn pipe() -> (i32, i32) { 31 | let mut fds = [0; 2]; 32 | assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0); 33 | (fds[0], fds[1]) 34 | } 35 | 36 | pub fn close(fd: i32) { 37 | assert_eq!(unsafe { libc::close(fd) }, 0); 38 | } 39 | 40 | pub fn dup2(fd1: i32, fd2: i32) { 41 | assert!(unsafe { libc::dup2(fd1, fd2) } > -1); 42 | } 43 | 44 | fn osstring2cstring(s: OsString) -> CString { 45 | unsafe { CString::from_vec_unchecked(s.into_vec()) } 46 | } 47 | 48 | pub fn execvp(cmd: &OsString) { 49 | let cstrings = split_string(cmd) 50 | .into_iter() 51 | .map(osstring2cstring) 52 | .collect::>(); 53 | let mut args = cstrings.iter().map(|c| c.as_ptr()).collect::>(); 54 | args.push(ptr::null()); 55 | errno::set_errno(errno::Errno(0)); 56 | unsafe { libc::execvp(args[0], args.as_ptr()) }; 57 | } 58 | 59 | // Helper wrappers around libc::* API 60 | pub fn fork() -> libc::pid_t { 61 | unsafe { libc::fork() } 62 | } 63 | } 64 | 65 | pub struct Pager; 66 | 67 | impl Pager { 68 | pub fn setup_pager() { 69 | let (git_pager, pager) = (std::env::var("GIT_PAGER"), std::env::var("PAGER")); 70 | 71 | let cmd = match (git_pager, pager) { 72 | (Ok(git_pager), _) => git_pager, 73 | (_, Ok(pager)) => pager, 74 | _ => PAGER_CMD.to_string(), 75 | }; 76 | 77 | let pager_cmd = OsString::from(cmd); 78 | 79 | for (k, v) in PAGER_ENV.iter() { 80 | std::env::set_var(k, v); 81 | } 82 | 83 | let (pager_stdin, main_stdout) = utils::pipe(); 84 | let pager_pid = utils::fork(); 85 | match pager_pid { 86 | -1 => { 87 | // Fork failed 88 | utils::close(pager_stdin); 89 | utils::close(main_stdout); 90 | } 91 | 0 => { 92 | // Child 93 | utils::dup2(main_stdout, libc::STDOUT_FILENO); 94 | utils::close(pager_stdin); 95 | } 96 | _ => { 97 | // Parent-- executes pager 98 | utils::dup2(pager_stdin, libc::STDIN_FILENO); 99 | utils::close(main_stdout); 100 | utils::execvp(&pager_cmd); 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/refs.rs: -------------------------------------------------------------------------------- 1 | use crate::lockfile::Lockfile; 2 | use crate::util; 3 | use regex::{Regex, RegexSet}; 4 | use std::fs::{self, DirEntry, File}; 5 | use std::io::{self, Read}; 6 | use std::path::{Path, PathBuf}; 7 | use std::cmp::{Ord, Ordering}; 8 | use std::collections::HashMap; 9 | 10 | lazy_static! { 11 | static ref INVALID_FILENAME: RegexSet = { 12 | RegexSet::new(&[ 13 | r"^\.", 14 | r"/\.", 15 | r"\.\.", 16 | r"/$", 17 | r"\.lock$", 18 | r"@\{", 19 | r"[\x00-\x20*:?\[\\^~\x7f]", 20 | ]) 21 | .unwrap() 22 | }; 23 | static ref SYMREF: Regex = Regex::new(r"^ref: (.+)$").unwrap(); 24 | } 25 | 26 | #[derive(Debug, PartialEq, Eq, PartialOrd)] 27 | pub enum Ref { 28 | Ref { oid: String }, 29 | SymRef { path: String }, 30 | } 31 | 32 | impl Ref { 33 | pub fn is_head(&self) -> bool { 34 | match self { 35 | Ref::Ref { .. } => false, 36 | Ref::SymRef { path } => path == "HEAD", 37 | } 38 | } 39 | 40 | pub fn path(&self) -> &str { 41 | match self { 42 | Ref::Ref { .. } => unimplemented!(), 43 | Ref::SymRef { path } => path, 44 | } 45 | } 46 | } 47 | 48 | impl Ord for Ref { 49 | fn cmp(&self, other: &Ref) -> Ordering { 50 | match (self, other) { 51 | (Ref::Ref { .. }, Ref::SymRef { ..} ) => Ordering::Less, 52 | (Ref::SymRef { .. }, Ref::Ref { ..} ) => Ordering::Greater, 53 | (Ref::SymRef { path: a }, Ref::SymRef { path: b } ) => a.cmp(b), 54 | (Ref::Ref { oid: a }, Ref::Ref { oid: b } ) => a.cmp(b), 55 | } 56 | } 57 | } 58 | 59 | pub struct Refs { 60 | pathname: PathBuf, 61 | } 62 | 63 | impl Refs { 64 | pub fn new(pathname: &Path) -> Refs { 65 | Refs { 66 | pathname: pathname.to_path_buf(), 67 | } 68 | } 69 | 70 | fn head_path(&self) -> PathBuf { 71 | (*self.pathname).join("HEAD") 72 | } 73 | 74 | fn refs_path(&self) -> PathBuf { 75 | (*self.pathname).join("refs") 76 | } 77 | 78 | fn heads_path(&self) -> PathBuf { 79 | (*self.pathname).join("refs/heads") 80 | } 81 | 82 | pub fn update_ref_file(&self, path: &Path, oid: &str) -> Result<(), std::io::Error> { 83 | let mut lock = Lockfile::new(path); 84 | lock.hold_for_update()?; 85 | Self::write_lockfile(lock, &oid) 86 | } 87 | 88 | pub fn update_head(&self, oid: &str) -> Result<(), std::io::Error> { 89 | self.update_symref(&self.head_path(), oid) 90 | } 91 | 92 | pub fn set_head(&self, revision: &str, oid: &str) -> Result<(), std::io::Error> { 93 | let path = self.heads_path().join(revision); 94 | 95 | if path.exists() { 96 | let relative = util::relative_path_from(Path::new(&path), &self.pathname); 97 | self.update_ref_file(&self.head_path(), &format!("ref: {}", relative)) 98 | } else { 99 | self.update_ref_file(&self.head_path(), oid) 100 | } 101 | } 102 | 103 | pub fn read_head(&self) -> Option { 104 | self.read_symref(&self.head_path()) 105 | } 106 | 107 | fn path_for_name(&self, name: &str) -> Option { 108 | let prefixes = [self.pathname.clone(), self.refs_path(), self.heads_path()]; 109 | for prefix in &prefixes { 110 | if prefix.join(name).exists() { 111 | return Some(prefix.join(name)); 112 | } 113 | } 114 | None 115 | } 116 | 117 | pub fn read_ref(&self, name: &str) -> Option { 118 | if let Some(path) = self.path_for_name(name) { 119 | self.read_symref(&path) 120 | } else { 121 | None 122 | } 123 | } 124 | 125 | /// Folows chain of references to resolve to an object ID 126 | pub fn read_oid(&self, r#ref: &Ref) -> Option { 127 | match r#ref { 128 | Ref::Ref { oid } => Some(oid.to_string()), 129 | Ref::SymRef { path } => self.read_ref(&path), 130 | } 131 | } 132 | 133 | pub fn read_oid_or_symref(path: &Path) -> Option { 134 | if path.exists() { 135 | let mut file = File::open(path).unwrap(); 136 | let mut contents = String::new(); 137 | file.read_to_string(&mut contents).unwrap(); 138 | 139 | if let Some(caps) = SYMREF.captures(&contents.trim()) { 140 | Some(Ref::SymRef { 141 | path: caps[1].to_string(), 142 | }) 143 | } else { 144 | Some(Ref::Ref { 145 | oid: contents.trim().to_string(), 146 | }) 147 | } 148 | } else { 149 | None 150 | } 151 | } 152 | 153 | pub fn read_symref(&self, path: &Path) -> Option { 154 | let r#ref = Self::read_oid_or_symref(path); 155 | 156 | match r#ref { 157 | Some(Ref::SymRef { path }) => self.read_symref(&self.pathname.join(&path)), 158 | Some(Ref::Ref { oid }) => Some(oid), 159 | None => None, 160 | } 161 | } 162 | 163 | pub fn update_symref(&self, path: &Path, oid: &str) -> Result<(), std::io::Error> { 164 | let mut lock = Lockfile::new(path); 165 | lock.hold_for_update()?; 166 | 167 | let r#ref = Self::read_oid_or_symref(path); 168 | match r#ref { 169 | None | Some(Ref::Ref { .. }) => Self::write_lockfile(lock, &oid), 170 | Some(Ref::SymRef { path }) => self.update_symref(&self.pathname.join(path), oid), 171 | } 172 | } 173 | 174 | fn write_lockfile(mut lock: Lockfile, oid: &str) -> Result<(), io::Error> { 175 | lock.write(&oid)?; 176 | lock.write("\n")?; 177 | lock.commit() 178 | } 179 | 180 | pub fn current_ref(&self, source: &str) -> Ref { 181 | let r#ref = Self::read_oid_or_symref(&self.pathname.join(source)); 182 | 183 | match r#ref { 184 | Some(Ref::SymRef { path }) => self.current_ref(&path), 185 | Some(Ref::Ref { .. }) | None => Ref::SymRef { 186 | path: source.to_string(), 187 | }, 188 | } 189 | } 190 | 191 | pub fn create_branch(&self, branch_name: &str, start_oid: &str) -> Result<(), String> { 192 | let path = self.heads_path().join(branch_name); 193 | 194 | if INVALID_FILENAME.matches(branch_name).into_iter().count() > 0 { 195 | return Err(format!("{} is not a valid branch name.\n", branch_name)); 196 | } 197 | 198 | if path.as_path().exists() { 199 | return Err(format!("A branch named {} already exists.\n", branch_name)); 200 | } 201 | 202 | File::create(&path).expect("failed to create refs file for branch"); 203 | self.update_ref_file(&path, start_oid) 204 | .map_err(|e| e.to_string()) 205 | } 206 | 207 | pub fn list_branches(&self) -> Vec { 208 | self.list_refs(&self.heads_path()) 209 | } 210 | 211 | fn name_to_symref(&self, name: DirEntry) -> Vec { 212 | let path = name.path(); 213 | if path.is_dir() { 214 | self.list_refs(&path) 215 | } else { 216 | let path = util::relative_path_from(&path, &self.pathname); 217 | vec![Ref::SymRef { path }] 218 | } 219 | } 220 | 221 | fn list_refs(&self, dirname: &Path) -> Vec { 222 | fs::read_dir(self.pathname.join(dirname)) 223 | .expect("failed to read dir") 224 | .flat_map(|name| self.name_to_symref(name.unwrap())) 225 | .collect() 226 | } 227 | 228 | fn list_all_refs(&self) -> Vec { 229 | let mut all_refs = vec![Ref::SymRef { path: "HEAD".to_string() }]; 230 | let mut refs = self.list_refs(&self.refs_path()); 231 | 232 | all_refs.append(&mut refs); 233 | all_refs 234 | } 235 | 236 | pub fn reverse_refs(&self) -> HashMap> { 237 | let mut table : HashMap> = HashMap::new(); 238 | 239 | let all_refs = self.list_all_refs(); 240 | 241 | for r#ref in all_refs { 242 | let oid = self.read_oid(&r#ref).unwrap(); // TODO: handle error 243 | let oid_refs = table.get_mut(&oid); 244 | 245 | if let Some(oid_refs) = oid_refs { 246 | oid_refs.push(r#ref); 247 | } else { 248 | table.insert(oid, vec![r#ref]); 249 | } 250 | 251 | } 252 | 253 | table 254 | } 255 | 256 | pub fn ref_short_name(&self, r#ref: &Ref) -> String { 257 | match r#ref { 258 | Ref::Ref { oid: _ } => unimplemented!(), 259 | Ref::SymRef { path } => { 260 | let path = self.pathname.join(path); 261 | 262 | let dirs = [self.heads_path(), self.pathname.clone()]; 263 | let prefix = dirs.iter().find(|dir| { 264 | path.parent() 265 | .expect("failed to get parent") 266 | .ancestors() 267 | .any(|parent| &parent == dir) 268 | }); 269 | 270 | let prefix = prefix.expect("could not find prefix"); 271 | util::relative_path_from(&path, prefix) 272 | } 273 | } 274 | } 275 | 276 | pub fn delete_branch(&self, branch_name: &str) -> Result { 277 | let path = self.heads_path().join(branch_name); 278 | 279 | let mut lockfile = Lockfile::new(&path); 280 | lockfile.hold_for_update().map_err(|e| e.to_string())?; 281 | 282 | if let Some(oid) = self.read_symref(&path) { 283 | fs::remove_file(path).map_err(|e| e.to_string())?; 284 | // To remove the .lock file 285 | lockfile.rollback().map_err(|e| e.to_string())?; 286 | Ok(oid) 287 | } else { 288 | return Err(format!("branch {} not found", branch_name)); 289 | } 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /src/repository/migration.rs: -------------------------------------------------------------------------------- 1 | use crate::database::tree::TreeEntry; 2 | use crate::index::Entry; 3 | use crate::repository::{ChangeType, Repository}; 4 | use std::collections::{BTreeSet, HashMap, HashSet}; 5 | use std::fs; 6 | use std::path::{Path, PathBuf}; 7 | 8 | lazy_static! { 9 | static ref MESSAGES: HashMap = { 10 | let mut m = HashMap::new(); 11 | m.insert( 12 | ConflictType::StaleFile, 13 | ( 14 | "Your local changes to the following files would be overwritten by checkout:", 15 | "Please commit your changes to stash them before you switch branches", 16 | ), 17 | ); 18 | m.insert( 19 | ConflictType::StaleDirectory, 20 | ( 21 | "Updating the following directories would lose untracked files in them:", 22 | "\n", 23 | ), 24 | ); 25 | m.insert( 26 | ConflictType::UntrackedOverwritten, 27 | ( 28 | "The following untracked working tree files would be overwritten by checkout:", 29 | "Please move or remove them before you switch branches", 30 | ), 31 | ); 32 | m.insert( 33 | ConflictType::UntrackedRemoved, 34 | ( 35 | "The following untracked working tree files would be removed by checkout:", 36 | "Please commit your changes to stash them before you switch branches", 37 | ), 38 | ); 39 | m 40 | }; 41 | } 42 | 43 | pub struct Migration<'a> { 44 | repo: &'a mut Repository, 45 | diff: HashMap, Option)>, 46 | pub changes: HashMap)>>, 47 | pub mkdirs: BTreeSet, 48 | pub rmdirs: BTreeSet, 49 | pub errors: Vec, 50 | pub conflicts: HashMap>, 51 | } 52 | 53 | #[derive(Hash, PartialEq, Eq)] 54 | pub enum ConflictType { 55 | StaleFile, 56 | StaleDirectory, 57 | UntrackedOverwritten, 58 | UntrackedRemoved, 59 | } 60 | 61 | #[derive(Hash, PartialEq, Eq, Debug)] 62 | pub enum Action { 63 | Create, 64 | Delete, 65 | Update, 66 | } 67 | 68 | impl<'a> Migration<'a> { 69 | pub fn new( 70 | repo: &'a mut Repository, 71 | tree_diff: HashMap, Option)>, 72 | ) -> Migration<'a> { 73 | // TODO: can be a struct instead(?) 74 | let mut changes = HashMap::new(); 75 | changes.insert(Action::Create, vec![]); 76 | changes.insert(Action::Delete, vec![]); 77 | changes.insert(Action::Update, vec![]); 78 | 79 | let conflicts = { 80 | let mut m = HashMap::new(); 81 | m.insert(ConflictType::StaleFile, HashSet::new()); 82 | m.insert(ConflictType::StaleDirectory, HashSet::new()); 83 | m.insert(ConflictType::UntrackedOverwritten, HashSet::new()); 84 | m.insert(ConflictType::UntrackedRemoved, HashSet::new()); 85 | m 86 | }; 87 | 88 | Migration { 89 | repo, 90 | diff: tree_diff, 91 | changes, 92 | mkdirs: BTreeSet::new(), 93 | rmdirs: BTreeSet::new(), 94 | errors: vec![], 95 | conflicts, 96 | } 97 | } 98 | pub fn apply_changes(&mut self) -> Result<(), String> { 99 | match self.plan_changes() { 100 | Ok(_) => (), 101 | Err(errors) => return Err(errors.join("\n")), 102 | } 103 | self.update_workspace()?; 104 | self.update_index(); 105 | 106 | Ok(()) 107 | } 108 | 109 | fn plan_changes(&mut self) -> Result<(), Vec> { 110 | for (path, (old_item, new_item)) in self.diff.clone() { 111 | self.check_for_conflict(&path, &old_item, &new_item); 112 | self.record_change(&path, old_item, new_item); 113 | } 114 | 115 | self.collect_errors() 116 | } 117 | 118 | fn insert_conflict(&mut self, conflict_type: &ConflictType, path: &Path) { 119 | if let Some(conflicts) = self.conflicts.get_mut(conflict_type) { 120 | conflicts.insert(path.to_path_buf()); 121 | } 122 | } 123 | 124 | fn check_for_conflict( 125 | &mut self, 126 | path: &Path, 127 | old_item: &Option, 128 | new_item: &Option, 129 | ) { 130 | let path_str = path.to_str().unwrap(); 131 | let entry = self.repo.index.entry_for_path(path_str).cloned(); 132 | if self.index_differs_from_trees(entry.as_ref(), old_item.as_ref(), new_item.as_ref()) { 133 | self.insert_conflict(&ConflictType::StaleFile, &path); 134 | return; 135 | } 136 | 137 | let stat = self.repo.workspace.stat_file(path_str).ok(); 138 | let error_type = self.get_error_type(&stat, &entry.as_ref(), new_item); 139 | 140 | if stat.is_none() { 141 | let parent = self.untracked_parent(path); 142 | if parent.is_some() { 143 | let parent = parent.unwrap(); 144 | let conflict_path = if entry.is_some() { path } else { &parent }; 145 | self.insert_conflict(&error_type, conflict_path); 146 | } 147 | } else if Self::stat_is_file(&stat) { 148 | let changed = self 149 | .repo 150 | .compare_index_to_workspace(entry.as_ref(), stat.as_ref()); 151 | if changed != ChangeType::NoChange { 152 | self.insert_conflict(&error_type, path); 153 | } 154 | } else if Self::stat_is_dir(&stat) { 155 | let trackable = self 156 | .repo 157 | .is_trackable_path(path_str, &stat.unwrap()) 158 | .ok() 159 | .unwrap_or(false); 160 | if trackable { 161 | self.insert_conflict(&error_type, path); 162 | } 163 | } 164 | } 165 | 166 | fn untracked_parent(&self, path: &'a Path) -> Option { 167 | let dirname = path.parent().expect("failed to get dirname"); 168 | for parent in dirname.ancestors() { 169 | let parent_path_str = parent.to_str().unwrap(); 170 | if parent_path_str == "" { 171 | continue; 172 | } 173 | 174 | if let Ok(parent_stat) = self.repo.workspace.stat_file(parent_path_str) { 175 | if parent_stat.is_dir() { 176 | continue; 177 | } 178 | 179 | if self 180 | .repo 181 | .is_trackable_path(parent_path_str, &parent_stat) 182 | .unwrap_or(false) 183 | { 184 | return Some(parent.to_path_buf()); 185 | } 186 | } 187 | } 188 | None 189 | } 190 | 191 | fn stat_is_dir(stat: &Option) -> bool { 192 | match stat { 193 | None => false, 194 | Some(stat) => stat.is_dir(), 195 | } 196 | } 197 | 198 | fn stat_is_file(stat: &Option) -> bool { 199 | match stat { 200 | None => false, 201 | Some(stat) => stat.is_file(), 202 | } 203 | } 204 | 205 | fn get_error_type( 206 | &self, 207 | stat: &Option, 208 | entry: &Option<&Entry>, 209 | item: &Option, 210 | ) -> ConflictType { 211 | if entry.is_some() { 212 | ConflictType::StaleFile 213 | } else if Self::stat_is_dir(&stat) { 214 | ConflictType::StaleDirectory 215 | } else if item.is_some() { 216 | ConflictType::UntrackedOverwritten 217 | } else { 218 | ConflictType::UntrackedRemoved 219 | } 220 | } 221 | 222 | fn index_differs_from_trees( 223 | &self, 224 | entry: Option<&Entry>, 225 | old_item: Option<&TreeEntry>, 226 | new_item: Option<&TreeEntry>, 227 | ) -> bool { 228 | self.repo.compare_tree_to_index(old_item, entry) != ChangeType::NoChange 229 | && self.repo.compare_tree_to_index(new_item, entry) != ChangeType::NoChange 230 | } 231 | 232 | fn collect_errors(&mut self) -> Result<(), Vec> { 233 | for (conflict_type, paths) in &self.conflicts { 234 | if paths.is_empty() { 235 | continue; 236 | } 237 | 238 | let (header, footer) = MESSAGES.get(&conflict_type).unwrap(); 239 | let mut error = vec![header.to_string()]; 240 | 241 | for p in paths { 242 | error.push(format!("\t{}", p.to_str().unwrap())); 243 | } 244 | 245 | error.push(footer.to_string()); 246 | error.push("\n".to_string()); 247 | 248 | self.errors.push(error[..].join("\n")); 249 | } 250 | 251 | if !self.errors.is_empty() { 252 | return Err(self.errors.clone()); 253 | } 254 | Ok(()) 255 | } 256 | 257 | fn record_change( 258 | &mut self, 259 | path: &Path, 260 | old_item: Option, 261 | new_item: Option, 262 | ) { 263 | let path_ancestors: BTreeSet<_> = path 264 | .parent() 265 | .expect("could not find parent") 266 | .ancestors() 267 | .map(|p| p.to_path_buf()) 268 | .filter(|p| p.parent().is_some()) // filter out root path 269 | .collect(); 270 | 271 | let action = if old_item.is_none() { 272 | self.mkdirs = self.mkdirs.union(&path_ancestors).cloned().collect(); 273 | Action::Create 274 | } else if new_item.is_none() { 275 | self.rmdirs = self.rmdirs.union(&path_ancestors).cloned().collect(); 276 | Action::Delete 277 | } else { 278 | self.mkdirs = self.mkdirs.union(&path_ancestors).cloned().collect(); 279 | Action::Update 280 | }; 281 | 282 | if let Some(action_changes) = self.changes.get_mut(&action) { 283 | action_changes.push((path.to_path_buf(), new_item)); 284 | } 285 | } 286 | 287 | fn update_workspace(&mut self) -> Result<(), String> { 288 | self.repo.workspace.apply_migration( 289 | &mut self.repo.database, 290 | &self.changes, 291 | &self.rmdirs, 292 | &self.mkdirs, 293 | ) 294 | } 295 | 296 | fn update_index(&mut self) { 297 | for (path, _) in self.changes.get(&Action::Delete).unwrap() { 298 | self.repo 299 | .index 300 | .remove(path.to_str().expect("failed to convert path to str")); 301 | } 302 | 303 | for action in &[Action::Create, Action::Update] { 304 | for (path, entry) in self.changes.get(action).unwrap() { 305 | let path = path.to_str().expect("failed to convert path to str"); 306 | let entry_oid = entry.clone().unwrap().get_oid(); 307 | let stat = self 308 | .repo 309 | .workspace 310 | .stat_file(path) 311 | .expect("failed to stat file"); 312 | self.repo.index.add(path, &entry_oid, &stat); 313 | } 314 | } 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /src/repository/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::database::blob::Blob; 2 | use crate::database::commit::Commit; 3 | use crate::database::object::Object; 4 | use crate::database::tree::TreeEntry; 5 | use crate::database::Database; 6 | use crate::database::ParsedObject; 7 | use crate::index; 8 | use crate::index::Index; 9 | use crate::refs::Refs; 10 | use crate::workspace::Workspace; 11 | use std::collections::{BTreeMap, BTreeSet, HashMap}; 12 | use std::fs; 13 | use std::path::{Path, PathBuf}; 14 | 15 | pub mod migration; 16 | use migration::Migration; 17 | 18 | #[derive(Clone, Copy, Hash, Eq, PartialEq, Debug)] 19 | pub enum ChangeType { 20 | Added, 21 | Modified, 22 | Deleted, 23 | Untracked, 24 | NoChange, 25 | } 26 | 27 | #[derive(Clone, Copy, Hash, Eq, PartialEq)] 28 | enum ChangeKind { 29 | Workspace, 30 | Index, 31 | } 32 | 33 | pub struct Repository { 34 | pub database: Database, 35 | pub index: Index, 36 | pub refs: Refs, 37 | pub workspace: Workspace, 38 | 39 | // status fields 40 | pub root_path: PathBuf, 41 | pub stats: HashMap, 42 | pub untracked: BTreeSet, 43 | pub changed: BTreeSet, 44 | pub workspace_changes: BTreeMap, 45 | pub index_changes: BTreeMap, 46 | pub head_tree: HashMap, 47 | } 48 | 49 | impl Repository { 50 | pub fn new(root_path: &Path) -> Repository { 51 | let git_path = root_path.join(".git"); 52 | let db_path = git_path.join("objects"); 53 | 54 | Repository { 55 | database: Database::new(&db_path), 56 | index: Index::new(&git_path.join("index")), 57 | refs: Refs::new(&git_path), 58 | workspace: Workspace::new(git_path.parent().unwrap()), 59 | 60 | root_path: root_path.to_path_buf(), 61 | stats: HashMap::new(), 62 | untracked: BTreeSet::new(), 63 | changed: BTreeSet::new(), 64 | workspace_changes: BTreeMap::new(), 65 | index_changes: BTreeMap::new(), 66 | head_tree: HashMap::new(), 67 | } 68 | } 69 | 70 | pub fn initialize_status(&mut self) -> Result<(), String> { 71 | self.scan_workspace(&self.root_path.clone()).unwrap(); 72 | self.load_head_tree(); 73 | self.check_index_entries().map_err(|e| e.to_string())?; 74 | self.collect_deleted_head_files(); 75 | 76 | Ok(()) 77 | } 78 | 79 | fn collect_deleted_head_files(&mut self) { 80 | let paths: Vec = { 81 | self.head_tree 82 | .iter() 83 | .map(|(path, _)| path.clone()) 84 | .collect() 85 | }; 86 | for path in paths { 87 | if !self.index.is_tracked_file(&path) { 88 | self.record_change(&path, ChangeKind::Index, ChangeType::Deleted); 89 | } 90 | } 91 | } 92 | 93 | fn load_head_tree(&mut self) { 94 | let head_oid = self.refs.read_head(); 95 | if let Some(head_oid) = head_oid { 96 | let commit: Commit = { 97 | if let ParsedObject::Commit(commit) = self.database.load(&head_oid) { 98 | commit.clone() 99 | } else { 100 | panic!("HEAD points to a non-commit"); 101 | } 102 | }; 103 | self.read_tree(&commit.tree_oid, Path::new("")); 104 | } 105 | } 106 | 107 | fn read_tree(&mut self, tree_oid: &str, prefix: &Path) { 108 | let entries = { 109 | if let ParsedObject::Tree(tree) = self.database.load(tree_oid) { 110 | tree.entries.clone() 111 | } else { 112 | BTreeMap::new() 113 | } 114 | }; 115 | 116 | for (name, entry) in entries { 117 | let path = prefix.join(name); 118 | 119 | if entry.is_tree() { 120 | self.read_tree(&entry.get_oid(), &path); 121 | } else { 122 | self.head_tree 123 | .insert(path.to_str().unwrap().to_string(), entry); 124 | } 125 | } 126 | } 127 | 128 | fn scan_workspace(&mut self, prefix: &Path) -> Result<(), std::io::Error> { 129 | for (mut path, stat) in self.workspace.list_dir(prefix)? { 130 | if self.index.is_tracked(&path) { 131 | if self.workspace.is_dir(&path) { 132 | self.scan_workspace(&self.workspace.abs_path(&path))?; 133 | } else { 134 | // path is file 135 | self.stats.insert(path.to_string(), stat); 136 | } 137 | } else if self.is_trackable_path(&path, &stat)? { 138 | if self.workspace.is_dir(&path) { 139 | path.push('/'); 140 | } 141 | self.untracked.insert(path); 142 | } 143 | } 144 | 145 | Ok(()) 146 | } 147 | 148 | fn check_index_entries(&mut self) -> Result<(), std::io::Error> { 149 | let entries: Vec = self 150 | .index 151 | .entries 152 | .iter() 153 | .map(|(_, entry)| entry.clone()) 154 | .collect(); 155 | for mut entry in entries { 156 | self.check_index_against_workspace(&mut entry); 157 | self.check_index_against_head_tree(&mut entry); 158 | } 159 | 160 | Ok(()) 161 | } 162 | 163 | fn record_change(&mut self, path: &str, change_kind: ChangeKind, change_type: ChangeType) { 164 | self.changed.insert(path.to_string()); 165 | 166 | let changes_map = match change_kind { 167 | ChangeKind::Index => &mut self.index_changes, 168 | ChangeKind::Workspace => &mut self.workspace_changes, 169 | }; 170 | 171 | changes_map.insert(path.to_string(), change_type); 172 | } 173 | 174 | fn compare_index_to_workspace( 175 | &self, 176 | entry: Option<&index::Entry>, 177 | stat: Option<&fs::Metadata>, 178 | ) -> ChangeType { 179 | if entry.is_none() { 180 | return ChangeType::Untracked; 181 | } 182 | if stat.is_none() { 183 | return ChangeType::Deleted; 184 | } 185 | 186 | // Checks above ensure `entry` and `stat` are not None below 187 | // this 188 | let entry = entry.unwrap(); 189 | let stat = stat.unwrap(); 190 | 191 | if !entry.stat_match(&stat) { 192 | return ChangeType::Modified; 193 | } 194 | 195 | if entry.times_match(&stat) { 196 | return ChangeType::NoChange; 197 | } 198 | 199 | let data = self 200 | .workspace 201 | .read_file(&entry.path) 202 | .expect("failed to read file"); 203 | let blob = Blob::new(data.as_bytes()); 204 | let oid = blob.get_oid(); 205 | 206 | if entry.oid != oid { 207 | return ChangeType::Modified; 208 | } 209 | ChangeType::NoChange 210 | } 211 | 212 | fn compare_tree_to_index( 213 | &self, 214 | item: Option<&TreeEntry>, 215 | entry: Option<&index::Entry>, 216 | ) -> ChangeType { 217 | if item.is_none() && entry.is_none() { 218 | return ChangeType::NoChange; 219 | } 220 | if item.is_none() { 221 | return ChangeType::Added; 222 | } 223 | if entry.is_none() { 224 | return ChangeType::Deleted; 225 | } 226 | 227 | // Checks above ensure `entry` and `stat` are not None below 228 | // this 229 | let entry = entry.unwrap(); 230 | let item = item.unwrap(); 231 | 232 | if !(entry.mode == item.mode() && entry.oid == item.get_oid()) { 233 | return ChangeType::Modified; 234 | } 235 | ChangeType::NoChange 236 | } 237 | 238 | /// Adds modified entries to self.changed 239 | fn check_index_against_workspace(&mut self, mut entry: &mut index::Entry) { 240 | let stat = self.stats.get(&entry.path); 241 | let status = self.compare_index_to_workspace(Some(entry), stat); 242 | if status == ChangeType::NoChange { 243 | let stat = stat.expect("empty stat"); 244 | self.index.update_entry_stat(&mut entry, &stat); 245 | } else { 246 | self.record_change(&entry.path, ChangeKind::Workspace, status); 247 | } 248 | } 249 | 250 | fn check_index_against_head_tree(&mut self, entry: &mut index::Entry) { 251 | let item = self.head_tree.get(&entry.path); 252 | let status = self.compare_tree_to_index(item, Some(entry)); 253 | if status != ChangeType::NoChange { 254 | self.record_change(&entry.path, ChangeKind::Index, status); 255 | } 256 | } 257 | 258 | /// Check if path is trackable but not currently tracked 259 | fn is_trackable_path(&self, path: &str, stat: &fs::Metadata) -> Result { 260 | if stat.is_file() { 261 | return Ok(!self.index.is_tracked_file(path)); 262 | } 263 | 264 | let items = self.workspace.list_dir(&self.workspace.abs_path(path))?; 265 | let (files, dirs): (Vec<(&String, &fs::Metadata)>, Vec<(&String, &fs::Metadata)>) = 266 | items.iter().partition(|(_path, stat)| stat.is_file()); 267 | 268 | for (file_path, file_stat) in files.iter() { 269 | if self.is_trackable_path(file_path, file_stat)? { 270 | return Ok(true); 271 | } 272 | } 273 | 274 | for (dir_path, dir_stat) in dirs.iter() { 275 | if self.is_trackable_path(dir_path, dir_stat)? { 276 | return Ok(true); 277 | } 278 | } 279 | 280 | Ok(false) 281 | } 282 | 283 | pub fn migration( 284 | &mut self, 285 | tree_diff: HashMap, Option)>, 286 | ) -> Migration { 287 | Migration::new(self, tree_diff) 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /src/revision.rs: -------------------------------------------------------------------------------- 1 | use crate::database::{commit, Database, ParsedObject}; 2 | use crate::repository::Repository; 3 | use regex::{Regex, RegexSet}; 4 | use std::collections::HashMap; 5 | use std::fmt; 6 | 7 | lazy_static! { 8 | static ref INVALID_NAME: RegexSet = { 9 | RegexSet::new(&[ 10 | r"^\.", 11 | r"/\.", 12 | r"\.\.", 13 | r"/$", 14 | r"\.lock$", 15 | r"@\{", 16 | r"[\x00-\x20*:?\[\\^~\x7f]", 17 | ]) 18 | .unwrap() 19 | }; 20 | static ref PARENT: Regex = Regex::new(r"^(.+)\^$").unwrap(); 21 | static ref ANCESTOR: Regex = Regex::new(r"^(.+)~(\d+)$").unwrap(); 22 | static ref REF_ALIASES: HashMap<&'static str, &'static str> = { 23 | let mut m = HashMap::new(); 24 | m.insert("@", "HEAD"); 25 | m 26 | }; 27 | } 28 | 29 | #[derive(Debug, Clone)] 30 | pub struct HintedError { 31 | pub message: String, 32 | pub hint: Vec, 33 | } 34 | 35 | impl fmt::Display for HintedError { 36 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 37 | writeln!(f, "{}", self.message)?; 38 | for line in &self.hint { 39 | writeln!(f, "hint: {}", line)?; 40 | } 41 | 42 | Ok(()) 43 | } 44 | } 45 | 46 | #[derive(Debug, Clone)] 47 | pub enum Rev { 48 | Ref { name: String }, 49 | Parent { rev: Box }, 50 | Ancestor { rev: Box, n: i32 }, 51 | } 52 | 53 | pub struct Revision<'a> { 54 | repo: &'a mut Repository, 55 | query: Rev, 56 | expr: String, 57 | errors: Vec, 58 | } 59 | 60 | impl<'a> Revision<'a> { 61 | pub fn new(repo: &'a mut Repository, expr: &str) -> Revision<'a> { 62 | Revision { 63 | repo, 64 | expr: expr.to_string(), 65 | query: Self::parse(expr).expect("Revision parse failed"), 66 | errors: vec![], 67 | } 68 | } 69 | 70 | pub fn parse(revision: &str) -> Option { 71 | if let Some(caps) = PARENT.captures(revision) { 72 | let rev = Revision::parse(&caps[1]).expect("parsing parent rev failed"); 73 | return Some(Rev::Parent { rev: Box::new(rev) }); 74 | } else if let Some(caps) = ANCESTOR.captures(revision) { 75 | let rev = Revision::parse(&caps[1]).expect("parsing ancestor rev failed"); 76 | return Some(Rev::Ancestor { 77 | rev: Box::new(rev), 78 | n: i32::from_str_radix(&caps[2], 10).expect("could not parse ancestor number"), 79 | }); 80 | } else if Revision::is_valid_ref(revision) { 81 | let rev = REF_ALIASES.get(revision).unwrap_or(&revision); 82 | Some(Rev::Ref { 83 | name: rev.to_string(), 84 | }) 85 | } else { 86 | None 87 | } 88 | } 89 | 90 | fn is_valid_ref(revision: &str) -> bool { 91 | INVALID_NAME.matches(revision).into_iter().count() == 0 92 | } 93 | 94 | pub fn resolve(&mut self) -> Result> { 95 | match self.resolve_query(self.query.clone()) { 96 | Some(revision) => { 97 | if self.load_commit(&revision).is_some() { 98 | Ok(revision) 99 | } else { 100 | Err(self.errors.clone()) 101 | } 102 | } 103 | None => Err(self.errors.clone()), 104 | } 105 | } 106 | 107 | /// Resolve Revision to commit object ID. 108 | pub fn resolve_query(&mut self, query: Rev) -> Option { 109 | match query { 110 | Rev::Ref { name } => self.read_ref(&name), 111 | Rev::Parent { rev } => { 112 | let oid = self.resolve_query(*rev).expect("Invalid parent rev"); 113 | self.commit_parent(&oid) 114 | } 115 | Rev::Ancestor { rev, n } => { 116 | let mut oid = self.resolve_query(*rev).expect("Invalid ancestor rev"); 117 | for _ in 0..n { 118 | if let Some(parent_oid) = self.commit_parent(&oid) { 119 | oid = parent_oid 120 | } else { 121 | break; 122 | } 123 | } 124 | Some(oid) 125 | } 126 | } 127 | } 128 | 129 | fn read_ref(&mut self, name: &str) -> Option { 130 | let symref = self.repo.refs.read_ref(name); 131 | if symref.is_some() { 132 | symref 133 | } else { 134 | let candidates = self.repo.database.prefix_match(name); 135 | if candidates.len() == 1 { 136 | Some(candidates[0].to_string()) 137 | } else { 138 | if candidates.len() > 1 { 139 | self.log_ambiguous_sha1(name, candidates); 140 | } 141 | None 142 | } 143 | } 144 | } 145 | 146 | fn log_ambiguous_sha1(&mut self, name: &str, mut candidates: Vec) { 147 | candidates.sort(); 148 | let message = format!("short SHA1 {} is ambiguous", name); 149 | let mut hint = vec!["The candidates are:".to_string()]; 150 | 151 | for oid in candidates { 152 | let object = self.repo.database.load(&oid); 153 | let long_oid = object.get_oid(); 154 | let short = Database::short_oid(&long_oid); 155 | let info = format!(" {} {}", short, object.obj_type()); 156 | 157 | let obj_message = if let ParsedObject::Commit(commit) = object { 158 | format!( 159 | "{} {} - {}", 160 | info, 161 | commit.author.short_date(), 162 | commit.title_line() 163 | ) 164 | } else { 165 | info 166 | }; 167 | hint.push(obj_message); 168 | } 169 | self.errors.push(HintedError { message, hint }); 170 | } 171 | 172 | fn commit_parent(&mut self, oid: &str) -> Option { 173 | match self.load_commit(oid) { 174 | Some(commit) => commit.parent.clone(), 175 | None => None, 176 | } 177 | } 178 | 179 | fn load_commit(&mut self, oid: &str) -> Option<&commit::Commit> { 180 | match self.repo.database.load(oid) { 181 | ParsedObject::Commit(commit) => Some(commit), 182 | object => { 183 | let message = format!("object {} is a {}, not a commit", oid, object.obj_type()); 184 | self.errors.push(HintedError { 185 | message, 186 | hint: vec![], 187 | }); 188 | None 189 | } 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use rand::distributions::Alphanumeric; 2 | use rand::{thread_rng, Rng}; 3 | use std::fmt::Write; 4 | use std::num::ParseIntError; 5 | use std::path::Path; 6 | 7 | pub fn decode_hex(s: &str) -> Result, ParseIntError> { 8 | (0..s.len()) 9 | .step_by(2) 10 | .map(|i| u8::from_str_radix(&s[i..i + 2], 16)) 11 | .collect() 12 | } 13 | 14 | pub fn encode_hex(bytes: &[u8]) -> String { 15 | let mut s = String::with_capacity(bytes.len() * 2); 16 | for &b in bytes { 17 | write!(&mut s, "{:02x}", b).expect("hex encoding failed"); 18 | } 19 | s 20 | } 21 | 22 | pub fn generate_temp_name() -> String { 23 | thread_rng().sample_iter(&Alphanumeric).take(6).collect() 24 | } 25 | 26 | pub fn relative_path_from(path: &Path, from: &Path) -> String { 27 | path.strip_prefix(from) 28 | .unwrap() 29 | .to_str() 30 | .unwrap() 31 | .to_string() 32 | } 33 | -------------------------------------------------------------------------------- /src/workspace.rs: -------------------------------------------------------------------------------- 1 | use crate::database::tree::{TreeEntry, TREE_MODE}; 2 | use crate::database::{Database, ParsedObject}; 3 | use crate::repository::migration::Action; 4 | use std::collections::{BTreeSet, HashMap}; 5 | use std::fs::{self, File, OpenOptions}; 6 | use std::io::prelude::*; 7 | use std::io::BufReader; 8 | use std::os::unix::fs::PermissionsExt; 9 | use std::path::{Path, PathBuf}; 10 | 11 | lazy_static! { 12 | static ref IGNORE_PATHS: Vec<&'static str> = { 13 | let v = vec![".git", "target"]; 14 | v 15 | }; 16 | } 17 | 18 | pub struct Workspace { 19 | path: PathBuf, 20 | } 21 | 22 | impl Workspace { 23 | pub fn new(path: &Path) -> Workspace { 24 | Workspace { 25 | path: path.to_path_buf(), 26 | } 27 | } 28 | 29 | pub fn abs_path(&self, rel_path: &str) -> PathBuf { 30 | self.path.join(rel_path) 31 | } 32 | 33 | pub fn is_dir(&self, rel_path: &str) -> bool { 34 | self.abs_path(rel_path).is_dir() 35 | } 36 | 37 | /// List contents of directory. Does NOT list contents of 38 | /// subdirectories 39 | pub fn list_dir(&self, dir: &Path) -> Result, std::io::Error> { 40 | let path = self.path.join(dir); 41 | 42 | let entries = fs::read_dir(&path)? 43 | .map(|f| f.unwrap().path()) 44 | .filter(|f| !IGNORE_PATHS.contains(&f.file_name().unwrap().to_str().unwrap())); 45 | let mut stats = HashMap::new(); 46 | 47 | for name in entries { 48 | let relative = self 49 | .path 50 | .join(&name) 51 | .strip_prefix(&self.path) 52 | .unwrap() 53 | .to_str() 54 | .unwrap() 55 | .to_string(); 56 | 57 | let stat = self.stat_file(&relative).expect("stat file failed"); 58 | stats.insert(relative, stat); 59 | } 60 | 61 | Ok(stats) 62 | } 63 | 64 | /// Return list of files in dir. Nested files are flattened 65 | /// strings eg. `a/b/c/inner.txt` 66 | pub fn list_files(&self, dir: &Path) -> Result, std::io::Error> { 67 | if dir.is_file() { 68 | return Ok(vec![dir 69 | .strip_prefix(&self.path) 70 | .unwrap() 71 | .to_str() 72 | .unwrap() 73 | .to_string()]); 74 | } 75 | 76 | if IGNORE_PATHS.contains(&dir.file_name().unwrap().to_str().unwrap()) { 77 | return Ok(vec![]); 78 | } 79 | 80 | let mut files = vec![]; 81 | for file in fs::read_dir(dir)? { 82 | let path = file?.path(); 83 | files.extend_from_slice(&self.list_files(&path)?); 84 | } 85 | Ok(files) 86 | } 87 | 88 | // TODO: Should return bytes instead? 89 | pub fn read_file(&self, file_name: &str) -> Result { 90 | let file = File::open(self.path.as_path().join(file_name))?; 91 | let mut buf_reader = BufReader::new(file); 92 | let mut contents = String::new(); 93 | 94 | buf_reader.read_to_string(&mut contents)?; 95 | Ok(contents) 96 | } 97 | 98 | pub fn stat_file(&self, file_name: &str) -> Result { 99 | fs::metadata(self.path.join(file_name)) 100 | } 101 | 102 | pub fn apply_migration( 103 | &self, 104 | database: &mut Database, 105 | changes: &HashMap)>>, 106 | rmdirs: &BTreeSet, 107 | mkdirs: &BTreeSet, 108 | ) -> Result<(), String> { 109 | self.apply_change_list(database, changes, Action::Delete) 110 | .map_err(|e| e.to_string())?; 111 | for dir in rmdirs.iter().rev() { 112 | let dir_path = self.path.join(dir); 113 | self.remove_directory(&dir_path).unwrap_or(()); 114 | } 115 | 116 | for dir in mkdirs.iter() { 117 | self.make_directory(dir).map_err(|e| e.to_string())?; 118 | } 119 | 120 | self.apply_change_list(database, changes, Action::Update) 121 | .map_err(|e| e.to_string())?; 122 | self.apply_change_list(database, changes, Action::Create) 123 | .map_err(|e| e.to_string()) 124 | } 125 | 126 | fn apply_change_list( 127 | &self, 128 | database: &mut Database, 129 | changes: &HashMap)>>, 130 | action: Action, 131 | ) -> std::io::Result<()> { 132 | let changes = changes.get(&action).unwrap().clone(); 133 | for (filename, entry) in changes { 134 | let path = self.path.join(filename); 135 | Self::remove_file_or_dir(&path)?; 136 | 137 | if action == Action::Delete { 138 | continue; 139 | } 140 | 141 | let mut file = OpenOptions::new() 142 | .write(true) 143 | .create_new(true) 144 | .open(&path)?; 145 | 146 | let entry = entry 147 | .expect("entry missing for non-delete"); 148 | 149 | if entry.mode() != TREE_MODE { 150 | let data = Self::blob_data(database, &entry.get_oid()); 151 | file.write_all(&data)?; 152 | 153 | // Set mode 154 | let metadata = file.metadata()?; 155 | let mut permissions = metadata.permissions(); 156 | permissions.set_mode(entry.mode()); 157 | fs::set_permissions(path, permissions)?; 158 | } 159 | } 160 | 161 | Ok(()) 162 | } 163 | 164 | pub fn blob_data(database: &mut Database, oid: &str) -> Vec { 165 | match database.load(oid) { 166 | ParsedObject::Blob(blob) => blob.data.clone(), 167 | _ => panic!("not a blob oid"), 168 | } 169 | } 170 | 171 | fn remove_file_or_dir(path: &Path) -> std::io::Result<()> { 172 | if path.is_dir() { 173 | std::fs::remove_dir_all(path) 174 | } else if path.is_file() { 175 | std::fs::remove_file(path) 176 | } else { 177 | Ok(()) 178 | } 179 | } 180 | 181 | fn remove_directory(&self, path: &Path) -> std::io::Result<()> { 182 | std::fs::remove_dir(path)?; 183 | Ok(()) 184 | } 185 | 186 | fn make_directory(&self, dirname: &Path) -> std::io::Result<()> { 187 | let path = self.path.join(dirname); 188 | 189 | if let Ok(stat) = self.stat_file(dirname.to_str().expect("conversion to str failed")) { 190 | if stat.is_file() { 191 | std::fs::remove_file(&path)?; 192 | } 193 | if !stat.is_dir() { 194 | std::fs::create_dir(&path)?; 195 | } 196 | } else { 197 | std::fs::create_dir(&path)?; 198 | } 199 | Ok(()) 200 | } 201 | } 202 | --------------------------------------------------------------------------------