├── .gitignore ├── .project ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── src ├── commit_diff_panel.rs ├── commit_diff_util.rs ├── commit_window.rs ├── diff_text_view_util.rs ├── gtk_utils.rs ├── history_window.rs ├── main.rs ├── railway.rs ├── repository_ext.rs ├── repository_manager.rs ├── resources │ ├── commit_window.ui │ ├── history_window.ui │ ├── resources.gresource │ └── resources.xml ├── static_resource.rs ├── station_cell_renderer.rs ├── station_renderer.rs ├── station_wrapper.rs └── window_manager.rs └── tests ├── railway_test.rs ├── repository_manager_test.rs └── util ├── mod.rs └── test_repo.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | *~ 3 | /.idea/ 4 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | rusttest 4 | 5 | 6 | 7 | 8 | 9 | com.github.rustdt.ide.core.Builder 10 | clean,full,incremental, 11 | 12 | 13 | 14 | 15 | 16 | com.github.rustdt.ide.core.nature 17 | 18 | 19 | -------------------------------------------------------------------------------- /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 = "android_system_properties" 7 | version = "0.1.5" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 10 | dependencies = [ 11 | "libc", 12 | ] 13 | 14 | [[package]] 15 | name = "anyhow" 16 | version = "1.0.66" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" 19 | 20 | [[package]] 21 | name = "atk" 22 | version = "0.16.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "39991bc421ddf72f70159011b323ff49b0f783cc676a7287c59453da2e2531cf" 25 | dependencies = [ 26 | "atk-sys", 27 | "bitflags 1.3.2", 28 | "glib", 29 | "libc", 30 | ] 31 | 32 | [[package]] 33 | name = "atk-sys" 34 | version = "0.16.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "11ad703eb64dc058024f0e57ccfa069e15a413b98dbd50a1a950e743b7f11148" 37 | dependencies = [ 38 | "glib-sys", 39 | "gobject-sys", 40 | "libc", 41 | "system-deps", 42 | ] 43 | 44 | [[package]] 45 | name = "autocfg" 46 | version = "1.1.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 49 | 50 | [[package]] 51 | name = "bitflags" 52 | version = "1.3.2" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 55 | 56 | [[package]] 57 | name = "bitflags" 58 | version = "2.5.0" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 61 | 62 | [[package]] 63 | name = "bumpalo" 64 | version = "3.11.1" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" 67 | 68 | [[package]] 69 | name = "cairo-rs" 70 | version = "0.16.3" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "247e1183fa769ac22121f92276dae52f89acaf297f24b1320019f439b6e3b46f" 73 | dependencies = [ 74 | "bitflags 1.3.2", 75 | "cairo-sys-rs", 76 | "glib", 77 | "libc", 78 | "once_cell", 79 | "thiserror", 80 | ] 81 | 82 | [[package]] 83 | name = "cairo-sys-rs" 84 | version = "0.16.3" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "7c48f4af05fabdcfa9658178e1326efa061853f040ce7d72e33af6885196f421" 87 | dependencies = [ 88 | "glib-sys", 89 | "libc", 90 | "system-deps", 91 | ] 92 | 93 | [[package]] 94 | name = "cc" 95 | version = "1.0.77" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" 98 | dependencies = [ 99 | "jobserver", 100 | ] 101 | 102 | [[package]] 103 | name = "cfg-expr" 104 | version = "0.11.0" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "b0357a6402b295ca3a86bc148e84df46c02e41f41fef186bda662557ef6328aa" 107 | dependencies = [ 108 | "smallvec", 109 | ] 110 | 111 | [[package]] 112 | name = "cfg-if" 113 | version = "1.0.0" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 116 | 117 | [[package]] 118 | name = "chrono" 119 | version = "0.4.23" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" 122 | dependencies = [ 123 | "iana-time-zone", 124 | "js-sys", 125 | "num-integer", 126 | "num-traits", 127 | "time", 128 | "wasm-bindgen", 129 | "winapi", 130 | ] 131 | 132 | [[package]] 133 | name = "codespan-reporting" 134 | version = "0.11.1" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" 137 | dependencies = [ 138 | "termcolor", 139 | "unicode-width", 140 | ] 141 | 142 | [[package]] 143 | name = "core-foundation-sys" 144 | version = "0.8.3" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" 147 | 148 | [[package]] 149 | name = "cxx" 150 | version = "1.0.83" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "bdf07d07d6531bfcdbe9b8b739b104610c6508dcc4d63b410585faf338241daf" 153 | dependencies = [ 154 | "cc", 155 | "cxxbridge-flags", 156 | "cxxbridge-macro", 157 | "link-cplusplus", 158 | ] 159 | 160 | [[package]] 161 | name = "cxx-build" 162 | version = "1.0.83" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "d2eb5b96ecdc99f72657332953d4d9c50135af1bac34277801cc3937906ebd39" 165 | dependencies = [ 166 | "cc", 167 | "codespan-reporting", 168 | "once_cell", 169 | "proc-macro2", 170 | "quote", 171 | "scratch", 172 | "syn", 173 | ] 174 | 175 | [[package]] 176 | name = "cxxbridge-flags" 177 | version = "1.0.83" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "ac040a39517fd1674e0f32177648334b0f4074625b5588a64519804ba0553b12" 180 | 181 | [[package]] 182 | name = "cxxbridge-macro" 183 | version = "1.0.83" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "1362b0ddcfc4eb0a1f57b68bd77dd99f0e826958a96abd0ae9bd092e114ffed6" 186 | dependencies = [ 187 | "proc-macro2", 188 | "quote", 189 | "syn", 190 | ] 191 | 192 | [[package]] 193 | name = "field-offset" 194 | version = "0.3.4" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "1e1c54951450cbd39f3dbcf1005ac413b49487dabf18a720ad2383eccfeffb92" 197 | dependencies = [ 198 | "memoffset", 199 | "rustc_version", 200 | ] 201 | 202 | [[package]] 203 | name = "form_urlencoded" 204 | version = "1.1.0" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" 207 | dependencies = [ 208 | "percent-encoding", 209 | ] 210 | 211 | [[package]] 212 | name = "fuchsia-cprng" 213 | version = "0.1.1" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" 216 | 217 | [[package]] 218 | name = "futures-channel" 219 | version = "0.3.25" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" 222 | dependencies = [ 223 | "futures-core", 224 | ] 225 | 226 | [[package]] 227 | name = "futures-core" 228 | version = "0.3.25" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" 231 | 232 | [[package]] 233 | name = "futures-executor" 234 | version = "0.3.25" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2" 237 | dependencies = [ 238 | "futures-core", 239 | "futures-task", 240 | "futures-util", 241 | ] 242 | 243 | [[package]] 244 | name = "futures-io" 245 | version = "0.3.25" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" 248 | 249 | [[package]] 250 | name = "futures-macro" 251 | version = "0.3.25" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" 254 | dependencies = [ 255 | "proc-macro2", 256 | "quote", 257 | "syn", 258 | ] 259 | 260 | [[package]] 261 | name = "futures-task" 262 | version = "0.3.25" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" 265 | 266 | [[package]] 267 | name = "futures-util" 268 | version = "0.3.25" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" 271 | dependencies = [ 272 | "futures-core", 273 | "futures-macro", 274 | "futures-task", 275 | "pin-project-lite", 276 | "pin-utils", 277 | "slab", 278 | ] 279 | 280 | [[package]] 281 | name = "gdk" 282 | version = "0.16.2" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "aa9cb33da481c6c040404a11f8212d193889e9b435db2c14fd86987f630d3ce1" 285 | dependencies = [ 286 | "bitflags 1.3.2", 287 | "cairo-rs", 288 | "gdk-pixbuf", 289 | "gdk-sys", 290 | "gio", 291 | "glib", 292 | "libc", 293 | "pango", 294 | ] 295 | 296 | [[package]] 297 | name = "gdk-pixbuf" 298 | version = "0.16.4" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "d3094f2b8578136d1929cade4e0fff82f573521b579e96cfc24af2458431f176" 301 | dependencies = [ 302 | "bitflags 1.3.2", 303 | "gdk-pixbuf-sys", 304 | "gio", 305 | "glib", 306 | "libc", 307 | ] 308 | 309 | [[package]] 310 | name = "gdk-pixbuf-sys" 311 | version = "0.16.3" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "3092cf797a5f1210479ea38070d9ae8a5b8e9f8f1be9f32f4643c529c7d70016" 314 | dependencies = [ 315 | "gio-sys", 316 | "glib-sys", 317 | "gobject-sys", 318 | "libc", 319 | "system-deps", 320 | ] 321 | 322 | [[package]] 323 | name = "gdk-sys" 324 | version = "0.16.0" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "d76354f97a913e55b984759a997b693aa7dc71068c9e98bcce51aa167a0a5c5a" 327 | dependencies = [ 328 | "cairo-sys-rs", 329 | "gdk-pixbuf-sys", 330 | "gio-sys", 331 | "glib-sys", 332 | "gobject-sys", 333 | "libc", 334 | "pango-sys", 335 | "pkg-config", 336 | "system-deps", 337 | ] 338 | 339 | [[package]] 340 | name = "gio" 341 | version = "0.16.3" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "1d4a17d999e6e4e05d87c2bb05b7140d47769bc53211711a33e2f91536458714" 344 | dependencies = [ 345 | "bitflags 1.3.2", 346 | "futures-channel", 347 | "futures-core", 348 | "futures-io", 349 | "futures-util", 350 | "gio-sys", 351 | "glib", 352 | "libc", 353 | "once_cell", 354 | "pin-project-lite", 355 | "smallvec", 356 | "thiserror", 357 | ] 358 | 359 | [[package]] 360 | name = "gio-sys" 361 | version = "0.16.3" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "e9b693b8e39d042a95547fc258a7b07349b1f0b48f4b2fa3108ba3c51c0b5229" 364 | dependencies = [ 365 | "glib-sys", 366 | "gobject-sys", 367 | "libc", 368 | "system-deps", 369 | "winapi", 370 | ] 371 | 372 | [[package]] 373 | name = "git2" 374 | version = "0.18.3" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70" 377 | dependencies = [ 378 | "bitflags 2.5.0", 379 | "libc", 380 | "libgit2-sys", 381 | "log", 382 | "openssl-probe", 383 | "openssl-sys", 384 | "url", 385 | ] 386 | 387 | [[package]] 388 | name = "glib" 389 | version = "0.16.5" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "0cd04d150a2c63e6779f43aec7e04f5374252479b7bed5f45146d9c0e821f161" 392 | dependencies = [ 393 | "bitflags 1.3.2", 394 | "futures-channel", 395 | "futures-core", 396 | "futures-executor", 397 | "futures-task", 398 | "futures-util", 399 | "gio-sys", 400 | "glib-macros", 401 | "glib-sys", 402 | "gobject-sys", 403 | "libc", 404 | "once_cell", 405 | "smallvec", 406 | "thiserror", 407 | ] 408 | 409 | [[package]] 410 | name = "glib-macros" 411 | version = "0.16.3" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "e084807350b01348b6d9dbabb724d1a0bb987f47a2c85de200e98e12e30733bf" 414 | dependencies = [ 415 | "anyhow", 416 | "heck", 417 | "proc-macro-crate", 418 | "proc-macro-error", 419 | "proc-macro2", 420 | "quote", 421 | "syn", 422 | ] 423 | 424 | [[package]] 425 | name = "glib-sys" 426 | version = "0.16.3" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "c61a4f46316d06bfa33a7ac22df6f0524c8be58e3db2d9ca99ccb1f357b62a65" 429 | dependencies = [ 430 | "libc", 431 | "system-deps", 432 | ] 433 | 434 | [[package]] 435 | name = "gobject-sys" 436 | version = "0.16.3" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "3520bb9c07ae2a12c7f2fbb24d4efc11231c8146a86956413fb1a79bb760a0f1" 439 | dependencies = [ 440 | "glib-sys", 441 | "libc", 442 | "system-deps", 443 | ] 444 | 445 | [[package]] 446 | name = "gtk" 447 | version = "0.16.2" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "e4d3507d43908c866c805f74c9dd593c0ce7ba5c38e576e41846639cdcd4bee6" 450 | dependencies = [ 451 | "atk", 452 | "bitflags 1.3.2", 453 | "cairo-rs", 454 | "field-offset", 455 | "futures-channel", 456 | "gdk", 457 | "gdk-pixbuf", 458 | "gio", 459 | "glib", 460 | "gtk-sys", 461 | "gtk3-macros", 462 | "libc", 463 | "once_cell", 464 | "pango", 465 | "pkg-config", 466 | ] 467 | 468 | [[package]] 469 | name = "gtk-sys" 470 | version = "0.16.0" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "89b5f8946685d5fe44497007786600c2f368ff6b1e61a16251c89f72a97520a3" 473 | dependencies = [ 474 | "atk-sys", 475 | "cairo-sys-rs", 476 | "gdk-pixbuf-sys", 477 | "gdk-sys", 478 | "gio-sys", 479 | "glib-sys", 480 | "gobject-sys", 481 | "libc", 482 | "pango-sys", 483 | "system-deps", 484 | ] 485 | 486 | [[package]] 487 | name = "gtk3-macros" 488 | version = "0.16.0" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "8cfd6557b1018b773e43c8de9d0d13581d6b36190d0501916cbec4731db5ccff" 491 | dependencies = [ 492 | "anyhow", 493 | "proc-macro-crate", 494 | "proc-macro-error", 495 | "proc-macro2", 496 | "quote", 497 | "syn", 498 | ] 499 | 500 | [[package]] 501 | name = "heck" 502 | version = "0.4.0" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" 505 | 506 | [[package]] 507 | name = "iana-time-zone" 508 | version = "0.1.53" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" 511 | dependencies = [ 512 | "android_system_properties", 513 | "core-foundation-sys", 514 | "iana-time-zone-haiku", 515 | "js-sys", 516 | "wasm-bindgen", 517 | "winapi", 518 | ] 519 | 520 | [[package]] 521 | name = "iana-time-zone-haiku" 522 | version = "0.1.1" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" 525 | dependencies = [ 526 | "cxx", 527 | "cxx-build", 528 | ] 529 | 530 | [[package]] 531 | name = "idna" 532 | version = "0.3.0" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" 535 | dependencies = [ 536 | "unicode-bidi", 537 | "unicode-normalization", 538 | ] 539 | 540 | [[package]] 541 | name = "jobserver" 542 | version = "0.1.25" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" 545 | dependencies = [ 546 | "libc", 547 | ] 548 | 549 | [[package]] 550 | name = "js-sys" 551 | version = "0.3.60" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" 554 | dependencies = [ 555 | "wasm-bindgen", 556 | ] 557 | 558 | [[package]] 559 | name = "libc" 560 | version = "0.2.138" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" 563 | 564 | [[package]] 565 | name = "libgit2-sys" 566 | version = "0.16.2+1.7.2" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8" 569 | dependencies = [ 570 | "cc", 571 | "libc", 572 | "libssh2-sys", 573 | "libz-sys", 574 | "openssl-sys", 575 | "pkg-config", 576 | ] 577 | 578 | [[package]] 579 | name = "libssh2-sys" 580 | version = "0.3.0" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" 583 | dependencies = [ 584 | "cc", 585 | "libc", 586 | "libz-sys", 587 | "openssl-sys", 588 | "pkg-config", 589 | "vcpkg", 590 | ] 591 | 592 | [[package]] 593 | name = "libz-sys" 594 | version = "1.1.8" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" 597 | dependencies = [ 598 | "cc", 599 | "libc", 600 | "pkg-config", 601 | "vcpkg", 602 | ] 603 | 604 | [[package]] 605 | name = "link-cplusplus" 606 | version = "1.0.7" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369" 609 | dependencies = [ 610 | "cc", 611 | ] 612 | 613 | [[package]] 614 | name = "log" 615 | version = "0.4.17" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 618 | dependencies = [ 619 | "cfg-if", 620 | ] 621 | 622 | [[package]] 623 | name = "memoffset" 624 | version = "0.6.5" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" 627 | dependencies = [ 628 | "autocfg", 629 | ] 630 | 631 | [[package]] 632 | name = "metal-git" 633 | version = "0.0.1" 634 | dependencies = [ 635 | "chrono", 636 | "git2", 637 | "glib", 638 | "gtk", 639 | "tempdir", 640 | ] 641 | 642 | [[package]] 643 | name = "num-integer" 644 | version = "0.1.45" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 647 | dependencies = [ 648 | "autocfg", 649 | "num-traits", 650 | ] 651 | 652 | [[package]] 653 | name = "num-traits" 654 | version = "0.2.15" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 657 | dependencies = [ 658 | "autocfg", 659 | ] 660 | 661 | [[package]] 662 | name = "once_cell" 663 | version = "1.16.0" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" 666 | 667 | [[package]] 668 | name = "openssl-probe" 669 | version = "0.1.5" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 672 | 673 | [[package]] 674 | name = "openssl-sys" 675 | version = "0.9.78" 676 | source = "registry+https://github.com/rust-lang/crates.io-index" 677 | checksum = "07d5c8cb6e57b3a3612064d7b18b117912b4ce70955c2504d4b741c9e244b132" 678 | dependencies = [ 679 | "autocfg", 680 | "cc", 681 | "libc", 682 | "pkg-config", 683 | "vcpkg", 684 | ] 685 | 686 | [[package]] 687 | name = "pango" 688 | version = "0.16.5" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "cdff66b271861037b89d028656184059e03b0b6ccb36003820be19f7200b1e94" 691 | dependencies = [ 692 | "bitflags 1.3.2", 693 | "gio", 694 | "glib", 695 | "libc", 696 | "once_cell", 697 | "pango-sys", 698 | ] 699 | 700 | [[package]] 701 | name = "pango-sys" 702 | version = "0.16.3" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "9e134909a9a293e04d2cc31928aa95679c5e4df954d0b85483159bd20d8f047f" 705 | dependencies = [ 706 | "glib-sys", 707 | "gobject-sys", 708 | "libc", 709 | "system-deps", 710 | ] 711 | 712 | [[package]] 713 | name = "percent-encoding" 714 | version = "2.2.0" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" 717 | 718 | [[package]] 719 | name = "pest" 720 | version = "2.5.1" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "cc8bed3549e0f9b0a2a78bf7c0018237a2cdf085eecbbc048e52612438e4e9d0" 723 | dependencies = [ 724 | "thiserror", 725 | "ucd-trie", 726 | ] 727 | 728 | [[package]] 729 | name = "pin-project-lite" 730 | version = "0.2.9" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 733 | 734 | [[package]] 735 | name = "pin-utils" 736 | version = "0.1.0" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 739 | 740 | [[package]] 741 | name = "pkg-config" 742 | version = "0.3.26" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" 745 | 746 | [[package]] 747 | name = "proc-macro-crate" 748 | version = "1.2.1" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" 751 | dependencies = [ 752 | "once_cell", 753 | "thiserror", 754 | "toml", 755 | ] 756 | 757 | [[package]] 758 | name = "proc-macro-error" 759 | version = "1.0.4" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 762 | dependencies = [ 763 | "proc-macro-error-attr", 764 | "proc-macro2", 765 | "quote", 766 | "syn", 767 | "version_check", 768 | ] 769 | 770 | [[package]] 771 | name = "proc-macro-error-attr" 772 | version = "1.0.4" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 775 | dependencies = [ 776 | "proc-macro2", 777 | "quote", 778 | "version_check", 779 | ] 780 | 781 | [[package]] 782 | name = "proc-macro2" 783 | version = "1.0.47" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" 786 | dependencies = [ 787 | "unicode-ident", 788 | ] 789 | 790 | [[package]] 791 | name = "quote" 792 | version = "1.0.21" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" 795 | dependencies = [ 796 | "proc-macro2", 797 | ] 798 | 799 | [[package]] 800 | name = "rand" 801 | version = "0.4.6" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" 804 | dependencies = [ 805 | "fuchsia-cprng", 806 | "libc", 807 | "rand_core 0.3.1", 808 | "rdrand", 809 | "winapi", 810 | ] 811 | 812 | [[package]] 813 | name = "rand_core" 814 | version = "0.3.1" 815 | source = "registry+https://github.com/rust-lang/crates.io-index" 816 | checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" 817 | dependencies = [ 818 | "rand_core 0.4.2", 819 | ] 820 | 821 | [[package]] 822 | name = "rand_core" 823 | version = "0.4.2" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" 826 | 827 | [[package]] 828 | name = "rdrand" 829 | version = "0.4.0" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" 832 | dependencies = [ 833 | "rand_core 0.3.1", 834 | ] 835 | 836 | [[package]] 837 | name = "remove_dir_all" 838 | version = "0.5.3" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 841 | dependencies = [ 842 | "winapi", 843 | ] 844 | 845 | [[package]] 846 | name = "rustc_version" 847 | version = "0.3.3" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" 850 | dependencies = [ 851 | "semver", 852 | ] 853 | 854 | [[package]] 855 | name = "scratch" 856 | version = "1.0.2" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" 859 | 860 | [[package]] 861 | name = "semver" 862 | version = "0.11.0" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" 865 | dependencies = [ 866 | "semver-parser", 867 | ] 868 | 869 | [[package]] 870 | name = "semver-parser" 871 | version = "0.10.2" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" 874 | dependencies = [ 875 | "pest", 876 | ] 877 | 878 | [[package]] 879 | name = "serde" 880 | version = "1.0.149" 881 | source = "registry+https://github.com/rust-lang/crates.io-index" 882 | checksum = "256b9932320c590e707b94576e3cc1f7c9024d0ee6612dfbcf1cb106cbe8e055" 883 | 884 | [[package]] 885 | name = "slab" 886 | version = "0.4.7" 887 | source = "registry+https://github.com/rust-lang/crates.io-index" 888 | checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" 889 | dependencies = [ 890 | "autocfg", 891 | ] 892 | 893 | [[package]] 894 | name = "smallvec" 895 | version = "1.10.0" 896 | source = "registry+https://github.com/rust-lang/crates.io-index" 897 | checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" 898 | 899 | [[package]] 900 | name = "syn" 901 | version = "1.0.105" 902 | source = "registry+https://github.com/rust-lang/crates.io-index" 903 | checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" 904 | dependencies = [ 905 | "proc-macro2", 906 | "quote", 907 | "unicode-ident", 908 | ] 909 | 910 | [[package]] 911 | name = "system-deps" 912 | version = "6.0.3" 913 | source = "registry+https://github.com/rust-lang/crates.io-index" 914 | checksum = "2955b1fe31e1fa2fbd1976b71cc69a606d7d4da16f6de3333d0c92d51419aeff" 915 | dependencies = [ 916 | "cfg-expr", 917 | "heck", 918 | "pkg-config", 919 | "toml", 920 | "version-compare", 921 | ] 922 | 923 | [[package]] 924 | name = "tempdir" 925 | version = "0.3.7" 926 | source = "registry+https://github.com/rust-lang/crates.io-index" 927 | checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" 928 | dependencies = [ 929 | "rand", 930 | "remove_dir_all", 931 | ] 932 | 933 | [[package]] 934 | name = "termcolor" 935 | version = "1.1.3" 936 | source = "registry+https://github.com/rust-lang/crates.io-index" 937 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 938 | dependencies = [ 939 | "winapi-util", 940 | ] 941 | 942 | [[package]] 943 | name = "thiserror" 944 | version = "1.0.37" 945 | source = "registry+https://github.com/rust-lang/crates.io-index" 946 | checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" 947 | dependencies = [ 948 | "thiserror-impl", 949 | ] 950 | 951 | [[package]] 952 | name = "thiserror-impl" 953 | version = "1.0.37" 954 | source = "registry+https://github.com/rust-lang/crates.io-index" 955 | checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" 956 | dependencies = [ 957 | "proc-macro2", 958 | "quote", 959 | "syn", 960 | ] 961 | 962 | [[package]] 963 | name = "time" 964 | version = "0.1.45" 965 | source = "registry+https://github.com/rust-lang/crates.io-index" 966 | checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" 967 | dependencies = [ 968 | "libc", 969 | "wasi", 970 | "winapi", 971 | ] 972 | 973 | [[package]] 974 | name = "tinyvec" 975 | version = "1.6.0" 976 | source = "registry+https://github.com/rust-lang/crates.io-index" 977 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 978 | dependencies = [ 979 | "tinyvec_macros", 980 | ] 981 | 982 | [[package]] 983 | name = "tinyvec_macros" 984 | version = "0.1.0" 985 | source = "registry+https://github.com/rust-lang/crates.io-index" 986 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 987 | 988 | [[package]] 989 | name = "toml" 990 | version = "0.5.9" 991 | source = "registry+https://github.com/rust-lang/crates.io-index" 992 | checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" 993 | dependencies = [ 994 | "serde", 995 | ] 996 | 997 | [[package]] 998 | name = "ucd-trie" 999 | version = "0.1.5" 1000 | source = "registry+https://github.com/rust-lang/crates.io-index" 1001 | checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" 1002 | 1003 | [[package]] 1004 | name = "unicode-bidi" 1005 | version = "0.3.8" 1006 | source = "registry+https://github.com/rust-lang/crates.io-index" 1007 | checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" 1008 | 1009 | [[package]] 1010 | name = "unicode-ident" 1011 | version = "1.0.5" 1012 | source = "registry+https://github.com/rust-lang/crates.io-index" 1013 | checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" 1014 | 1015 | [[package]] 1016 | name = "unicode-normalization" 1017 | version = "0.1.22" 1018 | source = "registry+https://github.com/rust-lang/crates.io-index" 1019 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 1020 | dependencies = [ 1021 | "tinyvec", 1022 | ] 1023 | 1024 | [[package]] 1025 | name = "unicode-width" 1026 | version = "0.1.10" 1027 | source = "registry+https://github.com/rust-lang/crates.io-index" 1028 | checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" 1029 | 1030 | [[package]] 1031 | name = "url" 1032 | version = "2.3.1" 1033 | source = "registry+https://github.com/rust-lang/crates.io-index" 1034 | checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" 1035 | dependencies = [ 1036 | "form_urlencoded", 1037 | "idna", 1038 | "percent-encoding", 1039 | ] 1040 | 1041 | [[package]] 1042 | name = "vcpkg" 1043 | version = "0.2.15" 1044 | source = "registry+https://github.com/rust-lang/crates.io-index" 1045 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1046 | 1047 | [[package]] 1048 | name = "version-compare" 1049 | version = "0.1.1" 1050 | source = "registry+https://github.com/rust-lang/crates.io-index" 1051 | checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" 1052 | 1053 | [[package]] 1054 | name = "version_check" 1055 | version = "0.9.4" 1056 | source = "registry+https://github.com/rust-lang/crates.io-index" 1057 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1058 | 1059 | [[package]] 1060 | name = "wasi" 1061 | version = "0.10.0+wasi-snapshot-preview1" 1062 | source = "registry+https://github.com/rust-lang/crates.io-index" 1063 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 1064 | 1065 | [[package]] 1066 | name = "wasm-bindgen" 1067 | version = "0.2.83" 1068 | source = "registry+https://github.com/rust-lang/crates.io-index" 1069 | checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" 1070 | dependencies = [ 1071 | "cfg-if", 1072 | "wasm-bindgen-macro", 1073 | ] 1074 | 1075 | [[package]] 1076 | name = "wasm-bindgen-backend" 1077 | version = "0.2.83" 1078 | source = "registry+https://github.com/rust-lang/crates.io-index" 1079 | checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" 1080 | dependencies = [ 1081 | "bumpalo", 1082 | "log", 1083 | "once_cell", 1084 | "proc-macro2", 1085 | "quote", 1086 | "syn", 1087 | "wasm-bindgen-shared", 1088 | ] 1089 | 1090 | [[package]] 1091 | name = "wasm-bindgen-macro" 1092 | version = "0.2.83" 1093 | source = "registry+https://github.com/rust-lang/crates.io-index" 1094 | checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" 1095 | dependencies = [ 1096 | "quote", 1097 | "wasm-bindgen-macro-support", 1098 | ] 1099 | 1100 | [[package]] 1101 | name = "wasm-bindgen-macro-support" 1102 | version = "0.2.83" 1103 | source = "registry+https://github.com/rust-lang/crates.io-index" 1104 | checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" 1105 | dependencies = [ 1106 | "proc-macro2", 1107 | "quote", 1108 | "syn", 1109 | "wasm-bindgen-backend", 1110 | "wasm-bindgen-shared", 1111 | ] 1112 | 1113 | [[package]] 1114 | name = "wasm-bindgen-shared" 1115 | version = "0.2.83" 1116 | source = "registry+https://github.com/rust-lang/crates.io-index" 1117 | checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" 1118 | 1119 | [[package]] 1120 | name = "winapi" 1121 | version = "0.3.9" 1122 | source = "registry+https://github.com/rust-lang/crates.io-index" 1123 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1124 | dependencies = [ 1125 | "winapi-i686-pc-windows-gnu", 1126 | "winapi-x86_64-pc-windows-gnu", 1127 | ] 1128 | 1129 | [[package]] 1130 | name = "winapi-i686-pc-windows-gnu" 1131 | version = "0.4.0" 1132 | source = "registry+https://github.com/rust-lang/crates.io-index" 1133 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1134 | 1135 | [[package]] 1136 | name = "winapi-util" 1137 | version = "0.1.5" 1138 | source = "registry+https://github.com/rust-lang/crates.io-index" 1139 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 1140 | dependencies = [ 1141 | "winapi", 1142 | ] 1143 | 1144 | [[package]] 1145 | name = "winapi-x86_64-pc-windows-gnu" 1146 | version = "0.4.0" 1147 | source = "registry+https://github.com/rust-lang/crates.io-index" 1148 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1149 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "metal-git" 3 | version = "0.0.1" 4 | authors = ["Yoichi Imai "] 5 | build = "build.rs" 6 | edition = "2021" 7 | 8 | [lib] 9 | name = "metal_git" 10 | path = "src/main.rs" 11 | 12 | [dependencies] 13 | git2 = "^0.18" 14 | gtk = { version = "^0.16" } 15 | glib = { version = "^0.16" } 16 | chrono = "*" 17 | 18 | [dev-dependencies] 19 | tempdir = "0.3" 20 | 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Yoichi Imai, All rights reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Metal Git 2 | 3 | # What's this? 4 | 5 | GUI Git Frontend written in gtk-rs (gtk+ Rust wrapper) and git2-rs (libgit2 Rust wrapper). 6 | 7 | # Caution 8 | 9 | This program is now in experimental stage. It may destroy your files or repositories, so use carefully. 10 | 11 | # How to build? 12 | 13 | Install build dependencies at first. This program requires gtk+3 and libgit2. 14 | 15 | In Ubuntu 14.04 / 16.04, run this: 16 | 17 | ``` 18 | $ sudo apt install libgtk-3-dev libgit2-dev cmake 19 | ``` 20 | 21 | If build dependencies are properly installed, cargo should succeed. 22 | 23 | ``` 24 | $ cargo run 25 | ``` 26 | 27 | This program opens a git repository at the working directory. 28 | 29 | If you want a binary, use cargo build. 30 | 31 | ``` 32 | $ cargo build --release 33 | $ sudo cp target/release/metal-git /usr/local/bin 34 | ``` 35 | 36 | # Author 37 | 38 | Yoichi Imai 39 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | 3 | fn main() { 4 | Command::new("glib-compile-resources") 5 | .args(&["--generate", "resources.xml"]) 6 | .current_dir("src/resources") 7 | .status() 8 | .unwrap(); 9 | } -------------------------------------------------------------------------------- /src/commit_diff_panel.rs: -------------------------------------------------------------------------------- 1 | use git2::{DiffOptions, Error, Oid}; 2 | use glib::{Cast, StaticType}; 3 | use gtk::prelude::GtkListStoreExt; 4 | use gtk::prelude::GtkListStoreExtManual; 5 | use gtk::prelude::TreeViewColumnExt; 6 | use gtk::traits::{ 7 | ContainerExt, PanedExt, TextViewExt, TreeModelExt, TreeSelectionExt, TreeViewExt, 8 | }; 9 | use gtk::Orientation; 10 | use std::cell::RefCell; 11 | use std::rc::Rc; 12 | 13 | use crate::commit_diff_util; 14 | use crate::commit_diff_util::ListCommitDiffResult; 15 | use crate::diff_text_view_util; 16 | use crate::diff_text_view_util::create_diff_text_buffer; 17 | use crate::repository_manager::RepositoryManager; 18 | 19 | pub struct CommitDiffPanel { 20 | paned: gtk::Paned, 21 | 22 | diff_list_store: gtk::ListStore, 23 | diff_tree_view: gtk::TreeView, 24 | 25 | commit_text_view: gtk::TextView, 26 | 27 | repository_manager: Rc, 28 | current_list_result: RefCell>>, 29 | } 30 | 31 | const COLUMN_FILENAME: u32 = 0; 32 | const COLUMN_INDEX: u32 = 1; 33 | 34 | impl CommitDiffPanel { 35 | pub fn new(repository_manager: Rc) -> Rc { 36 | let paned = gtk::Paned::new(Orientation::Horizontal); 37 | 38 | let diff_list_store = gtk::ListStore::new(&[ 39 | String::static_type(), // COLUMN_FILENAME 40 | u32::static_type(), // COLUMN_INDEX 41 | ]); 42 | 43 | let diff_tree_view = gtk::TreeView::new(); 44 | diff_tree_view.set_model(Some(&diff_list_store)); 45 | 46 | let renderer = gtk::CellRendererText::new(); 47 | let col = gtk::TreeViewColumn::new(); 48 | col.set_title("Filename"); 49 | col.pack_start(&renderer, false); 50 | col.add_attribute(&renderer, "text", COLUMN_FILENAME as i32); 51 | diff_tree_view.append_column(&col); 52 | 53 | let scrolled = gtk::ScrolledWindow::builder().build(); 54 | scrolled.add(&diff_tree_view); 55 | paned.pack1(&scrolled, true, false); 56 | 57 | let scrolled = gtk::ScrolledWindow::builder().build(); 58 | 59 | let diff_text_buffer = create_diff_text_buffer(); 60 | let commit_text_view = gtk::TextView::builder() 61 | .editable(false) 62 | .buffer(&diff_text_buffer) 63 | .monospace(true) 64 | .build(); 65 | scrolled.add(&commit_text_view); 66 | 67 | paned.pack2(&scrolled, true, false); 68 | 69 | let commit_diff_panel = Rc::new(CommitDiffPanel { 70 | paned, 71 | diff_list_store, 72 | diff_tree_view, 73 | commit_text_view, 74 | repository_manager, 75 | current_list_result: RefCell::new(None), 76 | }); 77 | 78 | commit_diff_panel.setup_tree_view(); 79 | 80 | commit_diff_panel 81 | } 82 | 83 | pub fn container(&self) -> gtk::Container { 84 | self.paned.clone().upcast::() 85 | } 86 | 87 | pub fn update_commit(&self, oid: Oid) -> Result<(), Error> { 88 | let result = 89 | commit_diff_util::list_commit_diff_files(self.repository_manager.as_ref(), oid)?; 90 | 91 | self.diff_list_store.clear(); 92 | 93 | for (i, x) in result.files.iter().enumerate() { 94 | let index: u32 = i as u32; 95 | self.diff_list_store.insert_with_values( 96 | None, 97 | &[ 98 | (COLUMN_FILENAME, &x.format_file_move()), 99 | (COLUMN_INDEX, &index), 100 | ], 101 | ); 102 | } 103 | 104 | self.current_list_result.replace(Some(Rc::new(result))); 105 | 106 | self.show_all_files_diff()?; 107 | 108 | Ok(()) 109 | } 110 | 111 | fn setup_tree_view(self: &Rc) { 112 | let selection = self.diff_tree_view.selection(); 113 | let w = Rc::downgrade(self); 114 | selection.connect_changed(move |x| { 115 | if let Some((model, iter)) = x.selected() { 116 | let index = model 117 | .value(&iter, COLUMN_INDEX as i32) 118 | .get::() 119 | .expect("Incorrect column type"); 120 | w.upgrade() 121 | .unwrap() 122 | .file_selected(index) 123 | .expect("Failed to select a file"); 124 | } 125 | }); 126 | } 127 | 128 | fn file_selected(self: &Rc, file_index: u32) -> Result<(), Error> { 129 | if let Some(list_result) = self.current_list_result.borrow().as_ref() { 130 | let repo = self.repository_manager.open()?; 131 | 132 | let entry = &list_result.files[file_index as usize]; 133 | 134 | let current_commit = repo.find_commit(list_result.current_oid)?; 135 | let parent_commit = repo.find_commit(list_result.parent_oid)?; 136 | 137 | let parent_tree = parent_commit.tree()?; 138 | let current_tree = current_commit.tree()?; 139 | 140 | let new_file_path = entry.new_file_path.as_ref().unwrap(); 141 | let mut opts = DiffOptions::new(); 142 | opts.pathspec(new_file_path); 143 | 144 | let diff = 145 | repo.diff_tree_to_tree(Some(&parent_tree), Some(¤t_tree), Some(&mut opts))?; 146 | 147 | if let Some(buffer) = self.commit_text_view.buffer() { 148 | diff_text_view_util::print_diff_to_text_view(&diff, &buffer); 149 | } 150 | } 151 | 152 | Ok(()) 153 | } 154 | 155 | fn show_all_files_diff(&self) -> Result<(), Error> { 156 | if let Some(list_result) = self.current_list_result.borrow().as_ref() { 157 | let repo = self.repository_manager.open()?; 158 | 159 | let current_commit = repo.find_commit(list_result.current_oid)?; 160 | let parent_commit = repo.find_commit(list_result.parent_oid)?; 161 | 162 | let parent_tree = parent_commit.tree()?; 163 | let current_tree = current_commit.tree()?; 164 | 165 | let diff = repo.diff_tree_to_tree(Some(&parent_tree), Some(¤t_tree), None)?; 166 | 167 | if let Some(buffer) = self.commit_text_view.buffer() { 168 | diff_text_view_util::print_diff_to_text_view(&diff, &buffer); 169 | } 170 | } 171 | 172 | Ok(()) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/commit_diff_util.rs: -------------------------------------------------------------------------------- 1 | use git2::{Error, Oid}; 2 | 3 | use crate::repository_manager::RepositoryManager; 4 | 5 | pub struct ListCommitDiffFileEntry { 6 | pub new_file_path: Option, 7 | pub old_file_path: Option, 8 | } 9 | 10 | pub struct ListCommitDiffResult { 11 | pub current_oid: Oid, 12 | pub parent_oid: Oid, 13 | pub files: Vec 14 | } 15 | 16 | pub fn list_commit_diff_files(repository_manager: &RepositoryManager, oid: Oid) -> Result { 17 | let repo = repository_manager.open()?; 18 | let current_commit = repo.find_commit(oid)?; 19 | // TODO: treat multiple commit 20 | let parent_commit = current_commit.parent(0)?; 21 | 22 | let parent_tree = parent_commit.tree()?; 23 | let current_tree = current_commit.tree()?; 24 | let diff = repo.diff_tree_to_tree(Some(&parent_tree), Some(¤t_tree), None)?; 25 | 26 | let mut files: Vec = Vec::new(); 27 | diff.foreach( 28 | &mut |delta, _| { 29 | let file_entry = ListCommitDiffFileEntry { 30 | old_file_path: delta.old_file().path().and_then(|p| p.to_str()).map(|s| s.to_string()), 31 | new_file_path: delta.new_file().path().and_then(|p| p.to_str()).map(|s| s.to_string()) 32 | }; 33 | files.push(file_entry); 34 | true 35 | }, 36 | None, 37 | None, 38 | None, 39 | )?; 40 | 41 | Ok(ListCommitDiffResult { 42 | current_oid: oid, 43 | parent_oid: parent_commit.id(), 44 | files 45 | }) 46 | } 47 | 48 | impl ListCommitDiffFileEntry { 49 | pub fn format_file_move(&self) -> String { 50 | let old_file_path = self.old_file_path.as_deref(); 51 | let new_file_path = self.new_file_path.as_deref(); 52 | 53 | if let Some(new_file) = new_file_path { 54 | if let Some(old_file) = old_file_path { 55 | if new_file != old_file { 56 | return format!("{} -> {}", old_file, new_file) 57 | } 58 | } 59 | 60 | return new_file.to_owned() 61 | } 62 | 63 | "".to_string() 64 | } 65 | } -------------------------------------------------------------------------------- /src/commit_window.rs: -------------------------------------------------------------------------------- 1 | use gtk::prelude::*; 2 | 3 | use std::rc::Rc; 4 | use std::cell::RefCell; 5 | use std::str; 6 | use std::path::Path; 7 | use std::fs; 8 | use std::fs::File; 9 | use std::io::Read; 10 | use std::collections::HashSet; 11 | use std::error; 12 | 13 | use git2::{Error, StatusOptions}; 14 | use git2::build::CheckoutBuilder; 15 | 16 | use crate::repository_manager::RepositoryManager; 17 | use crate::gtk_utils; 18 | use crate::repository_ext::RepositoryExt; 19 | use crate::diff_text_view_util; 20 | use crate::diff_text_view_util::create_diff_text_buffer; 21 | 22 | pub struct CommitWindow { 23 | window: gtk::Window, 24 | 25 | refresh_button: gtk::Button, 26 | revert_button: gtk::Button, 27 | 28 | stage_button: gtk::Button, 29 | unstage_button: gtk::Button, 30 | 31 | amend_checkbutton: gtk::CheckButton, 32 | commit_button: gtk::Button, 33 | 34 | work_tree_files_list_store: gtk::ListStore, 35 | work_tree_files_tree_view: gtk::TreeView, 36 | 37 | staged_files_list_store: gtk::ListStore, 38 | staged_files_tree_view: gtk::TreeView, 39 | 40 | diff_text_view: gtk::TextView, 41 | message_text_view: gtk::TextView, 42 | 43 | repository_manager: Rc, 44 | 45 | commited: RefCell ()>>, 46 | } 47 | 48 | const FILENAME_COLUMN: u32 = 0; 49 | 50 | enum TreeType { 51 | WorkDir, 52 | Index, 53 | } 54 | 55 | impl CommitWindow { 56 | pub fn new(repository_manager: Rc) -> Rc { 57 | let builder = gtk::Builder::from_resource("/org/sunnyone/MetalGit/commit_window.ui"); 58 | 59 | let commit_window = CommitWindow { 60 | repository_manager: repository_manager, 61 | 62 | window: builder.object("commit_window").unwrap(), 63 | 64 | refresh_button: builder.object("refresh_button").unwrap(), 65 | revert_button: builder.object("revert_button").unwrap(), 66 | 67 | stage_button: builder.object("stage_button").unwrap(), 68 | unstage_button: builder.object("unstage_button").unwrap(), 69 | 70 | amend_checkbutton: builder.object("amend_checkbutton").unwrap(), 71 | commit_button: builder.object("commit_button").unwrap(), 72 | 73 | work_tree_files_list_store: builder.object("work_tree_files_list_store").unwrap(), 74 | work_tree_files_tree_view: builder.object("work_tree_files_tree_view").unwrap(), 75 | 76 | staged_files_list_store: builder.object("staged_files_list_store").unwrap(), 77 | staged_files_tree_view: builder.object("staged_files_tree_view").unwrap(), 78 | 79 | diff_text_view: builder.object("diff_text_view").unwrap(), 80 | message_text_view: builder.object("message_text_view").unwrap(), 81 | 82 | commited: RefCell::new(Box::new(|| {})), 83 | }; 84 | 85 | let commit_window = Rc::new(commit_window); 86 | commit_window.diff_text_view.set_monospace(true); 87 | 88 | let diff_text_buffer = create_diff_text_buffer(); 89 | commit_window.diff_text_view.set_buffer(Some(&diff_text_buffer)); 90 | 91 | let w = Rc::downgrade(&commit_window); 92 | commit_window.window.connect_delete_event(move |_, _| { 93 | w.upgrade().unwrap().hide(); 94 | Inhibit(true) 95 | }); 96 | 97 | let w = Rc::downgrade(&commit_window); 98 | commit_window.refresh_button.connect_clicked(move |_| { 99 | w.upgrade().unwrap().refresh(); 100 | }); 101 | 102 | let w = Rc::downgrade(&commit_window); 103 | commit_window.work_tree_files_tree_view.selection().connect_changed(move |selection| { 104 | let file = Self::get_selection_selected_file_single(selection); 105 | 106 | if let Some(file) = file { 107 | dialog_when_error!("Failed to diff: {:?}", 108 | w.upgrade().unwrap().work_tree_files_selected(&file)); 109 | } 110 | }); 111 | 112 | let w = Rc::downgrade(&commit_window); 113 | commit_window.staged_files_tree_view.selection().connect_changed(move |selection| { 114 | let file = Self::get_selection_selected_file_single(selection); 115 | 116 | if let Some(file) = file { 117 | dialog_when_error!("Failed to diff: {:?}", 118 | w.upgrade().unwrap().index_files_selected(&file)); 119 | } 120 | }); 121 | 122 | let w = Rc::downgrade(&commit_window); 123 | commit_window.work_tree_files_tree_view 124 | .connect_row_activated(move |_tree_view, tree_path, _column| { 125 | let w_ = w.upgrade().unwrap(); 126 | let file = Self::get_file_from_tree_path(&w_.work_tree_files_list_store, 127 | tree_path); 128 | 129 | if let Some(file) = file { 130 | dialog_when_error!("Failed to stage: {:?}", 131 | w_.stage_files(vec![file])); 132 | } 133 | }); 134 | 135 | let w = Rc::downgrade(&commit_window); 136 | commit_window.staged_files_tree_view 137 | .connect_row_activated(move |_tree_view, tree_path, _column| { 138 | let w_ = w.upgrade().unwrap(); 139 | let file = Self::get_file_from_tree_path(&w_.staged_files_list_store, 140 | tree_path); 141 | 142 | if let Some(file) = file { 143 | dialog_when_error!("Failed to unstage: {:?}", 144 | w_.unstage_files(vec![file])); 145 | } 146 | }); 147 | 148 | let w = Rc::downgrade(&commit_window); 149 | commit_window.revert_button.connect_clicked(move |_| { 150 | dialog_when_error!("Failed to revert: {:?}", 151 | w.upgrade().unwrap().revert_button_clicked()); 152 | }); 153 | 154 | let w = Rc::downgrade(&commit_window); 155 | commit_window.stage_button.connect_clicked(move |_| { 156 | dialog_when_error!("Failed to stage: {:?}", 157 | w.upgrade().unwrap().stage_button_clicked()); 158 | }); 159 | 160 | let w = Rc::downgrade(&commit_window); 161 | commit_window.unstage_button.connect_clicked(move |_| { 162 | dialog_when_error!("Failed to unstage: {:?}", 163 | w.upgrade().unwrap().unstage_button_clicked()); 164 | }); 165 | 166 | 167 | let w = Rc::downgrade(&commit_window); 168 | commit_window.message_text_view.connect_key_press_event(move |_, key| { 169 | if key.state().intersects(gtk::gdk::ModifierType::CONTROL_MASK) { 170 | // TODO: works? 171 | // TODO: "KP_Enter" is nessessary? 172 | if key.keyval().name().map(|n| n == "Return").unwrap_or(false) { 173 | dialog_when_error!("Failed to commit: {:?}", 174 | w.upgrade().unwrap().commit_or_amend()); 175 | return Inhibit(true); 176 | } 177 | } 178 | Inhibit(false) 179 | }); 180 | 181 | let w = Rc::downgrade(&commit_window); 182 | commit_window.amend_checkbutton.connect_clicked(move |_| { 183 | dialog_when_error!("Failed to toggle amend: {:?}", 184 | w.upgrade().unwrap().amend_checkbutton_clicked()); 185 | }); 186 | 187 | let w = Rc::downgrade(&commit_window); 188 | commit_window.commit_button.connect_clicked(move |_| { 189 | dialog_when_error!("Failed to commit: {:?}", 190 | w.upgrade().unwrap().commit_or_amend()); 191 | }); 192 | 193 | commit_window 194 | } 195 | 196 | fn get_selection_selected_files(selection: >k::TreeSelection) -> Vec { 197 | let (tree_paths, model) = selection.selected_rows(); 198 | return tree_paths.iter() 199 | .map(|path| Self::get_file_from_tree_path(&model, &path).unwrap()) 200 | .collect(); 201 | } 202 | 203 | fn get_selection_selected_file_single(selection: >k::TreeSelection) -> Option { 204 | let mut files = Self::get_selection_selected_files(selection); 205 | 206 | if files.len() == 1 { 207 | Some(files.pop().unwrap()) 208 | } else { 209 | None 210 | } 211 | } 212 | 213 | fn set_selection_select_files(&self, 214 | selection: >k::TreeSelection, 215 | list_store: >k::ListStore, 216 | files: &Vec) { 217 | let mut fileset = HashSet::new(); 218 | for file in files { 219 | fileset.insert(file); 220 | } 221 | 222 | if let Some(tree_iter) = list_store.iter_first() { 223 | loop { 224 | let value = list_store.value(&tree_iter, FILENAME_COLUMN as i32); 225 | let filename = value.get::().unwrap(); 226 | if fileset.contains(&filename) { 227 | selection.select_iter(&tree_iter); 228 | } 229 | 230 | if !list_store.iter_next(&tree_iter) { 231 | break; 232 | } 233 | } 234 | } 235 | } 236 | 237 | fn get_file_from_tree_path(list_store: &T, 238 | tree_path: >k::TreePath) 239 | -> Option { 240 | list_store.iter(tree_path) 241 | .and_then(|iter| { 242 | let value = list_store.value(&iter, FILENAME_COLUMN as i32); 243 | let s = value.get::(); 244 | s.ok() 245 | }) 246 | } 247 | 248 | fn revert_button_clicked(&self) -> Result<(), Error> { 249 | let selection = self.work_tree_files_tree_view.selection(); 250 | let files = Self::get_selection_selected_files(&selection); 251 | if files.len() == 0 { 252 | return Ok(()); 253 | } 254 | 255 | self.revert(files) 256 | } 257 | 258 | fn revert(&self, files: Vec) -> Result<(), Error> { 259 | if files.len() == 0 { 260 | return Ok(()); 261 | } 262 | 263 | let repo = self.repository_manager.open()?; 264 | 265 | let mut builder = CheckoutBuilder::new(); 266 | let mut to_checkout = false; 267 | builder.force(); 268 | 269 | let mut remove_file_paths = Vec::new(); 270 | for file in &files { 271 | let file_path = Path::new(file); 272 | let status = repo.status_file(file_path)?; 273 | if status == git2::Status::WT_NEW { 274 | remove_file_paths.push(file_path); 275 | } else { 276 | to_checkout = true; 277 | builder.path(file_path); 278 | } 279 | } 280 | 281 | if to_checkout { 282 | repo.checkout_head(Some(&mut builder))?; 283 | } 284 | 285 | for file_path in &remove_file_paths { 286 | let path = repo.get_full_path(file_path).unwrap(); 287 | 288 | // TODO: convert error 289 | if let Err(error) = fs::remove_file(&path) { 290 | println!("Failed to remove {}: {}", path.to_string_lossy(), error); 291 | } 292 | } 293 | 294 | self.refresh(); 295 | 296 | Ok(()) 297 | } 298 | 299 | fn stage_button_clicked(&self) -> Result<(), Error> { 300 | let selection = self.work_tree_files_tree_view.selection(); 301 | let files = Self::get_selection_selected_files(&selection); 302 | 303 | self.stage_files(files)?; 304 | 305 | Ok(()) 306 | } 307 | 308 | fn stage_files(&self, files: Vec) -> Result<(), Error> { 309 | if files.len() == 0 { 310 | return Ok(()); 311 | } 312 | 313 | let repo = self.repository_manager.open()?; 314 | let mut index = repo.index()?; 315 | 316 | for file in files { 317 | let file = Path::new(&file); 318 | let path_repo = repo.get_full_path(file).unwrap(); 319 | if !fs::metadata(path_repo).is_ok() { 320 | index.remove_path(&file)?; 321 | } else { 322 | index.add_path(&file)?; 323 | } 324 | } 325 | 326 | index.write()?; 327 | 328 | // TODO: partial update 329 | self.refresh(); 330 | 331 | Ok(()) 332 | } 333 | 334 | fn unstage_button_clicked(&self) -> Result<(), Error> { 335 | let selection = self.staged_files_tree_view.selection(); 336 | let files = Self::get_selection_selected_files(&selection); 337 | 338 | self.unstage_files(files)?; 339 | 340 | Ok(()) 341 | } 342 | 343 | fn unstage_files(&self, files: Vec) -> Result<(), Error> { 344 | if files.len() == 0 { 345 | return Ok(()); 346 | } 347 | 348 | let repo = self.repository_manager.open()?; 349 | 350 | let head_ref = repo.head()?; 351 | let head_object = head_ref.peel(git2::ObjectType::Commit)?; 352 | 353 | repo.reset_default(Some(&head_object), files)?; 354 | 355 | // TODO: partial update 356 | self.refresh(); 357 | 358 | Ok(()) 359 | } 360 | 361 | fn amend_checkbutton_clicked(&self) -> Result<(), Error> { 362 | let to_amend = self.amend_checkbutton.is_active(); 363 | let commit_message = self.get_commit_message(); 364 | if !to_amend || commit_message.len() > 0 { 365 | return Ok(()); 366 | } 367 | 368 | let repo = self.repository_manager.open()?; 369 | 370 | let head_ref = repo.head()?; 371 | let head_object = head_ref.peel(git2::ObjectType::Commit)?; 372 | let head_commit = head_object.as_commit().unwrap(); 373 | let last_commit_message = head_commit.message(); 374 | 375 | if let Some(message) = last_commit_message { 376 | self.set_commit_message(&message); 377 | } 378 | 379 | Ok(()) 380 | } 381 | 382 | fn get_commit_message(&self) -> String { 383 | let buffer = self.message_text_view.buffer().unwrap(); 384 | let message = buffer.text(&buffer.start_iter(), &buffer.end_iter(), false) 385 | .unwrap(); 386 | message.to_string() 387 | } 388 | 389 | fn set_commit_message(&self, message: &str) { 390 | self.message_text_view.buffer().unwrap().set_text(message); 391 | } 392 | 393 | fn commit(&self, to_amend: bool) -> Result<(), Error> { 394 | let message = self.get_commit_message(); 395 | 396 | let repo = self.repository_manager.open()?; 397 | let signature = repo.signature()?; 398 | 399 | // TODO: initial repository does not have a commit 400 | let head_ref = repo.head()?; 401 | let head_object = head_ref.peel(git2::ObjectType::Commit)?; 402 | let head_commit = head_object.as_commit().unwrap(); 403 | 404 | let mut index = repo.index()?; 405 | let tree_oid = index.write_tree()?; 406 | // TODO: use find_tree 407 | let tree_object = repo.find_object(tree_oid, Some(git2::ObjectType::Tree))?; 408 | let tree = tree_object.as_tree().unwrap(); 409 | 410 | if !to_amend { 411 | repo.commit(Some("HEAD"), 412 | &signature, 413 | &signature, 414 | &message, 415 | &tree, 416 | &[head_commit])?; 417 | } else { 418 | head_commit.amend(Some("HEAD"), 419 | Some(&signature), 420 | Some(&signature), 421 | None, 422 | Some(&message), 423 | Some(&tree))?; 424 | } 425 | 426 | // self.hide(); 427 | self.refresh(); 428 | self.set_commit_message(""); 429 | 430 | self.commited.borrow()(); 431 | 432 | Ok(()) 433 | } 434 | 435 | fn commit_or_amend(&self) -> Result<(), Error> { 436 | let to_amend = self.amend_checkbutton.is_active(); 437 | 438 | self.commit(to_amend) 439 | } 440 | 441 | pub fn show(&self) { 442 | self.window.show_all(); 443 | self.refresh(); 444 | } 445 | 446 | pub fn hide(&self) { 447 | self.window.hide(); 448 | } 449 | 450 | pub fn refresh(&self) { 451 | let work_tree_selection = self.work_tree_files_tree_view.selection(); 452 | let staged_selection = self.staged_files_tree_view.selection(); 453 | 454 | // back selected files up 455 | let work_selected = Self::get_selection_selected_files(&work_tree_selection); 456 | let staged_selected = Self::get_selection_selected_files(&staged_selection); 457 | 458 | work_tree_selection.unselect_all(); 459 | staged_selection.unselect_all(); 460 | 461 | self.work_tree_files_list_store.clear(); 462 | self.staged_files_list_store.clear(); 463 | 464 | match collect_changed_status_items(&self.repository_manager) { 465 | Err(_) => crate::gtk_utils::message_box_error("Error!"), 466 | Ok(list) => { 467 | for item in list { 468 | let list_store = match item.tree_type { 469 | TreeType::WorkDir => &self.work_tree_files_list_store, 470 | TreeType::Index => &self.staged_files_list_store, 471 | }; 472 | 473 | let _ = list_store.insert_with_values(None, &[(FILENAME_COLUMN, &item.path)]); 474 | } 475 | } 476 | } 477 | 478 | self.set_selection_select_files(&work_tree_selection, 479 | &self.work_tree_files_list_store, 480 | &work_selected); 481 | self.set_selection_select_files(&staged_selection, 482 | &self.staged_files_list_store, 483 | &staged_selected); 484 | } 485 | 486 | pub fn work_tree_files_selected(&self, filename: &str) -> Result<(), Error> { 487 | let repo = self.repository_manager.open()?; 488 | 489 | let path = Path::new(filename); 490 | let status = repo.status_file(path)?; 491 | if status == git2::Status::WT_NEW { 492 | self.show_new_file(path); 493 | Ok(()) 494 | } else { 495 | let mut diff_opts = git2::DiffOptions::new(); 496 | diff_opts.pathspec(filename); 497 | let diff = repo.diff_index_to_workdir(None, Some(&mut diff_opts))?; 498 | 499 | self.show_diff(&diff); 500 | 501 | Ok(()) 502 | } 503 | } 504 | 505 | pub fn index_files_selected(&self, filename: &str) -> Result<(), Error> { 506 | let repo = self.repository_manager.open()?; 507 | 508 | let mut diff_opts = git2::DiffOptions::new(); 509 | diff_opts.pathspec(filename); 510 | 511 | let head_ref = repo.head()?; 512 | let head_tree_object = head_ref.peel(git2::ObjectType::Tree)?; 513 | let head_tree = head_tree_object.as_tree().unwrap(); 514 | 515 | let diff = repo.diff_tree_to_index(Some(&head_tree), None, Some(&mut diff_opts))?; 516 | 517 | self.show_diff(&diff); 518 | 519 | Ok(()) 520 | } 521 | 522 | fn show_diff(&self, diff: &git2::Diff) { 523 | let buffer = self.diff_text_view.buffer().unwrap(); 524 | 525 | diff_text_view_util::print_diff_to_text_view(diff, &buffer); 526 | } 527 | 528 | pub fn set_diff_all_add_text(&self, text: &str) { 529 | let buffer = self.diff_text_view.buffer().unwrap(); 530 | buffer.set_text(""); 531 | 532 | let mut iter = buffer.start_iter(); 533 | gtk_utils::text_buffer_insert_with_tag_by_name(&buffer, &mut iter, text, "add"); 534 | } 535 | 536 | fn show_new_file(&self, path_in_repository: &Path) { 537 | match self.read_contents(&path_in_repository) { 538 | Ok(s) => { 539 | self.set_diff_all_add_text(&s); 540 | } 541 | Err(err) => { 542 | let msg = format!("This file is not browsable: {}", err.to_string()); 543 | self.set_diff_all_add_text(&msg); 544 | } 545 | } 546 | } 547 | 548 | fn read_contents(&self, path_in_repository: &Path) -> Result> { 549 | let repo = self.repository_manager.open()?; 550 | 551 | let path = repo.get_full_path(path_in_repository).unwrap(); 552 | 553 | let mut s = String::new(); 554 | let mut f = File::open(path)?; 555 | f.read_to_string(&mut s)?; 556 | Ok(s) 557 | } 558 | 559 | pub fn connect_commited(&self, callback: F) 560 | where F: Fn() -> () + 'static 561 | { 562 | *self.commited.borrow_mut() = Box::new(callback); 563 | } 564 | } 565 | 566 | struct StatusItem { 567 | path: String, 568 | tree_type: TreeType, 569 | } 570 | 571 | fn collect_changed_status_items(repository_manager: &RepositoryManager) 572 | -> Result, Error> { 573 | let repo = repository_manager.open()?; 574 | if repo.is_bare() { 575 | return Err(Error::from_str("cannot report status on bare repository")); 576 | } 577 | 578 | let mut opts = StatusOptions::new(); 579 | opts.include_untracked(true).recurse_untracked_dirs(true); 580 | 581 | let statuses = repo.statuses(Some(&mut opts))?; 582 | let mut status_items: Vec = Vec::new(); 583 | 584 | for status in statuses.iter() { 585 | if status.path().is_none() { 586 | return Err(Error::from_str("Invalid file path exist")); 587 | } 588 | 589 | let path = status.path().unwrap(); 590 | if status.index_to_workdir().is_some() { 591 | status_items.push(StatusItem { 592 | tree_type: TreeType::WorkDir, 593 | path: path.to_string(), 594 | }); 595 | } 596 | if status.head_to_index().is_some() { 597 | status_items.push(StatusItem { 598 | tree_type: TreeType::Index, 599 | path: path.to_string(), 600 | }); 601 | } 602 | } 603 | 604 | Ok(status_items) 605 | } 606 | -------------------------------------------------------------------------------- /src/diff_text_view_util.rs: -------------------------------------------------------------------------------- 1 | use git2::Diff; 2 | use gtk::TextBuffer; 3 | use gtk::traits::{TextBufferExt, TextTagTableExt}; 4 | use crate::gtk_utils; 5 | use std::str; 6 | 7 | pub fn create_diff_text_buffer() -> gtk::TextBuffer { 8 | let tag_table = gtk::TextTagTable::new(); 9 | 10 | let add_tag = gtk::TextTag::builder() 11 | .name("add") 12 | .background("#ceead0") 13 | .foreground("black") 14 | .font("Normal") 15 | .build(); 16 | tag_table.add(&add_tag); 17 | 18 | let delete_tag = gtk::TextTag::builder() 19 | .name("delete") 20 | .background("#f2c6c6") 21 | .foreground("black") 22 | .font("Normal") 23 | .build(); 24 | tag_table.add(&delete_tag); 25 | 26 | let normal_tag = gtk::TextTag::builder() 27 | .name("normal") 28 | .background("white") 29 | .foreground("black") 30 | .font("Normal") 31 | .build(); 32 | tag_table.add(&normal_tag); 33 | 34 | let other_tag = gtk::TextTag::builder() 35 | .name("other") 36 | .background("#e6e6e6") 37 | .foreground("black") 38 | .font("Normal") 39 | .build(); 40 | tag_table.add(&other_tag); 41 | 42 | gtk::TextBuffer::builder() 43 | .tag_table(&tag_table) 44 | .build() 45 | } 46 | 47 | pub fn print_diff_to_text_view(diff: &Diff, buffer: &TextBuffer) { 48 | buffer.set_text(""); 49 | 50 | let mut iter = buffer.start_iter(); 51 | let _ = diff.print(git2::DiffFormat::Patch, |_delta, _hunk, line| { 52 | let o = line.origin(); 53 | let tag_name = match o { 54 | ' ' => "normal", 55 | '+' => "add", 56 | '-' => "delete", 57 | _ => "other", 58 | }; 59 | 60 | let mut str = String::new(); 61 | if o == '+' || o == '-' || o == ' ' { 62 | str.push(line.origin()); 63 | } 64 | str.push_str(str::from_utf8(line.content()).unwrap()); 65 | 66 | gtk_utils::text_buffer_insert_with_tag_by_name(&buffer, &mut iter, &str, tag_name); 67 | true 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /src/gtk_utils.rs: -------------------------------------------------------------------------------- 1 | use gtk::prelude::*; 2 | 3 | #[macro_export] 4 | macro_rules! dialog_when_error { 5 | ($message_template:expr, $e:expr) => ( 6 | if let Err(err) = $e { 7 | let msg = format!($message_template, err); 8 | crate::gtk_utils::message_box_error(&msg) 9 | } 10 | ) 11 | } 12 | 13 | pub fn message_box_error(message: &str) { 14 | let dialog = gtk::MessageDialog::new(None::<>k::Window>, 15 | gtk::DialogFlags::empty(), 16 | gtk::MessageType::Error, 17 | gtk::ButtonsType::Ok, 18 | message); 19 | dialog.run(); 20 | } 21 | 22 | pub fn text_buffer_insert_with_tag_by_name(buffer: >k::TextBuffer, 23 | iter: &mut gtk::TextIter, 24 | text: &str, 25 | tag_name: &str) { 26 | 27 | let start_offset = iter.offset(); 28 | buffer.insert(iter, text); 29 | let start_iter = buffer.iter_at_offset(start_offset); 30 | 31 | buffer.apply_tag_by_name(tag_name, &start_iter, &iter); 32 | } 33 | -------------------------------------------------------------------------------- /src/history_window.rs: -------------------------------------------------------------------------------- 1 | use crate::commit_diff_panel::CommitDiffPanel; 2 | use crate::railway; 3 | use crate::repository_manager::RepositoryManager; 4 | use crate::station_cell_renderer::StationCellRenderer; 5 | use crate::station_wrapper::StationWrapper; 6 | use crate::window_manager::WindowManager; 7 | use git2::Error; 8 | use std::rc::{Rc, Weak}; 9 | 10 | use gtk::prelude::{BuilderExtManual, GtkListStoreExtManual, NotebookExtManual}; 11 | use gtk::traits::{ 12 | ButtonExt, GtkListStoreExt, GtkWindowExt, TextBufferExt, TextViewExt, TreeModelExt, 13 | TreeSelectionExt, TreeViewColumnExt, TreeViewExt, WidgetExt, 14 | }; 15 | use gtk::Inhibit; 16 | 17 | pub struct HistoryWindow { 18 | window: gtk::Window, 19 | 20 | window_manager: Weak, 21 | repository_manager: Rc, 22 | 23 | commit_diff_panel: Rc, 24 | 25 | commit_button: gtk::Button, 26 | refresh_button: gtk::Button, 27 | 28 | commit_notebook: gtk::Notebook, 29 | 30 | history_treeview: gtk::TreeView, 31 | commit_textview: gtk::TextView, 32 | 33 | history_list_store: gtk::ListStore, 34 | } 35 | 36 | const COLUMN_SUBJECT: u32 = 0; 37 | const COLUMN_STATION: u32 = 1; 38 | const COLUMN_AUTHOR_NAME: u32 = 2; 39 | const COLUMN_TIME: u32 = 3; 40 | 41 | impl HistoryWindow { 42 | pub fn new( 43 | window_manager: Weak, 44 | repository_manager: Rc, 45 | ) -> Rc { 46 | let builder = gtk::Builder::from_resource("/org/sunnyone/MetalGit/history_window.ui"); 47 | 48 | let col_types = [ 49 | glib::types::Type::STRING, 50 | glib::types::Type::OBJECT, 51 | glib::types::Type::STRING, 52 | glib::types::Type::STRING, 53 | ]; 54 | 55 | let commit_diff_panel = CommitDiffPanel::new(Rc::clone(&repository_manager)); 56 | 57 | let history_window = HistoryWindow { 58 | window_manager, 59 | repository_manager, 60 | commit_diff_panel, 61 | 62 | window: builder.object("history_window").unwrap(), 63 | commit_button: builder.object("commit_button").unwrap(), 64 | refresh_button: builder.object("refresh_button").unwrap(), 65 | history_treeview: builder.object("history_treeview").unwrap(), 66 | commit_textview: builder.object("commit_textview").unwrap(), 67 | 68 | commit_notebook: builder.object("commit_notebook").unwrap(), 69 | 70 | history_list_store: gtk::ListStore::new(&col_types), 71 | }; 72 | 73 | let history_window = Rc::new(history_window); 74 | 75 | history_window.setup_history_tree(); 76 | 77 | let w = Rc::downgrade(&history_window); 78 | history_window.commit_button.connect_clicked(move |_| { 79 | w.upgrade().unwrap().commit_button_clicked(); 80 | }); 81 | 82 | let w = Rc::downgrade(&history_window); 83 | history_window.refresh_button.connect_clicked(move |_| { 84 | w.upgrade().unwrap().refresh_button_clicked(); 85 | }); 86 | 87 | let container = history_window.commit_diff_panel.container(); 88 | let label = gtk::Label::new(Some("Diff")); 89 | history_window 90 | .commit_notebook 91 | .append_page(&container, Some(&label)); 92 | 93 | history_window 94 | } 95 | 96 | fn setup_history_tree(self: &Rc) { 97 | let treeview = &self.history_treeview; 98 | let store = &self.history_list_store; 99 | 100 | treeview.set_model(Some(store)); 101 | 102 | let subject_renderer = StationCellRenderer::new(); 103 | let col = gtk::TreeViewColumn::new(); 104 | col.set_title("Subject"); 105 | col.pack_start(&subject_renderer, false); 106 | col.add_attribute(&subject_renderer, "markup", COLUMN_SUBJECT as i32); 107 | col.add_attribute(&subject_renderer, "station", COLUMN_STATION as i32); 108 | treeview.append_column(&col); 109 | 110 | let renderer = gtk::CellRendererText::new(); 111 | let col = gtk::TreeViewColumn::new(); 112 | col.set_title("Author"); 113 | col.pack_start(&renderer, false); 114 | col.add_attribute(&renderer, "text", COLUMN_AUTHOR_NAME as i32); 115 | treeview.append_column(&col); 116 | 117 | let renderer = gtk::CellRendererText::new(); 118 | let col = gtk::TreeViewColumn::new(); 119 | col.set_title("Time"); 120 | col.pack_start(&renderer, false); 121 | col.add_attribute(&renderer, "text", COLUMN_TIME as i32); 122 | treeview.append_column(&col); 123 | 124 | let commit_diff_panel = Rc::downgrade(&self.commit_diff_panel); 125 | let selection = treeview.selection(); 126 | let w = Rc::downgrade(self); 127 | selection.connect_changed(move |x| { 128 | if let Some((model, iter)) = x.selected() { 129 | let station_wrapper = 130 | model.value(&iter, COLUMN_STATION as i32) 131 | .get::() 132 | .expect("Incorrect column type"); 133 | let station = station_wrapper.get_station().unwrap(); 134 | w.upgrade().unwrap().commit_selected(&station).expect("Failed to get a commit"); 135 | 136 | if let Some(panel) = commit_diff_panel.upgrade() { 137 | panel.update_commit(station.oid).expect("Failed to update commit diff panel"); 138 | } 139 | } 140 | }); 141 | } 142 | 143 | pub fn connect_closed(&self, callback: F) 144 | where 145 | F: Fn() -> () + 'static, 146 | { 147 | self.window.connect_delete_event(move |_, _| { 148 | callback(); 149 | Inhibit(false) 150 | }); 151 | } 152 | 153 | pub fn show(&self) { 154 | self.refresh(); 155 | self.window.show_all(); 156 | } 157 | 158 | fn load_title(&self) -> Result<(), Error> { 159 | let repo = self.repository_manager.open()?; 160 | 161 | let mut title = String::new(); 162 | if let Ok(reference) = repo.head() { 163 | if let Some(head_shorthand) = reference.shorthand() { 164 | title.push('['); 165 | title.push_str(head_shorthand); 166 | title.push_str("] "); 167 | } 168 | } 169 | 170 | if let Some(path) = repo.workdir().and_then(|x| x.to_str()) { 171 | title.push('('); 172 | title.push_str(path); 173 | title.push_str(") - "); 174 | } 175 | 176 | title.push_str("Metal Git"); 177 | 178 | self.window.set_title(&title); 179 | 180 | Ok(()) 181 | } 182 | 183 | fn load_history(&self) -> Result<(), Error> { 184 | self.history_list_store.clear(); 185 | 186 | let stations = railway::collect_tree(&self.repository_manager)?; 187 | for station in stations { 188 | let subject = Self::create_subject_markup(&station); 189 | let author_name = station.author_name.clone(); 190 | let time = station.time.clone(); 191 | 192 | let mut station_wrapper = StationWrapper::new(); 193 | station_wrapper.set_station(station); 194 | 195 | self.history_list_store.insert_with_values( 196 | None, 197 | &[ 198 | (COLUMN_SUBJECT, &subject), 199 | (COLUMN_STATION, &station_wrapper), 200 | (COLUMN_AUTHOR_NAME, &author_name), 201 | (COLUMN_TIME, &time), 202 | ], 203 | ); 204 | } 205 | 206 | Ok(()) 207 | } 208 | 209 | fn create_subject_markup(station: &railway::RailwayStation) -> String { 210 | let mut markup = String::new(); 211 | 212 | for ref_name in &station.ref_names { 213 | let tag = format!( 214 | "[{}]", 215 | glib::markup_escape_text(&ref_name) 216 | ); 217 | markup.push_str(&tag); 218 | } 219 | 220 | markup.push(' '); 221 | markup.push_str(>k::glib::markup_escape_text(&station.subject)); 222 | 223 | markup 224 | } 225 | 226 | pub fn refresh(&self) { 227 | dialog_when_error!("Failed to load repository: {:?}", self.load_title()); 228 | dialog_when_error!("Failed to load history: {:?}", self.load_history()); 229 | } 230 | 231 | fn commit_button_clicked(&self) { 232 | self.window_manager.upgrade().unwrap().show_commit_window(); 233 | } 234 | 235 | fn refresh_button_clicked(&self) { 236 | self.refresh(); 237 | } 238 | 239 | fn commit_selected(&self, station: &railway::RailwayStation) -> Result<(), git2::Error> { 240 | let repo = self.repository_manager.open()?; 241 | let commit = repo.find_commit(station.oid)?; 242 | 243 | let text = format!("commit {} 244 | Author: {} <{}> 245 | Date: {} 246 | 247 | {}", 248 | station.oid, 249 | station.author_name, 250 | commit.author().email().unwrap_or(""), 251 | station.time, 252 | commit.message().unwrap_or("")); 253 | 254 | if let Some(buffer) = self.commit_textview.buffer() { 255 | buffer.set_text(&text); 256 | } 257 | 258 | Ok(()) 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate gtk; 2 | extern crate git2; 3 | extern crate glib; 4 | 5 | pub mod repository_manager; 6 | 7 | mod static_resource; 8 | #[macro_use] 9 | mod gtk_utils; 10 | 11 | mod station_renderer; 12 | mod station_cell_renderer; 13 | mod station_wrapper; 14 | 15 | mod commit_window; 16 | mod history_window; 17 | mod window_manager; 18 | mod commit_diff_panel; 19 | mod commit_diff_util; 20 | mod diff_text_view_util; 21 | 22 | mod repository_ext; 23 | 24 | pub mod railway; 25 | 26 | use std::rc::Rc; 27 | 28 | #[allow(dead_code)] 29 | fn main() { 30 | gtk::init().unwrap(); 31 | static_resource::init(); 32 | 33 | let repository_manager = repository_manager::RepositoryManager::new(); 34 | repository_manager.set_work_dir_path("."); 35 | 36 | let window_manager = Rc::new(window_manager::WindowManager::new(repository_manager)); 37 | window_manager.start(); 38 | 39 | // railway::collect_tree(".").unwrap(); 40 | 41 | gtk::main(); 42 | } 43 | -------------------------------------------------------------------------------- /src/railway.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | use std::collections::hash_map::Entry::{Occupied, Vacant}; 3 | use std::cell::RefCell; 4 | 5 | use chrono::*; 6 | 7 | use git2::{Error, Oid}; 8 | use crate::repository_manager::RepositoryManager; 9 | 10 | #[derive(Clone, PartialEq)] 11 | pub struct RailwayTrack { 12 | pub line_number: LineNumber, 13 | pub track_number: TrackNumber, 14 | pub is_active: bool, 15 | pub from_tracks: Vec, 16 | pub to_tracks: RefCell>, 17 | pub to_lines: Vec, 18 | } 19 | 20 | #[derive(PartialEq)] 21 | pub struct RailwayStation { 22 | pub tracks: Vec, 23 | pub oid: Oid, 24 | pub subject: String, 25 | pub ref_names: Vec, 26 | pub author_name: String, 27 | pub time: String, 28 | 29 | active_track_index: usize, 30 | } 31 | 32 | impl RailwayTrack { 33 | fn new(line_number: LineNumber, 34 | track_number: TrackNumber, 35 | is_active: bool, 36 | from_tracks: Vec, 37 | to_lines: Vec) 38 | -> RailwayTrack { 39 | RailwayTrack { 40 | line_number: line_number, 41 | track_number: track_number, 42 | is_active: is_active, 43 | from_tracks: from_tracks, 44 | to_lines: to_lines, 45 | to_tracks: RefCell::new(Vec::new()), 46 | } 47 | } 48 | 49 | pub fn dump_from_to(&self) -> String { 50 | let from_str = self.from_tracks 51 | .iter() 52 | .map(|x| format!("{}", x.0)) 53 | .collect::>() 54 | .join(","); 55 | let to_str = self.to_tracks 56 | .borrow() 57 | .iter() 58 | .map(|x| format!("{}", x.0)) 59 | .collect::>() 60 | .join(","); 61 | 62 | let active_char = if self.is_active { 63 | '*' 64 | } else { 65 | ' ' 66 | }; 67 | format!("[{} => {}{} => {}]", 68 | from_str, 69 | self.track_number.0, 70 | active_char, 71 | to_str) 72 | } 73 | } 74 | 75 | impl RailwayStation { 76 | fn new(commit: &git2::Commit, 77 | ref_names: Vec, 78 | tracks: Vec) 79 | -> RailwayStation { 80 | let active_track_index = tracks.iter().position(|x| x.is_active).unwrap(); 81 | 82 | let message = commit.message().unwrap_or(""); 83 | let mut message_lines = message.lines(); 84 | let first_line = message_lines.next().unwrap_or(""); 85 | 86 | let time = commit.time(); 87 | let commit_time = FixedOffset::east_opt(time.offset_minutes() * 60).unwrap().timestamp_opt(time.seconds(), 0).unwrap(); 88 | 89 | RailwayStation { 90 | tracks: tracks, 91 | active_track_index: active_track_index, 92 | oid: commit.id(), 93 | subject: first_line.to_string(), 94 | ref_names: ref_names, 95 | author_name: commit.author().name().unwrap_or("").to_string(), 96 | time: format!("{}", commit_time.format("%Y-%m-%d %H:%M:%S %Z")) 97 | } 98 | } 99 | 100 | pub fn dump_tracks(&self) -> String { 101 | self.tracks.iter().map(|x| x.dump_from_to()).collect::>().join(" | ") 102 | } 103 | 104 | pub fn active_track(&self) -> &RailwayTrack { 105 | self.tracks.get(self.active_track_index).unwrap() 106 | } 107 | } 108 | 109 | #[derive(PartialEq, Eq, Hash, PartialOrd, Ord, Clone, Debug, Copy)] 110 | pub struct LineNumber(usize); 111 | impl LineNumber { 112 | pub fn next_number(&self) -> LineNumber { 113 | LineNumber(self.0 + 1) 114 | } 115 | } 116 | 117 | #[derive(PartialEq, Eq, Hash, PartialOrd, Ord, Clone, Debug, Copy)] 118 | pub struct TrackNumber(usize); 119 | impl TrackNumber { 120 | pub fn as_usize(&self) -> usize { 121 | self.0 122 | } 123 | pub fn next_number(&self) -> TrackNumber { 124 | TrackNumber(self.0 + 1) 125 | } 126 | pub fn prev_number(&self) -> TrackNumber { 127 | TrackNumber(self.0 - 1) 128 | } 129 | } 130 | 131 | struct TrackLineMap { 132 | max_line_number: LineNumber, 133 | oid_line_map: HashMap, 134 | line_track_map: HashMap, 135 | } 136 | 137 | impl TrackLineMap { 138 | fn new() -> TrackLineMap { 139 | TrackLineMap { 140 | max_line_number: LineNumber(0), 141 | oid_line_map: HashMap::new(), 142 | line_track_map: HashMap::new(), 143 | } 144 | } 145 | 146 | fn acquire_line_number(&mut self) -> LineNumber { 147 | let line_number = self.max_line_number; 148 | self.max_line_number.0 += 1; 149 | line_number 150 | } 151 | 152 | fn is_oid_assigned(&self, oid: &Oid) -> bool { 153 | self.oid_line_map.get(oid).is_some() 154 | } 155 | 156 | fn take_line_number_or_aquire(&mut self, oid: &Oid) -> LineNumber { 157 | let line_number = match self.oid_line_map.remove(oid) { 158 | Some(line_number) => line_number, 159 | None => self.acquire_line_number(), // this require mut 160 | }; 161 | 162 | self.assign_track_number_if_required(&line_number); 163 | 164 | line_number 165 | } 166 | 167 | fn set_next_oid(&mut self, line_number: LineNumber, oid: Oid) { 168 | self.oid_line_map.insert(oid, line_number); 169 | } 170 | 171 | fn assign_track_number_if_required(&mut self, line_number: &LineNumber) { 172 | if self.line_track_map.get(line_number).is_some() { 173 | return; 174 | } 175 | 176 | let track_number = self.line_track_map 177 | .values() 178 | .max() 179 | .map(|x| (*x).next_number()) 180 | .unwrap_or_else(|| TrackNumber(0)); 181 | self.line_track_map.insert(*line_number, track_number); 182 | 183 | return; 184 | } 185 | 186 | fn line_track_numbers(&self) -> Vec<(&LineNumber, &TrackNumber)> { 187 | let mut line_track_vec = self.line_track_map.iter().collect::>(); 188 | line_track_vec.sort_by(|a, b| a.1.cmp(b.1)); 189 | 190 | line_track_vec 191 | } 192 | 193 | fn convert_line_to_track(&self, line: &LineNumber) -> Option { 194 | self.line_track_map.get(line).map(|x| *x) 195 | } 196 | 197 | fn vacuum_unused_track_numbers<'a, T>(&mut self, to_line_numbers: T) 198 | where T: Iterator 199 | { 200 | let to_lines = to_line_numbers.map(|x| *x).collect::>(); 201 | let all_lines = self.line_track_map.keys().map(|x| *x).collect::>(); 202 | let unused_lines = all_lines.difference(&to_lines).map(|x| x).collect::>(); 203 | 204 | // TODO: performance improvement 205 | for unused_line in &unused_lines { 206 | let unused_track = *self.line_track_map.get(unused_line).unwrap(); 207 | 208 | for (_line, track) in self.line_track_map.iter_mut() { 209 | if unused_track.0 < track.0 { 210 | *track = track.prev_number(); 211 | } 212 | } 213 | } 214 | 215 | for unused_line in unused_lines { 216 | self.line_track_map.remove(unused_line); 217 | } 218 | } 219 | } 220 | 221 | struct RefTable { 222 | oid_table: HashMap>, 223 | } 224 | 225 | impl RefTable { 226 | fn collect(repo: &git2::Repository) -> Result { 227 | let mut table = HashMap::>::new(); 228 | let refs = repo.references()?; 229 | for r in refs { 230 | let r = r?; 231 | if let Some(oid) = r.target() { 232 | if let Some(shorthand) = r.shorthand() { 233 | match table.entry(oid) { 234 | Occupied(mut entry) => { 235 | entry.get_mut().push(shorthand.to_owned()); 236 | } 237 | Vacant(entry) => { 238 | entry.insert(vec![shorthand.to_owned()]); 239 | } 240 | } 241 | } 242 | } 243 | } 244 | 245 | Ok(RefTable { oid_table: table }) 246 | } 247 | 248 | fn get_names_for_oid(&self, oid: &Oid) -> Vec { 249 | self.oid_table 250 | .get(oid) 251 | .map(|x| x.clone()) 252 | .unwrap_or_else(|| Vec::new()) 253 | } 254 | } 255 | 256 | pub fn collect_tree(repository_manager: &RepositoryManager) -> Result, Error> { 257 | let repo = repository_manager.open()?; 258 | 259 | let ref_table = RefTable::collect(&repo)?; 260 | 261 | let mut revwalk = repo.revwalk()?; 262 | 263 | revwalk.set_sorting(git2::Sort::TIME)?; 264 | revwalk.push_head()?; 265 | 266 | let mut track_line_map = TrackLineMap::new(); 267 | 268 | let mut stations = Vec::::new(); 269 | for oid in revwalk { 270 | let oid = oid?; 271 | let mut prev_to_map = HashMap::new(); 272 | if let Some(last_station) = stations.last() { 273 | track_line_map.vacuum_unused_track_numbers(last_station.tracks 274 | .iter() 275 | .flat_map(|x| &x.to_lines)); 276 | 277 | for track in &last_station.tracks { 278 | let mut to_tracks = Vec::new(); 279 | 280 | for to_line in track.to_lines.iter() { 281 | let to_track = track_line_map.convert_line_to_track(&to_line).unwrap(); 282 | 283 | prev_to_map.insert(*to_line, to_track); 284 | to_tracks.push(to_track); 285 | } 286 | 287 | *track.to_tracks.borrow_mut() = to_tracks; 288 | } 289 | } 290 | 291 | 292 | let commit = repo.find_commit(oid)?; 293 | let active_line_number = track_line_map.take_line_number_or_aquire(&oid); 294 | 295 | let mut is_first_non_merge = true; 296 | let mut active_to_line_numbers = Vec::::new(); 297 | for parent_id in commit.parent_ids() { 298 | let parent_line_number = if track_line_map.is_oid_assigned(&parent_id) { 299 | track_line_map.take_line_number_or_aquire(&parent_id) 300 | } else if is_first_non_merge { 301 | is_first_non_merge = false; 302 | // first parent uses this active line 303 | active_line_number 304 | } else { 305 | track_line_map.take_line_number_or_aquire(&parent_id) 306 | }; 307 | 308 | track_line_map.set_next_oid(parent_line_number, parent_id); 309 | active_to_line_numbers.push(parent_line_number); 310 | } 311 | 312 | let tracks = track_line_map.line_track_numbers() 313 | .iter() 314 | .map(|&(line_number, track_number)| { 315 | let prev_to_track = prev_to_map.get(&line_number); 316 | let mut from_lines = Vec::::new(); 317 | let mut from_tracks = Vec::::new(); 318 | if let Some(prev_to_track) = prev_to_track { 319 | from_lines.push(*line_number); 320 | from_tracks.push(*prev_to_track); 321 | } 322 | 323 | if *line_number == active_line_number { 324 | RailwayTrack::new(*line_number, 325 | *track_number, 326 | true, 327 | from_tracks, 328 | active_to_line_numbers.clone()) 329 | } else { 330 | RailwayTrack::new(*line_number, 331 | *track_number, 332 | false, 333 | from_tracks.clone(), 334 | from_lines) 335 | } 336 | }) 337 | .collect::>(); 338 | 339 | let ref_names = ref_table.get_names_for_oid(&oid); 340 | stations.push(RailwayStation::new(&commit, ref_names, tracks)); 341 | } 342 | 343 | // for station in stations.iter() { 344 | // println!("{}", station.dump_tracks()); 345 | // } 346 | Ok(stations) 347 | } 348 | -------------------------------------------------------------------------------- /src/repository_ext.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | pub trait RepositoryExt { 4 | fn get_full_path(&self, path_in_repository: &Path) -> Option; 5 | } 6 | 7 | impl RepositoryExt for git2::Repository { 8 | fn get_full_path(&self, path_in_repository: &Path) -> Option { 9 | self.workdir().map(|x| x.join(path_in_repository)) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/repository_manager.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | use git2::{Repository, Error}; 3 | use std::cell::RefCell; 4 | 5 | pub struct RepositoryManager { 6 | work_dir_path: RefCell, 7 | } 8 | 9 | impl RepositoryManager { 10 | pub fn new() -> Rc { 11 | Rc::new(RepositoryManager { work_dir_path: RefCell::new("".to_string()) }) 12 | } 13 | 14 | pub fn set_work_dir_path(&self, work_dir_path: &str) { 15 | *self.work_dir_path.borrow_mut() = work_dir_path.to_string(); 16 | } 17 | 18 | pub fn open(&self) -> Result { 19 | // TODO: check the path is set 20 | git2::Repository::discover(self.work_dir_path.borrow().as_str()) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/resources/commit_window.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 100 7 | 1 8 | 10 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 800 24 | 500 25 | False 26 | Commit Window 27 | 600 28 | 400 29 | 30 | 31 | True 32 | True 33 | 5 34 | 5 35 | 5 36 | 5 37 | 300 38 | 39 | 40 | 200 41 | True 42 | False 43 | vertical 44 | 45 | 46 | True 47 | False 48 | 49 | 50 | gtk-refresh 51 | True 52 | True 53 | True 54 | True 55 | True 56 | 57 | 58 | False 59 | True 60 | 5 61 | 0 62 | 63 | 64 | 65 | 66 | Revert 67 | True 68 | True 69 | True 70 | 71 | 72 | False 73 | True 74 | 5 75 | end 76 | 1 77 | 78 | 79 | 80 | 81 | False 82 | True 83 | 5 84 | 0 85 | 86 | 87 | 88 | 89 | True 90 | False 91 | 5 92 | 5 93 | 5 94 | 5 95 | 0 96 | none 97 | 98 | 99 | True 100 | False 101 | 102 | 103 | True 104 | True 105 | in 106 | 107 | 108 | True 109 | True 110 | natural 111 | work_tree_files_list_store 112 | False 113 | 0 114 | False 115 | 116 | 117 | multiple 118 | 119 | 120 | 121 | 122 | Filename 123 | 124 | 125 | 126 | 0 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | True 140 | False 141 | Working Directory 142 | 143 | 144 | 145 | 146 | True 147 | True 148 | 1 149 | 150 | 151 | 152 | 153 | True 154 | False 155 | 156 | 157 | ^ Unstage 158 | True 159 | True 160 | True 161 | 162 | 163 | False 164 | True 165 | 5 166 | 0 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | v Stage 175 | True 176 | True 177 | True 178 | 179 | 180 | False 181 | True 182 | 5 183 | end 184 | 2 185 | 186 | 187 | 188 | 189 | False 190 | True 191 | 5 192 | 2 193 | 194 | 195 | 196 | 197 | True 198 | False 199 | 0 200 | none 201 | 202 | 203 | True 204 | False 205 | 5 206 | 5 207 | 5 208 | 5 209 | 210 | 211 | True 212 | True 213 | in 214 | 215 | 216 | True 217 | True 218 | staged_files_list_store 219 | False 220 | 221 | 222 | 223 | 224 | 225 | Filename 226 | 227 | 228 | 229 | 0 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | True 243 | False 244 | Staging Area 245 | 246 | 247 | 248 | 249 | True 250 | True 251 | 3 252 | 253 | 254 | 255 | 256 | False 257 | False 258 | 259 | 260 | 261 | 262 | True 263 | True 264 | vertical 265 | 400 266 | 267 | 268 | True 269 | True 270 | 5 271 | 4 272 | 5 273 | 5 274 | in 275 | 276 | 277 | 400 278 | True 279 | True 280 | False 281 | 282 | 283 | 284 | 285 | False 286 | True 287 | 288 | 289 | 290 | 291 | True 292 | False 293 | vertical 294 | 295 | 296 | True 297 | True 298 | 5 299 | 5 300 | 5 301 | 5 302 | in 303 | 304 | 305 | True 306 | True 307 | 308 | 309 | 310 | 311 | True 312 | True 313 | 0 314 | 315 | 316 | 317 | 318 | True 319 | False 320 | 5 321 | 5 322 | 5 323 | 5 324 | 325 | 326 | 327 | 328 | 329 | amend 330 | True 331 | True 332 | False 333 | 0 334 | True 335 | 336 | 337 | False 338 | True 339 | 1 340 | 341 | 342 | 343 | 344 | Commit 345 | True 346 | True 347 | True 348 | 349 | 350 | False 351 | True 352 | end 353 | 2 354 | 355 | 356 | 357 | 358 | False 359 | True 360 | 1 361 | 362 | 363 | 364 | 365 | False 366 | False 367 | 368 | 369 | 370 | 371 | True 372 | True 373 | 374 | 375 | 376 | 377 | 378 | 379 | -------------------------------------------------------------------------------- /src/resources/history_window.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | False 7 | History Window 8 | 600 9 | 360 10 | 11 | 12 | True 13 | False 14 | vertical 15 | 16 | 17 | True 18 | False 19 | end 20 | 21 | 22 | Commit 23 | True 24 | True 25 | True 26 | 27 | 28 | False 29 | True 30 | 0 31 | 32 | 33 | 34 | 35 | gtk-refresh 36 | True 37 | True 38 | True 39 | True 40 | True 41 | 42 | 43 | True 44 | True 45 | 1 46 | 47 | 48 | 49 | 50 | False 51 | True 52 | 0 53 | 54 | 55 | 56 | 57 | True 58 | True 59 | vertical 60 | 100 61 | True 62 | 63 | 64 | True 65 | True 66 | in 67 | 68 | 69 | True 70 | True 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | False 79 | True 80 | 81 | 82 | 83 | 84 | True 85 | True 86 | 87 | 88 | True 89 | True 90 | False 91 | 92 | 93 | 94 | 95 | True 96 | False 97 | Commit 98 | 99 | 100 | False 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | True 118 | True 119 | 120 | 121 | 122 | 123 | True 124 | True 125 | 2 126 | 127 | 128 | 129 | 130 | True 131 | False 132 | 10 133 | 10 134 | 6 135 | 6 136 | vertical 137 | 2 138 | 139 | 140 | False 141 | True 142 | 3 143 | 144 | 145 | 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /src/resources/resources.gresource: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunnyone/metal-git/1f0a3e4bfe7db41197e37106b30188c03f89b010/src/resources/resources.gresource -------------------------------------------------------------------------------- /src/resources/resources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | commit_window.ui 5 | history_window.ui 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/static_resource.rs: -------------------------------------------------------------------------------- 1 | pub fn init() { 2 | let res_bytes = include_bytes!("resources/resources.gresource"); 3 | // gbytes and resource will not be freed 4 | let gbytes = gtk::glib::Bytes::from(res_bytes); 5 | let resource = gtk::gio::Resource::from_data(&gbytes).unwrap(); 6 | gtk::gio::functions::resources_register(&resource) 7 | } 8 | -------------------------------------------------------------------------------- /src/station_cell_renderer.rs: -------------------------------------------------------------------------------- 1 | use gtk::glib::prelude::*; 2 | use gtk::glib::subclass::prelude::*; 3 | use gtk::subclass::prelude::*; 4 | use std::cell::RefCell; 5 | use glib; 6 | use std::sync::OnceLock; 7 | 8 | const PROP_NUM: usize = 1; 9 | const PROP_STATION: usize = 2; 10 | 11 | mod imp { 12 | use glib::IsA; 13 | use gtk::Widget; 14 | use crate::station_wrapper::StationWrapper; 15 | use crate::station_renderer; 16 | use super::*; 17 | 18 | #[derive(Default)] 19 | pub struct StationCellRendererImpl { 20 | pub num: RefCell, 21 | pub station_wrapper: RefCell, 22 | } 23 | 24 | #[glib::object_subclass] 25 | impl ObjectSubclass for StationCellRendererImpl { 26 | const NAME: &'static str = "StationCellRenderer"; 27 | type Type = super::StationCellRenderer; 28 | type ParentType = gtk::CellRendererText; 29 | type Interfaces = (); 30 | } 31 | 32 | impl ObjectImpl for StationCellRendererImpl { 33 | fn properties() -> &'static [glib::ParamSpec] { 34 | static PROPERTIES: OnceLock> = OnceLock::new(); 35 | 36 | let value = PROPERTIES.get_or_init(|| { 37 | vec![ 38 | glib::ParamSpecInt::new( 39 | "num", 40 | "Num", 41 | "Num", 42 | 0, 43 | 65535, 44 | 0, 45 | glib::ParamFlags::READWRITE, 46 | ), 47 | glib::ParamSpecObject::new( 48 | "station", 49 | "Station", 50 | "Station", 51 | StationWrapper::static_type(), 52 | glib::ParamFlags::READWRITE, 53 | ), 54 | ] 55 | }); 56 | 57 | value.as_ref() 58 | } 59 | 60 | fn set_property(&self, _id: usize, value: &glib::Value, _pspec: &glib::ParamSpec) { 61 | match _id { 62 | PROP_NUM => { 63 | let name = value 64 | .get() 65 | .expect("type conformity checked by `Object::set_property`"); 66 | self.num.replace(name); 67 | } 68 | PROP_STATION => { 69 | let station_wrapper = value 70 | .get() 71 | .expect("type conformity checked by `Object::set_property`"); 72 | self.station_wrapper.replace(station_wrapper); 73 | } 74 | _ => unimplemented!(), 75 | } 76 | } 77 | 78 | fn property(&self, _id: usize, _pspec: &glib::ParamSpec) -> glib::Value { 79 | match _id { 80 | PROP_NUM => self.num.borrow().to_value(), 81 | PROP_STATION => self.station_wrapper.borrow().to_value(), 82 | _ => unimplemented!(), 83 | } 84 | } 85 | } 86 | 87 | impl CellRendererImpl for StationCellRendererImpl { 88 | fn render>( 89 | &self, 90 | cr: >k::cairo::Context, 91 | widget: &P, 92 | background_area: >k::Rectangle, 93 | cell_area: >k::Rectangle, 94 | flags: gtk::CellRendererState, 95 | ) { 96 | let rendered = self.station_wrapper.borrow().get_station().map(|x| { 97 | station_renderer::render(&x, &cr, &background_area, &cell_area).ok() 98 | }).flatten(); 99 | 100 | match rendered { 101 | Some((new_bg_rect, new_cell_rect)) => self.parent_render(cr, widget, &new_bg_rect, &new_cell_rect, flags), 102 | None => self.parent_render(cr, widget, background_area, cell_area, flags) 103 | } 104 | } 105 | } 106 | 107 | impl CellRendererTextImpl for StationCellRendererImpl {} 108 | } 109 | 110 | glib::wrapper! { 111 | pub struct StationCellRenderer(ObjectSubclass) @extends gtk::CellRenderer; 112 | } 113 | 114 | impl StationCellRenderer { 115 | pub fn new() -> Self { 116 | gtk::glib::Object::new(&[]) 117 | } 118 | } -------------------------------------------------------------------------------- /src/station_renderer.rs: -------------------------------------------------------------------------------- 1 | use std::f64::consts; 2 | use crate::railway; 3 | use gtk::cairo::{Error, LineCap}; 4 | 5 | fn commit_dot_box_size(cell_height: i32) -> (i32, i32) { 6 | (cell_height, cell_height) 7 | } 8 | 9 | fn commit_dot_radius(_box_width: i32, box_height: i32) -> i32 { 10 | (box_height as f64 / 3.5) as i32 11 | } 12 | 13 | fn calc_tracks_width(station: &railway::RailwayStation, box_width: i32) -> i32 { 14 | let max_track_number = station.tracks.iter().map(|x| x.track_number.as_usize()).max(); 15 | (max_track_number.unwrap_or(0) + 1) as i32 * box_width 16 | } 17 | 18 | fn merge_line_offset(box_height: i32) -> i32 { 19 | box_height / 4 20 | } 21 | 22 | fn box_x(start_x: i32, box_width: i32, index: usize) -> i32 { 23 | start_x + index as i32 * box_width 24 | } 25 | 26 | pub fn render(station: &railway::RailwayStation, 27 | context: >k::cairo::Context, 28 | bg_area: >k::Rectangle, 29 | cell_area: >k::Rectangle) 30 | -> Result<(gtk::Rectangle, gtk::Rectangle), Error> { 31 | 32 | let (box_width, box_height) = commit_dot_box_size(cell_area.height()); 33 | let dot_radius = commit_dot_radius(box_width, box_height); 34 | 35 | // let c = 0.1 * ((private.num + 1) as f64); 36 | let c = 0.1; 37 | context.set_source_rgb(c, c, 0.8); 38 | context.set_line_cap(LineCap::Square); 39 | 40 | for track in &station.tracks { 41 | let track_box_x = box_x(cell_area.x(), box_width, track.track_number.as_usize()) as f64; 42 | let track_box_y = cell_area.y() as f64; 43 | 44 | let center_x = track_box_x + box_width as f64 / 2.0; 45 | let center_y = track_box_y + box_height as f64 / 2.0; 46 | 47 | let merge_line_offset = merge_line_offset(box_height) as f64; 48 | 49 | if !track.from_tracks.is_empty() { 50 | let top_y = bg_area.y(); 51 | 52 | context.move_to(center_x, top_y as f64 + merge_line_offset); 53 | context.line_to(center_x, center_y as f64); 54 | context.stroke()?; 55 | 56 | for num in &track.from_tracks { 57 | context.move_to((box_x(cell_area.x(), box_width, num.as_usize()) + 58 | box_width / 2) as f64 + 1.0, 59 | top_y as f64); 60 | context.line_to(center_x, top_y as f64 + merge_line_offset); 61 | context.stroke()?; 62 | } 63 | } 64 | 65 | if !track.to_tracks.borrow().is_empty() { 66 | let bottom_y = bg_area.y() + bg_area.height(); 67 | 68 | context.move_to(center_x, center_y as f64); 69 | context.line_to(center_x, bottom_y as f64 - merge_line_offset); 70 | context.stroke()?; 71 | 72 | for num in track.to_tracks.borrow().iter() { 73 | context.move_to(center_x, bottom_y as f64 - merge_line_offset); 74 | context.line_to((box_x(cell_area.x(), box_width, num.as_usize()) + 75 | box_width / 2) as f64 + 1.0, 76 | bottom_y as f64); 77 | context.stroke()?; 78 | } 79 | } 80 | 81 | if track.is_active { 82 | // the center of circle is a little up because a bottom line is shorter than a top line (a more clean way is required) 83 | context.arc(center_x, 84 | center_y - merge_line_offset / 3.0, 85 | dot_radius as f64, 86 | 0.0, 87 | 2.0 * consts::PI); 88 | context.fill()?; 89 | } 90 | 91 | } 92 | 93 | let tracks_width = calc_tracks_width(station, box_width); 94 | Ok((gtk::Rectangle::new(bg_area.x() + tracks_width, bg_area.y(), bg_area.width(), bg_area.height()), 95 | gtk::Rectangle::new(cell_area.x() + tracks_width, cell_area.y(), cell_area.width(), cell_area.height()))) 96 | } 97 | -------------------------------------------------------------------------------- /src/station_wrapper.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::rc::Rc; 3 | 4 | use gtk::glib::subclass::prelude::*; 5 | 6 | use crate::railway::RailwayStation; 7 | 8 | mod imp { 9 | use super::*; 10 | 11 | #[derive(Default)] 12 | pub struct StationWrapperImpl { 13 | pub station: RefCell>>, 14 | } 15 | 16 | #[glib::object_subclass] 17 | impl ObjectSubclass for StationWrapperImpl { 18 | const NAME: &'static str = "StationWrapper"; 19 | type Type = super::StationWrapper; 20 | type ParentType = gtk::glib::Object; 21 | type Interfaces = (); 22 | } 23 | 24 | // Trait shared by all GObjects 25 | impl ObjectImpl for StationWrapperImpl {} 26 | } 27 | 28 | gtk::glib::wrapper! { 29 | pub struct StationWrapper(ObjectSubclass); 30 | } 31 | 32 | impl StationWrapper { 33 | pub fn new() -> Self { 34 | gtk::glib::Object::new(&[]) 35 | } 36 | 37 | pub fn get_impl(&self) -> &imp::StationWrapperImpl { 38 | imp::StationWrapperImpl::from_instance(self) 39 | } 40 | 41 | pub fn get_station(&self) -> Option> { 42 | let priv_ = imp::StationWrapperImpl::from_instance(self); 43 | let station_ref = priv_.station.borrow(); 44 | match station_ref.as_ref() { 45 | Some(x) => Some(Rc::clone(x)), 46 | None => None, 47 | } 48 | } 49 | 50 | pub fn set_station(&mut self, station: RailwayStation) { 51 | let priv_ = imp::StationWrapperImpl::from_instance(self); 52 | priv_.station.replace(Some(Rc::new(station))); 53 | } 54 | } 55 | 56 | impl Default for StationWrapper { 57 | fn default() -> Self { 58 | Self::new() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/window_manager.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | use std::cell::RefCell; 3 | use crate::history_window::HistoryWindow; 4 | use crate::commit_window::CommitWindow; 5 | use crate::repository_manager::RepositoryManager; 6 | 7 | pub struct WindowManager { 8 | windows: RefCell>, 9 | } 10 | 11 | struct Windows { 12 | history_window: Rc, 13 | commit_window: Rc, 14 | } 15 | 16 | impl WindowManager { 17 | pub fn new(repository_manager: Rc) -> Rc { 18 | let window_manager = Rc::new(WindowManager { windows: RefCell::new(None) }); 19 | 20 | let windows = Windows { 21 | history_window: HistoryWindow::new(Rc::downgrade(&window_manager), 22 | repository_manager.clone()), 23 | commit_window: CommitWindow::new(repository_manager.clone()), 24 | }; 25 | 26 | *window_manager.windows.borrow_mut() = Some(windows); 27 | 28 | window_manager 29 | } 30 | 31 | fn with_windows(&self, func: F) 32 | where F: Fn(&Windows) 33 | { 34 | let windows_ref = self.windows.borrow(); 35 | let windows = windows_ref.as_ref().unwrap(); 36 | func(windows); 37 | } 38 | 39 | pub fn start(&self) { 40 | self.with_windows(|windows| { 41 | windows.history_window.connect_closed(|| { 42 | gtk::main_quit(); 43 | }); 44 | 45 | windows.history_window.show(); 46 | }); 47 | } 48 | 49 | pub fn show_commit_window(&self) { 50 | self.with_windows(|windows| { 51 | // TODO: messaging should be done in another class 52 | let w = Rc::downgrade(&windows.history_window); 53 | windows.commit_window.connect_commited(move || { 54 | w.upgrade().unwrap().refresh(); 55 | }); 56 | 57 | windows.commit_window.show(); 58 | }); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/railway_test.rs: -------------------------------------------------------------------------------- 1 | extern crate git2; 2 | extern crate tempdir; 3 | extern crate metal_git; 4 | 5 | mod util; 6 | use crate::util::test_repo::TestRepo; 7 | use metal_git::railway; 8 | 9 | #[test] 10 | pub fn collect_tree_flat_two() { 11 | let test_repo = TestRepo::flat_two(); 12 | 13 | let stations = railway::collect_tree(&test_repo.repository_manager()).unwrap(); 14 | 15 | println!("Stations:"); 16 | for station in stations.iter() { 17 | println!("{}", station.dump_tracks()); 18 | } 19 | 20 | assert_eq!(2, stations.len()); 21 | let (b, a) = (&stations[0], &stations[1]); 22 | assert_eq!("B", b.subject); 23 | assert_eq!("A", a.subject); 24 | 25 | assert_eq!("[ => 0* => 0]", b.dump_tracks()); 26 | assert_eq!("[0 => 0* => ]", a.dump_tracks()); 27 | } 28 | 29 | 30 | #[test] 31 | pub fn collect_tree_two_parent_two_child() { 32 | let test_repo = TestRepo::two_parent_two_child(); 33 | 34 | let stations = railway::collect_tree(&test_repo.repository_manager()).unwrap(); 35 | 36 | println!("Stations:"); 37 | for station in stations.iter() { 38 | println!("{}", station.dump_tracks()); 39 | } 40 | 41 | assert_eq!(4, stations.len()); 42 | let (d, c, b, a) = (&stations[0], &stations[1], &stations[2], &stations[3]); 43 | assert_eq!("D", d.subject); 44 | assert_eq!("C", c.subject); 45 | assert_eq!("B", b.subject); 46 | assert_eq!("A", a.subject); 47 | 48 | assert_eq!("[ => 0* => 0,1] | [ => 1 => ]", d.dump_tracks()); 49 | assert_eq!("[0 => 0* => 0] | [1 => 1 => 1]", c.dump_tracks()); 50 | assert_eq!("[0 => 0 => 0] | [1 => 1* => 0]", b.dump_tracks()); 51 | assert_eq!("[0 => 0* => ]", a.dump_tracks()); 52 | } 53 | 54 | #[test] 55 | pub fn collect_tree_branch_merge_branch_merge() { 56 | let test_repo = TestRepo::branch_merge_branch_merge(); 57 | // test_repo.set_debug(); 58 | 59 | let stations = railway::collect_tree(&test_repo.repository_manager()).unwrap(); 60 | 61 | println!("Stations:"); 62 | for station in stations.iter() { 63 | println!("{}", station.dump_tracks()); 64 | } 65 | 66 | assert_eq!(6, stations.len()); 67 | let (f, e, d, c, b, a) = (&stations[0], 68 | &stations[1], 69 | &stations[2], 70 | &stations[3], 71 | &stations[4], 72 | &stations[5]); 73 | assert_eq!("F", f.subject); 74 | assert_eq!("E", e.subject); 75 | assert_eq!("D", d.subject); 76 | assert_eq!("C", c.subject); 77 | assert_eq!("B", b.subject); 78 | assert_eq!("A", a.subject); 79 | 80 | assert_eq!("[ => 0* => 0,1,2] | [ => 1 => ] | [ => 2 => ]", 81 | f.dump_tracks()); 82 | assert_eq!("[0 => 0 => 0] | [1 => 1 => 1] | [2 => 2* => 0]", 83 | e.dump_tracks()); 84 | assert_eq!("[0 => 0* => 0,2] | [1 => 1 => 1] | [ => 2 => ]", 85 | d.dump_tracks()); 86 | assert_eq!("[0 => 0 => 0] | [1 => 1* => 0] | [2 => 2 => 1]", 87 | c.dump_tracks()); 88 | assert_eq!("[0 => 0 => 0] | [1 => 1* => 0]", b.dump_tracks()); 89 | assert_eq!("[0 => 0* => ]", a.dump_tracks()); 90 | } 91 | -------------------------------------------------------------------------------- /tests/repository_manager_test.rs: -------------------------------------------------------------------------------- 1 | extern crate git2; 2 | extern crate tempdir; 3 | 4 | extern crate metal_git; 5 | 6 | mod util; 7 | 8 | use metal_git::repository_manager; 9 | use crate::util::test_repo::TestRepo; 10 | 11 | #[test] 12 | pub fn open() { 13 | let test_repo = TestRepo::flat_two(); 14 | 15 | let r = repository_manager::RepositoryManager::new(); 16 | r.set_work_dir_path(test_repo.path().to_str().unwrap()); 17 | 18 | let _ = r.open(); 19 | // expect not to panic 20 | } 21 | -------------------------------------------------------------------------------- /tests/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod test_repo; 2 | 3 | -------------------------------------------------------------------------------- /tests/util/test_repo.rs: -------------------------------------------------------------------------------- 1 | extern crate git2; 2 | extern crate tempdir; 3 | extern crate metal_git; 4 | 5 | use git2::{Repository, Signature, Commit, BranchType}; 6 | use std::thread::sleep; 7 | use tempdir::TempDir; 8 | use std::time::Duration; 9 | use std::path::Path; 10 | use std::cell::Cell; 11 | use std::rc::Rc; 12 | use std::process::Command; 13 | 14 | use metal_git::repository_manager::RepositoryManager; 15 | 16 | pub struct TestRepo { 17 | temp_dir: TempDir, 18 | debug_mode: Cell 19 | } 20 | 21 | impl Drop for TestRepo { 22 | fn drop(&mut self) { 23 | if self.debug_mode.get() { 24 | println!("TestRepo is here: {}", self.temp_dir.path().to_string_lossy()); 25 | 26 | let output = Command::new("sh") 27 | .arg("-c") 28 | .arg("git log --graph --pretty=format:%s --date-order | sed -e 's/^/ \\/\\/ /g'") 29 | // .arg("git log --graph --oneline --date-order | sed -e 's/^/ \\/\\/ /g'") 30 | .current_dir(self.temp_dir.path()) 31 | .output() 32 | .expect("Failed to execute sh/git command"); 33 | 34 | println!("git log: \n{}", String::from_utf8_lossy(&output.stdout)); 35 | 36 | println!("Waiting..."); 37 | sleep(Duration::from_secs(60)); 38 | println!("Done"); 39 | } 40 | } 41 | } 42 | 43 | fn test_commit<'repo, 'a>(repo: &'repo Repository, 44 | branch_name: &'repo str, 45 | message: &'repo str, 46 | parents: &'a [&'a Commit]) -> Commit<'repo> { 47 | let signature = Signature::now("test commit", "test@example.com").unwrap(); 48 | 49 | let treebuilder = repo.treebuilder(None).unwrap(); 50 | let tree_oid = treebuilder.write().unwrap(); 51 | let tree = repo.find_tree(tree_oid).unwrap(); 52 | 53 | // TODO: correct check 54 | let branch_exists = repo.find_branch(branch_name, BranchType::Local).is_ok(); 55 | 56 | let ref_name = format!("refs/heads/{}", branch_name); 57 | let commit_oid = repo.commit(if branch_exists { Some(&ref_name) } else { None }, 58 | &signature, 59 | &signature, 60 | message, 61 | &tree, 62 | parents 63 | ).expect("Failed to commit"); 64 | 65 | let commit = repo.find_commit(commit_oid).unwrap(); 66 | 67 | if !branch_exists { 68 | repo.branch(branch_name, &commit, true).unwrap(); 69 | } 70 | 71 | commit 72 | } 73 | 74 | #[allow(dead_code)] 75 | impl TestRepo { 76 | fn new(prefix: &str) -> TestRepo { 77 | let prefix_testrepo = format!("testrepo-{}", prefix); 78 | let temp_dir = TempDir::new(&prefix_testrepo).expect("Failed to create tempdir"); 79 | let _ = Repository::init(temp_dir.path()).unwrap(); 80 | 81 | TestRepo { 82 | temp_dir: temp_dir, 83 | debug_mode: Cell::new(false) 84 | } 85 | } 86 | 87 | pub fn path(&self) -> &Path { 88 | self.temp_dir.path() 89 | } 90 | 91 | pub fn repository_manager(&self) -> Rc { 92 | let r = RepositoryManager::new(); 93 | r.set_work_dir_path(self.path().to_str().unwrap()); 94 | r 95 | } 96 | 97 | fn repository(&self) -> Repository { 98 | Repository::open(self.path()).expect("Failed to open a test repository.") 99 | } 100 | 101 | #[allow(dead_code)] 102 | pub fn set_debug(&self) { 103 | self.debug_mode.set(true); 104 | } 105 | 106 | pub fn empty() -> TestRepo { 107 | let test_repo = Self::new("empty"); 108 | test_repo 109 | } 110 | 111 | // get comments with set_debug() 112 | // * Single commit 113 | pub fn single() -> TestRepo { 114 | let test_repo = Self::new("single"); 115 | let repo = test_repo.repository(); 116 | test_commit(&repo, "master", "Single commit", &[]); 117 | 118 | test_repo 119 | } 120 | 121 | // * B 122 | // * A 123 | pub fn flat_two() -> TestRepo { 124 | let test_repo = Self::new("flat-two"); 125 | let repo = test_repo.repository(); 126 | 127 | let a = test_commit(&repo, "master", "A", &[]); 128 | let _ = test_commit(&repo, "master", "B", &[&a]); 129 | 130 | test_repo 131 | } 132 | 133 | // * D 134 | // |\ 135 | // * | C 136 | // | * B 137 | // |/ 138 | // * A 139 | pub fn two_parent_two_child() -> TestRepo { 140 | let test_repo = Self::new("two_parent_two_child"); 141 | let repo = test_repo.repository(); 142 | 143 | let a = test_commit(&repo, "branch1", "A", &[]); 144 | let b = test_commit(&repo, "branch1", "B", &[&a]); 145 | let c = test_commit(&repo, "master", "C", &[&a]); 146 | 147 | let _ = test_commit(&repo, "master", "D", &[&c,&b]); 148 | 149 | test_repo 150 | } 151 | 152 | 153 | pub fn branch_merge_branch_merge() -> TestRepo { 154 | let test_repo = Self::new("branch_merge_branch_merge"); 155 | 156 | let repo = test_repo.repository(); 157 | 158 | // FIXME: how to create time ordered fast? 159 | let a = test_commit(&repo, "master", "A", &[]); 160 | sleep(Duration::from_secs(1)); 161 | let b = test_commit(&repo, "branch1", "B", &[&a]); 162 | sleep(Duration::from_secs(1)); 163 | let c = test_commit(&repo, "branchX", "C", &[&a]); // most outer line 164 | sleep(Duration::from_secs(1)); 165 | let d = test_commit(&repo, "master", "D", &[&a,&b]); 166 | sleep(Duration::from_secs(1)); 167 | let e = test_commit(&repo, "branch2", "E", &[&d]); 168 | sleep(Duration::from_secs(1)); 169 | let _ = test_commit(&repo, "master", "F", &[&d,&c,&e]); 170 | 171 | test_repo 172 | } 173 | } --------------------------------------------------------------------------------