feat: add commit, push handler

- add new feature commit changes and push to remote

Signed-off-by: Pakin <pakin.t@forth.co.th>
This commit is contained in:
Pakin 2026-01-22 17:20:03 +07:00
parent 2dd165b451
commit 3043f30012
4 changed files with 667 additions and 231 deletions

2
.gitignore vendored
View file

@ -1,5 +1,5 @@
/target
.tbcfg
.tbcfg*
*.txt
*.log

237
Cargo.lock generated
View file

@ -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"

View file

@ -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"] }

View file

@ -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<Mutex<Repository>>,
redis: Arc<Mutex<redis::Client>>,
}
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<AppState>, Query(param): Query<CheckoutParams>) -> impl IntoResponse {
let mut response: HashMap<String, Value> = HashMap::new();
async fn checkout_handler(
State(state): State<AppState>,
Query(param): Query<CheckoutParams>,
) -> impl IntoResponse {
let mut response: HashMap<String, Value> = 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::<Vec<String>>();
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::<Vec<String>>();
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<AppState>) -> impl IntoResponse {
let mut response: HashMap<String, Value> = HashMap::new();
if let Some(repo_path) = state.get_config("GIT_REPO_LOCAL_DEST") {
let rpath = repo_path.clone();
let mut response: HashMap<String, Value> = 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<String>,
#[serde(flatten)]
extra: HashMap<String, Value>,
}
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<AppState>,
// request body
Json(payload): Json<CommitBody>,
) -> 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<Mutex<redis::Client>>,
key: &str,
) -> Result<String, String> {
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<Value> {
//
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<Mutex<redis::Client>>,
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(&current_full_map.clone()) {
Ok(ss) => ss,
Err(_) => String::new(),
}
}
async fn commit_file_content(
repo: Arc<Mutex<Repository>>,
path: &str,
content: &[u8],
author: Signature,
message: &str,
) -> Result<git2::Oid, Box<dyn std::error::Error>> {
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<AppState>) -> 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<Mutex<Repository>>,
remote_name: &str,
branch: &str,
) -> Result<(), Box<dyn std::error::Error>> {
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(())
}
Ok(())
}