diff --git a/.github/workflows/build-project.yaml b/.github/workflows/build-project.yaml index 85a3b6b..405a11a 100644 --- a/.github/workflows/build-project.yaml +++ b/.github/workflows/build-project.yaml @@ -6,9 +6,10 @@ on: - main paths: - editor/** - - src/** + - crates/** - Cargo.lock - Cargo.toml + - clippy.toml jobs: build: @@ -16,14 +17,22 @@ jobs: steps: - uses: actions/checkout@v3 - name: Run formatter - run: cargo fmt + run: | + rustup component add rustfmt + cargo fmt - name: Run linter - run: cargo clippy -- --deny warnings + run: | + rustup component add clippy + cargo clippy - name: Audit run: | cargo install cargo-audit cargo audit --deny warnings + - name: Check for useless dependencies + run: | + cargo install cargo-machete + cargo machete - name: Run tests run: cargo test - name: Build the project - run: cargo build --release + run: cargo build --release \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 984079e..13cbfa3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,66 +1,62 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "anstream" -version = "0.6.11" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" dependencies = [ - "windows-sys 0.52.0", + "windows-sys", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "once_cell_polyfill", + "windows-sys", ] -[[package]] -name = "anyhow" -version = "1.0.80" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" - [[package]] name = "auth-git2" -version = "0.5.3" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41e7771d4ab6635cbd685ce8db215b29c78a468098126de77c57f3b2e6eb3757" +checksum = "4888bf91cce63baf1670512d0f12b5d636179a4abbad6504812ac8ab124b3efe" dependencies = [ "dirs", "git2", @@ -69,15 +65,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "1.3.2" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "block-buffer" @@ -90,25 +80,26 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.83" +version = "1.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac" dependencies = [ "jobserver", "libc", + "shlex", ] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "clap" -version = "4.5.0" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80c21025abd42669a92efc996ef13cfb2c5c627858421ea58d5c3b331a6c134f" +checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" dependencies = [ "clap_builder", "clap_derive", @@ -116,9 +107,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.0" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458bf1f341769dfcf849846f65dffdf9146daa56bcd2a47cb4e1de9915567c99" +checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" dependencies = [ "anstream", "anstyle", @@ -128,9 +119,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.0" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" +checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" dependencies = [ "heck", "proc-macro2", @@ -140,21 +131,21 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.0" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] @@ -181,40 +172,51 @@ dependencies = [ [[package]] name = "dirs" -version = "5.0.1" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.48.0", + "windows-sys", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] name = "errno" -version = "0.3.8" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys", ] [[package]] name = "fastrand" -version = "2.0.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "form_urlencoded" @@ -237,22 +239,34 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", - "wasi", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] name = "git2" -version = "0.18.2" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b3ba52851e73b46a4c3df1d89343741112003f0f6f13beb0dfac9e457c3fdcd" +checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110" dependencies = [ - "bitflags 2.4.2", + "bitflags", "libc", "libgit2-sys", "log", @@ -263,46 +277,150 @@ dependencies = [ [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", ] +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jobserver" -version = "0.1.28" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ + "getrandom 0.3.3", "libc", ] [[package]] name = "libc" -version = "0.2.153" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libgit2-sys" -version = "0.16.2+1.7.2" +version = "0.18.1+1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8" +checksum = "e1dcb20f84ffcdd825c7a311ae347cce604a6f084a767dec4a4929829645290e" dependencies = [ "cc", "libc", @@ -314,20 +432,19 @@ dependencies = [ [[package]] name = "libredox" -version = "0.0.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.4.2", + "bitflags", "libc", - "redox_syscall", ] [[package]] name = "libssh2-sys" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" +checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9" dependencies = [ "cc", "libc", @@ -339,9 +456,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.15" +version = "1.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037731f5d3aaa87a5675e895b63ddff1a87624bc29f77004ea829809654e48f6" +checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" dependencies = [ "cc", "libc", @@ -351,39 +468,51 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "log" -version = "0.4.20" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "once_cell" -version = "1.19.0" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "openssl-probe" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.99" +version = "0.9.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" dependencies = [ "cc", "libc", @@ -405,9 +534,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.7" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219c0dcc30b6a27553f9cc242972b67f75b60eb0db71f0b5462f38b058c41546" +checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" dependencies = [ "memchr", "thiserror", @@ -416,9 +545,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.7" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e1288dbd7786462961e69bfd4df7848c1e37e8b74303dbdab82c3a9cdd2809" +checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" dependencies = [ "pest", "pest_generator", @@ -426,9 +555,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.7" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1381c29a877c6d34b8c176e734f35d7f7f5b3adaefe940cb4d1bb7af94678e2e" +checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" dependencies = [ "pest", "pest_meta", @@ -439,9 +568,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.7.7" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0934d6907f148c22a3acbda520c7eed243ad7487a30f51f6ce52b58b7077a8a" +checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" dependencies = [ "once_cell", "pest", @@ -450,87 +579,87 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.35" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] -name = "redox_syscall" -version = "0.4.1" +name = "r-efi" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" [[package]] name = "redox_users" -version = "0.4.4" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ - "getrandom", + "getrandom 0.2.16", "libredox", "thiserror", ] -[[package]] -name = "rust_fzf" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edc3791ac59f1c4c161777ff46d65ecf06f3a8a24f6fa21f4fdd76dae52ef6" - [[package]] name = "rustix" -version = "0.38.31" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ - "bitflags 2.4.2", + "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys", ] [[package]] name = "ryu" -version = "1.0.16" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "serde" -version = "1.0.196" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.196" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -539,53 +668,84 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.113" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", "digest", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "strsim" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.48" +version = "2.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +checksum = "f6397daf94fa90f058bd0fd88429dd9e5738999cca8d701813c80723add80462" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tempfile" -version = "3.10.1" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ - "cfg-if", "fastrand", + "getrandom 0.3.3", + "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys", ] [[package]] @@ -600,18 +760,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.57" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.57" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", @@ -619,69 +779,55 @@ dependencies = [ ] [[package]] -name = "tinyvec" -version = "1.6.0" +name = "tinystr" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ - "tinyvec_macros", + "displaydoc", + "zerovec", ] -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "typenum" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "ucd-trie" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" - -[[package]] -name = "unicode-bidi" -version = "0.3.15" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "unicode-normalization" -version = "0.1.22" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" -dependencies = [ - "tinyvec", -] +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "url" -version = "2.5.0" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "vcpkg" @@ -691,9 +837,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasi" @@ -701,6 +847,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "winapi" version = "0.3.9" @@ -725,148 +880,245 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" -version = "0.48.0" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets 0.48.5", + "windows-targets", ] [[package]] -name = "windows-sys" -version = "0.52.0" +name = "windows-targets" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows-targets 0.52.0", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] -name = "windows-targets" -version = "0.48.5" +name = "windows_aarch64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] -name = "windows-targets" -version = "0.52.0" +name = "windows_aarch64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" -dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", -] +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" +name = "windows_i686_gnu" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.0" +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" +name = "windows_i686_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] -name = "windows_aarch64_msvc" -version = "0.52.0" +name = "windows_x86_64_gnu" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] -name = "windows_i686_gnu" -version = "0.48.5" +name = "windows_x86_64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] -name = "windows_i686_gnu" -version = "0.52.0" +name = "windows_x86_64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "windows_i686_msvc" -version = "0.48.5" +name = "wit-bindgen-rt" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] [[package]] -name = "windows_i686_msvc" -version = "0.52.0" +name = "writeable" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" +name = "yggit" +version = "1.0.0" +dependencies = [ + "clap", + "git2", + "yggit-config", + "yggit-core", + "yggit-db", + "yggit-git", + "yggit-ui", +] + +[[package]] +name = "yggit-config" +version = "1.0.0" +dependencies = [ + "git2", + "thiserror", +] + +[[package]] +name = "yggit-core" +version = "1.0.0" +dependencies = [ + "git2", + "serde", + "thiserror", + "yggit-db", + "yggit-git", + "yggit-parser", + "yggit-ui", +] + +[[package]] +name = "yggit-db" +version = "1.0.0" +dependencies = [ + "git2", + "serde", + "serde_json", + "thiserror", + "yggit-test", +] + +[[package]] +name = "yggit-git" +version = "1.0.0" +dependencies = [ + "auth-git2", + "git2", + "thiserror", + "yggit-test", +] + +[[package]] +name = "yggit-parser" +version = "1.0.0" +dependencies = [ + "pest", + "pest_derive", + "thiserror", +] + +[[package]] +name = "yggit-test" +version = "1.0.0" +dependencies = [ + "git2", + "tempfile", +] + +[[package]] +name = "yggit-ui" +version = "1.0.0" +dependencies = [ + "thiserror", +] + +[[package]] +name = "yoke" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] [[package]] -name = "windows_x86_64_gnu" -version = "0.52.0" +name = "yoke-derive" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] [[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" +name = "zerofrom" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] [[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.0" +name = "zerofrom-derive" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" +name = "zerotrie" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.52.0" +name = "zerovec" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] [[package]] -name = "yggit" -version = "0.0.6" +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ - "anyhow", - "auth-git2", - "clap", - "git2", - "pest", - "pest_derive", - "rust_fzf", - "serde", - "serde_json", - "tempfile", + "proc-macro2", + "quote", + "syn", ] diff --git a/Cargo.toml b/Cargo.toml index 28c59d0..3cdfc4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,25 +1,29 @@ -[package] -name = "yggit" -version = "0.0.6" -edition = "2021" -description = "small tool to manage my one branch git workflow" -license = "MIT AND Apache-2.0" -documentation = "https://github.com/Pilou97/yggit" -homepage = "https://github.com/Pilou97/yggit" -repository = "https://github.com/Pilou97/yggit" +[workspace] +members = [ + "crates/yggit", + "crates/yggit-config", + "crates/yggit-core", + "crates/yggit-db", + "crates/yggit-git", + "crates/yggit-parser", "crates/yggit-test", + "crates/yggit-ui", +] +resolver = "3" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[workspace.dependencies] +git2 = "0.20.2" +thiserror = "2.0.12" +serde = "1.0.219" +serde_json = "1.0.140" +auth-git2 = "0.5.8" +clap = "4.5.40" +tempfile = "3.20.0" -[dependencies] -clap = { version = "4.4.17", features = ["derive"] } -git2 = "0.18.2" -auth-git2 = "0.5.3" -serde = { version = "1.0.164", features = ["derive"] } -serde_json = "1.0.96" -rust_fzf = "0.1.1" -pest = "2.7.3" -pest_derive = "2.7.3" -anyhow = "1.0.80" - -[dev-dependencies] -tempfile = "3" +[workspace.lints.clippy] +disallowed-names = "deny" +assigning_clones = "deny" +bool_assert_comparison = "deny" +bool_comparison = "deny" +bool_to_int_with_if = "deny" +cargo_common_metadata = "deny" +case_sensitive_file_extension_comparisons = "deny" diff --git a/README.md b/README.md index 9ebb56e..383c116 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,15 @@ A tool to manage my git workflow. It allows me to have one branch, and to associate a commits to a specific branch with a interface like the rebase one -# How I am using it? +# How I am using it? What is my git workflow ? -First I use git to have a beautiful history. To do so I am using `git-rebase`. +First I am using `git` to have a beautiful history. To do so I am using `git-rebase`. My goal is to have a linear history and only incremental commits. IMO it's easier to review, and easier to manage when coding. By doing this exercice I've found out I am thinking a bit more before implemententing a solution -Then when I am ready to push my commits in different branch I just have to use `yggit push`. +Then I will want to split my history in many branches, because a bug fix or a feature can be done in many steps, so in many branches. -A _rebase like_ interface will open with the editor specified in your git configuration. +To do so I just have to run `yggit push` + +A _rebase like_ interface will open with the editor specified in your `git` configuration. > Do not edit/move your commit in this editor, it won't have any effects. @@ -28,7 +30,7 @@ I can also specify a custom upstream: # Warning -This project is poorly tested, use it at your own risk. +Even if I use this project daily in my work day. It is poorly tested, use it at your own risk. # Acknowledgements diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..89eda12 --- /dev/null +++ b/clippy.toml @@ -0,0 +1 @@ +disallowed-names = ["db", "repo", ".."] diff --git a/crates/yggit-config/Cargo.toml b/crates/yggit-config/Cargo.toml new file mode 100644 index 0000000..8729b49 --- /dev/null +++ b/crates/yggit-config/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "yggit-config" +version = "1.0.0" +edition = "2021" +description = "extracts git config and enforce value (see yggit)" +license = "MIT AND Apache-2.0" +documentation = "https://github.com/Pilou97/yggit" +homepage = "https://github.com/Pilou97/yggit" +repository = "https://github.com/Pilou97/yggit" +readme = "https://github.com/Pilou97/yggit" +keywords = ["cli", "workflow", "git"] +categories = ["cli"] + +[dependencies] +git2 = { workspace = true } +thiserror = { workspace = true } + +[lints] +workspace = true diff --git a/crates/yggit-config/src/lib.rs b/crates/yggit-config/src/lib.rs new file mode 100644 index 0000000..4b4fe51 --- /dev/null +++ b/crates/yggit-config/src/lib.rs @@ -0,0 +1,80 @@ +use git2::Repository; +use std::env; +use thiserror::Error; + +pub trait Config { + /// Returns the name of the signer + fn name(&self) -> &String; + + /// Returns the email of the signer + fn email(&self) -> &String; + + /// Returns the editor of the signer + fn editor(&self) -> &String; +} + +pub struct GitConfig { + name: String, + email: String, + editor: String, +} + +#[derive(Debug, Error)] +pub enum ConfigError { + #[error("Cannot read the git config")] + CannotReadConfig, + #[error("value {0} is not present in git config")] + ValueNotPresentInConfig(&'static str), + #[error("env variable {0} is not set")] + ValueNotPresentInEnv(&'static str), + #[error("notes.rewriteRef has to be set to \"refs/notes/commits\"")] + WrongRewriteRefValue, +} + +impl GitConfig { + pub fn new(repository: &Repository) -> Result { + let config = repository + .config() + .map_err(|_| ConfigError::CannotReadConfig)?; + + let name = config + .get_string("user.name") + .map_err(|_| ConfigError::ValueNotPresentInConfig("user.name"))?; + + let email = config + .get_string("user.email") + .map_err(|_| ConfigError::ValueNotPresentInConfig("user.email"))?; + + let editor = config.get_string("core.editor").or_else(|_| { + env::var("EDITOR").map_err(|_| ConfigError::ValueNotPresentInEnv("EDITOR")) + })?; + + // Force rewriteRef = "refs/notes/commits" to exist + let rewrite_ref = config + .get_string("notes.rewriteRef") + .map_err(|_| ConfigError::ValueNotPresentInConfig("notes.rewriteRef"))?; + if rewrite_ref != "refs/notes/commits" { + return Err(ConfigError::WrongRewriteRefValue); + } + + Ok(GitConfig { + name, + email, + editor, + }) + } +} + +impl Config for GitConfig { + fn name(&self) -> &String { + &self.name + } + + fn email(&self) -> &String { + &self.email + } + + fn editor(&self) -> &String { + &self.editor + } +} diff --git a/crates/yggit-core/Cargo.toml b/crates/yggit-core/Cargo.toml new file mode 100644 index 0000000..31d6116 --- /dev/null +++ b/crates/yggit-core/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "yggit-core" +version = "1.0.0" +edition = "2021" +description = "yygit logic" +license = "MIT AND Apache-2.0" +documentation = "https://github.com/Pilou97/yggit" +homepage = "https://github.com/Pilou97/yggit" +repository = "https://github.com/Pilou97/yggit" +readme = "https://github.com/Pilou97/yggit" +keywords = ["cli", "workflow", "git"] +categories = ["cli"] + +[dependencies] +git2 = { workspace = true } +serde = { workspace = true, features = ["derive"] } +yggit-db = { path = "../yggit-db" } +yggit-ui = { path = "../yggit-ui" } +yggit-parser = { path = "../yggit-parser" } +yggit-git = { path = "../yggit-git" } +thiserror = { workspace = true } + +[lints] +workspace = true diff --git a/crates/yggit-core/src/lib.rs b/crates/yggit-core/src/lib.rs new file mode 100644 index 0000000..9b7a0bf --- /dev/null +++ b/crates/yggit-core/src/lib.rs @@ -0,0 +1,247 @@ +use git2::Oid; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use thiserror::Error; +use yggit_db::{Database, DatabaseError, DatabaseRead}; +use yggit_git::{Git, GitError}; +use yggit_parser::{Commit, Line, Parser, ParserError}; +use yggit_ui::{Editor, EditorError}; + +#[derive(Debug, Error)] +pub enum CoreError { + #[error(transparent)] + GitError(GitError), + #[error(transparent)] + DatabaseError(DatabaseError), + #[error(transparent)] + EditorError(EditorError), + #[error(transparent)] + ParserError(ParserError), + #[error("cannot parse commit id {0}")] + OidParsing(String), +} + +#[derive(Serialize, Deserialize)] +struct Branch { + target: String, + origin: Option, +} + +const PUSH_FOOTER: &str = r#" +# Here is how to use yggit-push +# +# Commands: +# -> add a branch to the above commit +# -> : add a branch to the above commit +# +# What happens next? +# - All branches are pushed on origin, except if you specified a custom origin +# +# It's not a rebase, you can't edit commits nor reorder them +"#; + +pub fn push( + git: impl Git, + database: impl Database, + editor: impl Editor, + force: bool, + onto: Option, + no_push: bool, +) -> Result<(), CoreError> { + let onto = match onto { + Some(onto) => onto, + None => git.main().map_err(CoreError::GitError)?, + }; + + let commits = git.list_commits(&onto).map_err(CoreError::GitError)?; + + // Now let's retrieve the branch for the existing commits + let branches = commits + .iter() + .filter_map( + |commit| match database.read::(&commit.oid, "branch") { + Ok(Some(branch)) => Some(Ok((commit.clone(), branch))), + Ok(None) => None, + Err(err) => Some(Err(err)), + }, + ) + .collect::, DatabaseError>>() + .map_err(CoreError::DatabaseError)?; + + // Let's create a string with this, so that the user can edit it + let todo = commits + .iter() + .flat_map(|commit| { + let commit_line = Line::Commit(Commit { + sha: commit.oid.to_string(), + title: commit.title.to_string(), + }); + let branch_line = branches.get(commit).map(|branch| { + Line::Branch(yggit_parser::Branch { + origin: branch.origin.clone(), + name: branch.target.clone(), + }) + }); + match branch_line { + Some(branch_line) => vec![commit_line, branch_line], + None => vec![commit_line], + } + }) + .map(|line| line.to_string()) + .collect::>() + .join("\n"); + + // Now the user should modify the todo (or not) + let todo = editor + .edit(todo, PUSH_FOOTER) + .map_err(CoreError::EditorError)?; + + // Now we can parse it + let parsed_todo = Parser::parse_file(&todo).map_err(CoreError::ParserError)?; + + // Now we retrieve the branches and the correspoding oid from the todo + let branches = parsed_todo + .windows(2) + .filter_map(|tuple| { + let fst = tuple.first(); + let snd = tuple.get(1); + match (fst, snd) { + (Some(Line::Commit(commit)), Some(Line::Branch(branch))) => { + match Oid::from_str(&commit.sha) { + Ok(oid) => Some(Ok(( + oid, + Branch { + target: branch.name.clone(), + origin: branch.origin.clone(), + }, + ))), + Err(_) => Some(Err(CoreError::OidParsing(commit.sha.clone()))), + } + } + _ => None, + } + }) + .collect::, CoreError>>()?; + + // Now we need to save the state + commits.iter().try_for_each(|commit| { + database + .delete(&commit.oid, "branch") + .map_err(CoreError::DatabaseError)?; + match branches.get(&commit.oid) { + Some(branch) => { + database + .write(&commit.oid, "branch", branch) + .map_err(CoreError::DatabaseError)?; + Ok(()) + } + None => Ok(()), + } + })?; + + if no_push { + return Ok(()); + } + + // Now we can push + branches + .into_iter() + .map(|(oid, branch)| -> Result<(), CoreError> { + git.set_branch_to_commit(&branch.target, oid) + .map_err(CoreError::GitError)?; + let origin = branch.origin.unwrap_or("origin".to_string()); + + if force { + git.push_force_with_lease(&origin, &branch.target) + .map_err(CoreError::GitError)?; + } else { + git.push(&origin, &branch.target) + .map_err(CoreError::GitError)?; + } + + Ok(()) + }) + .collect::, CoreError>>()?; + Ok(()) +} + +const SHOW_FOOTER: &str = r#""#; + +pub fn show( + git: impl Git, + database: impl DatabaseRead, + editor: impl Editor, + onto: Option, +) -> Result<(), CoreError> { + let onto = match onto { + Some(onto) => onto, + None => git.main().map_err(CoreError::GitError)?, + }; + + let commits = git.list_commits(&onto).map_err(CoreError::GitError)?; + + // Now let's retrieve the branch for the existing commits + let branches = commits + .iter() + .filter_map( + |commit| match database.read::(&commit.oid, "branch") { + Ok(Some(branch)) => Some(Ok((commit.clone(), branch))), + Ok(None) => None, + Err(err) => Some(Err(err)), + }, + ) + .collect::, DatabaseError>>() + .map_err(CoreError::DatabaseError)?; + + // Let's create a string with this, so that the user can edit it + let todo = commits + .iter() + .flat_map(|commit| { + let commit_line = Line::Commit(Commit { + sha: commit.oid.to_string(), + title: commit.title.to_string(), + }); + let branch_line = branches.get(commit).map(|branch| { + Line::Branch(yggit_parser::Branch { + origin: branch.origin.clone(), + name: branch.target.clone(), + }) + }); + match branch_line { + Some(branch_line) => vec![commit_line, branch_line], + None => vec![commit_line], + } + }) + .map(|line| line.to_string()) + .collect::>() + .join("\n"); + + // Now the user should modify the todo (or not) + let _todo = editor + .no_edit(todo, SHOW_FOOTER) + .map_err(CoreError::EditorError)?; + + Ok(()) +} + +const _APPLY_FOOTER: &str = r#" +# Here is how to use yggit-apply +# +# Commands: +# -> add a branch to the above commit +# -> : add a branch to the above commit +# +# What happens next? +# - All commits will be associated to a branch, but NOTHING will be pushed +# +# It's not a rebase, you can't edit commits nor reorder them +"#; + +pub fn apply( + git: impl Git, + database: impl Database, + editor: impl Editor, + onto: Option, +) -> Result<(), CoreError> { + push(git, database, editor, false, onto, true) +} diff --git a/crates/yggit-db/Cargo.toml b/crates/yggit-db/Cargo.toml new file mode 100644 index 0000000..c79c708 --- /dev/null +++ b/crates/yggit-db/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "yggit-db" +version = "1.0.0" +edition = "2021" +description = "abstraction over git notes" +license = "MIT AND Apache-2.0" +documentation = "https://github.com/Pilou97/yggit" +homepage = "https://github.com/Pilou97/yggit" +repository = "https://github.com/Pilou97/yggit" +readme = "https://github.com/Pilou97/yggit" +keywords = ["cli", "workflow", "git"] +categories = ["cli"] + +[dependencies] +git2 = { workspace = true } +thiserror = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } + +[dev-dependencies] +yggit-test = { path = "../yggit-test" } + +[lints] +workspace = true diff --git a/crates/yggit-db/src/lib.rs b/crates/yggit-db/src/lib.rs new file mode 100644 index 0000000..e654c01 --- /dev/null +++ b/crates/yggit-db/src/lib.rs @@ -0,0 +1,152 @@ +use git2::{Oid, Repository, Signature}; +use serde::{de::DeserializeOwned, Serialize}; +use serde_json::Value; +use std::collections::HashMap; +use thiserror::Error; + +/// Simple key value store +/// +/// The values are stored in the commit note +/// Don't forget to set rewriteRef to "refs/notes/commits" +pub struct GitDatabase<'a> { + repository: &'a Repository, + name: String, + email: String, +} + +#[derive(Error, Debug)] +pub enum DatabaseError { + #[error("Unknown error")] + Unknown, + #[error("Cannot serialize value")] + CannotSerializeValue, + #[error("Cannot deserialize value")] + CannotDeserializeValue, + #[error("Cannot serialize database")] + CannotSerialize, + #[error("Cannot open database")] + CannotOpen, + #[error("Cannot close database")] + CannotClose, +} + +pub trait DatabaseRead { + /// Retrieve data from the commit note + fn read(&self, oid: &Oid, key: &str) -> Result, DatabaseError> + where + D: DeserializeOwned; +} + +pub trait DatabaseWrite { + /// Stores data in the commit note + fn write(&self, oid: &Oid, key: &str, data: &D) -> Result<(), DatabaseError> + where + D: Serialize; +} + +pub trait DatabaseDelete { + /// Delete the key for a given note + fn delete(&self, oid: &Oid, key: &str) -> Result<(), DatabaseError>; +} + +pub trait Database: DatabaseRead + DatabaseDelete + DatabaseWrite {} + +impl<'a> GitDatabase<'a> { + pub fn new(repository: &'a Repository, name: String, email: String) -> Self { + GitDatabase { + repository, + name, + email, + } + } + + /// Read the notes stored for the given Oid + fn read_note(&self, oid: &Oid) -> HashMap { + self.repository + .find_note(None, *oid) + .map(|note| { + let message = note.message().unwrap_or_default(); + serde_json::from_str::>(message).unwrap_or_default() + }) + .unwrap_or_default() + } + + /// Write the note and erase the old one + fn write_note(&self, oid: &Oid, note: HashMap) -> Result<(), DatabaseError> { + let note = serde_json::to_string(¬e).map_err(|_| DatabaseError::CannotSerialize)?; + let author = Signature::now(&self.name, &self.email).unwrap(); + self.repository + .note(&author, &author, None, *oid, ¬e, true) + .map(|_| ()) + .map_err(|_| DatabaseError::CannotClose) + } +} + +impl DatabaseWrite for GitDatabase<'_> { + fn write(&self, oid: &Oid, key: &str, data: &D) -> Result<(), DatabaseError> + where + D: Serialize, + { + let mut note = self.read_note(oid); + + let data = serde_json::to_value(data).map_err(|_| DatabaseError::CannotSerializeValue)?; + note.insert(key.to_string(), data); + + self.write_note(oid, note) + } +} +impl DatabaseRead for GitDatabase<'_> { + fn read(&self, oid: &Oid, key: &str) -> Result, DatabaseError> + where + D: DeserializeOwned, + { + let note = self.read_note(oid); + + let Some(value) = note.get(key) else { + return Ok(None); + }; + serde_json::from_value::(value.clone()) + .map(Some) + .map_err(|_| DatabaseError::CannotDeserializeValue) + } +} + +impl DatabaseDelete for GitDatabase<'_> { + fn delete(&self, oid: &Oid, key: &str) -> Result<(), DatabaseError> { + let mut note = self.read_note(oid); + note.remove(key); + self.write_note(oid, note) + } +} + +impl Database for GitDatabase<'_> {} + +#[cfg(test)] +mod tests { + use crate::{DatabaseDelete, DatabaseRead, DatabaseWrite, GitDatabase}; + use yggit_test::TempRepository; + + #[test] + fn test_get_note() { + // Init the repository + let repository = TempRepository::new(); + repository.set_identity("Bob", "example@example.com"); + repository.add_file("README.md", "a cool readme"); + repository.commit("a commit message"); + let repo = repository.as_ref(); + // Get the head commit + let id = repo.head().unwrap().peel_to_commit().unwrap().id(); + + // Test the db + let database = GitDatabase::new(&repo, "My name".into(), "My email".into()); + + assert!(database.read::(&id, "hello").unwrap().is_none()); + assert!(database.write(&id, "hello", &"data".to_string()).is_ok()); + assert_eq!( + "data", + database.read::(&id, "hello").unwrap().unwrap() + ); + database.delete(&id, &"hello").expect("should work"); + assert!(database.read::(&id, "hello").unwrap().is_none()); + } +} diff --git a/crates/yggit-git/Cargo.toml b/crates/yggit-git/Cargo.toml new file mode 100644 index 0000000..3d56ca2 --- /dev/null +++ b/crates/yggit-git/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "yggit-git" +version = "1.0.0" +edition = "2021" +description = "abstraction over git using git2" +license = "MIT AND Apache-2.0" +documentation = "https://github.com/Pilou97/yggit" +homepage = "https://github.com/Pilou97/yggit" +repository = "https://github.com/Pilou97/yggit" +readme = "https://github.com/Pilou97/yggit" +keywords = ["cli", "workflow", "git"] +categories = ["cli"] + +[dependencies] +git2 = { workspace = true } +thiserror = { workspace = true } +auth-git2 = { workspace = true } + +[dev-dependencies] +yggit-test = { path = "../yggit-test" } + +[lints] +workspace = true diff --git a/crates/yggit-git/src/lib.rs b/crates/yggit-git/src/lib.rs new file mode 100644 index 0000000..563f0c1 --- /dev/null +++ b/crates/yggit-git/src/lib.rs @@ -0,0 +1,385 @@ +use std::{ + borrow::Cow, + hash::Hash, + sync::{Arc, Mutex}, +}; + +use auth_git2::GitAuthenticator; +use git2::{Error, Oid, Repository}; +use thiserror::Error; + +pub trait Git { + /// Returns the main branch + fn main(&self) -> Result; + + /// List all the commits + /// From HEAD to unit parameter + fn list_commits(&self, until: &str) -> Result, GitError>; + + /// equivalent of `git push --force-with-lease` + fn push_force_with_lease(&self, origin: &str, branch: &str) -> Result<(), GitError>; + + /// Equivalent of `git push --force` + fn push_force(&self, origin: &str, branch: &str) -> Result<(), GitError>; + + /// Equivalent of `git push` + fn push(&self, origin: &str, branch: &str) -> Result<(), GitError>; + + /// Set a branch to a given commit + fn set_branch_to_commit(&self, branch: &str, oid: Oid) -> Result<(), GitError>; +} + +/// A git client using git2.rs +pub struct GitClient<'a> { + repository: &'a Repository, + auth: GitAuthenticator, +} + +enum PushMode { + Normal, + Force, + ForceWithLease, +} + +#[derive(Debug)] +pub enum NegotiationResult { + NoPushNeeded, + RemoteDiverged, + AllowedToPush, + AllowedToPushNewBranch, +} + +#[derive(Clone)] +pub struct Commit { + pub oid: Oid, + pub title: String, +} + +impl Hash for Commit { + fn hash(&self, state: &mut H) { + self.oid.hash(state); + } +} + +impl PartialEq for Commit { + fn eq(&self, other: &Self) -> bool { + self.oid == other.oid + } +} + +impl Eq for Commit {} + +#[derive(Debug, Error)] +pub enum GitError { + #[error("No main branch was found")] + NoMainBranch, + #[error("The branch [{0}] was not found")] + BranchNotFound(String), + #[error("Commit of branch [{0}] was not found")] + CommitOfBranchNotFound(String), + #[error("Cannot list commits")] + CannotListCommit, + #[error("Oid is not valid")] + InvalidOid, + #[error("Head is not present")] + HeadNotPresent, + #[error("Cannot reach {0} from {0}")] + CannotReach(Oid, Oid), + #[error("Config not found")] + ConfigNotFound, + #[error("Remote {0} not found")] + RemoteNotFound(String), + #[error("Branch {branch} wasn't pushed on {origin}")] + NotPushed { + branch: String, + origin: String, + reason: Cow<'static, str>, + }, + #[error("Feature {0} is not yet implemented")] + NotYetImplemented(&'static str), + #[error("Commit {0} was not found")] + CommitNotFound(Oid), +} + +impl<'a> GitClient<'a> { + pub fn new(repository: &'a Repository) -> Self { + GitClient { + repository, + auth: GitAuthenticator::new(), + } + } + + /// Simple push + /// Returns Ok(()) if the push was not needed + fn custom_push(&self, origin: &str, branch: &str, mode: PushMode) -> Result<(), GitError> { + let git_config = self + .repository + .config() + .map_err(|_| GitError::ConfigNotFound)?; + let mut push_options = git2::PushOptions::new(); + + let mut remote_callbacks = git2::RemoteCallbacks::new(); + remote_callbacks.credentials(self.auth.credentials(&git_config)); + + let fetch_refname = match &mode { + PushMode::Normal => format!("refs/heads/{branch}"), + PushMode::Force => format!("+refs/heads/{branch}"), + PushMode::ForceWithLease => format!("refs/heads/{branch}"), + }; + + let mut remote = self + .repository + .find_remote(origin) + .map_err(|_| GitError::RemoteNotFound(origin.to_string()))?; + + let negotiation_result = Arc::new(Mutex::new(None)); + let negotiation_result_read = Arc::clone(&negotiation_result); + match mode { + PushMode::Normal | PushMode::Force => { + remote_callbacks.push_negotiation(move |remote_updates| { + let mut negotiation_result = negotiation_result.lock().unwrap(); + let Some(remote_update) = remote_updates.iter().next() else { + *negotiation_result = Some(NegotiationResult::NoPushNeeded); + return Err(Error::from_str("not updates to be done")); + }; + + if remote_update.src() == git2::Oid::zero() { + *negotiation_result = Some(NegotiationResult::AllowedToPushNewBranch); + return Ok(()); + } + + *negotiation_result = Some(NegotiationResult::AllowedToPush); + Ok(()) + }); + } + PushMode::ForceWithLease => { + remote_callbacks.push_negotiation(move |remote_updates| { + let null = git2::Oid::zero(); + let mut negotiation_result = negotiation_result.lock().unwrap(); + let Some(remote_update) = remote_updates.iter().next() else { + *negotiation_result = Some(NegotiationResult::NoPushNeeded); + return Err(Error::from_str("not updates to be done")); + }; + + if remote_update.src() == null { + *negotiation_result = Some(NegotiationResult::AllowedToPushNewBranch); + return Ok(()); + } + + // Comparing src with local origin + let remote_origin_oid = remote_update.src(); + // Get the head of this branch + let local_origin_oid = { + let local_origin_name = remote_update + .src_refname() + .ok_or(Error::from_str("cannot parse source refname"))?; + let upstream_name = local_origin_name + .strip_prefix("refs/heads/") + .ok_or(Error::from_str("cannot strip local origin name"))?; + self.repository + .find_reference(&format!("refs/remotes/{}/{}", origin, upstream_name)) + .ok() + .and_then(|reference| reference.peel_to_commit().ok()) + .map(|commit| commit.id()) + .ok_or(Error::from_str("cannot find the commit reference hash"))? + }; + if remote_origin_oid == local_origin_oid { + *negotiation_result = Some(NegotiationResult::AllowedToPush); + Ok(()) + } else { + *negotiation_result = Some(NegotiationResult::RemoteDiverged); + Err(Error::from_str("Origins have divered")) + } + }); + } + }; + push_options.remote_callbacks(remote_callbacks); + let push_res = remote.push(&[fetch_refname], Some(&mut push_options)); + + let negotiation_result = negotiation_result_read.lock().unwrap(); + let negotiation_result = negotiation_result.as_ref().unwrap(); + + match (negotiation_result, push_res) { + (NegotiationResult::NoPushNeeded, _) => Ok(()), + (NegotiationResult::RemoteDiverged, _) => Err(GitError::NotPushed { + branch: branch.to_string(), + origin: origin.to_string(), + reason: "the origin on the server is not the same as the local one".into(), + }), + (NegotiationResult::AllowedToPush, Err(err)) => Err(GitError::NotPushed { + branch: branch.to_string(), + origin: origin.to_string(), + reason: err.message().to_string().into(), + }), + (NegotiationResult::AllowedToPushNewBranch, Err(err)) => Err(GitError::NotPushed { + branch: branch.to_string(), + origin: origin.to_string(), + reason: err.message().to_string().into(), + }), + (NegotiationResult::AllowedToPush, Ok(())) + | (NegotiationResult::AllowedToPushNewBranch, Ok(())) => Ok(()), + } + } +} + +impl Git for GitClient<'_> { + fn main(&self) -> Result { + if let Ok(head) = self.repository.find_reference("HEAD") { + if let Some(target) = head.symbolic_target() { + if let Some(branch_name) = target.rsplit('/').next() { + return Ok(branch_name.to_string()); + } + } + } + Err(GitError::NoMainBranch) + } + + fn list_commits(&self, until: &str) -> Result, GitError> { + let head = self + .repository + .head() + .map_err(|_| GitError::HeadNotPresent)? + .target() + .ok_or(GitError::HeadNotPresent)?; + + let until_branch = self + .repository + .find_branch(until, git2::BranchType::Local) + .map_err(|_| GitError::BranchNotFound(until.to_string()))?; + + let until_commit = until_branch + .get() + .peel_to_commit() + .map_err(|_| GitError::CommitOfBranchNotFound(until.to_string()))?; + + if head == until_commit.id() { + return Ok(vec![]); + } + + // make sure until is a parent of HEAD + if !self + .repository + .graph_descendant_of(head, until_commit.id()) + .map_err(|_| GitError::CannotReach(until_commit.id(), head))? + { + return Err(GitError::CannotReach(until_commit.id(), head)); + } + + let mut revwalk = self + .repository + .revwalk() + .map_err(|_| GitError::CannotListCommit)?; + revwalk + .push_head() + .map_err(|_| GitError::CannotListCommit)?; + + let mut commits = Vec::default(); + + for oid in revwalk { + let oid = oid.map_err(|_| GitError::InvalidOid)?; + + if oid == until_commit.id() { + break; + } + + // Get the commit + let commit = self + .repository + .find_commit(oid) + .map_err(|_| GitError::CommitNotFound(oid))?; + // Get the title and the description + let mut message = commit.message().unwrap_or_default().splitn(2, '\n'); + // Title is on the first line of the message + let title = message.next().unwrap_or_default().to_string(); + + // The commit has to be found, because it's listed from the revwalk + commits.push(Commit { oid, title }); + } + commits.reverse(); + + Ok(commits) + } + + /// Equivalent of `git push --force-with-lease` + fn push_force_with_lease(&self, origin: &str, branch: &str) -> Result<(), GitError> { + self.custom_push(origin, branch, PushMode::ForceWithLease) + } + + /// Equivalent of `git push --force` + fn push_force(&self, origin: &str, branch: &str) -> Result<(), GitError> { + self.custom_push(origin, branch, PushMode::Force) + } + + /// Equivalent of `git push` + fn push(&self, origin: &str, branch: &str) -> Result<(), GitError> { + self.custom_push(origin, branch, PushMode::Normal) + } + + fn set_branch_to_commit(&self, branch: &str, oid: Oid) -> Result<(), GitError> { + let commit = self + .repository + .find_commit(oid) + .map_err(|_| GitError::CommitNotFound(oid))?; + + self.repository + .branch(branch, &commit, true) + .map_err(|_| GitError::BranchNotFound(branch.to_string()))?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::{Git, GitClient, GitError}; + use yggit_test::TempRepository; + + #[test] + fn test_main_branch() { + let repository = TempRepository::new(); + repository.set_identity("Bob", "example@example.com"); + let git = GitClient::new(repository.as_ref()); + assert_eq!(git.main().unwrap(), "main"); + } + + #[test] + fn test_list_commits() { + let repository = TempRepository::new(); + repository.set_identity("Bob", "example@example.com"); + repository.add_file("README.md", "# My Project"); + repository.commit("a cool commit name"); + + repository.checkout_b("feat"); + repository.add_file("README.md", "# My Second Project"); + repository.commit("a cool commit name"); + + let git = GitClient::new(repository.as_ref()); + git.list_commits("main").expect("to work"); + } + + #[test] + fn test_list_commits_unknown_branch() { + let repository = TempRepository::new(); + repository.set_identity("Bob", "example@example.com"); + repository.add_file("README.md", "# My Project 2"); + repository.commit("a cool commit name"); + + let git = GitClient::new(repository.as_ref()); + let Err(GitError::BranchNotFound(branch_name)) = git.list_commits("whouhouhou") else { + panic!("expecting an error") + }; + assert_eq!(branch_name, "whouhouhou") + } + + #[test] + fn test_list_commits_empty_branch() { + let repository = TempRepository::new(); + repository.set_identity("Bob", "example@example.com"); + repository.add_file("README.md", "# My Project 2"); + repository.commit("a cool commit name"); + + let git = GitClient::new(repository.as_ref()); + let commits = git.list_commits("main").expect("it should be listable"); + assert_eq!(commits.len(), 0) + } +} diff --git a/crates/yggit-parser/Cargo.toml b/crates/yggit-parser/Cargo.toml new file mode 100644 index 0000000..c98ab7a --- /dev/null +++ b/crates/yggit-parser/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "yggit-parser" +version = "1.0.0" +edition = "2021" +description = "implement rebase like parsing for yggit" +license = "MIT AND Apache-2.0" +documentation = "https://github.com/Pilou97/yggit" +homepage = "https://github.com/Pilou97/yggit" +repository = "https://github.com/Pilou97/yggit" +readme = "https://github.com/Pilou97/yggit" +keywords = ["cli", "workflow", "git"] +categories = ["cli"] + +[dependencies] +pest = { version = "2.8.0" } +pest_derive = "2.8.0" +thiserror = { workspace = true } + +[lints] +workspace = true diff --git a/crates/yggit-parser/src/lib.rs b/crates/yggit-parser/src/lib.rs new file mode 100644 index 0000000..7733c7e --- /dev/null +++ b/crates/yggit-parser/src/lib.rs @@ -0,0 +1,184 @@ +use std::fmt::Display; + +use thiserror::Error; + +#[derive(Debug, PartialEq, Eq)] +pub struct Commit { + pub sha: String, + pub title: String, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Branch { + pub origin: Option, + pub name: String, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum Line { + Commit(Commit), + Branch(Branch), +} + +#[derive(pest_derive::Parser)] +#[grammar = "parser.pest"] +pub struct Parser; + +#[derive(Debug, Error)] +pub enum ParserError { + #[error("Unknown error happened")] + Unknown, + #[error("Should be a line")] + IsNotLine { line: String }, + #[error("The file is not correct")] + IsNotFile, + #[error("Token expected")] + TokenExpected, + #[error("Invalid token")] + InvalidToken, +} + +macro_rules! as_str { + ($pair:expr, $rule:expr) => { + match $pair { + Some(pair) => { + if pair.as_rule() == $rule { + pair.as_str().to_string() + } else { + return Err(ParserError::InvalidToken); + } + } + _ => return Err(ParserError::TokenExpected), + } + }; +} + +impl Parser { + pub fn parse_file(file: &str) -> Result, ParserError> { + use pest::Parser; + let pairs = Self::parse(Rule::file, file).map_err(|_| ParserError::IsNotFile)?; + + let mut file = vec![]; + for pair in pairs { + pair.as_str().to_string(); + match pair.as_rule() { + Rule::file => { + for line in pair.into_inner() { + let line = match line.as_rule() { + Rule::git_commit => { + let mut commit = line.into_inner(); + let sha = as_str!(commit.next(), Rule::commit_hash); + let title = as_str!(commit.next(), Rule::commit_title); + Line::Commit(Commit { sha, title }) + } + Rule::branch => { + let mut branch = line.into_inner(); + let origin_or_name = branch.next(); + let (origin, name) = + match origin_or_name.as_ref().map(|pair| pair.as_rule()) { + Some(Rule::origin) => { + let origin = origin_or_name + .map(|pair| pair.as_str().to_string()); + let name = as_str!(branch.next(), Rule::branch_name); + (origin, name) + } + Some(Rule::branch_name) => { + let name = origin_or_name.unwrap().as_str().to_string(); + (None, name) + } + _ => return Err(ParserError::InvalidToken), + }; + Line::Branch(Branch { origin, name }) + } + Rule::EOI => continue, + Rule::comment => continue, // for now we ignore the comments + _ => return Err(ParserError::InvalidToken), + }; + file.push(line); + } + } + _ => { + return Err(ParserError::IsNotLine { + line: pair.as_str().to_string(), + }); + } + } + } + Ok(file) + } +} + +impl Display for Line { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Line::Commit(commit) => write!(f, "{} {}", commit.sha, commit.title), + Line::Branch(branch) => match &branch.origin { + Some(origin) => write!(f, "-> {}:{}", origin, branch.name), + None => write!(f, "-> {}", branch.name), + }, + } + } +} + +#[cfg(test)] +mod tests { + use crate::{Branch, Commit, Line, Parser}; + + #[test] + fn test_parser_roundtrip() { + let file = format!( + " +afd1ebed7162bc404e0cc169d25fb4b01806eb2c chore: upgrade&é\"'(§è!çà) rust toolchain +afd1ebed7162bc404e0cc169d25fb4b01806eb2c chore: upgrade rust toolchain + +afd1ebed7162bc404e0cc169d25fb4b01806eb2c chore: upgrade rust toolchain +-> awesome + +afd1ebed7162bc404e0cc169d25fb4b01806eb2c chore: upgrade rust toolchain +-> origin:awesome + +afd1ebed7162bc404e0cc169d25fb4b01806eb2c :tada: hello gitmoji +-> origin:awesome +" + ); + + let lines = Parser::parse_file(&file).expect("it should be parsed"); + assert_eq!( + lines, + vec![ + Line::Commit(Commit { + sha: "afd1ebed7162bc404e0cc169d25fb4b01806eb2c".into(), + title: "chore: upgrade&é\"'(§è!çà) rust toolchain".into() + }), + Line::Commit(Commit { + sha: "afd1ebed7162bc404e0cc169d25fb4b01806eb2c".into(), + title: "chore: upgrade rust toolchain".into() + }), + Line::Commit(Commit { + sha: "afd1ebed7162bc404e0cc169d25fb4b01806eb2c".into(), + title: "chore: upgrade rust toolchain".into() + }), + Line::Branch(Branch { + origin: None, + name: "awesome".into(), + }), + Line::Commit(Commit { + sha: "afd1ebed7162bc404e0cc169d25fb4b01806eb2c".into(), + title: "chore: upgrade rust toolchain".into() + }), + Line::Branch(Branch { + name: "awesome".into(), + origin: Some("origin".to_string()) + }), + Line::Commit(Commit { + sha: "afd1ebed7162bc404e0cc169d25fb4b01806eb2c".into(), + title: ":tada: hello gitmoji".into() + }), + Line::Branch(Branch { + name: "awesome".into(), + origin: Some("origin".to_string()) + }), + ] + ) + } +} diff --git a/crates/yggit-parser/src/parser.pest b/crates/yggit-parser/src/parser.pest new file mode 100644 index 0000000..e20b5cb --- /dev/null +++ b/crates/yggit-parser/src/parser.pest @@ -0,0 +1,11 @@ +commit_hash = { ASCII_HEX_DIGIT{40} } +commit_title = { (!"\n" ~ ANY)+ } +git_commit = { commit_hash ~ WHITE_SPACE ~ commit_title } + +origin = { (!"\n" ~ !":" ~ ANY)+ } +branch_name = { (!"\n" ~ ANY)+ } +branch = { "->" ~ WHITE_SPACE* ~ (origin ~ ":")? ~ branch_name } + +comment = { "#" ~ (!"\n" ~ ANY)* } + +file = { SOI ~ ((git_commit | branch | comment)? ~ NEWLINE)* ~ WHITE_SPACE* ~ EOI } diff --git a/crates/yggit-test/Cargo.toml b/crates/yggit-test/Cargo.toml new file mode 100644 index 0000000..f453644 --- /dev/null +++ b/crates/yggit-test/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "yggit-test" +version = "1.0.0" +edition = "2021" +description = "yggit test library" +license = "MIT AND Apache-2.0" +documentation = "https://github.com/Pilou97/yggit" +homepage = "https://github.com/Pilou97/yggit" +repository = "https://github.com/Pilou97/yggit" +readme = "https://github.com/Pilou97/yggit" +keywords = ["cli", "workflow", "git"] +categories = ["cli"] + +[dependencies] +git2 = { workspace = true } +tempfile = { workspace = true } + +[lints] +workspace = true diff --git a/crates/yggit-test/src/lib.rs b/crates/yggit-test/src/lib.rs new file mode 100644 index 0000000..2942bcb --- /dev/null +++ b/crates/yggit-test/src/lib.rs @@ -0,0 +1,166 @@ +use git2::Repository; +use std::fmt::Debug; +use std::io::Write; +use std::sync::Arc; +use std::{fs::File, path::Path, process::Command}; +use tempfile::TempDir; + +macro_rules! assert_cmd { + ($expr:expr, $reason:expr) => { + let result = $expr.unwrap(); + let stderr = String::from_utf8(result.stderr).unwrap_or("Error when parsing stderr".into()); + let stdout = String::from_utf8(result.stdout).unwrap_or("Error when parsing stdout".into()); + + assert!( + result.status.success(), + "{}, stderr: \n{}\nstdout:\n {}", + $reason, + stderr, + stdout + ); + }; +} + +pub struct TempRepository { + pub bare_dir: Arc, + pub cloned_dir: TempDir, + pub repository: Repository, +} + +impl Debug for TempRepository { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TempRepository") + .field("bare_dir", &self.bare_dir) + .field("cloned_dir", &self.cloned_dir) + .finish() + } +} + +impl AsRef for TempRepository { + fn as_ref(&self) -> &Repository { + &self.repository + } +} + +impl Default for TempRepository { + fn default() -> Self { + Self::new() + } +} + +impl TempRepository { + pub fn new() -> Self { + let bare_dir = TempDir::with_suffix(".git").expect("should be able to create bare folder"); + assert_cmd!( + Command::new("git") + .current_dir(bare_dir.as_ref()) + .arg("init") + .arg("--initial-branch") + .arg("main") + .arg("--bare") + .output(), + "git init bare should work" + ); + + let cloned_dir = TempDir::new().expect("should be able to create cloned folder"); + assert_cmd!( + Command::new("git") + .arg("clone") + .arg(bare_dir.as_ref()) + .arg(cloned_dir.as_ref()) + .output(), + "git clone should work" + ); + + let repository = Repository::discover(&cloned_dir).unwrap(); + Self { + bare_dir: Arc::new(bare_dir), + cloned_dir, + repository, + } + } + + pub fn set_identity(&self, name: &str, email: &str) { + // git config user.email "your.email@example.com" + assert_cmd!( + Command::new("git") + .current_dir(self.cloned_dir.as_ref()) + .arg("config") + .arg("user.email") + .arg(email) + .output(), + "set email should work" + ); + + // git config user.name "Your Name" + assert_cmd!( + Command::new("git") + .current_dir(self.cloned_dir.as_ref()) + .arg("config") + .arg("user.name") + .arg(name) + .output(), + "set name should work" + ); + } + + pub fn add_file(&self, filename: &str, content: &str) { + let filepath = Path::new(&self.cloned_dir.path()).join(filename); + let mut file = File::create(&filepath).unwrap(); + writeln!(file, "{}", content).unwrap(); + + assert_cmd!( + Command::new("git") + .current_dir(&self.cloned_dir) + .arg("add") + .arg(filepath) + .output(), + "git add should work" + ); + } + + pub fn commit(&self, message: &str) { + assert_cmd!( + Command::new("git") + .current_dir(&self.cloned_dir) + .arg("commit") + .arg("-m") + .arg(message) + .output(), + "git commit should work" + ); + } + + pub fn checkout_b(&self, branch: &str) { + assert_cmd!( + Command::new("git") + .current_dir(&self.cloned_dir) + .arg("checkout") + .arg("-b") + .arg(branch) + .output(), + "git checkout should work" + ); + } +} + +impl Clone for TempRepository { + fn clone(&self) -> Self { + let cloned_dir = TempDir::new().expect("should be able to create cloned folder"); + assert_cmd!( + Command::new("git") + .arg("clone") + .arg(self.bare_dir.as_ref().as_ref()) + .arg(cloned_dir.as_ref()) + .output(), + "git clone should work" + ); + let repository = Repository::discover(&cloned_dir).unwrap(); + + Self { + bare_dir: self.bare_dir.clone(), + cloned_dir, + repository, + } + } +} diff --git a/crates/yggit-ui/Cargo.toml b/crates/yggit-ui/Cargo.toml new file mode 100644 index 0000000..22e5951 --- /dev/null +++ b/crates/yggit-ui/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "yggit-ui" +version = "1.0.0" +edition = "2021" +description = "yggit interface" +license = "MIT AND Apache-2.0" +documentation = "https://github.com/Pilou97/yggit" +homepage = "https://github.com/Pilou97/yggit" +repository = "https://github.com/Pilou97/yggit" +readme = "https://github.com/Pilou97/yggit" +keywords = ["cli", "workflow", "git"] +categories = ["cli"] + +[dependencies] +thiserror = { workspace = true } + +[lints] +workspace = true diff --git a/crates/yggit-ui/src/lib.rs b/crates/yggit-ui/src/lib.rs new file mode 100644 index 0000000..9b34e77 --- /dev/null +++ b/crates/yggit-ui/src/lib.rs @@ -0,0 +1,51 @@ +use std::process::Command; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum EditorError { + #[error("Cannot edit todo's list because [{0}]")] + CannotEdit(&'static str), +} + +pub trait Editor { + fn edit(&self, content: String, footer: &'static str) -> Result; + + fn no_edit(&self, content: String, footer: &'static str) -> Result<(), EditorError>; +} + +pub struct GitEditor { + editor: String, +} + +impl GitEditor { + pub fn new(editor: String) -> GitEditor { + GitEditor { editor } + } +} + +impl Editor for GitEditor { + fn edit(&self, content: String, footer: &'static str) -> Result { + // We need to create a file + let file_path = "/tmp/yggit"; + let output = format!("{}\n{}", content, footer); + std::fs::write(file_path, output) + .map_err(|_| EditorError::CannotEdit("cannot initiate todo's list"))?; + + let output = Command::new(&self.editor) + .arg(file_path) + .status() + .map_err(|_| EditorError::CannotEdit("cannot open todo in editor"))?; + + let true = output.success() else { + return Err(EditorError::CannotEdit("editor did not correctly end")); + }; + let content = std::fs::read_to_string(file_path) + .map_err(|_| EditorError::CannotEdit("cannot read result from editor"))?; + Ok(content) + } + + fn no_edit(&self, content: String, footer: &'static str) -> Result<(), EditorError> { + println!("{}\n{}", content, footer); + Ok(()) + } +} diff --git a/crates/yggit/Cargo.toml b/crates/yggit/Cargo.toml new file mode 100644 index 0000000..374fe75 --- /dev/null +++ b/crates/yggit/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "yggit" +version = "1.0.0" +edition = "2021" +description = "small tool to manage my one branch git workflow" +license = "MIT AND Apache-2.0" +documentation = "https://github.com/Pilou97/yggit" +homepage = "https://github.com/Pilou97/yggit" +repository = "https://github.com/Pilou97/yggit" +readme = "https://github.com/Pilou97/yggit" +keywords = ["cli", "workflow", "git"] +categories = ["cli"] + +[dependencies] +yggit-db = { path = "../yggit-db" } +yggit-git = { path = "../yggit-git" } +yggit-ui = { path = "../yggit-ui" } +yggit-core = { path = "../yggit-core" } +yggit-config = { path = "../yggit-config" } +clap = { workspace = true, features = ["derive"] } +git2 = { workspace = true } + +[lints] +workspace = true diff --git a/crates/yggit/src/main.rs b/crates/yggit/src/main.rs new file mode 100644 index 0000000..0b311bf --- /dev/null +++ b/crates/yggit/src/main.rs @@ -0,0 +1,74 @@ +use clap::{arg, command, Args, Parser, Subcommand}; +use git2::Repository; +use yggit_config::{Config, GitConfig}; +use yggit_core::{apply, push, show, CoreError}; +use yggit_db::GitDatabase; +use yggit_git::GitClient; +use yggit_ui::GitEditor; + +#[derive(Debug, Parser)] +#[command(name = "yggit")] +#[command(about = "Git stacked workflow manager", long_about = None)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Debug, Subcommand)] +enum Commands { + Push(Push), + Show(Show), + Apply(Apply), +} + +#[derive(Debug, Args)] +pub struct Push { + /// use --force to force the branch updates + /// by default it has the behavior of force-with-lease + #[arg(short, long, default_value_t = false)] + force: bool, + #[arg(long)] + /// The starting point of your branch + onto: Option, + /// use --no-push to only change the commit notes + /// by default the push will be done + #[arg(short, long, default_value_t = true)] + no_push: bool, +} + +#[derive(Debug, Args)] +pub struct Show { + #[arg(long)] + /// The starting point of your branch + onto: Option, +} + +#[derive(Debug, Args)] +pub struct Apply { + #[arg(long)] + /// The starting point of your branch + onto: Option, +} + +fn main() -> Result<(), CoreError> { + let args = Cli::parse(); + + // open the repository + let repository = Repository::discover(".").expect("you need to open a valid repository"); + + // init the dependencies + let config = GitConfig::new(&repository).expect("invalid config"); + let git = GitClient::new(&repository); + let database = GitDatabase::new(&repository, config.name().into(), config.email().into()); + let editor = GitEditor::new(config.editor().to_string()); + + match args.command { + Commands::Push(Push { + force, + onto, + no_push, + }) => push(git, database, editor, force, onto, no_push), + Commands::Show(Show { onto }) => show(git, database, editor, onto), + Commands::Apply(Apply { onto }) => apply(git, database, editor, onto), + } +} diff --git a/justfile b/justfile new file mode 100644 index 0000000..4104076 --- /dev/null +++ b/justfile @@ -0,0 +1,11 @@ +install: + cargo install cargo-audit --locked + cargo install cargo-machete + +check: + cargo fmt + cargo clippy + cargo audit --deny warnings + cargo machete + cargo check + cargo test diff --git a/rust-toolchain b/rust-toolchain index 283edc6..59d7d10 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -1.74.0 \ No newline at end of file +1.86.0 \ No newline at end of file diff --git a/src/commands/apply.rs b/src/commands/apply.rs deleted file mode 100644 index d85e2f2..0000000 --- a/src/commands/apply.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::{ - core::{apply, save_note}, - git::Git, - parser::{commits_to_string, instruction_from_string}, -}; -use anyhow::{Context, Result}; -use clap::Args; - -#[derive(Debug, Args)] -pub struct Apply {} - -const COMMENTS: &str = r#" -# Here is how to use yggit -# -# Commands: -# -> add a branch to the above commit -# -> : add a branch to the above commit -# -# What happens next? -# - All branches are pushed on origin, except if you specified a custom origin -# -# It's not a rebase, you can't edit commits nor reorder them -"#; - -impl Apply { - pub fn execute(&self, git: Git) -> Result<()> { - let commits = git.list_commits()?; - let output = commits_to_string(commits); - - let file_path = "/tmp/yggit"; - - let output = format!("{}\n{}", output, COMMENTS); - std::fs::write(file_path, output).context("Cannot write yggit file to filesystem")?; - - let content = git.edit_file(file_path)?; - - let commits = instruction_from_string(content).context("Cannot parse instructions")?; - - save_note(&git, commits)?; - - apply(&git)?; - - Ok(()) - } -} diff --git a/src/commands/mod.rs b/src/commands/mod.rs deleted file mode 100644 index 62d5d7f..0000000 --- a/src/commands/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod push; -pub mod show; -pub mod apply; \ No newline at end of file diff --git a/src/commands/push.rs b/src/commands/push.rs deleted file mode 100644 index 5c694eb..0000000 --- a/src/commands/push.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::{ - core::{apply, push_from_notes, save_note}, - git::Git, - parser::{commits_to_string, instruction_from_string}, -}; -use anyhow::{Context, Result}; -use clap::Args; - -#[derive(Debug, Args)] -pub struct Push { - /// use --force to update branches, - /// by default it is using --force-with-lease - #[arg(short, long, default_value_t = false)] - force: bool, -} - -const COMMENTS: &str = r#" -# Here is how to use yggit -# -# Commands: -# -> add a branch to the above commit -# -> : add a branch to the above commit -# -# What happens next? -# - All branches are pushed on origin, except if you specified a custom origin -# -# It's not a rebase, you can't edit commits nor reorder them -"#; - -impl Push { - pub fn execute(&self, git: Git) -> Result<()> { - let commits = git.list_commits()?; - let output = commits_to_string(commits); - - let file_path = "/tmp/yggit"; - - let output = format!("{}\n{}", output, COMMENTS); - std::fs::write(file_path, output).context("cannot write file to disk")?; - - let content = git.edit_file(file_path)?; - - let commits = instruction_from_string(content).context("Cannot parse instruction")?; - - save_note(&git, commits)?; - apply(&git)?; - push_from_notes(&git, self.force)?; - - Ok(()) - } -} diff --git a/src/commands/show.rs b/src/commands/show.rs deleted file mode 100644 index 6710ec9..0000000 --- a/src/commands/show.rs +++ /dev/null @@ -1,15 +0,0 @@ -use crate::{git::Git, parser::commits_to_string}; -use anyhow::Result; -use clap::Args; - -#[derive(Debug, Args)] -pub struct Show {} - -impl Show { - pub fn execute(&self, git: Git) -> Result<()> { - let commits = git.list_commits()?; - let output = commits_to_string(commits); - println!("{}", output.trim()); - Ok(()) - } -} diff --git a/src/core.rs b/src/core.rs deleted file mode 100644 index 528857a..0000000 --- a/src/core.rs +++ /dev/null @@ -1,102 +0,0 @@ -use crate::{ - git::{EnhancedCommit, Git}, - parser::Target, -}; -use anyhow::{Context, Result}; -use serde::{Deserialize, Serialize}; - -#[derive(Deserialize, Serialize)] -pub struct Push { - pub origin: Option, - pub branch: String, -} - -#[derive(Deserialize, Serialize)] -pub struct Note { - pub push: Option, -} - -/// Save the note to the commit -/// -/// Also deletes note if there is nothing new -pub fn save_note(git: &Git, commits: Vec) -> Result<()> { - for commit in commits { - // Extract information from commit - let crate::parser::Commit { hash, target, .. } = commit; - - let is_empty = target.is_none(); - - if is_empty { - git.delete_note(&hash)?; - } else { - // Create the note - let note = Note { - push: target.map(|Target { origin, branch }| Push { origin, branch }), - }; - - // Save the note - git.set_note(hash, note) - .context("Cannot write note to commit")?; - } - } - Ok(()) -} - -/// Execute the instructions from the notes -/// to change the head of the given branches -pub fn apply(git: &Git) -> Result<()> { - let commits = git.list_commits()?; - - // Update the commits - for commit in &commits { - let EnhancedCommit { - id, - note: - Some(Note { - push: Some(Push { branch, origin: _ }), - .. - }), - .. - } = commit - else { - continue; - }; - // Set the head of the branch to the given commit - git.set_branch_to_commit(branch, *id)?; // TODO: manage error - } - Ok(()) -} - -/// Push the branches to origin -/// -/// If force is set to true it will use --force -/// Otherwise it uses --force-with-lease -pub fn push_from_notes(git: &Git, force: bool) -> Result<()> { - let commits = git.list_commits()?; - // Push everything - for commit in &commits { - let EnhancedCommit { - note: - Some(Note { - push: Some(Push { origin, branch }), - .. - }), - .. - } = commit - else { - continue; - }; - - let origin = origin - .clone() - .unwrap_or(git.config.yggit.default_upstream.clone()); - - if force { - git.push_force(&origin, branch)?; - } else { - // default case - git.push_force_with_lease(&origin, branch)?; - } - } - Ok(()) -} diff --git a/src/git/config.rs b/src/git/config.rs deleted file mode 100644 index aac7bb9..0000000 --- a/src/git/config.rs +++ /dev/null @@ -1,309 +0,0 @@ -use anyhow::{Context, Result}; - -#[derive(Debug)] -pub struct GitConfig { - pub user: User, - pub core: Core, - pub yggit: Yggit, -} - -#[derive(Debug)] -pub struct User { - pub email: String, - pub name: String, -} - -#[derive(Debug)] -pub struct Core { - pub editor: String, -} - -#[derive(Debug)] -pub struct Yggit { - // Default upstream of a branch - pub default_upstream: String, -} - -impl GitConfig { - /// Parse the git config and return a Config - /// - /// It parses the following field: - /// - user.email : required - /// - user.name : required - /// - notes.rewriteRef = "refs/notes/commits" : required - /// - yggit.defaultUpstream : optional, default(origin) - pub fn parse(config: git2::Config) -> Result { - let email = config - .get_string("user.email") - .context("email not found in configuration")?; - - let name = config - .get_string("user.name") - .context("name not found in configuration")?; - - let editor = (match config.get_string("core.editor") { - Ok(editor) => Ok(editor), - Err(_) => std::env::var("EDITOR").context("editor not found in configuration"), - })?; - - // Force rewriteRef = "refs/notes/commits" to exist - let rewrite_ref = config - .get_string("notes.rewriteRef") - .context("notes.rewriteRef wasn't found")?; - if rewrite_ref != "refs/notes/commits" { - println!("rewriteRef should be set to \"refs/notes/commits\""); - return Err(anyhow::Error::msg( - "rewriteRef should be set to \"refs/notes/commits\"", - )); - } - - let default_upstream = config - .get_string("yggit.defaultUpstream") - .unwrap_or("origin".to_string()); - - Ok(Self { - user: User { email, name }, - core: Core { editor }, - yggit: Yggit { default_upstream }, - }) - } -} - -#[cfg(test)] -mod tests { - use super::GitConfig; - use anyhow::{Context, Result}; - use std::{fs::File, io::Write, path::Path}; - use tempfile::TempDir; - - impl GitConfig { - fn open(path: &Path) -> Result { - let config = git2::Config::open(path).context("config not found")?; - Self::parse(config) - } - } - - #[test] - fn test_open() { - let tmp_dir = TempDir::new().expect("should be created"); - let config = concat!( - "[user]\n", - "email = kenobi@example.com\n", - "name = Obi-Wan\n", - "[core]\n", - "editor = neovim\n", - "[notes]\n", - "rewriteRef = refs/notes/commits\n", - "[yggit]\n", - "defaultUpstream = origin\n" - ); - - let path = tmp_dir.path().join(".gitconfig"); - let mut file = File::create(&path).expect("gitconfig should be created"); - file.write(config.as_bytes()).expect("should be written"); - - let config = GitConfig::open(&path).expect("should be open"); - assert_eq!(config.user.email, "kenobi@example.com"); - assert_eq!(config.user.name, "Obi-Wan"); - assert_eq!(config.core.editor, "neovim"); - assert_eq!(config.yggit.default_upstream, "origin"); - } - - #[test] - fn test_open_missing_email() { - let tmp_dir = TempDir::new().expect("should be created"); - let config = concat!( - "[user]\n", - "name = Obi-Wan\n", - "[core]\n", - "editor = neovim\n", - "[notes]\n", - "rewriteRef = refs/notes/commits\n", - "[yggit]\n", - "defaultUpstream = origin\n" - ); - - let path = tmp_dir.path().join(".gitconfig"); - let mut file = File::create(&path).expect("gitconfig should be created"); - file.write(config.as_bytes()).expect("should be written"); - - let config = GitConfig::open(&path); - assert!(config.is_err()); - assert_eq!( - config.unwrap_err().to_string(), - "email not found in configuration" - ) - } - - #[test] - fn test_open_missing_name() { - let tmp_dir = TempDir::new().expect("should be created"); - let config = concat!( - "[user]\n", - "email = kenobi@example.com\n", - "[core]\n", - "editor = neovim\n", - "[notes]\n", - "rewriteRef = refs/notes/commits\n", - "[yggit]\n", - "defaultUpstream = origin\n" - ); - - let path = tmp_dir.path().join(".gitconfig"); - let mut file = File::create(&path).expect("gitconfig should be created"); - file.write(config.as_bytes()).expect("should be written"); - - let config = GitConfig::open(&path); - assert!(config.is_err()); - assert_eq!( - config.unwrap_err().to_string(), - "name not found in configuration" - ) - } - - #[test] - fn test_open_missing_editor() { - let tmp_dir = TempDir::new().expect("should be created"); - std::env::remove_var("EDITOR"); - - let config = concat!( - "[user]\n", - "email = kenobi@example.com\n", - "name = Obi-Wan\n", - "[notes]\n", - "rewriteRef = refs/notes/commits\n", - "[yggit]\n", - "defaultUpstream = origin\n" - ); - - let path = tmp_dir.path().join(".gitconfig"); - let mut file = File::create(&path).expect("gitconfig should be created"); - file.write(config.as_bytes()).expect("should be written"); - - let config = GitConfig::open(&path); - assert!(config.is_err()); - assert_eq!( - config.unwrap_err().to_string(), - "editor not found in configuration" - ); - - // Other test that set the EDITOR var - let tmp_dir = TempDir::new().expect("should be created"); - std::env::set_var("EDITOR", "emacs"); - - let config = concat!( - "[user]\n", - "email = kenobi@example.com\n", - "name = Obi-Wan\n", - "[notes]\n", - "rewriteRef = refs/notes/commits\n", - "[yggit]\n", - "defaultUpstream = origin\n" - ); - - let path = tmp_dir.path().join(".gitconfig"); - let mut file = File::create(&path).expect("gitconfig should be created"); - file.write(config.as_bytes()).expect("should be written"); - - let config = GitConfig::open(&path).expect("should be ok"); - assert_eq!(config.core.editor, "emacs"); - } - - #[test] - fn test_open_missing_rewrite_ref() { - let tmp_dir = TempDir::new().expect("should be created"); - - let config = concat!( - "[user]\n", - "email = kenobi@example.com\n", - "name = Obi-Wan\n", - "[core]\n", - "editor = neovim\n", - "[yggit]\n", - "defaultUpstream = origin\n" - ); - - let path = tmp_dir.path().join(".gitconfig"); - let mut file = File::create(&path).expect("gitconfig should be created"); - file.write(config.as_bytes()).expect("should be written"); - - let config = GitConfig::open(&path); - assert!(config.is_err()); - assert_eq!( - config.unwrap_err().to_string(), - "notes.rewriteRef wasn't found" - ) - } - - #[test] - fn test_open_wrong_rewrite_ref() { - let tmp_dir = TempDir::new().expect("should be created"); - - let config = concat!( - "[user]\n", - "email = kenobi@example.com\n", - "name = Obi-Wan\n", - "[core]\n", - "editor = neovim\n", - "[notes]\n", - "rewriteRef = wrong-value\n", - "[yggit]\n", - "defaultUpstream = origin\n" - ); - - let path = tmp_dir.path().join(".gitconfig"); - let mut file = File::create(&path).expect("gitconfig should be created"); - file.write(config.as_bytes()).expect("should be written"); - - let config = GitConfig::open(&path); - assert!(config.is_err()); - assert_eq!( - config.unwrap_err().to_string(), - "rewriteRef should be set to \"refs/notes/commits\"" - ) - } - - #[test] - fn test_default_upstream() { - let tmp_dir = TempDir::new().expect("should be created"); - let config = concat!( - "[user]\n", - "email = kenobi@example.com\n", - "name = Obi-Wan\n", - "[core]\n", - "editor = neovim\n", - "[notes]\n", - "rewriteRef = refs/notes/commits\n", - ); - - let path = tmp_dir.path().join(".gitconfig"); - let mut file = File::create(&path).expect("gitconfig should be created"); - file.write(config.as_bytes()).expect("should be written"); - - let config = GitConfig::open(&path).expect("should be open"); - assert_eq!(config.yggit.default_upstream, "origin"); - } - - #[test] - fn test_default_upstream_with_different_value() { - let tmp_dir = TempDir::new().expect("should be created"); - let config = concat!( - "[user]\n", - "email = kenobi@example.com\n", - "name = Obi-Wan\n", - "[core]\n", - "editor = neovim\n", - "[notes]\n", - "rewriteRef = refs/notes/commits\n", - "[yggit]\n", - "defaultUpstream = upstream" - ); - - let path = tmp_dir.path().join(".gitconfig"); - let mut file = File::create(&path).expect("gitconfig should be created"); - file.write(config.as_bytes()).expect("should be written"); - - let config = GitConfig::open(&path).expect("should be open"); - assert_eq!(config.yggit.default_upstream, "upstream"); - } -} diff --git a/src/git/git.rs b/src/git/git.rs deleted file mode 100644 index dfba8c6..0000000 --- a/src/git/git.rs +++ /dev/null @@ -1,852 +0,0 @@ -use super::config::GitConfig; -use anyhow::{Context, Result}; -use auth_git2::GitAuthenticator; -use git2::{Branch, BranchType, Error, ErrorCode, Oid, Repository, Signature}; -use serde::{de::DeserializeOwned, Serialize}; -use std::{ - path::PathBuf, - process::Command, - str::FromStr, - sync::{Arc, Mutex}, -}; - -pub struct Git { - repository: Repository, - signature: Signature<'static>, - pub config: GitConfig, - auth: GitAuthenticator, -} - -pub struct EnhancedCommit { - pub id: Oid, - pub title: String, - pub description: Option, - pub note: Option, -} - -#[allow(dead_code)] -enum PushMode { - Normal, - Force, - ForceWithLease, -} - -impl Git { - /// Open a repository at the given path - /// Also load the signature from the .gitconfig - pub fn open(path: &str) -> Result { - // The path can be absolute or not - let path = if path.starts_with('/') { - PathBuf::from_str(path).context("invalid absolute path")? - } else { - let current_dir = std::env::current_dir().context("cannot open current directory")?; - current_dir.join(path) - }; - let repository = Repository::discover(path).context("repository not found")?; - let config = repository.config().context("config not found")?; - let gitconfig = GitConfig::parse(config)?; - let signature = Signature::now(&gitconfig.user.name, &gitconfig.user.email) - .context("cannot compute signature")?; - Ok(Git { - repository, - signature, - config: gitconfig, - auth: GitAuthenticator::new(), - }) - } - - /// Returns the main branch of the repository - /// - /// The branch can be either main or master - /// If main exists it will be returned as the main branch - /// If main does not exist, master will be returned as the main branch - pub fn main_branch(&self) -> Option { - let branches = ["main", "master"]; - - for branch in branches { - let branch = self.repository.find_branch(branch, BranchType::Local); - if let Ok(branch) = branch { - return Some(branch); - } - } - None - } - - /// List the commit in a repository with the attached note - pub fn list_commits(&self) -> Result>> - where - N: DeserializeOwned, - { - // Find the commit of the "main" branch - let main_branch = self.main_branch().context("main/master to exist")?; - - let main_commit = main_branch - .get() - .peel_to_commit() - .context("main branch is not found")?; - - let mut revwalk = self - .repository - .revwalk() - .context("Cannot rev walk the branch")?; - revwalk.push_head().context("There is no head")?; - - let mut commits = Vec::default(); - - for oid in revwalk { - let oid = oid.context("not a valid oid")?; - - if oid == main_commit.id() { - break; - } - - // The commit has to be found, because it's listed from the revwalk - let commit = self - .find_commit(oid) - .ok_or(anyhow::Error::msg("commit not found: not possible"))?; - - commits.push(commit); - } - commits.reverse(); - Ok(commits) - } - - fn push(&self, origin: &str, branch: &str, mode: PushMode) -> Result<()> { - println!("pushing {}:{}", origin, branch); - let fetch_refname = format!("refs/heads/{}", branch); - let git_config = self - .repository - .config() - .context("git config is not present")?; - let mut push_options = git2::PushOptions::new(); - - let mut remote_callbacks = git2::RemoteCallbacks::new(); - remote_callbacks.credentials(self.auth.credentials(&git_config)); - - enum PushError { - NotYetImplemented, - NoUpdate, // Should not happen - RemoteOriginDiverged, // Used when using force-with-lease - } - - enum PushStatus { - Pushed, - NewBranchPushed, - Error(PushError), - } - - let error: Arc>> = Arc::new(Mutex::new(None)); - let cloned_external_variable = Arc::clone(&error); - - remote_callbacks.push_negotiation(move |remote_updates| { - let mut status = cloned_external_variable.lock().unwrap(); - let null = git2::Oid::zero(); - let Some(remote_update) = remote_updates.iter().next() else { - *status = Some(PushStatus::Error(PushError::NoUpdate)); - return Err(Error::from_str("not updates to be done")); - }; - - if remote_update.src() == null { - *status = Some(PushStatus::NewBranchPushed); - return Ok(()); - } - - match mode { - PushMode::Normal => { - // last commit of remote has to be known in current branch - *status = Some(PushStatus::Error(PushError::NotYetImplemented)); - Err(Error::from_str("not yet implemented")) - } - PushMode::Force => { - *status = Some(PushStatus::Pushed); - Ok(()) - } - PushMode::ForceWithLease => { - // Comparing src with local origin - let remote_origin_oid = remote_update.src(); - // Get the head of this branch - let local_origin_oid = { - let local_origin_name = remote_update - .src_refname() - .ok_or(Error::from_str("cannot parse source refname"))?; - let upstream_name = local_origin_name - .strip_prefix("refs/heads/") - .ok_or(Error::from_str("cannot strip local origin name"))?; - self.repository - .find_reference(&format!("refs/remotes/{}/{}", origin, upstream_name)) - .ok() - .and_then(|reference| reference.peel_to_commit().ok()) - .map(|commit| commit.id()) - .ok_or(Error::from_str("cannot find the commit reference hash"))? - }; - if remote_origin_oid == local_origin_oid { - *status = Some(PushStatus::Pushed); - Ok(()) - } else { - *status = Some(PushStatus::Error(PushError::RemoteOriginDiverged)); - Err(Error::from_str("Origins have divered")) - } - } - } - }); - - push_options.remote_callbacks(remote_callbacks); - - let mut remote = self - .repository - .find_remote(origin) - .context("Cannot find origin")?; - let _ = remote.push( - &[format!("+{}", fetch_refname).as_str()], - Some(&mut push_options), - ); - - let status = error.lock().unwrap(); - let status = status.as_ref(); - match status { - Some(PushStatus::Error(PushError::NoUpdate)) => { - println!("no update to be done"); - Err(anyhow::Error::msg("not pushed")) - } - Some(PushStatus::Error(PushError::NotYetImplemented)) => { - println!("not yet implemented"); - Err(anyhow::Error::msg("not yet implemented")) - } - Some(PushStatus::Error(PushError::RemoteOriginDiverged)) => { - println!("remote {origin}:{branch} has diverged"); - Err(anyhow::Error::msg("remote has diverged")) - } - Some(PushStatus::Pushed) => { - println!("{origin}:{branch} pushed"); - Ok(()) - } - Some(PushStatus::NewBranchPushed) => { - println!("{origin}:{branch} pushed, new branch created"); - Ok(()) - } - None => { - // TODO: this case should be removed - println!("this case should not happen"); - Ok(()) - } - } - } - - /// Equivalent of `git push --force-with-lease` - pub fn push_force_with_lease(&self, origin: &str, branch: &str) -> Result<()> { - self.push(origin, branch, PushMode::ForceWithLease) - } - - /// Equivalent of `git push --force` - pub fn push_force(&self, origin: &str, branch: &str) -> Result<()> { - self.push(origin, branch, PushMode::Force) - } - - /// Delete a note - /// - /// Does not return any error when you delete nothing - pub fn delete_note(&self, oid: &Oid) -> Result<()> { - let result = self - .repository - .note_delete(*oid, None, &self.signature, &self.signature); - if let Err(ref err) = result { - if err.code() == ErrorCode::NotFound { - return Ok(()); - } - } - result.context("cannot delete note") - } - - /// Set the note of a given oid - /// - /// The note will be serialize to json format - pub fn set_note(&self, oid: Oid, note: N) -> Result<()> - where - N: Serialize, - { - let note = serde_json::to_string(¬e).context("Cannot convert note to json string")?; - - self.repository - .note(&self.signature, &self.signature, None, oid, ¬e, true) - .map(|_| ()) - .context("cannot write note") - } - - /// Returns the note of a given oid - fn find_note(&self, oid: Oid) -> Option - where - N: DeserializeOwned, - { - self.repository - .find_note(None, oid) - .map(|note| note.message().map(|str| str.to_string())) - .ok() - .flatten() - .and_then(|string| { - // Removes empty lines - // Takes the last line - // So that it's compatible with merging fixup commits - // When two commits are merged, the note are also merged - // The note of the most recent commit is taking into account then - string - .split('\n') - .filter(|str| !str.trim().is_empty()) - .last() - .map(ToString::to_string) - }) - .and_then(|str| serde_json::from_str(&str).ok()) - } - - /// Retrieve a commit with its node - pub fn find_commit(&self, oid: Oid) -> Option> - where - N: DeserializeOwned, - { - // Get the commit - let commit = self.repository.find_commit(oid).ok()?; - // Get the associated note - let note: Option = self.find_note(oid); - // Get the title and the description - let mut message = commit.message().unwrap_or_default().splitn(2, '\n'); - // Title is on the first line of the message - let title = message.next().unwrap_or_default().to_string(); - // Remaining lines are for the description - let description = message.next().map(str::to_string); - - Some(EnhancedCommit { - id: oid, - title, - description, - note, - }) - } - - /// Set the head of the given branch to the given commit - pub fn set_branch_to_commit(&self, branch: &str, oid: Oid) -> Result<()> { - let commit = self - .repository - .find_commit(oid) - .context("Cannot find commit")?; - - self.repository - .branch(branch, &commit, true) - .context("Cannot find branch")?; - - Ok(()) - } - - /// Open the given file with the user's editor and returns the content of this file - pub fn edit_file(&self, file_path: &str) -> Result { - let output = Command::new(&self.config.core.editor) - .arg(file_path) - .status() - .context("Failed to open editor")?; - let true = output.success() else { - return Err(anyhow::Error::msg("Editor did not end successfully")); - }; - let content = - std::fs::read_to_string(file_path).context("Cannot read string from editor")?; - Ok(content) - } -} - -#[cfg(test)] -mod tests { - use git2::Oid; - use serde::Serialize; - use std::{ - io::Write, - process::{Command, Stdio}, - }; - use tempfile::TempDir; - - use crate::git::config::{Core, GitConfig, User, Yggit}; - - use super::Git; - - macro_rules! execute_commands { - ($($cmd:expr $(, $arg:expr)*)* ) => { - { - $( - let cmd_string = format!("{} {}", $cmd, vec![$($arg),*].join(" ")); - println!("{}", cmd_string); - let child = Command::new($cmd) - $(.arg($arg))* - .stdout(Stdio::piped()) - .spawn() - .expect("Failed to spawn child process"); - - let output = child.wait_with_output().expect("Failed to read stdout"); - if !output.status.success() { - panic!("the command did not succeed"); - } - String::from_utf8(output.stdout).expect("should be parasable") - )* - } - }; - } - - macro_rules! git { - ($self:ident, $($args:expr),* ) => { - execute_commands!("git", "-C", &$self.path(), $($args),*) - }; - } - - macro_rules! git_config { - ($self:ident, $($args:expr),* ) => { - git!($self, "config", "--local", $($args),*) - }; - } - - struct GitTmp { - bare: Option, - directory: TempDir, - } - - impl Clone for GitTmp { - fn clone(&self) -> Self { - let clone = TempDir::new().expect("directory should be created"); - let Some(ref bare) = self.bare else { - todo!("no bare repository: impossible to clone") - }; - - execute_commands!( - "git", - "clone", - &format!("file://{}", bare.path().to_str().unwrap().to_string()), - &clone.path().to_str().unwrap().to_string() - ); - - let git = Self { - bare: None, - directory: clone, - }; - - git.init_config(); - - git - } - } - - /// Helper that execute git command - /// - /// So that git.rs can be tested against the git binary - impl GitTmp { - /// Create a repository with a bare one - fn init_bare(initial_branch: &str) -> Self { - let bare = tempfile::Builder::new() - .suffix(".git") - .tempdir() - .expect("git bare folder to be created"); - - execute_commands!( - "git", - "-C", - &bare.path().to_str().unwrap().to_string(), - "init", - "--initial-branch", - initial_branch, - "--bare" - ); - - // Then we clone it - let clone = TempDir::new().expect("Directory should be created"); - - execute_commands!( - "git", - "clone", - &format!("file://{}", bare.path().to_str().unwrap().to_string()), - &clone.path().to_str().unwrap().to_string() - ); - - let git = Self { - bare: Some(bare), - directory: clone, - }; - - git.init_config(); - - git - } - - /// This function has to be called in each constructor - /// Later we can add an optional argument Config - fn init_config(&self) { - // TODO: put this in config.rs as dummy in test module - let config = GitConfig { - user: User { - email: "example@example.com".to_string(), - name: "Obi-wan".to_string(), - }, - core: Core { - editor: "theforce".to_string(), // The editor is not tested - }, - yggit: Yggit { - default_upstream: "origin".to_string(), - }, - }; - - git_config!(self, "user.email", config.user.email.as_str()); - git_config!(self, "user.name", config.user.name.as_str()); - git_config!(self, "core.editor", config.core.editor.as_str()); - git_config!( - self, - "yggit.defaultUpstream", - config.yggit.default_upstream.as_str() - ); - git_config!(self, "notes.rewriteRef", "refs/notes/commits"); - } - - /// Add a file to the repository - fn new_file(&self, file_name: &str, content: &str) { - let path = self.directory.path().join(file_name); - let mut file = std::fs::File::create(path).expect("file should be created"); - file.write_all(content.as_bytes()) - .expect("should have written file to disk"); - } - - /// Add all files to the next commit - fn add_all(&self) { - let _ = git!(self, "add", "."); - } - - /// Commit the change - fn commit(&self, commit_name: &str) -> Oid { - let _ = git!(self, "commit", "-m", commit_name); - let oid = git!(self, "rev-parse", "HEAD"); - let oid = oid.trim(); - - Oid::from_str(&oid).unwrap() - } - - fn add_note(&self, oid: Oid, note: &N) - where - N: Serialize, - { - let json = serde_json::to_string(note).expect("note"); - git!(self, "notes", "add", "-m", &json, &oid.to_string()); - } - - fn push(&self) { - git!(self, "push", "--force"); - } - - /// Returns the path of the repository - fn path(&self) -> String { - self.directory.path().to_str().unwrap().to_string() - } - - /// Modifies the title of HEAD - fn amend(&self, title: &str) { - git!(self, "commit", "--amend", "-m", title); - } - - /// pull the repository - fn pull(&self) { - git!(self, "pull"); - } - - fn create_branch(&self, branch_name: &str) { - git!(self, "checkout", "-b", branch_name); - } - } - - #[test] - fn test_open_repository() { - let repo = GitTmp::init_bare("main"); - let _ = Git::open(&repo.path()).expect("repo should exist"); - } - - #[test] - fn test_open_repository_not_found() { - let tmp = TempDir::new().expect("the folder should be created"); - let result = Git::open(tmp.path().to_str().unwrap()); - assert!(result.is_err()) - } - - #[test] - fn test_open_relative_repository() { - let _ = Git::open("."); - } - - /// helper that initialize a repository with one commit - /// - /// It returns the head and the repository - fn init_repo_with_commit() -> (Oid, GitTmp) { - let repo = GitTmp::init_bare("main"); - repo.new_file( - "readme.md", - concat!("# Star wars", "\n", "Hello there\n", "General Kenobi\n"), - ); - repo.add_all(); - let oid = repo.commit("first commit"); - repo.add_note(oid, &"my super note".to_string()); - (oid, repo) - } - - #[test] - fn test_find_commit() { - let (head, repo) = init_repo_with_commit(); - let git = Git::open(&repo.path()).expect("should be able to open the repository"); - let commit = git - .find_commit::(head) - .expect("commit should be present"); - assert_eq!(commit.title, "first commit"); - } - - #[test] - fn test_commit_not_found() { - let (_, repo) = init_repo_with_commit(); - let git = Git::open(&repo.path()).expect("should be able to open the repository"); - let commit = git.find_commit::(Oid::zero()); - assert!(commit.is_none()) - } - - #[test] - fn test_get_note() { - let (head, repo) = init_repo_with_commit(); - let git = Git::open(&repo.path()).expect("should be able to open the repository"); - let note = git - .find_note::(head) - .expect("the note has to be present"); - assert_eq!(note, "my super note"); - } - - #[test] - fn test_get_no_note() { - let (_, repo) = init_repo_with_commit(); - let git = Git::open(&repo.path()).expect("should be able to open the repository"); - let note = git.find_note::(Oid::zero()); - assert!(note.is_none()); - } - - #[test] - fn test_delete_note() { - let (head, repo) = init_repo_with_commit(); - let git = Git::open(&repo.path()).expect("should be able to open the repository"); - let note = git - .find_note::(head) - .expect("the note has to be present"); - assert_eq!(note, "my super note"); - git.delete_note(&head).expect("not should be deleted"); - let note = git.find_note::(head); - assert!(note.is_none()) - } - - #[test] - fn test_set_note() { - let (head, repo) = init_repo_with_commit(); - let git = Git::open(&repo.path()).expect("should be able to open the repository"); - git.set_note(head, "a note").expect("not should be written"); - let note = git - .find_note::(head) - .expect("the note has to be present"); - assert_eq!(note, "a note"); - } - - #[test] - fn test_overwrite_note() { - let (head, repo) = init_repo_with_commit(); - - let git = Git::open(&repo.path()).expect("should be able to open the repository"); - git.set_note(head, "a note").expect("not should be written"); - git.set_note(head, "a note 2") - .expect("not should be written"); - - let note = git - .find_note::(head) - .expect("the note has to be present"); - - assert_eq!(note, "a note 2"); - } - - #[test] - fn test_delete_note_two_times() { - let (head, repo) = init_repo_with_commit(); - - let git = Git::open(&repo.path()).expect("should be able to open the repository"); - - git.delete_note(&head).expect("should work"); - git.delete_note(&head).expect("should work"); - } - - #[test] - fn test_push_force_with_lease_refused() { - let repo = GitTmp::init_bare("main"); - let clone = repo.clone(); - - repo.new_file( - "readme.md", - concat!("# Star wars", "\n", "Hello there\n", "General Kenobi\n"), - ); - repo.add_all(); - repo.commit("first commit"); - repo.new_file( - "other_file.md", - concat!( - "# Pride and prejudice", - "\n", - "I love you. Most ardently.\n" - ), - ); - repo.add_all(); - repo.commit("pride and prejudice"); - - // Let's open git in clone - let git = Git::open(&clone.path()).expect("git should be open"); - // let's add a file and commit it - clone.new_file("yolo.md", "some content"); - clone.add_all(); - clone.commit("my first commit"); - clone.push(); // To create a local remote - - // let's push force from repo - // it will delete the history of clone - repo.push(); - // the push force with lease should be refused because the origin has divered - let result = git.push_force_with_lease("origin", "main"); - assert!(result.is_err()); - } - - #[test] - fn test_push_force_with_lease_accepted() { - let repo = GitTmp::init_bare("main"); - let clone = repo.clone(); - - repo.new_file( - "readme.md", - concat!("# Star wars", "\n", "Hello there\n", "General Kenobi\n"), - ); - repo.add_all(); - repo.commit("first commit"); - repo.new_file( - "other_file.md", - concat!( - "# Pride and prejudice", - "\n", - "I love you. Most ardently.\n" - ), - ); - repo.add_all(); - repo.commit("pride and prejudice"); - repo.push(); - - // Let's open git in clone - let git = Git::open(&clone.path()).expect("git should be open"); - // let's add a file and commit it - clone.pull(); - clone.amend("hello again"); // The history has been modified - clone.new_file("anotherfile.md", "hello other file"); - clone.add_all(); - clone.commit("new commit"); - // the two origins matched, so we can erase the history - let result = git.push_force_with_lease("origin", "main"); - assert!(result.is_ok()); - } - - #[test] - fn test_push_force() { - let repo = GitTmp::init_bare("main"); - let clone = repo.clone(); - - repo.new_file( - "readme.md", - concat!("# Star wars", "\n", "Hello there\n", "General Kenobi\n"), - ); - repo.add_all(); - repo.commit("first commit"); - repo.new_file( - "other_file.md", - concat!( - "# Pride and prejudice", - "\n", - "I love you. Most ardently.\n" - ), - ); - repo.add_all(); - repo.commit("pride and prejudice"); - - // Let's open git in clone - let git = Git::open(&clone.path()).expect("git should be open"); - // let's add a file and commit it - clone.new_file("yolo.md", "some content"); - clone.add_all(); - clone.commit("my first commit"); - clone.push(); // To create a local remote - - // let's push force from repo - // it will delete the history of clone - repo.push(); - // This test is based on the push_force_with_lease one - // where push --force-with-lease fails, push --force has to work - let result = git.push_force("origin", "main"); - assert!(result.is_ok()); - } - - // Testing `main_branch` - - /// Initializes a repository with a main branch - fn init_main_branch_test(initial_branch: &str) -> GitTmp { - let repo = GitTmp::init_bare(initial_branch); - repo.new_file( - "readme.md", - concat!("# Star wars", "\n", "Hello there\n", "General Kenobi\n"), - ); - repo.add_all(); - repo.commit("first commit"); - repo.push(); - repo - } - - #[test] - fn test_find_main_branch_main() { - let repo = init_main_branch_test("main"); - let git = Git::open(&repo.path()).unwrap(); - let branch = git.main_branch().unwrap(); - let branch = branch.name().unwrap().unwrap(); - assert_eq!(branch, "main"); - } - - #[test] - fn test_find_main_branch_master() { - let repo = init_main_branch_test("master"); - let git = Git::open(&repo.path()).unwrap(); - let branch = git.main_branch().unwrap(); - let branch = branch.name().unwrap().unwrap(); - assert_eq!(branch, "master"); - } - - #[test] - fn test_find_unknown_branch() { - let repo = init_main_branch_test("unknown"); - let git = Git::open(&repo.path()).unwrap(); - let branch = git.main_branch(); - assert!(branch.is_none()) - } - - #[test] - fn test_list_commits_from_main() { - let (_, repo) = init_repo_with_commit(); - let git = Git::open(&repo.path()).unwrap(); - let commits = git.list_commits::().unwrap(); - assert_eq!(commits.len(), 0) // because we are on main - } - - #[test] - fn test_list_commits_from_other_branch() { - let (_, repo) = init_repo_with_commit(); - repo.create_branch("test"); - repo.new_file("hey", "hey"); - repo.add_all(); - let oid = repo.commit("first commit on my branch"); - - let git = Git::open(&repo.path()).unwrap(); - let commits = git.list_commits::().unwrap(); - assert_eq!(commits.len(), 1); - let commit = commits.iter().next().unwrap(); - assert_eq!(commit.id, oid); - assert_eq!(commit.note, None); - assert_eq!(commit.title, "first commit on my branch"); - assert_eq!(commit.description, Some("".to_string())); // TODO: empty string should not be allowed - } -} diff --git a/src/git/mod.rs b/src/git/mod.rs deleted file mode 100644 index 4881d3d..0000000 --- a/src/git/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod config; - -#[allow(clippy::module_inception)] -mod git; - -pub use git::EnhancedCommit; -pub use git::Git; diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 8dd480b..0000000 --- a/src/main.rs +++ /dev/null @@ -1,39 +0,0 @@ -use clap::Parser; -use clap::Subcommand; -use commands::apply::Apply; -use commands::push::Push; -use commands::show::Show; -use git::Git; - -mod commands; -mod core; -mod git; -mod parser; - -#[derive(Debug, Parser)] // requires `derive` feature -#[command(name = "yggit")] -#[command(version, about = "Git project manager", long_about = None)] -struct Cli { - #[command(subcommand)] - command: Commands, -} - -#[derive(Debug, Subcommand)] -enum Commands { - Push(Push), - Show(Show), - Apply(Apply), -} - -fn main() { - let args = Cli::parse(); - - let git = Git::open(".").unwrap(); - - match args.command { - Commands::Push(push) => push.execute(git), - Commands::Show(show) => show.execute(git), - Commands::Apply(apply) => apply.execute(git), - } - .unwrap() -} diff --git a/src/parser.rs b/src/parser.rs deleted file mode 100644 index 0af74c7..0000000 --- a/src/parser.rs +++ /dev/null @@ -1,159 +0,0 @@ -// Git related - -use crate::{ - core::{Note, Push}, - git::EnhancedCommit, -}; -use git2::Oid; -use pest::{iterators::Pair, Parser}; -use pest_derive::Parser; - -pub fn commits_to_string(commits: Vec>) -> String { - let mut output = String::default(); - for commit in commits { - output = format!("{}{} {}\n", output, commit.id, commit.title); - if let Some(Note { push }) = commit.note { - if let Some(Push { - origin: Some(origin), - branch, - }) = &push - { - output = format!("{}-> {}:{}\n", output, origin, branch); - } else if let Some(Push { - origin: None, - branch, - }) = &push - { - output = format!("{}-> {}\n", output, branch); - } - // An empty line is added so that is cleaner to differentiate the different MR - if push.is_some() { - output = format!("{}\n", output); - } - } - } - output -} - -#[derive(Parser)] -#[grammar = "parser/yggit.pest"] -struct YggitParser; - -#[derive(Debug, Clone)] -pub struct Target { - pub origin: Option, - pub branch: String, -} - -#[derive(Debug, Clone)] -pub struct Commit { - pub hash: Oid, - #[allow(dead_code)] - pub title: String, - pub target: Option, -} - -fn parse_target(pair: Pair) -> Option { - let target = pair.into_inner(); - - let mut parsed_origin = None; - let mut parsed_branch = None; - - for pair in target.into_iter() { - match pair.as_rule() { - Rule::origin => { - parsed_origin = Some(pair.as_str().to_string()); - } - Rule::branch_name => { - parsed_branch = Some(pair.as_str().to_string()); - } - _ => (), - } - } - let parsed_branch = parsed_branch?; - - Some(Target { - origin: parsed_origin, - branch: parsed_branch, - }) -} - -fn parse_commit(pair: Pair) -> Option { - let mut commit = pair.into_inner(); - - let git_commit = commit.next()?; - let mut git_commit = git_commit.into_inner(); - - let hash = git_commit.next()?; - let hash = Oid::from_str(hash.as_str()).ok()?; - - let title = git_commit.next()?; - let title = title.as_str(); - - let mut target = None; - - // Optional target - for pair in commit { - if let Rule::target = pair.as_rule() { - target = parse_target(pair); - } - } - - Some(Commit { - hash, - title: title.to_string(), - target, - }) -} - -fn parse_value(pair: Pair) -> Option> { - match pair.as_rule() { - Rule::commits => { - let mut commits = Vec::default(); - for pair in pair.into_inner() { - let commit = parse_commit(pair)?; - commits.push(commit); - } - Some(commits) - } - _ => None, - } -} - -pub fn instruction_from_string(input: String) -> Option> { - let pair = YggitParser::parse(Rule::commits, &input) - .map_err(|err| println!("{err}")) - .ok()? - .next()?; - let commits = parse_value(pair)?; - - Some(commits) -} - -#[cfg(test)] -mod test { - use pest::Parser; - - use super::{Rule, YggitParser}; - - #[test] - fn test_hash() { - let input = "f8fa32837b2f1438a3a55a9341002920ace7978c"; - let result = YggitParser::parse(Rule::commit_hash, &input).expect("should be parsed"); - assert_eq!(result.as_str(), input) - } - - #[test] - fn test_commit_title() { - let input = "project: add .vscode in gitignore"; - let result = YggitParser::parse(Rule::commit_title, &input).expect("should be parsed"); - assert_eq!(result.as_str(), input) - } - - #[test] - fn test_git_commit() { - let input = "f8fa32837b2f1438a3a55a9341002920ace7978c project: add .vscode in gitignore\n"; - let result = YggitParser::parse(Rule::git_commit, &input).expect("should be parsed"); - assert_eq!(result.as_str(), input) - } -} diff --git a/src/parser/yggit.pest b/src/parser/yggit.pest deleted file mode 100644 index dd4478d..0000000 --- a/src/parser/yggit.pest +++ /dev/null @@ -1,14 +0,0 @@ -commit_hash = { ASCII_HEX_DIGIT{40} } -commit_title = { (ASCII_ALPHANUMERIC | "@" | "-" | "_" | "/" | ":" | " " | "!" | "(" | ")" | "#" | ".")+ } -git_commit = { commit_hash ~ WHITE_SPACE ~ commit_title ~ NEWLINE } - -branch_tag = _{ "->" } -origin = { ASCII_ALPHANUMERIC+ } -branch_name = { (ASCII_ALPHANUMERIC | "@" | "-" | "_" | "/")+ } -target = { branch_tag ~ WHITE_SPACE* ~ (origin ~ ":")? ~ branch_name ~ NEWLINE } - -commit = { - git_commit ~ (target ~ NEWLINE*){, 1} ~ NEWLINE* -} - -commits = { commit+ }