A tool to help managing forked repos with their own history
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

Initial commit

webbeef def01543

+3231
+1
.gitignore
··· 1 + target/
+858
Cargo.lock
··· 1 + # This file is automatically @generated by Cargo. 2 + # It is not intended for manual editing. 3 + version = 4 4 + 5 + [[package]] 6 + name = "anstream" 7 + version = "0.6.21" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" 10 + dependencies = [ 11 + "anstyle", 12 + "anstyle-parse", 13 + "anstyle-query", 14 + "anstyle-wincon", 15 + "colorchoice", 16 + "is_terminal_polyfill", 17 + "utf8parse", 18 + ] 19 + 20 + [[package]] 21 + name = "anstyle" 22 + version = "1.0.13" 23 + source = "registry+https://github.com/rust-lang/crates.io-index" 24 + checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" 25 + 26 + [[package]] 27 + name = "anstyle-parse" 28 + version = "0.2.7" 29 + source = "registry+https://github.com/rust-lang/crates.io-index" 30 + checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 31 + dependencies = [ 32 + "utf8parse", 33 + ] 34 + 35 + [[package]] 36 + name = "anstyle-query" 37 + version = "1.1.5" 38 + source = "registry+https://github.com/rust-lang/crates.io-index" 39 + checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" 40 + dependencies = [ 41 + "windows-sys", 42 + ] 43 + 44 + [[package]] 45 + name = "anstyle-wincon" 46 + version = "3.0.11" 47 + source = "registry+https://github.com/rust-lang/crates.io-index" 48 + checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" 49 + dependencies = [ 50 + "anstyle", 51 + "once_cell_polyfill", 52 + "windows-sys", 53 + ] 54 + 55 + [[package]] 56 + name = "anyhow" 57 + version = "1.0.100" 58 + source = "registry+https://github.com/rust-lang/crates.io-index" 59 + checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" 60 + 61 + [[package]] 62 + name = "bitflags" 63 + version = "2.10.0" 64 + source = "registry+https://github.com/rust-lang/crates.io-index" 65 + checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" 66 + 67 + [[package]] 68 + name = "cc" 69 + version = "1.2.53" 70 + source = "registry+https://github.com/rust-lang/crates.io-index" 71 + checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" 72 + dependencies = [ 73 + "find-msvc-tools", 74 + "jobserver", 75 + "libc", 76 + "shlex", 77 + ] 78 + 79 + [[package]] 80 + name = "cfg-if" 81 + version = "1.0.4" 82 + source = "registry+https://github.com/rust-lang/crates.io-index" 83 + checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 84 + 85 + [[package]] 86 + name = "clap" 87 + version = "4.5.54" 88 + source = "registry+https://github.com/rust-lang/crates.io-index" 89 + checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" 90 + dependencies = [ 91 + "clap_builder", 92 + "clap_derive", 93 + ] 94 + 95 + [[package]] 96 + name = "clap_builder" 97 + version = "4.5.54" 98 + source = "registry+https://github.com/rust-lang/crates.io-index" 99 + checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" 100 + dependencies = [ 101 + "anstream", 102 + "anstyle", 103 + "clap_lex", 104 + "strsim", 105 + ] 106 + 107 + [[package]] 108 + name = "clap_derive" 109 + version = "4.5.49" 110 + source = "registry+https://github.com/rust-lang/crates.io-index" 111 + checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" 112 + dependencies = [ 113 + "heck", 114 + "proc-macro2", 115 + "quote", 116 + "syn", 117 + ] 118 + 119 + [[package]] 120 + name = "clap_lex" 121 + version = "0.7.7" 122 + source = "registry+https://github.com/rust-lang/crates.io-index" 123 + checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" 124 + 125 + [[package]] 126 + name = "colorchoice" 127 + version = "1.0.4" 128 + source = "registry+https://github.com/rust-lang/crates.io-index" 129 + checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 130 + 131 + [[package]] 132 + name = "diffy" 133 + version = "0.4.2" 134 + source = "registry+https://github.com/rust-lang/crates.io-index" 135 + checksum = "b545b8c50194bdd008283985ab0b31dba153cfd5b3066a92770634fbc0d7d291" 136 + dependencies = [ 137 + "nu-ansi-term", 138 + ] 139 + 140 + [[package]] 141 + name = "displaydoc" 142 + version = "0.2.5" 143 + source = "registry+https://github.com/rust-lang/crates.io-index" 144 + checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 145 + dependencies = [ 146 + "proc-macro2", 147 + "quote", 148 + "syn", 149 + ] 150 + 151 + [[package]] 152 + name = "equivalent" 153 + version = "1.0.2" 154 + source = "registry+https://github.com/rust-lang/crates.io-index" 155 + checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 156 + 157 + [[package]] 158 + name = "errno" 159 + version = "0.3.14" 160 + source = "registry+https://github.com/rust-lang/crates.io-index" 161 + checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" 162 + dependencies = [ 163 + "libc", 164 + "windows-sys", 165 + ] 166 + 167 + [[package]] 168 + name = "fastrand" 169 + version = "2.3.0" 170 + source = "registry+https://github.com/rust-lang/crates.io-index" 171 + checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 172 + 173 + [[package]] 174 + name = "find-msvc-tools" 175 + version = "0.1.8" 176 + source = "registry+https://github.com/rust-lang/crates.io-index" 177 + checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" 178 + 179 + [[package]] 180 + name = "forkme" 181 + version = "0.1.0" 182 + dependencies = [ 183 + "anyhow", 184 + "clap", 185 + "diffy", 186 + "git2", 187 + "serde", 188 + "tempfile", 189 + "toml", 190 + "walkdir", 191 + ] 192 + 193 + [[package]] 194 + name = "form_urlencoded" 195 + version = "1.2.2" 196 + source = "registry+https://github.com/rust-lang/crates.io-index" 197 + checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" 198 + dependencies = [ 199 + "percent-encoding", 200 + ] 201 + 202 + [[package]] 203 + name = "getrandom" 204 + version = "0.3.4" 205 + source = "registry+https://github.com/rust-lang/crates.io-index" 206 + checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" 207 + dependencies = [ 208 + "cfg-if", 209 + "libc", 210 + "r-efi", 211 + "wasip2", 212 + ] 213 + 214 + [[package]] 215 + name = "git2" 216 + version = "0.20.3" 217 + source = "registry+https://github.com/rust-lang/crates.io-index" 218 + checksum = "3e2b37e2f62729cdada11f0e6b3b6fe383c69c29fc619e391223e12856af308c" 219 + dependencies = [ 220 + "bitflags", 221 + "libc", 222 + "libgit2-sys", 223 + "log", 224 + "openssl-probe", 225 + "openssl-sys", 226 + "url", 227 + ] 228 + 229 + [[package]] 230 + name = "hashbrown" 231 + version = "0.16.1" 232 + source = "registry+https://github.com/rust-lang/crates.io-index" 233 + checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" 234 + 235 + [[package]] 236 + name = "heck" 237 + version = "0.5.0" 238 + source = "registry+https://github.com/rust-lang/crates.io-index" 239 + checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 240 + 241 + [[package]] 242 + name = "icu_collections" 243 + version = "2.1.1" 244 + source = "registry+https://github.com/rust-lang/crates.io-index" 245 + checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" 246 + dependencies = [ 247 + "displaydoc", 248 + "potential_utf", 249 + "yoke", 250 + "zerofrom", 251 + "zerovec", 252 + ] 253 + 254 + [[package]] 255 + name = "icu_locale_core" 256 + version = "2.1.1" 257 + source = "registry+https://github.com/rust-lang/crates.io-index" 258 + checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" 259 + dependencies = [ 260 + "displaydoc", 261 + "litemap", 262 + "tinystr", 263 + "writeable", 264 + "zerovec", 265 + ] 266 + 267 + [[package]] 268 + name = "icu_normalizer" 269 + version = "2.1.1" 270 + source = "registry+https://github.com/rust-lang/crates.io-index" 271 + checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" 272 + dependencies = [ 273 + "icu_collections", 274 + "icu_normalizer_data", 275 + "icu_properties", 276 + "icu_provider", 277 + "smallvec", 278 + "zerovec", 279 + ] 280 + 281 + [[package]] 282 + name = "icu_normalizer_data" 283 + version = "2.1.1" 284 + source = "registry+https://github.com/rust-lang/crates.io-index" 285 + checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" 286 + 287 + [[package]] 288 + name = "icu_properties" 289 + version = "2.1.2" 290 + source = "registry+https://github.com/rust-lang/crates.io-index" 291 + checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" 292 + dependencies = [ 293 + "icu_collections", 294 + "icu_locale_core", 295 + "icu_properties_data", 296 + "icu_provider", 297 + "zerotrie", 298 + "zerovec", 299 + ] 300 + 301 + [[package]] 302 + name = "icu_properties_data" 303 + version = "2.1.2" 304 + source = "registry+https://github.com/rust-lang/crates.io-index" 305 + checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" 306 + 307 + [[package]] 308 + name = "icu_provider" 309 + version = "2.1.1" 310 + source = "registry+https://github.com/rust-lang/crates.io-index" 311 + checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" 312 + dependencies = [ 313 + "displaydoc", 314 + "icu_locale_core", 315 + "writeable", 316 + "yoke", 317 + "zerofrom", 318 + "zerotrie", 319 + "zerovec", 320 + ] 321 + 322 + [[package]] 323 + name = "idna" 324 + version = "1.1.0" 325 + source = "registry+https://github.com/rust-lang/crates.io-index" 326 + checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" 327 + dependencies = [ 328 + "idna_adapter", 329 + "smallvec", 330 + "utf8_iter", 331 + ] 332 + 333 + [[package]] 334 + name = "idna_adapter" 335 + version = "1.2.1" 336 + source = "registry+https://github.com/rust-lang/crates.io-index" 337 + checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 338 + dependencies = [ 339 + "icu_normalizer", 340 + "icu_properties", 341 + ] 342 + 343 + [[package]] 344 + name = "indexmap" 345 + version = "2.13.0" 346 + source = "registry+https://github.com/rust-lang/crates.io-index" 347 + checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" 348 + dependencies = [ 349 + "equivalent", 350 + "hashbrown", 351 + ] 352 + 353 + [[package]] 354 + name = "is_terminal_polyfill" 355 + version = "1.70.2" 356 + source = "registry+https://github.com/rust-lang/crates.io-index" 357 + checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" 358 + 359 + [[package]] 360 + name = "jobserver" 361 + version = "0.1.34" 362 + source = "registry+https://github.com/rust-lang/crates.io-index" 363 + checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" 364 + dependencies = [ 365 + "getrandom", 366 + "libc", 367 + ] 368 + 369 + [[package]] 370 + name = "libc" 371 + version = "0.2.180" 372 + source = "registry+https://github.com/rust-lang/crates.io-index" 373 + checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" 374 + 375 + [[package]] 376 + name = "libgit2-sys" 377 + version = "0.18.3+1.9.2" 378 + source = "registry+https://github.com/rust-lang/crates.io-index" 379 + checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" 380 + dependencies = [ 381 + "cc", 382 + "libc", 383 + "libssh2-sys", 384 + "libz-sys", 385 + "openssl-sys", 386 + "pkg-config", 387 + ] 388 + 389 + [[package]] 390 + name = "libssh2-sys" 391 + version = "0.3.1" 392 + source = "registry+https://github.com/rust-lang/crates.io-index" 393 + checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9" 394 + dependencies = [ 395 + "cc", 396 + "libc", 397 + "libz-sys", 398 + "openssl-sys", 399 + "pkg-config", 400 + "vcpkg", 401 + ] 402 + 403 + [[package]] 404 + name = "libz-sys" 405 + version = "1.1.23" 406 + source = "registry+https://github.com/rust-lang/crates.io-index" 407 + checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" 408 + dependencies = [ 409 + "cc", 410 + "libc", 411 + "pkg-config", 412 + "vcpkg", 413 + ] 414 + 415 + [[package]] 416 + name = "linux-raw-sys" 417 + version = "0.11.0" 418 + source = "registry+https://github.com/rust-lang/crates.io-index" 419 + checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" 420 + 421 + [[package]] 422 + name = "litemap" 423 + version = "0.8.1" 424 + source = "registry+https://github.com/rust-lang/crates.io-index" 425 + checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" 426 + 427 + [[package]] 428 + name = "log" 429 + version = "0.4.29" 430 + source = "registry+https://github.com/rust-lang/crates.io-index" 431 + checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" 432 + 433 + [[package]] 434 + name = "nu-ansi-term" 435 + version = "0.50.3" 436 + source = "registry+https://github.com/rust-lang/crates.io-index" 437 + checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" 438 + dependencies = [ 439 + "windows-sys", 440 + ] 441 + 442 + [[package]] 443 + name = "once_cell" 444 + version = "1.21.3" 445 + source = "registry+https://github.com/rust-lang/crates.io-index" 446 + checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 447 + 448 + [[package]] 449 + name = "once_cell_polyfill" 450 + version = "1.70.2" 451 + source = "registry+https://github.com/rust-lang/crates.io-index" 452 + checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" 453 + 454 + [[package]] 455 + name = "openssl-probe" 456 + version = "0.1.6" 457 + source = "registry+https://github.com/rust-lang/crates.io-index" 458 + checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 459 + 460 + [[package]] 461 + name = "openssl-sys" 462 + version = "0.9.111" 463 + source = "registry+https://github.com/rust-lang/crates.io-index" 464 + checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" 465 + dependencies = [ 466 + "cc", 467 + "libc", 468 + "pkg-config", 469 + "vcpkg", 470 + ] 471 + 472 + [[package]] 473 + name = "percent-encoding" 474 + version = "2.3.2" 475 + source = "registry+https://github.com/rust-lang/crates.io-index" 476 + checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 477 + 478 + [[package]] 479 + name = "pkg-config" 480 + version = "0.3.32" 481 + source = "registry+https://github.com/rust-lang/crates.io-index" 482 + checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 483 + 484 + [[package]] 485 + name = "potential_utf" 486 + version = "0.1.4" 487 + source = "registry+https://github.com/rust-lang/crates.io-index" 488 + checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" 489 + dependencies = [ 490 + "zerovec", 491 + ] 492 + 493 + [[package]] 494 + name = "proc-macro2" 495 + version = "1.0.105" 496 + source = "registry+https://github.com/rust-lang/crates.io-index" 497 + checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" 498 + dependencies = [ 499 + "unicode-ident", 500 + ] 501 + 502 + [[package]] 503 + name = "quote" 504 + version = "1.0.43" 505 + source = "registry+https://github.com/rust-lang/crates.io-index" 506 + checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" 507 + dependencies = [ 508 + "proc-macro2", 509 + ] 510 + 511 + [[package]] 512 + name = "r-efi" 513 + version = "5.3.0" 514 + source = "registry+https://github.com/rust-lang/crates.io-index" 515 + checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 516 + 517 + [[package]] 518 + name = "rustix" 519 + version = "1.1.3" 520 + source = "registry+https://github.com/rust-lang/crates.io-index" 521 + checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" 522 + dependencies = [ 523 + "bitflags", 524 + "errno", 525 + "libc", 526 + "linux-raw-sys", 527 + "windows-sys", 528 + ] 529 + 530 + [[package]] 531 + name = "same-file" 532 + version = "1.0.6" 533 + source = "registry+https://github.com/rust-lang/crates.io-index" 534 + checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 535 + dependencies = [ 536 + "winapi-util", 537 + ] 538 + 539 + [[package]] 540 + name = "serde" 541 + version = "1.0.228" 542 + source = "registry+https://github.com/rust-lang/crates.io-index" 543 + checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 544 + dependencies = [ 545 + "serde_core", 546 + "serde_derive", 547 + ] 548 + 549 + [[package]] 550 + name = "serde_core" 551 + version = "1.0.228" 552 + source = "registry+https://github.com/rust-lang/crates.io-index" 553 + checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 554 + dependencies = [ 555 + "serde_derive", 556 + ] 557 + 558 + [[package]] 559 + name = "serde_derive" 560 + version = "1.0.228" 561 + source = "registry+https://github.com/rust-lang/crates.io-index" 562 + checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 563 + dependencies = [ 564 + "proc-macro2", 565 + "quote", 566 + "syn", 567 + ] 568 + 569 + [[package]] 570 + name = "serde_spanned" 571 + version = "1.0.4" 572 + source = "registry+https://github.com/rust-lang/crates.io-index" 573 + checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" 574 + dependencies = [ 575 + "serde_core", 576 + ] 577 + 578 + [[package]] 579 + name = "shlex" 580 + version = "1.3.0" 581 + source = "registry+https://github.com/rust-lang/crates.io-index" 582 + checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 583 + 584 + [[package]] 585 + name = "smallvec" 586 + version = "1.15.1" 587 + source = "registry+https://github.com/rust-lang/crates.io-index" 588 + checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 589 + 590 + [[package]] 591 + name = "stable_deref_trait" 592 + version = "1.2.1" 593 + source = "registry+https://github.com/rust-lang/crates.io-index" 594 + checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" 595 + 596 + [[package]] 597 + name = "strsim" 598 + version = "0.11.1" 599 + source = "registry+https://github.com/rust-lang/crates.io-index" 600 + checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 601 + 602 + [[package]] 603 + name = "syn" 604 + version = "2.0.114" 605 + source = "registry+https://github.com/rust-lang/crates.io-index" 606 + checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" 607 + dependencies = [ 608 + "proc-macro2", 609 + "quote", 610 + "unicode-ident", 611 + ] 612 + 613 + [[package]] 614 + name = "synstructure" 615 + version = "0.13.2" 616 + source = "registry+https://github.com/rust-lang/crates.io-index" 617 + checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 618 + dependencies = [ 619 + "proc-macro2", 620 + "quote", 621 + "syn", 622 + ] 623 + 624 + [[package]] 625 + name = "tempfile" 626 + version = "3.24.0" 627 + source = "registry+https://github.com/rust-lang/crates.io-index" 628 + checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" 629 + dependencies = [ 630 + "fastrand", 631 + "getrandom", 632 + "once_cell", 633 + "rustix", 634 + "windows-sys", 635 + ] 636 + 637 + [[package]] 638 + name = "tinystr" 639 + version = "0.8.2" 640 + source = "registry+https://github.com/rust-lang/crates.io-index" 641 + checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" 642 + dependencies = [ 643 + "displaydoc", 644 + "zerovec", 645 + ] 646 + 647 + [[package]] 648 + name = "toml" 649 + version = "0.9.11+spec-1.1.0" 650 + source = "registry+https://github.com/rust-lang/crates.io-index" 651 + checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" 652 + dependencies = [ 653 + "indexmap", 654 + "serde_core", 655 + "serde_spanned", 656 + "toml_datetime", 657 + "toml_parser", 658 + "toml_writer", 659 + "winnow", 660 + ] 661 + 662 + [[package]] 663 + name = "toml_datetime" 664 + version = "0.7.5+spec-1.1.0" 665 + source = "registry+https://github.com/rust-lang/crates.io-index" 666 + checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" 667 + dependencies = [ 668 + "serde_core", 669 + ] 670 + 671 + [[package]] 672 + name = "toml_parser" 673 + version = "1.0.6+spec-1.1.0" 674 + source = "registry+https://github.com/rust-lang/crates.io-index" 675 + checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" 676 + dependencies = [ 677 + "winnow", 678 + ] 679 + 680 + [[package]] 681 + name = "toml_writer" 682 + version = "1.0.6+spec-1.1.0" 683 + source = "registry+https://github.com/rust-lang/crates.io-index" 684 + checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" 685 + 686 + [[package]] 687 + name = "unicode-ident" 688 + version = "1.0.22" 689 + source = "registry+https://github.com/rust-lang/crates.io-index" 690 + checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" 691 + 692 + [[package]] 693 + name = "url" 694 + version = "2.5.8" 695 + source = "registry+https://github.com/rust-lang/crates.io-index" 696 + checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" 697 + dependencies = [ 698 + "form_urlencoded", 699 + "idna", 700 + "percent-encoding", 701 + "serde", 702 + ] 703 + 704 + [[package]] 705 + name = "utf8_iter" 706 + version = "1.0.4" 707 + source = "registry+https://github.com/rust-lang/crates.io-index" 708 + checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 709 + 710 + [[package]] 711 + name = "utf8parse" 712 + version = "0.2.2" 713 + source = "registry+https://github.com/rust-lang/crates.io-index" 714 + checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 715 + 716 + [[package]] 717 + name = "vcpkg" 718 + version = "0.2.15" 719 + source = "registry+https://github.com/rust-lang/crates.io-index" 720 + checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 721 + 722 + [[package]] 723 + name = "walkdir" 724 + version = "2.5.0" 725 + source = "registry+https://github.com/rust-lang/crates.io-index" 726 + checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 727 + dependencies = [ 728 + "same-file", 729 + "winapi-util", 730 + ] 731 + 732 + [[package]] 733 + name = "wasip2" 734 + version = "1.0.2+wasi-0.2.9" 735 + source = "registry+https://github.com/rust-lang/crates.io-index" 736 + checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" 737 + dependencies = [ 738 + "wit-bindgen", 739 + ] 740 + 741 + [[package]] 742 + name = "winapi-util" 743 + version = "0.1.11" 744 + source = "registry+https://github.com/rust-lang/crates.io-index" 745 + checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" 746 + dependencies = [ 747 + "windows-sys", 748 + ] 749 + 750 + [[package]] 751 + name = "windows-link" 752 + version = "0.2.1" 753 + source = "registry+https://github.com/rust-lang/crates.io-index" 754 + checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 755 + 756 + [[package]] 757 + name = "windows-sys" 758 + version = "0.61.2" 759 + source = "registry+https://github.com/rust-lang/crates.io-index" 760 + checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 761 + dependencies = [ 762 + "windows-link", 763 + ] 764 + 765 + [[package]] 766 + name = "winnow" 767 + version = "0.7.14" 768 + source = "registry+https://github.com/rust-lang/crates.io-index" 769 + checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" 770 + 771 + [[package]] 772 + name = "wit-bindgen" 773 + version = "0.51.0" 774 + source = "registry+https://github.com/rust-lang/crates.io-index" 775 + checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" 776 + 777 + [[package]] 778 + name = "writeable" 779 + version = "0.6.2" 780 + source = "registry+https://github.com/rust-lang/crates.io-index" 781 + checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" 782 + 783 + [[package]] 784 + name = "yoke" 785 + version = "0.8.1" 786 + source = "registry+https://github.com/rust-lang/crates.io-index" 787 + checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" 788 + dependencies = [ 789 + "stable_deref_trait", 790 + "yoke-derive", 791 + "zerofrom", 792 + ] 793 + 794 + [[package]] 795 + name = "yoke-derive" 796 + version = "0.8.1" 797 + source = "registry+https://github.com/rust-lang/crates.io-index" 798 + checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" 799 + dependencies = [ 800 + "proc-macro2", 801 + "quote", 802 + "syn", 803 + "synstructure", 804 + ] 805 + 806 + [[package]] 807 + name = "zerofrom" 808 + version = "0.1.6" 809 + source = "registry+https://github.com/rust-lang/crates.io-index" 810 + checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 811 + dependencies = [ 812 + "zerofrom-derive", 813 + ] 814 + 815 + [[package]] 816 + name = "zerofrom-derive" 817 + version = "0.1.6" 818 + source = "registry+https://github.com/rust-lang/crates.io-index" 819 + checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 820 + dependencies = [ 821 + "proc-macro2", 822 + "quote", 823 + "syn", 824 + "synstructure", 825 + ] 826 + 827 + [[package]] 828 + name = "zerotrie" 829 + version = "0.2.3" 830 + source = "registry+https://github.com/rust-lang/crates.io-index" 831 + checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" 832 + dependencies = [ 833 + "displaydoc", 834 + "yoke", 835 + "zerofrom", 836 + ] 837 + 838 + [[package]] 839 + name = "zerovec" 840 + version = "0.11.5" 841 + source = "registry+https://github.com/rust-lang/crates.io-index" 842 + checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" 843 + dependencies = [ 844 + "yoke", 845 + "zerofrom", 846 + "zerovec-derive", 847 + ] 848 + 849 + [[package]] 850 + name = "zerovec-derive" 851 + version = "0.11.2" 852 + source = "registry+https://github.com/rust-lang/crates.io-index" 853 + checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" 854 + dependencies = [ 855 + "proc-macro2", 856 + "quote", 857 + "syn", 858 + ]
+22
Cargo.toml
··· 1 + [package] 2 + name = "forkme" 3 + version = "0.1.0" 4 + edition = "2021" 5 + description = "A tool for managing forks using a patch-based approach" 6 + license = "AGPL-3.0-only" 7 + repository = "https://tangled.org/me.webbeef.org/forkme" 8 + readme = "README.md" 9 + keywords = ["git", "fork", "patch", "diff", "maintenance"] 10 + categories = ["command-line-utilities", "development-tools"] 11 + 12 + [dependencies] 13 + anyhow = "1" 14 + clap = { version = "4", features = ["derive"] } 15 + diffy = "0.4" 16 + git2 = "0.20" 17 + serde = { version = "1", features = ["derive"] } 18 + toml = "0.9" 19 + walkdir = "2" 20 + 21 + [dev-dependencies] 22 + tempfile = "3"
+661
LICENSE.txt
··· 1 + GNU AFFERO GENERAL PUBLIC LICENSE 2 + Version 3, 19 November 2007 3 + 4 + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> 5 + Everyone is permitted to copy and distribute verbatim copies 6 + of this license document, but changing it is not allowed. 7 + 8 + Preamble 9 + 10 + The GNU Affero General Public License is a free, copyleft license for 11 + software and other kinds of works, specifically designed to ensure 12 + cooperation with the community in the case of network server software. 13 + 14 + The licenses for most software and other practical works are designed 15 + to take away your freedom to share and change the works. By contrast, 16 + our General Public Licenses are intended to guarantee your freedom to 17 + share and change all versions of a program--to make sure it remains free 18 + software for all its users. 19 + 20 + When we speak of free software, we are referring to freedom, not 21 + price. Our General Public Licenses are designed to make sure that you 22 + have the freedom to distribute copies of free software (and charge for 23 + them if you wish), that you receive source code or can get it if you 24 + want it, that you can change the software or use pieces of it in new 25 + free programs, and that you know you can do these things. 26 + 27 + Developers that use our General Public Licenses protect your rights 28 + with two steps: (1) assert copyright on the software, and (2) offer 29 + you this License which gives you legal permission to copy, distribute 30 + and/or modify the software. 31 + 32 + A secondary benefit of defending all users' freedom is that 33 + improvements made in alternate versions of the program, if they 34 + receive widespread use, become available for other developers to 35 + incorporate. Many developers of free software are heartened and 36 + encouraged by the resulting cooperation. However, in the case of 37 + software used on network servers, this result may fail to come about. 38 + The GNU General Public License permits making a modified version and 39 + letting the public access it on a server without ever releasing its 40 + source code to the public. 41 + 42 + The GNU Affero General Public License is designed specifically to 43 + ensure that, in such cases, the modified source code becomes available 44 + to the community. It requires the operator of a network server to 45 + provide the source code of the modified version running there to the 46 + users of that server. Therefore, public use of a modified version, on 47 + a publicly accessible server, gives the public access to the source 48 + code of the modified version. 49 + 50 + An older license, called the Affero General Public License and 51 + published by Affero, was designed to accomplish similar goals. This is 52 + a different license, not a version of the Affero GPL, but Affero has 53 + released a new version of the Affero GPL which permits relicensing under 54 + this license. 55 + 56 + The precise terms and conditions for copying, distribution and 57 + modification follow. 58 + 59 + TERMS AND CONDITIONS 60 + 61 + 0. Definitions. 62 + 63 + "This License" refers to version 3 of the GNU Affero General Public License. 64 + 65 + "Copyright" also means copyright-like laws that apply to other kinds of 66 + works, such as semiconductor masks. 67 + 68 + "The Program" refers to any copyrightable work licensed under this 69 + License. Each licensee is addressed as "you". "Licensees" and 70 + "recipients" may be individuals or organizations. 71 + 72 + To "modify" a work means to copy from or adapt all or part of the work 73 + in a fashion requiring copyright permission, other than the making of an 74 + exact copy. The resulting work is called a "modified version" of the 75 + earlier work or a work "based on" the earlier work. 76 + 77 + A "covered work" means either the unmodified Program or a work based 78 + on the Program. 79 + 80 + To "propagate" a work means to do anything with it that, without 81 + permission, would make you directly or secondarily liable for 82 + infringement under applicable copyright law, except executing it on a 83 + computer or modifying a private copy. Propagation includes copying, 84 + distribution (with or without modification), making available to the 85 + public, and in some countries other activities as well. 86 + 87 + To "convey" a work means any kind of propagation that enables other 88 + parties to make or receive copies. Mere interaction with a user through 89 + a computer network, with no transfer of a copy, is not conveying. 90 + 91 + An interactive user interface displays "Appropriate Legal Notices" 92 + to the extent that it includes a convenient and prominently visible 93 + feature that (1) displays an appropriate copyright notice, and (2) 94 + tells the user that there is no warranty for the work (except to the 95 + extent that warranties are provided), that licensees may convey the 96 + work under this License, and how to view a copy of this License. If 97 + the interface presents a list of user commands or options, such as a 98 + menu, a prominent item in the list meets this criterion. 99 + 100 + 1. Source Code. 101 + 102 + The "source code" for a work means the preferred form of the work 103 + for making modifications to it. "Object code" means any non-source 104 + form of a work. 105 + 106 + A "Standard Interface" means an interface that either is an official 107 + standard defined by a recognized standards body, or, in the case of 108 + interfaces specified for a particular programming language, one that 109 + is widely used among developers working in that language. 110 + 111 + The "System Libraries" of an executable work include anything, other 112 + than the work as a whole, that (a) is included in the normal form of 113 + packaging a Major Component, but which is not part of that Major 114 + Component, and (b) serves only to enable use of the work with that 115 + Major Component, or to implement a Standard Interface for which an 116 + implementation is available to the public in source code form. A 117 + "Major Component", in this context, means a major essential component 118 + (kernel, window system, and so on) of the specific operating system 119 + (if any) on which the executable work runs, or a compiler used to 120 + produce the work, or an object code interpreter used to run it. 121 + 122 + The "Corresponding Source" for a work in object code form means all 123 + the source code needed to generate, install, and (for an executable 124 + work) run the object code and to modify the work, including scripts to 125 + control those activities. However, it does not include the work's 126 + System Libraries, or general-purpose tools or generally available free 127 + programs which are used unmodified in performing those activities but 128 + which are not part of the work. For example, Corresponding Source 129 + includes interface definition files associated with source files for 130 + the work, and the source code for shared libraries and dynamically 131 + linked subprograms that the work is specifically designed to require, 132 + such as by intimate data communication or control flow between those 133 + subprograms and other parts of the work. 134 + 135 + The Corresponding Source need not include anything that users 136 + can regenerate automatically from other parts of the Corresponding 137 + Source. 138 + 139 + The Corresponding Source for a work in source code form is that 140 + same work. 141 + 142 + 2. Basic Permissions. 143 + 144 + All rights granted under this License are granted for the term of 145 + copyright on the Program, and are irrevocable provided the stated 146 + conditions are met. This License explicitly affirms your unlimited 147 + permission to run the unmodified Program. The output from running a 148 + covered work is covered by this License only if the output, given its 149 + content, constitutes a covered work. This License acknowledges your 150 + rights of fair use or other equivalent, as provided by copyright law. 151 + 152 + You may make, run and propagate covered works that you do not 153 + convey, without conditions so long as your license otherwise remains 154 + in force. You may convey covered works to others for the sole purpose 155 + of having them make modifications exclusively for you, or provide you 156 + with facilities for running those works, provided that you comply with 157 + the terms of this License in conveying all material for which you do 158 + not control copyright. Those thus making or running the covered works 159 + for you must do so exclusively on your behalf, under your direction 160 + and control, on terms that prohibit them from making any copies of 161 + your copyrighted material outside their relationship with you. 162 + 163 + Conveying under any other circumstances is permitted solely under 164 + the conditions stated below. Sublicensing is not allowed; section 10 165 + makes it unnecessary. 166 + 167 + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 + 169 + No covered work shall be deemed part of an effective technological 170 + measure under any applicable law fulfilling obligations under article 171 + 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 + similar laws prohibiting or restricting circumvention of such 173 + measures. 174 + 175 + When you convey a covered work, you waive any legal power to forbid 176 + circumvention of technological measures to the extent such circumvention 177 + is effected by exercising rights under this License with respect to 178 + the covered work, and you disclaim any intention to limit operation or 179 + modification of the work as a means of enforcing, against the work's 180 + users, your or third parties' legal rights to forbid circumvention of 181 + technological measures. 182 + 183 + 4. Conveying Verbatim Copies. 184 + 185 + You may convey verbatim copies of the Program's source code as you 186 + receive it, in any medium, provided that you conspicuously and 187 + appropriately publish on each copy an appropriate copyright notice; 188 + keep intact all notices stating that this License and any 189 + non-permissive terms added in accord with section 7 apply to the code; 190 + keep intact all notices of the absence of any warranty; and give all 191 + recipients a copy of this License along with the Program. 192 + 193 + You may charge any price or no price for each copy that you convey, 194 + and you may offer support or warranty protection for a fee. 195 + 196 + 5. Conveying Modified Source Versions. 197 + 198 + You may convey a work based on the Program, or the modifications to 199 + produce it from the Program, in the form of source code under the 200 + terms of section 4, provided that you also meet all of these conditions: 201 + 202 + a) The work must carry prominent notices stating that you modified 203 + it, and giving a relevant date. 204 + 205 + b) The work must carry prominent notices stating that it is 206 + released under this License and any conditions added under section 207 + 7. This requirement modifies the requirement in section 4 to 208 + "keep intact all notices". 209 + 210 + c) You must license the entire work, as a whole, under this 211 + License to anyone who comes into possession of a copy. This 212 + License will therefore apply, along with any applicable section 7 213 + additional terms, to the whole of the work, and all its parts, 214 + regardless of how they are packaged. This License gives no 215 + permission to license the work in any other way, but it does not 216 + invalidate such permission if you have separately received it. 217 + 218 + d) If the work has interactive user interfaces, each must display 219 + Appropriate Legal Notices; however, if the Program has interactive 220 + interfaces that do not display Appropriate Legal Notices, your 221 + work need not make them do so. 222 + 223 + A compilation of a covered work with other separate and independent 224 + works, which are not by their nature extensions of the covered work, 225 + and which are not combined with it such as to form a larger program, 226 + in or on a volume of a storage or distribution medium, is called an 227 + "aggregate" if the compilation and its resulting copyright are not 228 + used to limit the access or legal rights of the compilation's users 229 + beyond what the individual works permit. Inclusion of a covered work 230 + in an aggregate does not cause this License to apply to the other 231 + parts of the aggregate. 232 + 233 + 6. Conveying Non-Source Forms. 234 + 235 + You may convey a covered work in object code form under the terms 236 + of sections 4 and 5, provided that you also convey the 237 + machine-readable Corresponding Source under the terms of this License, 238 + in one of these ways: 239 + 240 + a) Convey the object code in, or embodied in, a physical product 241 + (including a physical distribution medium), accompanied by the 242 + Corresponding Source fixed on a durable physical medium 243 + customarily used for software interchange. 244 + 245 + b) Convey the object code in, or embodied in, a physical product 246 + (including a physical distribution medium), accompanied by a 247 + written offer, valid for at least three years and valid for as 248 + long as you offer spare parts or customer support for that product 249 + model, to give anyone who possesses the object code either (1) a 250 + copy of the Corresponding Source for all the software in the 251 + product that is covered by this License, on a durable physical 252 + medium customarily used for software interchange, for a price no 253 + more than your reasonable cost of physically performing this 254 + conveying of source, or (2) access to copy the 255 + Corresponding Source from a network server at no charge. 256 + 257 + c) Convey individual copies of the object code with a copy of the 258 + written offer to provide the Corresponding Source. This 259 + alternative is allowed only occasionally and noncommercially, and 260 + only if you received the object code with such an offer, in accord 261 + with subsection 6b. 262 + 263 + d) Convey the object code by offering access from a designated 264 + place (gratis or for a charge), and offer equivalent access to the 265 + Corresponding Source in the same way through the same place at no 266 + further charge. You need not require recipients to copy the 267 + Corresponding Source along with the object code. If the place to 268 + copy the object code is a network server, the Corresponding Source 269 + may be on a different server (operated by you or a third party) 270 + that supports equivalent copying facilities, provided you maintain 271 + clear directions next to the object code saying where to find the 272 + Corresponding Source. Regardless of what server hosts the 273 + Corresponding Source, you remain obligated to ensure that it is 274 + available for as long as needed to satisfy these requirements. 275 + 276 + e) Convey the object code using peer-to-peer transmission, provided 277 + you inform other peers where the object code and Corresponding 278 + Source of the work are being offered to the general public at no 279 + charge under subsection 6d. 280 + 281 + A separable portion of the object code, whose source code is excluded 282 + from the Corresponding Source as a System Library, need not be 283 + included in conveying the object code work. 284 + 285 + A "User Product" is either (1) a "consumer product", which means any 286 + tangible personal property which is normally used for personal, family, 287 + or household purposes, or (2) anything designed or sold for incorporation 288 + into a dwelling. In determining whether a product is a consumer product, 289 + doubtful cases shall be resolved in favor of coverage. For a particular 290 + product received by a particular user, "normally used" refers to a 291 + typical or common use of that class of product, regardless of the status 292 + of the particular user or of the way in which the particular user 293 + actually uses, or expects or is expected to use, the product. A product 294 + is a consumer product regardless of whether the product has substantial 295 + commercial, industrial or non-consumer uses, unless such uses represent 296 + the only significant mode of use of the product. 297 + 298 + "Installation Information" for a User Product means any methods, 299 + procedures, authorization keys, or other information required to install 300 + and execute modified versions of a covered work in that User Product from 301 + a modified version of its Corresponding Source. The information must 302 + suffice to ensure that the continued functioning of the modified object 303 + code is in no case prevented or interfered with solely because 304 + modification has been made. 305 + 306 + If you convey an object code work under this section in, or with, or 307 + specifically for use in, a User Product, and the conveying occurs as 308 + part of a transaction in which the right of possession and use of the 309 + User Product is transferred to the recipient in perpetuity or for a 310 + fixed term (regardless of how the transaction is characterized), the 311 + Corresponding Source conveyed under this section must be accompanied 312 + by the Installation Information. But this requirement does not apply 313 + if neither you nor any third party retains the ability to install 314 + modified object code on the User Product (for example, the work has 315 + been installed in ROM). 316 + 317 + The requirement to provide Installation Information does not include a 318 + requirement to continue to provide support service, warranty, or updates 319 + for a work that has been modified or installed by the recipient, or for 320 + the User Product in which it has been modified or installed. Access to a 321 + network may be denied when the modification itself materially and 322 + adversely affects the operation of the network or violates the rules and 323 + protocols for communication across the network. 324 + 325 + Corresponding Source conveyed, and Installation Information provided, 326 + in accord with this section must be in a format that is publicly 327 + documented (and with an implementation available to the public in 328 + source code form), and must require no special password or key for 329 + unpacking, reading or copying. 330 + 331 + 7. Additional Terms. 332 + 333 + "Additional permissions" are terms that supplement the terms of this 334 + License by making exceptions from one or more of its conditions. 335 + Additional permissions that are applicable to the entire Program shall 336 + be treated as though they were included in this License, to the extent 337 + that they are valid under applicable law. If additional permissions 338 + apply only to part of the Program, that part may be used separately 339 + under those permissions, but the entire Program remains governed by 340 + this License without regard to the additional permissions. 341 + 342 + When you convey a copy of a covered work, you may at your option 343 + remove any additional permissions from that copy, or from any part of 344 + it. (Additional permissions may be written to require their own 345 + removal in certain cases when you modify the work.) You may place 346 + additional permissions on material, added by you to a covered work, 347 + for which you have or can give appropriate copyright permission. 348 + 349 + Notwithstanding any other provision of this License, for material you 350 + add to a covered work, you may (if authorized by the copyright holders of 351 + that material) supplement the terms of this License with terms: 352 + 353 + a) Disclaiming warranty or limiting liability differently from the 354 + terms of sections 15 and 16 of this License; or 355 + 356 + b) Requiring preservation of specified reasonable legal notices or 357 + author attributions in that material or in the Appropriate Legal 358 + Notices displayed by works containing it; or 359 + 360 + c) Prohibiting misrepresentation of the origin of that material, or 361 + requiring that modified versions of such material be marked in 362 + reasonable ways as different from the original version; or 363 + 364 + d) Limiting the use for publicity purposes of names of licensors or 365 + authors of the material; or 366 + 367 + e) Declining to grant rights under trademark law for use of some 368 + trade names, trademarks, or service marks; or 369 + 370 + f) Requiring indemnification of licensors and authors of that 371 + material by anyone who conveys the material (or modified versions of 372 + it) with contractual assumptions of liability to the recipient, for 373 + any liability that these contractual assumptions directly impose on 374 + those licensors and authors. 375 + 376 + All other non-permissive additional terms are considered "further 377 + restrictions" within the meaning of section 10. If the Program as you 378 + received it, or any part of it, contains a notice stating that it is 379 + governed by this License along with a term that is a further 380 + restriction, you may remove that term. If a license document contains 381 + a further restriction but permits relicensing or conveying under this 382 + License, you may add to a covered work material governed by the terms 383 + of that license document, provided that the further restriction does 384 + not survive such relicensing or conveying. 385 + 386 + If you add terms to a covered work in accord with this section, you 387 + must place, in the relevant source files, a statement of the 388 + additional terms that apply to those files, or a notice indicating 389 + where to find the applicable terms. 390 + 391 + Additional terms, permissive or non-permissive, may be stated in the 392 + form of a separately written license, or stated as exceptions; 393 + the above requirements apply either way. 394 + 395 + 8. Termination. 396 + 397 + You may not propagate or modify a covered work except as expressly 398 + provided under this License. Any attempt otherwise to propagate or 399 + modify it is void, and will automatically terminate your rights under 400 + this License (including any patent licenses granted under the third 401 + paragraph of section 11). 402 + 403 + However, if you cease all violation of this License, then your 404 + license from a particular copyright holder is reinstated (a) 405 + provisionally, unless and until the copyright holder explicitly and 406 + finally terminates your license, and (b) permanently, if the copyright 407 + holder fails to notify you of the violation by some reasonable means 408 + prior to 60 days after the cessation. 409 + 410 + Moreover, your license from a particular copyright holder is 411 + reinstated permanently if the copyright holder notifies you of the 412 + violation by some reasonable means, this is the first time you have 413 + received notice of violation of this License (for any work) from that 414 + copyright holder, and you cure the violation prior to 30 days after 415 + your receipt of the notice. 416 + 417 + Termination of your rights under this section does not terminate the 418 + licenses of parties who have received copies or rights from you under 419 + this License. If your rights have been terminated and not permanently 420 + reinstated, you do not qualify to receive new licenses for the same 421 + material under section 10. 422 + 423 + 9. Acceptance Not Required for Having Copies. 424 + 425 + You are not required to accept this License in order to receive or 426 + run a copy of the Program. Ancillary propagation of a covered work 427 + occurring solely as a consequence of using peer-to-peer transmission 428 + to receive a copy likewise does not require acceptance. However, 429 + nothing other than this License grants you permission to propagate or 430 + modify any covered work. These actions infringe copyright if you do 431 + not accept this License. Therefore, by modifying or propagating a 432 + covered work, you indicate your acceptance of this License to do so. 433 + 434 + 10. Automatic Licensing of Downstream Recipients. 435 + 436 + Each time you convey a covered work, the recipient automatically 437 + receives a license from the original licensors, to run, modify and 438 + propagate that work, subject to this License. You are not responsible 439 + for enforcing compliance by third parties with this License. 440 + 441 + An "entity transaction" is a transaction transferring control of an 442 + organization, or substantially all assets of one, or subdividing an 443 + organization, or merging organizations. If propagation of a covered 444 + work results from an entity transaction, each party to that 445 + transaction who receives a copy of the work also receives whatever 446 + licenses to the work the party's predecessor in interest had or could 447 + give under the previous paragraph, plus a right to possession of the 448 + Corresponding Source of the work from the predecessor in interest, if 449 + the predecessor has it or can get it with reasonable efforts. 450 + 451 + You may not impose any further restrictions on the exercise of the 452 + rights granted or affirmed under this License. For example, you may 453 + not impose a license fee, royalty, or other charge for exercise of 454 + rights granted under this License, and you may not initiate litigation 455 + (including a cross-claim or counterclaim in a lawsuit) alleging that 456 + any patent claim is infringed by making, using, selling, offering for 457 + sale, or importing the Program or any portion of it. 458 + 459 + 11. Patents. 460 + 461 + A "contributor" is a copyright holder who authorizes use under this 462 + License of the Program or a work on which the Program is based. The 463 + work thus licensed is called the contributor's "contributor version". 464 + 465 + A contributor's "essential patent claims" are all patent claims 466 + owned or controlled by the contributor, whether already acquired or 467 + hereafter acquired, that would be infringed by some manner, permitted 468 + by this License, of making, using, or selling its contributor version, 469 + but do not include claims that would be infringed only as a 470 + consequence of further modification of the contributor version. For 471 + purposes of this definition, "control" includes the right to grant 472 + patent sublicenses in a manner consistent with the requirements of 473 + this License. 474 + 475 + Each contributor grants you a non-exclusive, worldwide, royalty-free 476 + patent license under the contributor's essential patent claims, to 477 + make, use, sell, offer for sale, import and otherwise run, modify and 478 + propagate the contents of its contributor version. 479 + 480 + In the following three paragraphs, a "patent license" is any express 481 + agreement or commitment, however denominated, not to enforce a patent 482 + (such as an express permission to practice a patent or covenant not to 483 + sue for patent infringement). To "grant" such a patent license to a 484 + party means to make such an agreement or commitment not to enforce a 485 + patent against the party. 486 + 487 + If you convey a covered work, knowingly relying on a patent license, 488 + and the Corresponding Source of the work is not available for anyone 489 + to copy, free of charge and under the terms of this License, through a 490 + publicly available network server or other readily accessible means, 491 + then you must either (1) cause the Corresponding Source to be so 492 + available, or (2) arrange to deprive yourself of the benefit of the 493 + patent license for this particular work, or (3) arrange, in a manner 494 + consistent with the requirements of this License, to extend the patent 495 + license to downstream recipients. "Knowingly relying" means you have 496 + actual knowledge that, but for the patent license, your conveying the 497 + covered work in a country, or your recipient's use of the covered work 498 + in a country, would infringe one or more identifiable patents in that 499 + country that you have reason to believe are valid. 500 + 501 + If, pursuant to or in connection with a single transaction or 502 + arrangement, you convey, or propagate by procuring conveyance of, a 503 + covered work, and grant a patent license to some of the parties 504 + receiving the covered work authorizing them to use, propagate, modify 505 + or convey a specific copy of the covered work, then the patent license 506 + you grant is automatically extended to all recipients of the covered 507 + work and works based on it. 508 + 509 + A patent license is "discriminatory" if it does not include within 510 + the scope of its coverage, prohibits the exercise of, or is 511 + conditioned on the non-exercise of one or more of the rights that are 512 + specifically granted under this License. You may not convey a covered 513 + work if you are a party to an arrangement with a third party that is 514 + in the business of distributing software, under which you make payment 515 + to the third party based on the extent of your activity of conveying 516 + the work, and under which the third party grants, to any of the 517 + parties who would receive the covered work from you, a discriminatory 518 + patent license (a) in connection with copies of the covered work 519 + conveyed by you (or copies made from those copies), or (b) primarily 520 + for and in connection with specific products or compilations that 521 + contain the covered work, unless you entered into that arrangement, 522 + or that patent license was granted, prior to 28 March 2007. 523 + 524 + Nothing in this License shall be construed as excluding or limiting 525 + any implied license or other defenses to infringement that may 526 + otherwise be available to you under applicable patent law. 527 + 528 + 12. No Surrender of Others' Freedom. 529 + 530 + If conditions are imposed on you (whether by court order, agreement or 531 + otherwise) that contradict the conditions of this License, they do not 532 + excuse you from the conditions of this License. If you cannot convey a 533 + covered work so as to satisfy simultaneously your obligations under this 534 + License and any other pertinent obligations, then as a consequence you may 535 + not convey it at all. For example, if you agree to terms that obligate you 536 + to collect a royalty for further conveying from those to whom you convey 537 + the Program, the only way you could satisfy both those terms and this 538 + License would be to refrain entirely from conveying the Program. 539 + 540 + 13. Remote Network Interaction; Use with the GNU General Public License. 541 + 542 + Notwithstanding any other provision of this License, if you modify the 543 + Program, your modified version must prominently offer all users 544 + interacting with it remotely through a computer network (if your version 545 + supports such interaction) an opportunity to receive the Corresponding 546 + Source of your version by providing access to the Corresponding Source 547 + from a network server at no charge, through some standard or customary 548 + means of facilitating copying of software. This Corresponding Source 549 + shall include the Corresponding Source for any work covered by version 3 550 + of the GNU General Public License that is incorporated pursuant to the 551 + following paragraph. 552 + 553 + Notwithstanding any other provision of this License, you have 554 + permission to link or combine any covered work with a work licensed 555 + under version 3 of the GNU General Public License into a single 556 + combined work, and to convey the resulting work. The terms of this 557 + License will continue to apply to the part which is the covered work, 558 + but the work with which it is combined will remain governed by version 559 + 3 of the GNU General Public License. 560 + 561 + 14. Revised Versions of this License. 562 + 563 + The Free Software Foundation may publish revised and/or new versions of 564 + the GNU Affero General Public License from time to time. Such new versions 565 + will be similar in spirit to the present version, but may differ in detail to 566 + address new problems or concerns. 567 + 568 + Each version is given a distinguishing version number. If the 569 + Program specifies that a certain numbered version of the GNU Affero General 570 + Public License "or any later version" applies to it, you have the 571 + option of following the terms and conditions either of that numbered 572 + version or of any later version published by the Free Software 573 + Foundation. If the Program does not specify a version number of the 574 + GNU Affero General Public License, you may choose any version ever published 575 + by the Free Software Foundation. 576 + 577 + If the Program specifies that a proxy can decide which future 578 + versions of the GNU Affero General Public License can be used, that proxy's 579 + public statement of acceptance of a version permanently authorizes you 580 + to choose that version for the Program. 581 + 582 + Later license versions may give you additional or different 583 + permissions. However, no additional obligations are imposed on any 584 + author or copyright holder as a result of your choosing to follow a 585 + later version. 586 + 587 + 15. Disclaimer of Warranty. 588 + 589 + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 + APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 + HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 + OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 + THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 + PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 + IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 + ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 + 598 + 16. Limitation of Liability. 599 + 600 + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 + WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 + THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 + GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 + USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 + DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 + PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 + EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 + SUCH DAMAGES. 609 + 610 + 17. Interpretation of Sections 15 and 16. 611 + 612 + If the disclaimer of warranty and limitation of liability provided 613 + above cannot be given local legal effect according to their terms, 614 + reviewing courts shall apply local law that most closely approximates 615 + an absolute waiver of all civil liability in connection with the 616 + Program, unless a warranty or assumption of liability accompanies a 617 + copy of the Program in return for a fee. 618 + 619 + END OF TERMS AND CONDITIONS 620 + 621 + How to Apply These Terms to Your New Programs 622 + 623 + If you develop a new program, and you want it to be of the greatest 624 + possible use to the public, the best way to achieve this is to make it 625 + free software which everyone can redistribute and change under these terms. 626 + 627 + To do so, attach the following notices to the program. It is safest 628 + to attach them to the start of each source file to most effectively 629 + state the exclusion of warranty; and each file should have at least 630 + the "copyright" line and a pointer to where the full notice is found. 631 + 632 + <one line to give the program's name and a brief idea of what it does.> 633 + Copyright (C) <year> <name of author> 634 + 635 + This program is free software: you can redistribute it and/or modify 636 + it under the terms of the GNU Affero General Public License as published by 637 + the Free Software Foundation, either version 3 of the License, or 638 + (at your option) any later version. 639 + 640 + This program is distributed in the hope that it will be useful, 641 + but WITHOUT ANY WARRANTY; without even the implied warranty of 642 + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 + GNU Affero General Public License for more details. 644 + 645 + You should have received a copy of the GNU Affero General Public License 646 + along with this program. If not, see <https://www.gnu.org/licenses/>. 647 + 648 + Also add information on how to contact you by electronic and paper mail. 649 + 650 + If your software can interact with users remotely through a computer 651 + network, you should also make sure that it provides a way for users to 652 + get its source. For example, if your program is a web application, its 653 + interface could display a "Source" link that leads users to an archive 654 + of the code. There are many ways you could offer source, and different 655 + solutions will be better for different programs; see section 13 for the 656 + specific requirements. 657 + 658 + You should also get your employer (if you work as a programmer) or school, 659 + if any, to sign a "copyright disclaimer" for the program, if necessary. 660 + For more information on this, and how to apply and follow the GNU AGPL, see 661 + <https://www.gnu.org/licenses/>.
+114
README.md
··· 1 + # forkme 2 + 3 + A tool for managing forks of large projects using a patch-based approach. 4 + 5 + ## The Problem 6 + 7 + When maintaining a fork of a large project, there are two common approaches: 8 + 9 + - **Rebase**: Rewrite your changes on top of upstream. Clean history, but requires force-pushing. 10 + - **Merge**: Merge upstream into your branch. Preserves history, but creates merge commits that may not build cleanly, making bisecting difficult. 11 + 12 + ## The Solution 13 + 14 + **forkme** takes a different approach: keep your changes as a set of patches, organized by file path. This gives you: 15 + 16 + - Clean history of your own changes 17 + - Simple conflict resolution during upstream updates 18 + - No force-pushes required 19 + 20 + ## Project Structure 21 + 22 + A forkme-managed project looks like this: 23 + 24 + ``` 25 + my-fork/ 26 + ├── forkme.toml # Configuration (upstream URL, branch) 27 + ├── patches/ # Your patches, organized by file path 28 + │ ├── src/ 29 + │ │ └── main.rs.patch 30 + │ └── Cargo.toml.patch 31 + └── source/ # Upstream repo with your changes (in .gitignore) 32 + ``` 33 + 34 + Only `forkme.toml` and `patches/` are committed to your repository. 35 + 36 + ## Installation 37 + 38 + ```bash 39 + cargo install --path . 40 + ``` 41 + 42 + ## Usage 43 + 44 + ### Initialize a new project 45 + 46 + ```bash 47 + forkme init --url https://github.com/user/repo --branch main 48 + ``` 49 + 50 + This will: 51 + - Create `forkme.toml` with upstream configuration 52 + - Clone the upstream repo into `source/` 53 + - Create a `forkme` branch for your work 54 + - Set up `.gitignore` to exclude `source/` 55 + 56 + ### Make changes 57 + 58 + Work in the `source/` directory on the `forkme` branch: 59 + 60 + ```bash 61 + cd source 62 + # edit files, make commits 63 + git commit -m "My changes" 64 + ``` 65 + 66 + ### Sync changes to patches 67 + 68 + ```bash 69 + forkme sync 70 + ``` 71 + 72 + This updates patch files in `patches/` based on your changes. 73 + 74 + ### Update from upstream 75 + 76 + ```bash 77 + forkme update 78 + ``` 79 + 80 + This fetches upstream and rebases your `forkme` branch. If there are conflicts, resolve them with standard git commands, then run `forkme sync` to regenerate patches. 81 + 82 + ### Apply patches (after fresh clone) 83 + 84 + ```bash 85 + forkme init # Uses existing forkme.toml 86 + ``` 87 + 88 + Or to reset and reapply: 89 + 90 + ```bash 91 + forkme apply 92 + ``` 93 + 94 + ### Check status 95 + 96 + ```bash 97 + forkme status # Project overview 98 + forkme stats # Patch statistics (added/modified/deleted) 99 + ``` 100 + 101 + ## Commands 102 + 103 + | Command | Description | 104 + |---------|-------------| 105 + | `init --url <url> [--branch <branch>]` | Initialize project with upstream repo | 106 + | `apply` | Reset to upstream and reapply all patches | 107 + | `sync` | Generate patches from current changes | 108 + | `update` | Fetch upstream and rebase | 109 + | `status` | Show project status | 110 + | `stats` | Show patch statistics | 111 + 112 + ## License 113 + 114 + AGPL 3.0
+97
src/commands/apply.rs
··· 1 + use anyhow::{bail, Result}; 2 + use git2::Repository; 3 + use std::fs; 4 + use std::path::Path; 5 + 6 + use crate::config::Config; 7 + use crate::git::{self, SOURCE_DIR}; 8 + use crate::patch::{self, PatchEntry}; 9 + 10 + pub fn run() -> Result<()> { 11 + let config = Config::load()?; 12 + let repo = git::open_repo()?; 13 + 14 + if !git::is_working_tree_clean(&repo)? { 15 + bail!( 16 + "Working tree in {} has uncommitted changes. Please commit or stash them first.", 17 + SOURCE_DIR 18 + ); 19 + } 20 + 21 + git::ensure_on_forkme_branch(&repo)?; 22 + git::reset_to_upstream(&repo, &config.upstream.branch)?; 23 + 24 + apply_patches(&repo)?; 25 + 26 + println!("\nPatches applied successfully."); 27 + 28 + Ok(()) 29 + } 30 + 31 + pub fn apply_patches(repo: &Repository) -> Result<()> { 32 + let entries = patch::list_all_entries()?; 33 + 34 + if entries.is_empty() { 35 + println!("No patches to apply."); 36 + return Ok(()); 37 + } 38 + 39 + println!("Applying {} entries...", entries.len()); 40 + 41 + for entry in &entries { 42 + let file_path = entry.file_path(); 43 + let source_file_path = Path::new(SOURCE_DIR).join(file_path); 44 + 45 + match entry { 46 + PatchEntry::Deleted(_) => { 47 + // Delete the file 48 + if source_file_path.exists() { 49 + fs::remove_file(&source_file_path)?; 50 + println!(" deleted {}", file_path); 51 + } 52 + } 53 + 54 + PatchEntry::Binary(_) => { 55 + // Copy binary file directly 56 + let content = patch::read_binary(file_path)?; 57 + if let Some(parent) = source_file_path.parent() { 58 + fs::create_dir_all(parent)?; 59 + } 60 + fs::write(&source_file_path, content)?; 61 + println!(" copied (binary) {}", file_path); 62 + } 63 + 64 + PatchEntry::TextPatch(_) => { 65 + // Apply text patch 66 + let patch_content = patch::read_patch(file_path)?; 67 + 68 + // Detect if new file by checking hunk header 69 + let is_new_file = patch_content.lines().any(|l| l.starts_with("@@ -0,0")); 70 + 71 + if is_new_file { 72 + // Create new file from patch 73 + let new_content = patch::apply_patch("", &patch_content)?; 74 + if let Some(parent) = source_file_path.parent() { 75 + fs::create_dir_all(parent)?; 76 + } 77 + fs::write(&source_file_path, new_content)?; 78 + println!(" created {}", file_path); 79 + } else { 80 + // Modify existing file 81 + let original = fs::read_to_string(&source_file_path)?; 82 + let patched = patch::apply_patch(&original, &patch_content)?; 83 + fs::write(&source_file_path, patched)?; 84 + println!(" patched {}", file_path); 85 + } 86 + } 87 + } 88 + } 89 + 90 + // Commit the changes 91 + if !entries.is_empty() { 92 + git::commit_changes(repo, "Apply forkme patches")?; 93 + println!("Committed patched changes."); 94 + } 95 + 96 + Ok(()) 97 + }
+108
src/commands/init.rs
··· 1 + use anyhow::{bail, Context, Result}; 2 + use std::fs::{self, OpenOptions}; 3 + use std::io::Write; 4 + use std::path::Path; 5 + 6 + use crate::config::{Config, Upstream}; 7 + use crate::git::{self, SOURCE_DIR}; 8 + use crate::patch; 9 + 10 + pub fn run(url: Option<String>, branch: &str) -> Result<()> { 11 + // Determine the URL - either from argument or existing config 12 + let url = match url { 13 + Some(u) => u, 14 + None => { 15 + if Config::exists() { 16 + let config = Config::load()?; 17 + config.upstream.url 18 + } else { 19 + bail!("No URL provided and no forkme.toml found. Use --url to specify upstream repository."); 20 + } 21 + } 22 + }; 23 + 24 + // Check if source directory already exists 25 + if Path::new(SOURCE_DIR).exists() { 26 + bail!( 27 + "Source directory '{}' already exists. Remove it first if you want to reinitialize.", 28 + SOURCE_DIR 29 + ); 30 + } 31 + 32 + // Create/update the config file 33 + let config = Config { 34 + upstream: Upstream { 35 + url: url.clone(), 36 + branch: branch.into(), 37 + }, 38 + }; 39 + config 40 + .save() 41 + .with_context(|| "Failed to save forkme.toml")?; 42 + println!("Created forkme.toml"); 43 + 44 + // Clone the repository 45 + let repo = git::clone_repo(&url, &branch)?; 46 + 47 + // Create the forkme branch 48 + git::create_forkme_branch(&repo, &branch)?; 49 + 50 + // Create patches directory 51 + patch::ensure_patches_dir()?; 52 + println!("Created patches/ directory"); 53 + 54 + // Add source/ to .gitignore if not already there 55 + add_to_gitignore(SOURCE_DIR)?; 56 + 57 + // Apply any existing patches 58 + let patches = patch::list_patches()?; 59 + if !patches.is_empty() { 60 + println!("Found {} existing patches, applying...", patches.len()); 61 + super::apply::apply_patches(&repo)?; 62 + } 63 + 64 + println!("\nInitialization complete!"); 65 + println!("You can now work in the {} directory.", SOURCE_DIR); 66 + println!("Use 'forkme sync' to save your changes as patches."); 67 + 68 + Ok(()) 69 + } 70 + 71 + fn add_to_gitignore(entry: &str) -> Result<()> { 72 + let gitignore_path = Path::new(".gitignore"); 73 + let entry_line = format!("{}/", entry); 74 + 75 + // Check if .gitignore exists and if entry is already there 76 + if gitignore_path.exists() { 77 + let content = fs::read_to_string(gitignore_path)?; 78 + for line in content.lines() { 79 + let trimmed = line.trim(); 80 + if trimmed == entry 81 + || trimmed == entry_line.trim_end_matches('/') 82 + || trimmed == entry_line 83 + { 84 + println!(".gitignore already contains {}", entry); 85 + return Ok(()); 86 + } 87 + } 88 + } 89 + 90 + // Append to .gitignore 91 + let mut file = OpenOptions::new() 92 + .create(true) 93 + .append(true) 94 + .open(gitignore_path)?; 95 + 96 + // Add newline before if file doesn't end with one 97 + if gitignore_path.exists() { 98 + let content = fs::read_to_string(gitignore_path)?; 99 + if !content.is_empty() && !content.ends_with('\n') { 100 + writeln!(file)?; 101 + } 102 + } 103 + 104 + writeln!(file, "{}", entry_line)?; 105 + println!("Added {} to .gitignore", entry_line); 106 + 107 + Ok(()) 108 + }
+6
src/commands/mod.rs
··· 1 + pub mod apply; 2 + pub mod init; 3 + pub mod stats; 4 + pub mod status; 5 + pub mod sync; 6 + pub mod update;
+79
src/commands/stats.rs
··· 1 + use anyhow::Result; 2 + 3 + use crate::patch::{self, PatchEntry}; 4 + 5 + pub fn run() -> Result<()> { 6 + let entries = patch::list_all_entries()?; 7 + 8 + if entries.is_empty() { 9 + println!("No patches found."); 10 + return Ok(()); 11 + } 12 + 13 + let mut text_added = 0; 14 + let mut text_modified = 0; 15 + let mut binary_added = 0; 16 + let mut deleted = 0; 17 + 18 + for entry in &entries { 19 + match entry { 20 + PatchEntry::Deleted(_) => { 21 + deleted += 1; 22 + } 23 + PatchEntry::Binary(_) => { 24 + // Binary files are always considered "added" since we store the full file 25 + // We could check if the file exists in upstream, but for simplicity 26 + // we'll classify based on whether there's an upstream version 27 + // For now, count all binaries as added (new files) 28 + // A more sophisticated approach would check the source repo 29 + binary_added += 1; 30 + } 31 + PatchEntry::TextPatch(file_path) => { 32 + let patch_content = patch::read_patch(file_path)?; 33 + match classify_text_patch(&patch_content) { 34 + TextPatchType::Added => text_added += 1, 35 + TextPatchType::Modified => text_modified += 1, 36 + } 37 + } 38 + } 39 + } 40 + 41 + let total_added = text_added + binary_added; 42 + let total_modified = text_modified; 43 + let total = entries.len(); 44 + 45 + println!("Patch statistics"); 46 + println!("================"); 47 + println!(); 48 + println!( 49 + " Added: {:>4} ({} text, {} binary)", 50 + total_added, text_added, binary_added 51 + ); 52 + println!(" Modified: {:>4}", total_modified); 53 + println!(" Deleted: {:>4}", deleted); 54 + println!(" ─────────────"); 55 + println!(" Total: {:>4}", total); 56 + 57 + Ok(()) 58 + } 59 + 60 + enum TextPatchType { 61 + Added, 62 + Modified, 63 + } 64 + 65 + fn classify_text_patch(content: &str) -> TextPatchType { 66 + // Look for the first hunk header 67 + for line in content.lines() { 68 + if line.starts_with("@@") { 69 + // Parse hunk header: @@ -start,count +start,count @@ 70 + // New file: -0,0 means nothing in original 71 + if line.contains("-0,0") { 72 + return TextPatchType::Added; 73 + } 74 + return TextPatchType::Modified; 75 + } 76 + } 77 + // Default to modified if we can't determine 78 + TextPatchType::Modified 79 + }
+70
src/commands/status.rs
··· 1 + use anyhow::Result; 2 + use std::path::Path; 3 + 4 + use crate::config::Config; 5 + use crate::git::{self, SOURCE_DIR}; 6 + use crate::patch; 7 + 8 + pub fn run() -> Result<()> { 9 + // Check if config exists 10 + if !Config::exists() { 11 + println!("Not a forkme project (no forkme.toml found)"); 12 + return Ok(()); 13 + } 14 + 15 + let config = Config::load()?; 16 + println!("Forkme project status"); 17 + println!("====================="); 18 + println!(); 19 + 20 + // Upstream info 21 + println!("Upstream:"); 22 + println!(" URL: {}", config.upstream.url); 23 + println!(" Branch: {}", config.upstream.branch); 24 + println!(); 25 + 26 + // Source directory status 27 + println!("Source directory:"); 28 + if Path::new(SOURCE_DIR).exists() { 29 + let repo = git::open_repo()?; 30 + 31 + // Current branch 32 + let head = repo.head()?; 33 + let branch_name = head.shorthand().unwrap_or("(detached)"); 34 + println!(" Branch: {}", branch_name); 35 + 36 + if branch_name != git::FORKME_BRANCH { 37 + println!(" ⚠ Not on '{}' branch", git::FORKME_BRANCH); 38 + } 39 + 40 + if git::is_working_tree_clean(&repo)? { 41 + println!(" Working tree: clean"); 42 + } else { 43 + println!(" Working tree: has uncommitted changes"); 44 + } 45 + 46 + let changes = git::get_changes_from_upstream(&repo, &config.upstream.branch)?; 47 + let existing_patches = patch::list_patches()?; 48 + 49 + if changes.len() != existing_patches.len() { 50 + println!(" Patches: may be out of sync (run 'forkme sync' to update)"); 51 + } else { 52 + println!(" Patches: {} files patched", existing_patches.len()); 53 + } 54 + } else { 55 + println!(" Not initialized (run 'forkme init' first)"); 56 + } 57 + 58 + println!(); 59 + 60 + // Patches info 61 + println!("Patches directory:"); 62 + let patches = patch::list_patches()?; 63 + if patches.is_empty() { 64 + println!(" No patches"); 65 + } else { 66 + println!(" {} patch file(s)", patches.len()); 67 + } 68 + 69 + Ok(()) 70 + }
+96
src/commands/sync.rs
··· 1 + use anyhow::Result; 2 + use std::collections::HashSet; 3 + 4 + use crate::config::Config; 5 + use crate::git::{self, FileContent}; 6 + use crate::patch::{self, PatchEntry}; 7 + 8 + pub fn run() -> Result<()> { 9 + let config = Config::load()?; 10 + let repo = git::open_repo()?; 11 + git::ensure_on_forkme_branch(&repo)?; 12 + 13 + let changes = git::get_changes_from_upstream(&repo, &config.upstream.branch)?; 14 + 15 + if changes.is_empty() { 16 + println!("No changes from upstream. Patches are up to date."); 17 + return Ok(()); 18 + } 19 + 20 + // Track which files have been processed 21 + let mut processed_files: HashSet<String> = HashSet::new(); 22 + 23 + // Generate and save patches/binaries 24 + for change in &changes { 25 + // First, remove any existing entries for this file (clean slate) 26 + patch::delete_all_for_file(&change.path)?; 27 + 28 + match (&change.old_content, &change.new_content) { 29 + // File deleted 30 + (Some(_), None) => { 31 + patch::save_deleted_marker(&change.path)?; 32 + processed_files.insert(change.path.clone()); 33 + println!(" deleted {}", change.path); 34 + } 35 + 36 + // File added or modified 37 + (old, Some(new_content)) => { 38 + let is_new = old.is_none(); 39 + 40 + match new_content { 41 + FileContent::Binary(bytes) => { 42 + // Save binary file directly 43 + patch::save_binary(&change.path, bytes)?; 44 + processed_files.insert(change.path.clone()); 45 + let status = if is_new { 46 + "added (binary)" 47 + } else { 48 + "modified (binary)" 49 + }; 50 + println!(" {} {}", status, change.path); 51 + } 52 + FileContent::Text(new_text) => { 53 + // Generate text patch 54 + let old_text = old.as_ref().and_then(|c| c.as_text()).unwrap_or(""); 55 + let patch_content = patch::generate_patch(Some(old_text), Some(new_text)); 56 + 57 + // Skip empty patches (shouldn't happen, but just in case) 58 + if patch_content.lines().count() <= 2 { 59 + continue; 60 + } 61 + 62 + patch::save_patch(&change.path, &patch_content)?; 63 + processed_files.insert(change.path.clone()); 64 + let status = if is_new { "added" } else { "modified" }; 65 + println!(" {} {}", status, change.path); 66 + } 67 + } 68 + } 69 + 70 + // No content (shouldn't happen) 71 + (None, None) => continue, 72 + } 73 + } 74 + 75 + // Remove entries for files that are no longer modified 76 + let existing_entries = patch::list_all_entries()?; 77 + for entry in existing_entries { 78 + let file_path = entry.file_path(); 79 + if !processed_files.contains(file_path) { 80 + patch::delete_all_for_file(file_path)?; 81 + let suffix = match entry { 82 + PatchEntry::TextPatch(_) => ".patch", 83 + PatchEntry::Binary(_) => " (binary)", 84 + PatchEntry::Deleted(_) => ".deleted", 85 + }; 86 + println!(" removed {}{}", file_path, suffix); 87 + } 88 + } 89 + 90 + // Clean up empty directories 91 + patch::cleanup_empty_dirs()?; 92 + 93 + println!("\nSynced {} files.", processed_files.len()); 94 + 95 + Ok(()) 96 + }
+68
src/commands/update.rs
··· 1 + use anyhow::{bail, Context, Result}; 2 + use std::process::Command; 3 + 4 + use crate::config::Config; 5 + use crate::git::{self, SOURCE_DIR}; 6 + 7 + pub fn run() -> Result<()> { 8 + let config = Config::load()?; 9 + let repo = git::open_repo()?; 10 + git::ensure_on_forkme_branch(&repo)?; 11 + 12 + if !git::is_working_tree_clean(&repo)? { 13 + bail!("Working tree has uncommitted changes. Please commit or stash them before updating."); 14 + } 15 + 16 + let upstream_branch = &config.upstream.branch; 17 + 18 + println!("Fetching from origin..."); 19 + let fetch_status = Command::new("git") 20 + .args(["fetch", "origin"]) 21 + .current_dir(SOURCE_DIR) 22 + .status() 23 + .context("Failed to run git fetch")?; 24 + 25 + if !fetch_status.success() { 26 + bail!("git fetch failed"); 27 + } 28 + 29 + let head_before = repo.head()?.peel_to_commit()?.id(); 30 + 31 + // Rebase onto upstream 32 + println!("Rebasing onto origin/{}...", upstream_branch); 33 + let rebase_status = Command::new("git") 34 + .args(["rebase", &format!("origin/{}", upstream_branch)]) 35 + .current_dir(SOURCE_DIR) 36 + .status() 37 + .context("Failed to run git rebase")?; 38 + 39 + if !rebase_status.success() { 40 + println!(); 41 + println!("Rebase encountered conflicts."); 42 + println!(); 43 + println!("To resolve:"); 44 + println!(" 1. cd {}", SOURCE_DIR); 45 + println!(" 2. Fix conflicts in the listed files"); 46 + println!(" 3. git add <fixed files>"); 47 + println!(" 4. git rebase --continue"); 48 + println!(" 5. Repeat until rebase is complete"); 49 + println!(" 6. Run 'forkme sync' to update patches"); 50 + println!(); 51 + println!("To abort the rebase: git rebase --abort"); 52 + return Ok(()); 53 + } 54 + 55 + // Check if anything changed 56 + let repo = git::open_repo()?; // Re-open to get fresh state 57 + let head_after = repo.head()?.peel_to_commit()?.id(); 58 + 59 + if head_before == head_after { 60 + println!("Already up to date."); 61 + } else { 62 + println!(); 63 + println!("Rebase successful!"); 64 + println!("Run 'forkme sync' to update your patches."); 65 + } 66 + 67 + Ok(()) 68 + }
+111
src/config.rs
··· 1 + use anyhow::{Context, Result}; 2 + use serde::{Deserialize, Serialize}; 3 + use std::fs; 4 + use std::path::Path; 5 + 6 + const CONFIG_FILE: &str = "forkme.toml"; 7 + 8 + #[derive(Debug, Serialize, Deserialize)] 9 + pub struct Config { 10 + pub upstream: Upstream, 11 + } 12 + 13 + #[derive(Debug, Serialize, Deserialize)] 14 + pub struct Upstream { 15 + pub url: String, 16 + pub branch: String, 17 + } 18 + 19 + impl Config { 20 + pub fn load() -> Result<Self> { 21 + Self::load_from(CONFIG_FILE) 22 + } 23 + 24 + pub fn load_from<P: AsRef<Path>>(path: P) -> Result<Self> { 25 + let content = fs::read_to_string(path.as_ref()) 26 + .with_context(|| format!("Failed to read {}", path.as_ref().display()))?; 27 + toml::from_str(&content).with_context(|| "Failed to parse forkme.toml") 28 + } 29 + 30 + pub fn save(&self) -> Result<()> { 31 + self.save_to(CONFIG_FILE) 32 + } 33 + 34 + pub fn save_to<P: AsRef<Path>>(&self, path: P) -> Result<()> { 35 + let content = toml::to_string_pretty(self)?; 36 + fs::write(path.as_ref(), content) 37 + .with_context(|| format!("Failed to write {}", path.as_ref().display()))?; 38 + Ok(()) 39 + } 40 + 41 + pub fn exists() -> bool { 42 + Path::new(CONFIG_FILE).exists() 43 + } 44 + } 45 + 46 + #[cfg(test)] 47 + mod tests { 48 + use super::*; 49 + use tempfile::NamedTempFile; 50 + 51 + #[test] 52 + fn test_config_save_and_load() { 53 + let temp_file = NamedTempFile::new().unwrap(); 54 + let path = temp_file.path(); 55 + 56 + let config = Config { 57 + upstream: Upstream { 58 + url: "https://github.com/test/repo.git".to_string(), 59 + branch: "main".to_string(), 60 + }, 61 + }; 62 + 63 + config.save_to(path).unwrap(); 64 + let loaded = Config::load_from(path).unwrap(); 65 + 66 + assert_eq!(loaded.upstream.url, "https://github.com/test/repo.git"); 67 + assert_eq!(loaded.upstream.branch, "main"); 68 + } 69 + 70 + #[test] 71 + fn test_config_toml_format() { 72 + let temp_file = NamedTempFile::new().unwrap(); 73 + let path = temp_file.path(); 74 + 75 + let config = Config { 76 + upstream: Upstream { 77 + url: "https://github.com/test/repo.git".to_string(), 78 + branch: "develop".to_string(), 79 + }, 80 + }; 81 + 82 + config.save_to(path).unwrap(); 83 + let content = fs::read_to_string(path).unwrap(); 84 + 85 + assert!(content.contains("[upstream]")); 86 + assert!(content.contains("url = \"https://github.com/test/repo.git\"")); 87 + assert!(content.contains("branch = \"develop\"")); 88 + } 89 + 90 + #[test] 91 + fn test_load_invalid_toml() { 92 + let temp_file = NamedTempFile::new().unwrap(); 93 + let path = temp_file.path(); 94 + 95 + fs::write(path, "invalid toml content {{{").unwrap(); 96 + let result = Config::load_from(path); 97 + 98 + assert!(result.is_err()); 99 + } 100 + 101 + #[test] 102 + fn test_load_missing_field() { 103 + let temp_file = NamedTempFile::new().unwrap(); 104 + let path = temp_file.path(); 105 + 106 + fs::write(path, "[upstream]\nurl = \"test\"").unwrap(); 107 + let result = Config::load_from(path); 108 + 109 + assert!(result.is_err()); 110 + } 111 + }
+207
src/git.rs
··· 1 + use anyhow::{bail, Context, Result}; 2 + use git2::{DiffOptions, Repository, ResetType}; 3 + use std::path::Path; 4 + 5 + pub const SOURCE_DIR: &str = "source"; 6 + pub const FORKME_BRANCH: &str = "forkme"; 7 + 8 + pub fn clone_repo(url: &str, branch: &str) -> Result<Repository> { 9 + println!("Cloning {} (branch: {})...", url, branch); 10 + 11 + let mut builder = git2::build::RepoBuilder::new(); 12 + builder.branch(branch); 13 + 14 + let repo = builder 15 + .clone(url, Path::new(SOURCE_DIR)) 16 + .with_context(|| format!("Failed to clone {} into {}", url, SOURCE_DIR))?; 17 + 18 + println!("Clone complete."); 19 + Ok(repo) 20 + } 21 + 22 + pub fn open_repo() -> Result<Repository> { 23 + Repository::open(SOURCE_DIR) 24 + .with_context(|| format!("Failed to open repository at {}", SOURCE_DIR)) 25 + } 26 + 27 + pub fn create_forkme_branch(repo: &Repository, upstream_branch: &str) -> Result<()> { 28 + let upstream_ref = format!("origin/{}", upstream_branch); 29 + let reference = repo 30 + .find_reference(&format!("refs/remotes/{}", upstream_ref)) 31 + .with_context(|| format!("Failed to find remote branch {}", upstream_ref))?; 32 + 33 + let commit = reference.peel_to_commit()?; 34 + 35 + // Create the forkme branch 36 + repo.branch(FORKME_BRANCH, &commit, false) 37 + .with_context(|| format!("Failed to create branch {}", FORKME_BRANCH))?; 38 + 39 + // Checkout the forkme branch 40 + let obj = repo.revparse_single(&format!("refs/heads/{}", FORKME_BRANCH))?; 41 + repo.checkout_tree(&obj, None)?; 42 + repo.set_head(&format!("refs/heads/{}", FORKME_BRANCH))?; 43 + 44 + println!("Created and checked out branch '{}'", FORKME_BRANCH); 45 + Ok(()) 46 + } 47 + 48 + pub fn get_upstream_commit(repo: &Repository, branch: &str) -> Result<git2::Oid> { 49 + let upstream_ref = format!("refs/remotes/origin/{}", branch); 50 + let reference = repo 51 + .find_reference(&upstream_ref) 52 + .with_context(|| format!("Failed to find upstream branch {}", branch))?; 53 + 54 + Ok(reference.peel_to_commit()?.id()) 55 + } 56 + 57 + pub fn reset_to_upstream(repo: &Repository, branch: &str) -> Result<()> { 58 + let upstream_oid = get_upstream_commit(repo, branch)?; 59 + let commit = repo.find_commit(upstream_oid)?; 60 + let obj = commit.as_object(); 61 + 62 + repo.reset(obj, ResetType::Hard, None) 63 + .with_context(|| "Failed to reset to upstream")?; 64 + 65 + println!("Reset to upstream {}", branch); 66 + Ok(()) 67 + } 68 + 69 + pub fn is_working_tree_clean(repo: &Repository) -> Result<bool> { 70 + let statuses = repo.statuses(None)?; 71 + Ok(statuses.is_empty()) 72 + } 73 + 74 + pub fn ensure_on_forkme_branch(repo: &Repository) -> Result<()> { 75 + let head = repo.head()?; 76 + let branch_name = head.shorthand().unwrap_or(""); 77 + 78 + if branch_name != FORKME_BRANCH { 79 + bail!( 80 + "Not on '{}' branch. Currently on '{}'. Please checkout '{}'.", 81 + FORKME_BRANCH, 82 + branch_name, 83 + FORKME_BRANCH 84 + ); 85 + } 86 + Ok(()) 87 + } 88 + 89 + pub enum FileContent { 90 + Text(String), 91 + Binary(Vec<u8>), 92 + } 93 + 94 + impl FileContent { 95 + #[allow(dead_code)] 96 + pub fn is_binary(&self) -> bool { 97 + matches!(self, FileContent::Binary(_)) 98 + } 99 + 100 + pub fn as_text(&self) -> Option<&str> { 101 + match self { 102 + FileContent::Text(s) => Some(s), 103 + FileContent::Binary(_) => None, 104 + } 105 + } 106 + 107 + #[allow(dead_code)] 108 + pub fn as_bytes(&self) -> &[u8] { 109 + match self { 110 + FileContent::Text(s) => s.as_bytes(), 111 + FileContent::Binary(b) => b, 112 + } 113 + } 114 + } 115 + 116 + pub struct FileDiff { 117 + pub path: String, 118 + pub old_content: Option<FileContent>, 119 + pub new_content: Option<FileContent>, 120 + } 121 + 122 + pub fn get_changes_from_upstream( 123 + repo: &Repository, 124 + upstream_branch: &str, 125 + ) -> Result<Vec<FileDiff>> { 126 + let upstream_oid = get_upstream_commit(repo, upstream_branch)?; 127 + let upstream_commit = repo.find_commit(upstream_oid)?; 128 + let upstream_tree = upstream_commit.tree()?; 129 + 130 + let head = repo.head()?; 131 + let head_commit = head.peel_to_commit()?; 132 + let head_tree = head_commit.tree()?; 133 + 134 + let mut diff_opts = DiffOptions::new(); 135 + let diff = 136 + repo.diff_tree_to_tree(Some(&upstream_tree), Some(&head_tree), Some(&mut diff_opts))?; 137 + 138 + let mut changes = Vec::new(); 139 + 140 + diff.foreach( 141 + &mut |delta, _| { 142 + let path = delta 143 + .new_file() 144 + .path() 145 + .or_else(|| delta.old_file().path()) 146 + .map(|p| p.to_string_lossy().to_string()) 147 + .unwrap_or_default(); 148 + 149 + let old_content = if delta.old_file().id().is_zero() { 150 + None 151 + } else { 152 + repo.find_blob(delta.old_file().id()) 153 + .ok() 154 + .map(|blob| blob_to_content(&blob)) 155 + }; 156 + 157 + let new_content = if delta.new_file().id().is_zero() { 158 + None 159 + } else { 160 + repo.find_blob(delta.new_file().id()) 161 + .ok() 162 + .map(|blob| blob_to_content(&blob)) 163 + }; 164 + 165 + changes.push(FileDiff { 166 + path, 167 + old_content, 168 + new_content, 169 + }); 170 + 171 + true 172 + }, 173 + None, 174 + None, 175 + None, 176 + )?; 177 + 178 + Ok(changes) 179 + } 180 + 181 + fn blob_to_content(blob: &git2::Blob) -> FileContent { 182 + let bytes = blob.content(); 183 + // Check if content is valid UTF-8 and doesn't contain null bytes 184 + if let Ok(text) = std::str::from_utf8(bytes) { 185 + if !bytes.contains(&0) { 186 + return FileContent::Text(text.to_string()); 187 + } 188 + } 189 + FileContent::Binary(bytes.to_vec()) 190 + } 191 + 192 + pub fn commit_changes(repo: &Repository, message: &str) -> Result<()> { 193 + let mut index = repo.index()?; 194 + index.add_all(["*"].iter(), git2::IndexAddOption::DEFAULT, None)?; 195 + index.write()?; 196 + 197 + let tree_id = index.write_tree()?; 198 + let tree = repo.find_tree(tree_id)?; 199 + 200 + let head = repo.head()?; 201 + let parent = head.peel_to_commit()?; 202 + 203 + let sig = repo.signature()?; 204 + repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &[&parent])?; 205 + 206 + Ok(()) 207 + }
+4
src/lib.rs
··· 1 + pub mod commands; 2 + pub mod config; 3 + pub mod git; 4 + pub mod patch;
+60
src/main.rs
··· 1 + use anyhow::Result; 2 + use clap::{Parser, Subcommand}; 3 + 4 + mod commands; 5 + mod config; 6 + mod git; 7 + mod patch; 8 + 9 + #[derive(Parser)] 10 + #[command(name = "forkme")] 11 + #[command(about = "A tool for managing forks using a patch-based approach")] 12 + #[command(version)] 13 + struct Cli { 14 + #[command(subcommand)] 15 + command: Commands, 16 + } 17 + 18 + #[derive(Subcommand)] 19 + enum Commands { 20 + /// Initialize a forkme-managed project 21 + Init { 22 + /// URL of the upstream repository 23 + #[arg(long)] 24 + url: Option<String>, 25 + 26 + /// Branch to track from upstream 27 + #[arg(long, default_value = "main")] 28 + branch: String, 29 + }, 30 + 31 + /// Apply patches to the source directory 32 + Apply, 33 + 34 + /// Sync changes from source back to patches 35 + Sync, 36 + 37 + /// Show the current status of the forkme project 38 + Status, 39 + 40 + /// Show statistics about patches 41 + Stats, 42 + 43 + /// Update: fetch upstream and rebase forkme branch 44 + Update, 45 + } 46 + 47 + fn main() -> Result<()> { 48 + let cli = Cli::parse(); 49 + 50 + match cli.command { 51 + Commands::Init { url, branch } => commands::init::run(url, &branch)?, 52 + Commands::Apply => commands::apply::run()?, 53 + Commands::Sync => commands::sync::run()?, 54 + Commands::Status => commands::status::run()?, 55 + Commands::Stats => commands::stats::run()?, 56 + Commands::Update => commands::update::run()?, 57 + } 58 + 59 + Ok(()) 60 + }
+332
src/patch.rs
··· 1 + use anyhow::{Context, Result}; 2 + use diffy::{apply, create_patch, Patch}; 3 + use std::fs; 4 + use std::path::{Path, PathBuf}; 5 + use walkdir::WalkDir; 6 + 7 + pub const PATCHES_DIR: &str = "patches"; 8 + 9 + // Extension for text patches 10 + const PATCH_EXT: &str = ".patch"; 11 + // Extension for deleted file markers 12 + const DELETED_EXT: &str = ".deleted"; 13 + 14 + pub fn generate_patch(old_content: Option<&str>, new_content: Option<&str>) -> String { 15 + let old = old_content.unwrap_or(""); 16 + let new = new_content.unwrap_or(""); 17 + create_patch(old, new).to_string() 18 + } 19 + 20 + pub fn apply_patch(original: &str, patch_content: &str) -> Result<String> { 21 + let patch: Patch<'_, str> = Patch::from_str(patch_content)?; 22 + apply(original, &patch).with_context(|| "Failed to apply patch") 23 + } 24 + 25 + pub fn patch_path_for_file(file_path: &str) -> PathBuf { 26 + PathBuf::from(PATCHES_DIR).join(format!("{}{}", file_path, PATCH_EXT)) 27 + } 28 + 29 + pub fn binary_path_for_file(file_path: &str) -> PathBuf { 30 + PathBuf::from(PATCHES_DIR).join(file_path) 31 + } 32 + 33 + pub fn deleted_path_for_file(file_path: &str) -> PathBuf { 34 + PathBuf::from(PATCHES_DIR).join(format!("{}{}", file_path, DELETED_EXT)) 35 + } 36 + 37 + pub fn file_path_from_patch(patch_path: &Path) -> Option<String> { 38 + let patches_prefix = PathBuf::from(PATCHES_DIR); 39 + patch_path 40 + .strip_prefix(&patches_prefix) 41 + .ok() 42 + .and_then(|p| p.to_str()) 43 + .map(|s| s.trim_end_matches(PATCH_EXT).to_string()) 44 + } 45 + 46 + pub fn file_path_from_binary(binary_path: &Path) -> Option<String> { 47 + let patches_prefix = PathBuf::from(PATCHES_DIR); 48 + binary_path 49 + .strip_prefix(&patches_prefix) 50 + .ok() 51 + .and_then(|p| p.to_str()) 52 + .map(|s| s.to_string()) 53 + } 54 + 55 + pub fn file_path_from_deleted(deleted_path: &Path) -> Option<String> { 56 + let patches_prefix = PathBuf::from(PATCHES_DIR); 57 + deleted_path 58 + .strip_prefix(&patches_prefix) 59 + .ok() 60 + .and_then(|p| p.to_str()) 61 + .map(|s| s.trim_end_matches(DELETED_EXT).to_string()) 62 + } 63 + 64 + pub fn save_patch(file_path: &str, patch_content: &str) -> Result<()> { 65 + let patch_file = patch_path_for_file(file_path); 66 + 67 + if let Some(parent) = patch_file.parent() { 68 + fs::create_dir_all(parent)?; 69 + } 70 + 71 + fs::write(&patch_file, patch_content) 72 + .with_context(|| format!("Failed to write patch file {}", patch_file.display()))?; 73 + 74 + Ok(()) 75 + } 76 + 77 + pub fn save_binary(file_path: &str, content: &[u8]) -> Result<()> { 78 + let binary_file = binary_path_for_file(file_path); 79 + 80 + if let Some(parent) = binary_file.parent() { 81 + fs::create_dir_all(parent)?; 82 + } 83 + 84 + fs::write(&binary_file, content) 85 + .with_context(|| format!("Failed to write binary file {}", binary_file.display()))?; 86 + 87 + Ok(()) 88 + } 89 + 90 + pub fn save_deleted_marker(file_path: &str) -> Result<()> { 91 + let deleted_file = deleted_path_for_file(file_path); 92 + 93 + if let Some(parent) = deleted_file.parent() { 94 + fs::create_dir_all(parent)?; 95 + } 96 + 97 + fs::write(&deleted_file, "") 98 + .with_context(|| format!("Failed to write deleted marker {}", deleted_file.display()))?; 99 + 100 + Ok(()) 101 + } 102 + 103 + pub fn read_patch(file_path: &str) -> Result<String> { 104 + let patch_file = patch_path_for_file(file_path); 105 + fs::read_to_string(&patch_file) 106 + .with_context(|| format!("Failed to read patch file {}", patch_file.display())) 107 + } 108 + 109 + pub fn read_binary(file_path: &str) -> Result<Vec<u8>> { 110 + let binary_file = binary_path_for_file(file_path); 111 + fs::read(&binary_file) 112 + .with_context(|| format!("Failed to read binary file {}", binary_file.display())) 113 + } 114 + 115 + pub fn delete_patch(file_path: &str) -> Result<()> { 116 + let patch_file = patch_path_for_file(file_path); 117 + if patch_file.exists() { 118 + fs::remove_file(&patch_file)?; 119 + } 120 + Ok(()) 121 + } 122 + 123 + pub fn delete_binary(file_path: &str) -> Result<()> { 124 + let binary_file = binary_path_for_file(file_path); 125 + if binary_file.exists() { 126 + fs::remove_file(&binary_file)?; 127 + } 128 + Ok(()) 129 + } 130 + 131 + pub fn delete_deleted_marker(file_path: &str) -> Result<()> { 132 + let deleted_file = deleted_path_for_file(file_path); 133 + if deleted_file.exists() { 134 + fs::remove_file(&deleted_file)?; 135 + } 136 + Ok(()) 137 + } 138 + 139 + pub fn delete_all_for_file(file_path: &str) -> Result<()> { 140 + delete_patch(file_path)?; 141 + delete_binary(file_path)?; 142 + delete_deleted_marker(file_path)?; 143 + Ok(()) 144 + } 145 + 146 + #[derive(Debug, Clone, PartialEq)] 147 + pub enum PatchEntry { 148 + TextPatch(String), // file path 149 + Binary(String), // file path 150 + Deleted(String), // file path 151 + } 152 + 153 + impl PatchEntry { 154 + pub fn file_path(&self) -> &str { 155 + match self { 156 + PatchEntry::TextPatch(p) => p, 157 + PatchEntry::Binary(p) => p, 158 + PatchEntry::Deleted(p) => p, 159 + } 160 + } 161 + } 162 + 163 + pub fn list_all_entries() -> Result<Vec<PatchEntry>> { 164 + let patches_dir = Path::new(PATCHES_DIR); 165 + if !patches_dir.exists() { 166 + return Ok(Vec::new()); 167 + } 168 + 169 + let mut entries = Vec::new(); 170 + 171 + for entry in WalkDir::new(patches_dir) 172 + .into_iter() 173 + .filter_map(|e| e.ok()) 174 + .filter(|e| e.file_type().is_file()) 175 + { 176 + let path = entry.path(); 177 + let path_str = path.to_string_lossy(); 178 + 179 + if path_str.ends_with(PATCH_EXT) { 180 + if let Some(file_path) = file_path_from_patch(path) { 181 + entries.push(PatchEntry::TextPatch(file_path)); 182 + } 183 + } else if path_str.ends_with(DELETED_EXT) { 184 + if let Some(file_path) = file_path_from_deleted(path) { 185 + entries.push(PatchEntry::Deleted(file_path)); 186 + } 187 + } else { 188 + // It's a binary file (no special extension) 189 + if let Some(file_path) = file_path_from_binary(path) { 190 + entries.push(PatchEntry::Binary(file_path)); 191 + } 192 + } 193 + } 194 + 195 + Ok(entries) 196 + } 197 + 198 + pub fn list_patches() -> Result<Vec<String>> { 199 + let entries = list_all_entries()?; 200 + Ok(entries 201 + .into_iter() 202 + .filter_map(|e| match e { 203 + PatchEntry::TextPatch(p) => Some(p), 204 + _ => None, 205 + }) 206 + .collect()) 207 + } 208 + 209 + pub fn ensure_patches_dir() -> Result<()> { 210 + fs::create_dir_all(PATCHES_DIR)?; 211 + Ok(()) 212 + } 213 + 214 + pub fn cleanup_empty_dirs() -> Result<()> { 215 + let patches_dir = Path::new(PATCHES_DIR); 216 + if !patches_dir.exists() { 217 + return Ok(()); 218 + } 219 + 220 + // Collect directories in reverse depth order (deepest first) 221 + let mut dirs: Vec<_> = WalkDir::new(patches_dir) 222 + .into_iter() 223 + .filter_map(|e| e.ok()) 224 + .filter(|e| e.file_type().is_dir()) 225 + .map(|e| e.path().to_path_buf()) 226 + .collect(); 227 + 228 + dirs.sort_by_key(|b| std::cmp::Reverse(b.components().count())); 229 + 230 + for dir in dirs { 231 + if dir == patches_dir { 232 + continue; 233 + } 234 + // Try to remove; will fail if not empty (which is fine) 235 + let _ = fs::remove_dir(&dir); 236 + } 237 + 238 + Ok(()) 239 + } 240 + 241 + #[cfg(test)] 242 + mod tests { 243 + use super::*; 244 + use std::path::Path; 245 + 246 + #[test] 247 + fn test_generate_patch_addition() { 248 + let patch = generate_patch(None, Some("hello\nworld\n")); 249 + assert!(patch.contains("@@ -0,0 +1,2 @@")); 250 + assert!(patch.contains("+hello")); 251 + assert!(patch.contains("+world")); 252 + } 253 + 254 + #[test] 255 + fn test_generate_patch_modification() { 256 + let patch = generate_patch(Some("hello\n"), Some("hello\nworld\n")); 257 + assert!(patch.contains("+world")); 258 + } 259 + 260 + #[test] 261 + fn test_generate_patch_deletion() { 262 + let patch = generate_patch(Some("hello\nworld\n"), None); 263 + assert!(patch.contains("-hello")); 264 + assert!(patch.contains("-world")); 265 + } 266 + 267 + #[test] 268 + fn test_apply_patch_new_file() { 269 + let patch = generate_patch(None, Some("hello\nworld\n")); 270 + let result = apply_patch("", &patch).unwrap(); 271 + assert_eq!(result, "hello\nworld\n"); 272 + } 273 + 274 + #[test] 275 + fn test_apply_patch_modification() { 276 + let original = "line1\nline2\nline3\n"; 277 + let modified = "line1\nmodified\nline3\n"; 278 + let patch = generate_patch(Some(original), Some(modified)); 279 + let result = apply_patch(original, &patch).unwrap(); 280 + assert_eq!(result, modified); 281 + } 282 + 283 + #[test] 284 + fn test_patch_path_for_file() { 285 + let path = patch_path_for_file("src/main.rs"); 286 + assert_eq!(path, PathBuf::from("patches/src/main.rs.patch")); 287 + } 288 + 289 + #[test] 290 + fn test_binary_path_for_file() { 291 + let path = binary_path_for_file("assets/logo.png"); 292 + assert_eq!(path, PathBuf::from("patches/assets/logo.png")); 293 + } 294 + 295 + #[test] 296 + fn test_deleted_path_for_file() { 297 + let path = deleted_path_for_file("old/file.rs"); 298 + assert_eq!(path, PathBuf::from("patches/old/file.rs.deleted")); 299 + } 300 + 301 + #[test] 302 + fn test_file_path_from_patch() { 303 + let patch_path = Path::new("patches/src/lib.rs.patch"); 304 + let file_path = file_path_from_patch(patch_path); 305 + assert_eq!(file_path, Some("src/lib.rs".to_string())); 306 + } 307 + 308 + #[test] 309 + fn test_file_path_from_binary() { 310 + let binary_path = Path::new("patches/assets/image.png"); 311 + let file_path = file_path_from_binary(binary_path); 312 + assert_eq!(file_path, Some("assets/image.png".to_string())); 313 + } 314 + 315 + #[test] 316 + fn test_file_path_from_deleted() { 317 + let deleted_path = Path::new("patches/old/removed.rs.deleted"); 318 + let file_path = file_path_from_deleted(deleted_path); 319 + assert_eq!(file_path, Some("old/removed.rs".to_string())); 320 + } 321 + 322 + #[test] 323 + fn test_patch_entry_file_path() { 324 + let text = PatchEntry::TextPatch("src/main.rs".to_string()); 325 + let binary = PatchEntry::Binary("logo.png".to_string()); 326 + let deleted = PatchEntry::Deleted("old.rs".to_string()); 327 + 328 + assert_eq!(text.file_path(), "src/main.rs"); 329 + assert_eq!(binary.file_path(), "logo.png"); 330 + assert_eq!(deleted.file_path(), "old.rs"); 331 + } 332 + }
+337
tests/integration.rs
··· 1 + //! Integration tests for forkme 2 + 3 + use std::fs; 4 + use tempfile::TempDir; 5 + 6 + use forkme::config::{Config, Upstream}; 7 + use forkme::git::FileContent; 8 + use forkme::patch::{self, PatchEntry}; 9 + use git2::Repository; 10 + 11 + /// Helper to create a test git repository using git2 12 + fn create_test_repo(dir: &std::path::Path) -> Repository { 13 + let repo = Repository::init(dir).unwrap(); 14 + 15 + // Configure repo 16 + let mut config = repo.config().unwrap(); 17 + config.set_str("user.email", "test@test.com").unwrap(); 18 + config.set_str("user.name", "Test User").unwrap(); 19 + 20 + repo 21 + } 22 + 23 + /// Helper to commit files to a repo 24 + fn commit_files(repo: &Repository, files: &[(&str, &str)], message: &str) { 25 + let mut index = repo.index().unwrap(); 26 + 27 + for (path, content) in files { 28 + let full_path = repo.workdir().unwrap().join(path); 29 + if let Some(parent) = full_path.parent() { 30 + fs::create_dir_all(parent).unwrap(); 31 + } 32 + fs::write(&full_path, content).unwrap(); 33 + index.add_path(std::path::Path::new(path)).unwrap(); 34 + } 35 + 36 + index.write().unwrap(); 37 + let tree_id = index.write_tree().unwrap(); 38 + let tree = repo.find_tree(tree_id).unwrap(); 39 + 40 + let sig = repo.signature().unwrap(); 41 + 42 + // Get parent commit if exists 43 + let parent = repo.head().ok().and_then(|h| h.peel_to_commit().ok()); 44 + 45 + match parent { 46 + Some(p) => { 47 + repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &[&p]) 48 + .unwrap(); 49 + } 50 + None => { 51 + repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &[]) 52 + .unwrap(); 53 + } 54 + } 55 + } 56 + 57 + #[test] 58 + fn test_patch_generation_and_application() { 59 + let original = "line1\nline2\nline3\n"; 60 + let modified = "line1\nmodified_line2\nline3\nnew_line4\n"; 61 + 62 + // Generate patch 63 + let patch_content = patch::generate_patch(Some(original), Some(modified)); 64 + 65 + // Verify patch contains expected changes 66 + assert!(patch_content.contains("-line2")); 67 + assert!(patch_content.contains("+modified_line2")); 68 + assert!(patch_content.contains("+new_line4")); 69 + 70 + // Apply patch 71 + let result = patch::apply_patch(original, &patch_content).unwrap(); 72 + assert_eq!(result, modified); 73 + } 74 + 75 + #[test] 76 + fn test_config_roundtrip() { 77 + let temp_dir = TempDir::new().unwrap(); 78 + let config_path = temp_dir.path().join("forkme.toml"); 79 + 80 + let config = Config { 81 + upstream: Upstream { 82 + url: "https://github.com/example/repo.git".to_string(), 83 + branch: "main".to_string(), 84 + }, 85 + }; 86 + 87 + config.save_to(&config_path).unwrap(); 88 + 89 + let loaded = Config::load_from(&config_path).unwrap(); 90 + assert_eq!(loaded.upstream.url, config.upstream.url); 91 + assert_eq!(loaded.upstream.branch, config.upstream.branch); 92 + } 93 + 94 + #[test] 95 + fn test_patch_entry_types() { 96 + let text = PatchEntry::TextPatch("src/lib.rs".to_string()); 97 + let binary = PatchEntry::Binary("assets/logo.png".to_string()); 98 + let deleted = PatchEntry::Deleted("old_file.rs".to_string()); 99 + 100 + assert_eq!(text.file_path(), "src/lib.rs"); 101 + assert_eq!(binary.file_path(), "assets/logo.png"); 102 + assert_eq!(deleted.file_path(), "old_file.rs"); 103 + } 104 + 105 + #[test] 106 + fn test_new_file_patch() { 107 + // Create a patch for a completely new file 108 + let new_content = "pub fn new_function() {\n // todo\n}\n"; 109 + let patch_content = patch::generate_patch(None, Some(new_content)); 110 + 111 + // Should indicate this is a new file (starts from nothing) 112 + assert!(patch_content.contains("@@ -0,0")); 113 + 114 + // Apply to empty string should work 115 + let result = patch::apply_patch("", &patch_content).unwrap(); 116 + assert_eq!(result, new_content); 117 + } 118 + 119 + #[test] 120 + fn test_file_deletion_patch() { 121 + let old_content = "this file will be deleted\n"; 122 + let patch_content = patch::generate_patch(Some(old_content), None); 123 + 124 + // Should indicate deletion 125 + assert!(patch_content.contains("-this file will be deleted")); 126 + } 127 + 128 + #[test] 129 + fn test_file_content_text_detection() { 130 + let text = FileContent::Text("hello world".to_string()); 131 + assert!(!text.is_binary()); 132 + assert_eq!(text.as_text(), Some("hello world")); 133 + } 134 + 135 + #[test] 136 + fn test_file_content_binary_detection() { 137 + let binary = FileContent::Binary(vec![0x89, 0x50, 0x4E, 0x47]); // PNG magic bytes 138 + assert!(binary.is_binary()); 139 + assert_eq!(binary.as_text(), None); 140 + assert_eq!(binary.as_bytes(), &[0x89, 0x50, 0x4E, 0x47]); 141 + } 142 + 143 + #[test] 144 + fn test_create_repo_with_git2() { 145 + let temp_dir = TempDir::new().unwrap(); 146 + let repo = create_test_repo(temp_dir.path()); 147 + 148 + // Create initial commit 149 + commit_files( 150 + &repo, 151 + &[("README.md", "# Test\n"), ("src/main.rs", "fn main() {}\n")], 152 + "Initial commit", 153 + ); 154 + 155 + // Verify commit exists 156 + let head = repo.head().unwrap(); 157 + let commit = head.peel_to_commit().unwrap(); 158 + assert_eq!(commit.message(), Some("Initial commit")); 159 + 160 + // Verify files exist 161 + assert!(temp_dir.path().join("README.md").exists()); 162 + assert!(temp_dir.path().join("src/main.rs").exists()); 163 + } 164 + 165 + #[test] 166 + fn test_repo_diff_detection() { 167 + let temp_dir = TempDir::new().unwrap(); 168 + let repo = create_test_repo(temp_dir.path()); 169 + 170 + // Create initial commit 171 + commit_files(&repo, &[("file.txt", "original\n")], "Initial"); 172 + 173 + // Create second commit with changes 174 + commit_files(&repo, &[("file.txt", "modified\n")], "Modified"); 175 + 176 + // Get the two commits 177 + let head = repo.head().unwrap().peel_to_commit().unwrap(); 178 + let parent = head.parent(0).unwrap(); 179 + 180 + // Diff between commits 181 + let old_tree = parent.tree().unwrap(); 182 + let new_tree = head.tree().unwrap(); 183 + let diff = repo 184 + .diff_tree_to_tree(Some(&old_tree), Some(&new_tree), None) 185 + .unwrap(); 186 + 187 + // Should have one file changed 188 + assert_eq!(diff.deltas().count(), 1); 189 + } 190 + 191 + /// End-to-end test simulating the forkme workflow: 192 + /// 1. Create upstream repo with initial commit on main 193 + /// 2. Clone and create forkme branch 194 + /// 3. Make changes on forkme branch 195 + /// 4. Use library functions to generate patches 196 + /// 5. Verify patches can be applied to restore changes 197 + #[test] 198 + fn test_full_sync_workflow() { 199 + // Create "upstream" repo 200 + let upstream_dir = TempDir::new().unwrap(); 201 + let upstream_repo = create_test_repo(upstream_dir.path()); 202 + 203 + // Initial commit on main 204 + commit_files( 205 + &upstream_repo, 206 + &[ 207 + ("README.md", "# Original Project\n"), 208 + ("src/lib.rs", "pub fn original() {}\n"), 209 + ], 210 + "Initial commit", 211 + ); 212 + 213 + // Create "local" repo (simulating clone) 214 + let local_dir = TempDir::new().unwrap(); 215 + let local_repo = 216 + Repository::clone(upstream_dir.path().to_str().unwrap(), local_dir.path()).unwrap(); 217 + 218 + // Configure local repo 219 + let mut config = local_repo.config().unwrap(); 220 + config.set_str("user.email", "test@test.com").unwrap(); 221 + config.set_str("user.name", "Test User").unwrap(); 222 + 223 + // Create forkme branch from origin/main (which is now called origin/master or main) 224 + let head = local_repo.head().unwrap(); 225 + let head_commit = head.peel_to_commit().unwrap(); 226 + local_repo.branch("forkme", &head_commit, false).unwrap(); 227 + 228 + // Checkout forkme branch 229 + let obj = local_repo.revparse_single("refs/heads/forkme").unwrap(); 230 + local_repo.checkout_tree(&obj, None).unwrap(); 231 + local_repo.set_head("refs/heads/forkme").unwrap(); 232 + 233 + // Make changes on forkme branch 234 + commit_files( 235 + &local_repo, 236 + &[ 237 + ("README.md", "# Modified Project\n\nWith extra content.\n"), 238 + ( 239 + "src/lib.rs", 240 + "pub fn original() {}\n\npub fn new_function() {\n // added by fork\n}\n", 241 + ), 242 + ( 243 + "src/new_file.rs", 244 + "// Completely new file\npub fn fork_only() {}\n", 245 + ), 246 + ], 247 + "Fork modifications", 248 + ); 249 + 250 + // Now simulate what sync does: get changes between upstream and forkme 251 + // First, find the upstream commit (origin/master or origin/main) 252 + let upstream_ref = local_repo 253 + .find_reference("refs/remotes/origin/master") 254 + .or_else(|_| local_repo.find_reference("refs/remotes/origin/main")) 255 + .unwrap(); 256 + let upstream_commit = upstream_ref.peel_to_commit().unwrap(); 257 + let upstream_tree = upstream_commit.tree().unwrap(); 258 + 259 + let forkme_head = local_repo.head().unwrap().peel_to_commit().unwrap(); 260 + let forkme_tree = forkme_head.tree().unwrap(); 261 + 262 + // Get diff 263 + let diff = local_repo 264 + .diff_tree_to_tree(Some(&upstream_tree), Some(&forkme_tree), None) 265 + .unwrap(); 266 + 267 + // Should have 3 files changed 268 + assert_eq!(diff.deltas().count(), 3); 269 + 270 + // Collect changes and generate patches 271 + let mut patches: Vec<(String, String)> = Vec::new(); 272 + 273 + diff.foreach( 274 + &mut |delta, _| { 275 + let path = delta 276 + .new_file() 277 + .path() 278 + .unwrap() 279 + .to_string_lossy() 280 + .to_string(); 281 + 282 + let old_content = if delta.old_file().id().is_zero() { 283 + None 284 + } else { 285 + local_repo 286 + .find_blob(delta.old_file().id()) 287 + .ok() 288 + .and_then(|blob| String::from_utf8(blob.content().to_vec()).ok()) 289 + }; 290 + 291 + let new_content = if delta.new_file().id().is_zero() { 292 + None 293 + } else { 294 + local_repo 295 + .find_blob(delta.new_file().id()) 296 + .ok() 297 + .and_then(|blob| String::from_utf8(blob.content().to_vec()).ok()) 298 + }; 299 + 300 + let patch_content = 301 + patch::generate_patch(old_content.as_deref(), new_content.as_deref()); 302 + patches.push((path, patch_content)); 303 + true 304 + }, 305 + None, 306 + None, 307 + None, 308 + ) 309 + .unwrap(); 310 + 311 + // Verify we have patches for all 3 files 312 + assert_eq!(patches.len(), 3); 313 + 314 + // Verify README patch contains the changes 315 + let readme_patch = patches.iter().find(|(p, _)| p == "README.md").unwrap(); 316 + assert!(readme_patch.1.contains("-# Original Project")); 317 + assert!(readme_patch.1.contains("+# Modified Project")); 318 + 319 + // Verify new file patch 320 + let new_file_patch = patches 321 + .iter() 322 + .find(|(p, _)| p == "src/new_file.rs") 323 + .unwrap(); 324 + assert!(new_file_patch.1.contains("@@ -0,0")); // New file indicator 325 + 326 + // Test applying patches to original content restores the fork changes 327 + let original_readme = "# Original Project\n"; 328 + let patched_readme = patch::apply_patch(original_readme, &readme_patch.1).unwrap(); 329 + assert_eq!( 330 + patched_readme, 331 + "# Modified Project\n\nWith extra content.\n" 332 + ); 333 + 334 + // Test new file can be created from patch 335 + let new_file_content = patch::apply_patch("", &new_file_patch.1).unwrap(); 336 + assert!(new_file_content.contains("pub fn fork_only()")); 337 + }