From 3043f3001263234e3ea9c45b54684f4c98fc493f Mon Sep 17 00:00:00 2001 From: Pakin Date: Thu, 22 Jan 2026 17:20:03 +0700 Subject: [PATCH] feat: add commit, push handler - add new feature commit changes and push to remote Signed-off-by: Pakin --- .gitignore | 2 +- Cargo.lock | 237 ++++++++++++------- Cargo.toml | 2 + src/app.rs | 657 ++++++++++++++++++++++++++++++++++++++++------------- 4 files changed, 667 insertions(+), 231 deletions(-) diff --git a/.gitignore b/.gitignore index db92e80..2b1bee5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ /target -.tbcfg +.tbcfg* *.txt *.log diff --git a/Cargo.lock b/Cargo.lock index 844e65e..b68a3f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -102,6 +102,12 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arcstr" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03918c3dbd7701a85c6b9887732e2921175f26c350b4563841d0958c21d57e6d" + [[package]] name = "async-trait" version = "0.1.89" @@ -177,6 +183,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "base64" version = "0.22.1" @@ -221,9 +238,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.51" +version = "1.2.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" +checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" dependencies = [ "find-msvc-tools", "jobserver", @@ -239,9 +256,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "iana-time-zone", "js-sys", @@ -266,6 +283,16 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "constant_time_eq" version = "0.3.1" @@ -466,21 +493,20 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "filetime" -version = "0.2.26" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" dependencies = [ "cfg-if", "libc", "libredox", - "windows-sys 0.60.2", ] [[package]] name = "find-msvc-tools" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" [[package]] name = "fixedbitset" @@ -490,13 +516,13 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.1.5" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" dependencies = [ "crc32fast", - "libz-rs-sys", "miniz_oxide", + "zlib-rs", ] [[package]] @@ -505,6 +531,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foreign-types" version = "0.3.2" @@ -566,6 +598,7 @@ dependencies = [ "futures-task", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -580,9 +613,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", @@ -635,6 +668,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -928,7 +970,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", ] [[package]] @@ -1013,9 +1055,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -1029,9 +1071,9 @@ checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" [[package]] name = "libc" -version = "0.2.179" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libgit2-sys" @@ -1075,7 +1117,7 @@ dependencies = [ [[package]] name = "libtbr" version = "0.1.1" -source = "git+https://pakin-inspiron-15-3530.tail110d9.ts.net/pakin/libtbr.git#9ef23af0fd411c1434d5d8fe967a717eca445b96" +source = "git+https://pakin-inspiron-15-3530.tail110d9.ts.net/pakin/libtbr.git#2b6b0626642a41a582864052e909e2585886ff89" dependencies = [ "chrono", "flate2", @@ -1090,15 +1132,6 @@ dependencies = [ "zip", ] -[[package]] -name = "libz-rs-sys" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c10501e7805cee23da17c7790e59df2870c0d4043ec6d03f67d31e2b53e77415" -dependencies = [ - "zlib-rs", -] - [[package]] name = "libz-sys" version = "1.1.23" @@ -1210,12 +1243,31 @@ dependencies = [ "tempfile", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1322,11 +1374,12 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "petgraph" -version = "0.7.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ "fixedbitset", + "hashbrown 0.15.5", "indexmap", ] @@ -1425,18 +1478,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.105" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "prost" -version = "0.14.1" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" +checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" dependencies = [ "bytes", "prost-derive", @@ -1444,15 +1497,14 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.14.1" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac6c3320f9abac597dcbc668774ef006702672474aad53c6d596b62e487b40b1" +checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" dependencies = [ "heck", "itertools", "log", "multimap", - "once_cell", "petgraph", "prettyplease", "prost", @@ -1466,9 +1518,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.14.1" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" +checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" dependencies = [ "anyhow", "itertools", @@ -1479,9 +1531,9 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.14.1" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72" +checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" dependencies = [ "prost", ] @@ -1499,9 +1551,9 @@ dependencies = [ [[package]] name = "pulldown-cmark-to-cmark" -version = "21.1.0" +version = "22.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8246feae3db61428fd0bb94285c690b460e4517d83152377543ca802357785f1" +checksum = "50793def1b900256624a709439404384204a5dc3a6ec580281bfaac35e882e90" dependencies = [ "pulldown-cmark", ] @@ -1543,9 +1595,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] @@ -1570,6 +1622,24 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redis" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfe20977fe93830c0e9817a16fbf1ed1cfd8d4bba366087a1841d2c6033c251" +dependencies = [ + "arcstr", + "combine", + "itoa", + "num-bigint", + "percent-encoding", + "ryu", + "sha1_smol", + "socket2", + "url", + "xxhash-rust", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1665,7 +1735,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -1699,18 +1769,18 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.2" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ "ring", "rustls-pki-types", @@ -1854,6 +1924,12 @@ dependencies = [ "digest", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.10.9" @@ -1989,12 +2065,14 @@ name = "tbm-git-repo-service" version = "0.1.0" dependencies = [ "axum", + "axum-macros", "env_logger", "git2", "libgit2-sys", "libtbr", "log", "prost", + "redis", "reqwest", "serde", "serde_json", @@ -2019,22 +2097,22 @@ dependencies = [ [[package]] name = "time" -version = "0.3.44" +version = "0.3.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" dependencies = [ "deranged", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" [[package]] name = "tinystr" @@ -2188,9 +2266,9 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -2360,18 +2438,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", @@ -2382,11 +2460,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -2395,9 +2474,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2405,9 +2484,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ "bumpalo", "proc-macro2", @@ -2418,18 +2497,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" dependencies = [ "js-sys", "wasm-bindgen", @@ -2672,9 +2751,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "writeable" @@ -2692,6 +2771,12 @@ dependencies = [ "rustix", ] +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + [[package]] name = "yoke" version = "0.8.1" @@ -2844,9 +2929,9 @@ checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" [[package]] name = "zmij" -version = "1.0.12" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8" +checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" [[package]] name = "zopfli" diff --git a/Cargo.toml b/Cargo.toml index 9d14956..24a0aa3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,12 +5,14 @@ edition = "2024" [dependencies] axum = "0.8.7" +axum-macros = "0.5.0" env_logger = "0.11.8" git2 = { version = "0.20.3", features = ["https", "ssh"] } libgit2-sys = { version = "0.18.3", features = ["ssh"] } libtbr = { git = "https://pakin-inspiron-15-3530.tail110d9.ts.net/pakin/libtbr.git", version = "0.1.1" } log = "0.4.29" prost = "0.14.1" +redis = "1.0.2" reqwest = { version = "0.12.25", features = ["json"] } serde = { version = "1.0.228", features = ["derive"] } serde_json = { version = "1.0.145", features = ["preserve_order"] } diff --git a/src/app.rs b/src/app.rs index 77f23aa..6a87c2a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,197 +1,546 @@ -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; -use axum::{extract::{Query, State}, response::IntoResponse, routing::get, Json, Router}; -use git2::{Repository, RemoteCallbacks, FetchOptions, Cred}; -use log::{error, warn}; -use libtbr::recipe_functions::common; -use serde::Deserialize; -use serde_json::{json, Value}; +use axum::{ + Json, Router, + extract::{Query, State}, + response::IntoResponse, + routing::{get, post}, +}; +use axum_macros::debug_handler; +use git2::{Cred, FetchOptions, PushOptions, RemoteCallbacks, Repository}; +use libtbr::{models, recipe_functions::common}; +use log::{error, info, warn}; +use redis::TypedCommands; +use serde::{Deserialize, Serialize}; +use serde_json::{Value, json}; +use tokio::sync::Mutex; use crate::{gcm, reg}; #[derive(Clone)] pub struct AppState { - cached_country_names: Vec<&'static str>, - configures: gcm::Configure + // cached_country_names: Vec<&'static str>, + configures: gcm::Configure, + repo: Arc>, + redis: Arc>, } impl AppState { - pub fn check_country_existed(&self, req: &str) -> bool { - self.cached_country_names.contains(&req) - } + // pub fn check_country_existed(&self, req: &str) -> bool { + // self.cached_country_names.contains(&req) + // } - pub fn get_config(&self, config_name: &str) -> Option<&String> { - self.configures.get(config_name) - } + pub fn get_config(&self, config_name: &str) -> Option<&String> { + self.configures.get(config_name) + } + + pub fn get_all_configures(self) -> gcm::Configure { + self.configures.clone() + } } #[derive(Deserialize)] struct CheckoutParams { - path: String + path: String, } -async fn checkout_handler(State(state): State, Query(param): Query) -> impl IntoResponse { - let mut response: HashMap = HashMap::new(); +async fn checkout_handler( + State(state): State, + Query(param): Query, +) -> impl IntoResponse { + let mut response: HashMap = HashMap::new(); - let repo_path = state.get_config("GIT_REPO_LOCAL_DEST"); - if repo_path.is_none() { - response.insert("error".to_string(), json!("config repo dest not found".to_string())); - return ( - axum::http::StatusCode::INTERNAL_SERVER_ERROR, - Json(json!(response)) - ); - } - - let legit_path = param.path.as_str(); - - // match param.path.as_str() { - // legit_path if param.path.contains("/") || state.check_country_existed(param.path.as_str()) || param.path.is_empty() => { - - // } - // _ => { - // let error_log = "requested path is unexpected"; - // error!("{error_log}"); - // response.insert("error".to_string(), json!(error_log)); - // } - // } - let rpath = repo_path.unwrap().clone(); - let repo = match Repository::open_bare(rpath) { - Ok(repo) => repo, - Err(_) => { - let error_log = "unable to open repo as bare"; - error!("{error_log}"); - response.insert("error".to_string(), json!(error_log)); - return ( - axum::http::StatusCode::INTERNAL_SERVER_ERROR, - Json(json!(response)) - ); + let repo_path = state.get_config("GIT_REPO_LOCAL_DEST"); + if repo_path.is_none() { + response.insert( + "error".to_string(), + json!("config repo dest not found".to_string()), + ); + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + Json(json!(response)), + ); } - }; - let fpath = format!("master:{}", legit_path); - let obj = match repo.revparse_single(&fpath){ - Ok(obj) => obj, - Err(e) => { - let error_log = format!("unexpected revparse single: {err}", err = e.message()); - error!("{error_log}"); - response.insert("error".to_string(), json!(error_log.clone())); - return ( - axum::http::StatusCode::INTERNAL_SERVER_ERROR, - Json(json!(response)) - ); - } - }; + let legit_path = param.path.as_str(); - if let Some(blob) = obj.as_blob() { - let content = unsafe { - str::from_utf8_unchecked(blob.content()) + // match param.path.as_str() { + // legit_path if param.path.contains("/") || state.check_country_existed(param.path.as_str()) || param.path.is_empty() => { + + // } + // _ => { + // let error_log = "requested path is unexpected"; + // error!("{error_log}"); + // response.insert("error".to_string(), json!(error_log)); + // } + // } + let rpath = repo_path.unwrap().clone(); + let repo = match Repository::open_bare(rpath) { + Ok(repo) => repo, + Err(_) => { + let error_log = "unable to open repo as bare"; + error!("{error_log}"); + response.insert("error".to_string(), json!(error_log)); + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + Json(json!(response)), + ); + } }; - response.insert("result".to_string(), json!(content.to_string())); - } else if let Some(tree) = obj.as_tree() { - let dir_list = tree.iter().map(|x| x.name().unwrap_or("").to_string()).collect::>(); - response.insert("result".to_string(), json!(dir_list)); - } else { - let error_log = "not obj nor tree"; - error!("{error_log}"); - response.insert("error".to_string(), json!(error_log)); - return ( - axum::http::StatusCode::BAD_REQUEST, - Json(json!(response)) - ) - } - ( - axum::http::StatusCode::OK, - Json(json!(response)) - ) + let fpath = format!("master:{}", legit_path); + let obj = match repo.revparse_single(&fpath) { + Ok(obj) => obj, + Err(e) => { + let error_log = format!("unexpected revparse single: {err}", err = e.message()); + error!("{error_log}"); + response.insert("error".to_string(), json!(error_log.clone())); + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + Json(json!(response)), + ); + } + }; + + if let Some(blob) = obj.as_blob() { + let content = unsafe { str::from_utf8_unchecked(blob.content()) }; + response.insert("result".to_string(), json!(content.to_string())); + } else if let Some(tree) = obj.as_tree() { + let dir_list = tree + .iter() + .map(|x| x.name().unwrap_or("").to_string()) + .collect::>(); + response.insert("result".to_string(), json!(dir_list)); + } else { + let error_log = "not obj nor tree"; + error!("{error_log}"); + response.insert("error".to_string(), json!(error_log)); + return (axum::http::StatusCode::BAD_REQUEST, Json(json!(response))); + } + + (axum::http::StatusCode::OK, Json(json!(response))) } async fn fetch_handler(State(state): State) -> impl IntoResponse { - let mut response: HashMap = HashMap::new(); - if let Some(repo_path) = state.get_config("GIT_REPO_LOCAL_DEST") { - let rpath = repo_path.clone(); + let mut response: HashMap = HashMap::new(); + if let Some(repo_path) = state.get_config("GIT_REPO_LOCAL_DEST") { + let rpath = repo_path.clone(); - let repo = match Repository::open_bare(rpath) { - Ok(repo) => repo, - Err(_) => { - let error_log = "unable to open bare repo"; + let repo = match Repository::open_bare(rpath) { + Ok(repo) => repo, + Err(_) => { + let error_log = "unable to open bare repo"; + error!("{error_log}"); + response.insert("error".to_string(), json!(error_log)); + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + Json(json!(response)), + ); + } + }; + + let mut remote = match repo.find_remote("origin") { + Ok(remote) => remote, + Err(e) => { + let error_log = format!("unable to find remote, {}", e.message()); + error!("{error_log}"); + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + Json(json!(response)), + ); + } + }; + + if state.get_config("GIT_REPO_USERNAME").is_none() + || state.get_config("GIT_REPO_PASSWORD").is_none() + { + warn!("username or password not provided may cause fetching fail"); + } + + let mut callbacks = RemoteCallbacks::new(); + callbacks.credentials(|_, _, _| { + Cred::userpass_plaintext( + state + .get_config("GIT_REPO_USERNAME") + .unwrap_or(&"".to_string()), + state + .get_config("GIT_REPO_PASSWORD") + .unwrap_or(&"".to_string()), + ) + }); + + let mut fetch_options = FetchOptions::new(); + fetch_options.remote_callbacks(callbacks); + + match remote.fetch(&["origin"], Some(&mut fetch_options), None) { + Ok(_) => {} + Err(e) => { + error!("error while fetching {}", e.message()); + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({"error": format!("error while fetching {}", e.message())})), + ); + } + } + } else { + let error_log = "cannot find local repo"; error!("{error_log}"); response.insert("error".to_string(), json!(error_log)); return ( - axum::http::StatusCode::INTERNAL_SERVER_ERROR, - Json(json!(response)) + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + Json(json!(response)), ); - } - }; - - let mut remote = match repo.find_remote("origin") { - Ok(remote) => remote, - Err(e) => { - let error_log = format!("unable to find remote, {}", e.message()); - error!("{error_log}"); - return ( - axum::http::StatusCode::INTERNAL_SERVER_ERROR, - Json(json!(response)) - ); - } - }; - - if state.get_config("GIT_REPO_USERNAME").is_none() || state.get_config("GIT_REPO_PASSWORD").is_none() { - warn!("username or password not provided may cause fetching fail"); } - let mut callbacks = RemoteCallbacks::new(); - callbacks.credentials(|_, _, _| { - Cred::userpass_plaintext( - state.get_config("GIT_REPO_USERNAME").unwrap_or(&"".to_string()), - state.get_config("GIT_REPO_PASSWORD").unwrap_or(&"".to_string()) - ) - }); + ( + axum::http::StatusCode::OK, + Json(json!({"result": "fetch success"})), + ) +} - let mut fetch_options = FetchOptions::new(); - fetch_options.remote_callbacks(callbacks); +// { path: "/path/to/file", signature: { +// username: "", email: "" +// }, patch_key: "edit_id_from_redis"} +#[derive(Deserialize)] +struct CommitBody { + // Actual file path + path: String, + // Signature of user + signature: Signature, + // Key to grep content changes from redis + patch_key: String, + // user message + message: Option, + #[serde(flatten)] + extra: HashMap, +} - match remote.fetch(&["origin"], Some(&mut fetch_options), None) { - Ok(_) => {} - Err(e) => { - error!("error while fetching {}", e.message()); - return ( - axum::http::StatusCode::INTERNAL_SERVER_ERROR, - Json(json!({"error": format!("error while fetching {}", e.message())})) - ); - } +#[derive(Deserialize)] +struct Signature { + username: String, + email: String, +} + +#[debug_handler] +async fn commit_handler( + State(state): State, + // request body + Json(payload): Json, +) -> impl IntoResponse { + let mut content = match fetch_content_from_redis(state.redis.clone(), &payload.patch_key).await + { + Ok(c) => c, + Err(e) => { + return ( + axum::http::StatusCode::BAD_REQUEST, + Json(json!({"error": e})), + ); + } + }; + + let is_patch_file = content.starts_with("patch"); + // do apply patch first + if is_patch_file { + content = apply_patch_to_file(state.redis.clone(), &payload.path, &mut content).await; } - } else { - let error_log = "cannot find local repo"; - error!("{error_log}"); - response.insert("error".to_string(), json!(error_log)); - return ( - axum::http::StatusCode::INTERNAL_SERVER_ERROR, - Json(json!(response)) - ); - } - ( - axum::http::StatusCode::OK, - Json(json!({"result": "fetch success"})) - ) + let commit_oid = match commit_file_content( + state.repo, + &payload.path, + &content.as_bytes(), + payload.signature, + &payload.message.unwrap_or("update: from api".to_string()), + ) + .await + { + Ok(oid) => oid, + Err(e) => { + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({"error": e.to_string()})), + ); + } + }; + + // save history + let redis_pre_lock = state.redis.clone(); + { + if let Ok(mut rl) = redis_pre_lock.try_lock() { + let _ = rl.rpush(format!("{}.history", payload.path), payload.patch_key); + } + } + + ( + axum::http::StatusCode::OK, + Json(json!({"result": format!("{commit_oid}")})), + ) +} + +async fn fetch_content_from_redis( + redis: Arc>, + key: &str, +) -> Result { + match redis.lock().await.get(key) { + Ok(s) => { + if let Some(res) = s { + Ok(res) + } else { + Err(format!("result error from key: {key}")) + } + } + Err(e) => Err(format!("redis get failed: {e}")), + } +} + +fn read_patch_from_str(patch_changes: &str) -> Option { + // + + let pv = patch_changes.replace("patch ", ""); + + let change_map: Value = match serde_json::from_str(&pv) { + Ok(s) => s, + Err(_) => return None, + }; + Some(change_map) +} + +fn get_product_code(v: &Value) -> Option<&str> { + v.get("productCode")?.as_str() +} + +fn diff_apply(target: &mut Value, patch: &Value) -> Result<(), String> { + match (target, patch) { + (Value::Object(target_map), Value::Object(patch_map)) => { + for (k, v_patch) in patch_map { + let v_target = target_map + .get_mut(k) + .ok_or_else(|| format!("Unknown key in patch: {k}"))?; + + diff_apply(v_target, v_patch)?; + } + + Ok(()) + } + (Value::Array(target_arr), Value::Array(patch_arr)) => { + for patch_elem in patch_arr { + // NOTE: support only `Recipe01` + let patch_id = get_product_code(patch_elem) + .ok_or("Patch array element missing product code")?; + let target_elem = target_arr + .iter_mut() + .find(|e| get_product_code(e) == Some(patch_id)) + .ok_or_else(|| format!("Unknown id in patch array: {patch_id}"))?; + + diff_apply(target_elem, patch_elem)?; + } + + Ok(()) + } + (target_slot, patch_value) => { + *target_slot = patch_value.clone(); + Ok(()) + } + } +} + +async fn apply_patch_to_file( + redis: Arc>, + path: &str, + patch_changes: &mut String, +) -> String { + use libtbr::*; + // expect the path to already has in redis + let full_file = match fetch_content_from_redis(redis, path).await { + Ok(f) => f, + Err(_) => String::new(), + }; + + if full_file.is_empty() { + return full_file; + } + + // read into struct + // + let full_recipe: models::recipe::Recipe = match serde_json::from_str(&full_file) { + Ok(f) => f, + Err(_) => return String::new(), + }; + + // read patch + let patch_map = read_patch_from_str(patch_changes); + let mut current_full_map = match serde_json::to_value(full_recipe.clone()) { + Ok(m) => m, + Err(_) => return String::new(), + }; + + if let Some(pm) = patch_map { + match diff_apply(&mut current_full_map, &pm) { + Ok(_) => {} + Err(x) => { + error!("error while applied patch: {x}"); + } + } + } + + match serde_json::to_string(¤t_full_map.clone()) { + Ok(ss) => ss, + Err(_) => String::new(), + } +} + +async fn commit_file_content( + repo: Arc>, + path: &str, + content: &[u8], + author: Signature, + message: &str, +) -> Result> { + let repo_clone = repo.clone(); + let blob_oid = repo_clone.lock().await.blob(content)?; + info!("blob oid: {blob_oid}"); + let mut index = repo_clone.lock().await.index()?; + info!("index pass"); + let rlock = repo_clone.try_lock()?; + let parent = match rlock.head() { + Ok(head) => { + let commit = head.peel_to_commit()?; + index.read_tree(&commit.tree()?)?; + Some(commit.clone()) + } + Err(_) => None, + }; + + info!("parent pass"); + + index.add(&git2::IndexEntry { + ctime: git2::IndexTime::new(0, 0), + mtime: git2::IndexTime::new(0, 0), + dev: 0, + ino: 0, + mode: 0o100644, + uid: 0, + gid: 0, + file_size: content.len() as u32, + id: blob_oid, + flags: 0, + flags_extended: 0, + path: path.as_bytes().to_vec(), + })?; + + info!("index added"); + + let tree_oid = index.write_tree()?; + + info!("write to tree"); + // let tlock = repo_clone.try_lock()?; + info!("acquire try lock"); + let tree = rlock.find_tree(tree_oid)?; + info!("find tree ok"); + + let sig = git2::Signature::now(&author.username, &author.email)?; + info!("generated signature"); + + let parents: Vec<&git2::Commit> = parent.iter().collect(); + let oid = rlock.commit( + Some("refs/heads/master"), + &sig, + &sig, + message, + &tree, + &parents, + )?; + + info!("commit oid: {oid}"); + + Ok(oid) +} + +async fn push_handler(State(state): State) -> impl IntoResponse { + let config = state.clone().get_all_configures(); + let repo = state.repo.clone(); + let remote_name = match state.get_config("GIT_REPO_REMOTE") { + Some(s) => s, + None => { + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({"error": "repo remote not existed"})), + ); + } + }; + + let branch = "master"; + + if let Err(e) = push(config, repo, remote_name, branch) { + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({"error": e.to_string()})), + ); + } + + ( + axum::http::StatusCode::OK, + Json(json!({"result": "push completed"})), + ) +} + +fn push( + config: gcm::Configure, + repo: Arc>, + remote_name: &str, + branch: &str, +) -> Result<(), Box> { + if let Ok(rlock) = repo.try_lock() { + let mut rem = rlock.find_remote(remote_name)?; + let mut callback = RemoteCallbacks::new(); + callback.credentials(|_url, _user, _allowed| { + Cred::userpass_plaintext( + config.get("GIT_REPO_USERNAME").unwrap_or(&"".to_string()), + config.get("GIT_REPO_PASSWORD").unwrap_or(&"".to_string()), + ) + }); + let mut push_opts = PushOptions::new(); + push_opts.remote_callbacks(callback); + let refspec = format!("refs/heads/{0}:refs/heads/{0}", branch); + rem.push(&[&refspec], Some(&mut push_opts))?; + + return Ok(()); + } + + Err("cannot lock repo".into()) } pub async fn run(config: gcm::Configure) -> gcm::StandardResult { - let state = AppState { - cached_country_names: common::valid_country_name(), - configures: config.clone() - }; + let state = AppState { + // cached_country_names: common::valid_country_name(), + configures: config.clone(), + repo: Arc::new(Mutex::new(Repository::open_bare( + config.get("GIT_REPO_LOCAL_DEST").unwrap_or(&"".to_string()), + )?)), + redis: Arc::new(Mutex::new(redis::Client::open(format!( + "redis://{}:{}", + config.get("REDIS_URI").unwrap_or(&"".to_string()), + config.get("REDIS_PORT").unwrap_or(&"".to_string()) + ))?)), + }; - let app = Router::new() - .route("/checkout", get(checkout_handler)) - .route("/fetch", get(fetch_handler)) - .route("/healthz", get(reg::health)) - .with_state(state); + let app = Router::new() + .route("/checkout", get(checkout_handler)) + .route("/fetch", get(fetch_handler)) + .route("/commit", post(commit_handler)) + .route("/push", get(push_handler)) + .route("/healthz", get(reg::health)) + .with_state(state); - let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", config.clone().get("PUBLIC_PORT").unwrap_or(&"36583".to_string()))).await?; + let listener = tokio::net::TcpListener::bind(format!( + "0.0.0.0:{}", + config + .clone() + .get("PUBLIC_PORT") + .unwrap_or(&"36583".to_string()) + )) + .await?; - axum::serve(listener, app).await?; + axum::serve(listener, app).await?; - Ok(()) -} \ No newline at end of file + Ok(()) +}