diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 000000000..b6cf3ff25 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +patchable = ["run", "--bin", "patchable", "--"] diff --git a/.gitignore b/.gitignore index 1adb750f0..e1a295d51 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ -# Byte-compiled / optimized / DLL files +# compiled / optimized files __pycache__/ *.py[cod] +target/ +# Patchable working files patchable-work/ diff --git a/CHANGELOG.md b/CHANGELOG.md index e2dd14df2..1aad3e819 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,12 +27,14 @@ All notable changes to this project will be documented in this file. - trino-cli: Add version 470 ([#999]). - trino-storage-connector: Add version 470 ([#999]). - superset: Add version `4.1.1` ([#991]). +- Added Patchable patch management tool ([#1003]). ### Changed - kafka: Bump 3.8.0 to 3.8.1 ([#995]). - Update registry references to oci ([#989]). - trino-storage-connector: Move the build out of trino/ for easier patching ([#996]). +- druid 26.0.0: Migrate to patchable ([#1003]). ### Removed @@ -71,6 +73,7 @@ All notable changes to this project will be documented in this file. [#997]: https://github.com/stackabletech/docker-images/pull/997 [#999]: https://github.com/stackabletech/docker-images/pull/999 [#1000]: https://github.com/stackabletech/docker-images/pull/1000 +[#1003]: https://github.com/stackabletech/docker-images/pull/1003 ## [24.11.1] - 2025-01-14 diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 000000000..71183abcd --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1142 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys", +] + +[[package]] +name = "bitflags" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" + +[[package]] +name = "cc" +version = "1.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4730490333d58093109dc02c23174c3f4d490998c3fed3cc8e82d57afedb9cf" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.5.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[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 = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi", + "windows-targets", +] + +[[package]] +name = "git2" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fda788993cc341f69012feba8bf45c0ba4f3291fcc08e214b4d5a7332d88aff" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "url", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "libgit2-sys" +version = "0.18.0+1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1a117465e7e1597e8febea8bb0c410f1c7fb93b1e1cddf34363f8390367ffec" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + +[[package]] +name = "libssh2-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9b68e50e6e0b26f672573834882eb57759f6db9b3be2ea3c35c91188bb4eaa" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + +[[package]] +name = "log" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "patchable" +version = "0.1.0" +dependencies = [ + "clap", + "git2", + "serde", + "snafu", + "tempfile", + "time", + "toml", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "serde" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "snafu" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "223891c85e2a29c3fe8fb900c1fae5e69c2e42415e3177752e8718475efa5019" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c3c6b7927ffe7ecaa769ee0e3994da3b8cafc8f444578982c83ecb161af917" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tempfile" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" +dependencies = [ + "cfg-if", + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "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_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e49d2d35d3fad69b39b94139037ecfb4f359f08958b9c11e7315ce770462419" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 000000000..a019301e4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[workspace] +members = ["rust/*"] +resolver = "2" + +[workspace.dependencies] +clap = { version = "4.5.27", features = ["derive"] } +git2 = "0.20.0" +serde = { version = "1.0.217", features = ["derive"] } +snafu = "0.8.5" +tempfile = "3.16.0" +time = { version = "0.3.37", features = ["parsing"] } +toml = "0.8.19" +tracing = "0.1.41" +tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } diff --git a/README.md b/README.md index 9c13854fa..4e3b6f3d7 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,123 @@ bake --image-version 0.0.0-dev The GitHub action called `Build (and optionally publish) 0.0.0-dev images` can be triggered manually to do build all images in all versions. When triggered manually it will _not_ push the images to the registry. +## Patches + +Many products apply Stackable-specific patches, managed by [Patchable](rust/patchable). + +Patchable doesn't _edit_ anything by itself. Instead, it's a uniform way to apply a set of patches +to an upstream Git repository, and then export your local changes back into patch files. + +It doesn't care about how you make your local changes - you can edit the branch created by +patchable using any Git frontend, such as the git CLI or [jj](https://jj-vcs.github.io/jj/latest/). + +This way, the patch files are the global source of truth and track the history of our patch series, +while you can still use the same familiar Git tools to manipulate them. + +### Check out patched sources locally + +> [!NOTE] +> This is not required for building images, but is used for when hacking on or debugging patch series. + +```sh +# Fetches the upstream repository (if required), and creates a git worktree to work with it +# It also creates two branches: +# - patchable/{version} (HEAD, has all patches applied) +# - patchable/base/{version} (the upstream) +pushd $(cargo patchable checkout druid 26.0.0) + +# Commit to add new patches +# NOTE: the commit message will be used to construct the patch filename. Spaces +# will be converted to hyphens automatically. +git commit + +# Rebase against the base commit to edit or remove patches +git rebase --interactive patchable/base/26.0.0 +# jj edit also works, but make sure to go back to the tip before exporting + +# When done, export your patches and commit them (to docker-images) +popd +cargo patchable export druid 26.0.0 +git status +``` + +> ![CAUTION] +> `cargo patchable export` exports whatever is currently checked out (`HEAD`) in the worktree. +> If you use `jj edit` (or `git switch`) then you _must_ go back to the tip before exporting, or +> any patches after that point will be omitted from the export. + +### Initialize a new patch series + +Patchable stores metadata about each patch series in its `patchable.toml`, and will not be able to check out +a patch series that lacks one. It can be generated using `cargo patchable init`: + +```sh +cargo patchable init druid 28.0.0 --upstream https://github.com/apache/druid.git --base druid-28.0.0 +cargo patchable checkout druid 28.0.0 +``` + +### Importing patch series into Patchable + +Patchable is stricter about applying invalid patches (both metadata and patches themselves) than Git is. + +If an initial `cargo patchable checkout` fails then `git am` can be useful for the initial migration: + +```sh +# Create Patchable configuration for the new version, if it doesn't already exist +cargo patchable init druid 30.0.0 --upstream https://github.com/apache/druid.git --base druid-30.0.0 +# Check out the upstream base commit, without trying to apply the patches +pushd $(cargo patchable checkout druid 30.0.0 --base-only) + +# Apply the patch series +git am ../../../stackable/patches/30.0.0/*.patch +# Resolve any conflicts that arise, and `git am --continue` until done + +# Leave and export the new patch series! +popd +cargo patchable export druid 30.0.0 +``` + +### Porting patch series to a new version + +Patchable doesn't support restoring a patch series that doesn't apply cleanly. Instead, use `git cherry-pick` to rebase the patch series. + +For example, let's try rebasing our patch series from Druid 26.0.0 to Druid 28.0.0 (which is not packaged by SDP): + +```sh +# Restore the old version +# In addition to creating the version worktree, this also creates the branches patchable/26.0.0 (26.0.0 with our patches applied) and +# patchable/base/26.0.0 (upstream 26.0.0 with no patches). +cargo patchable checkout druid 26.0.0 +# Tell Patchable about the new version 28.0.0, which can be fetched from https://github.com/apache/druid.git, and has the tag druid-28.0.0 +cargo patchable init druid 28.0.0 --upstream https://github.com/apache/druid.git --base druid-28.0.0 +# Create and go to the worktree for the new version +pushd $(cargo patchable checkout druid 28.0.0) + +# Cherry pick the old patch series +git cherry-pick patchable/base/26.0.0..patchable/26.0.0 +# Solve conflicts and `git cherry-pick --continue` until done +# You can also use `git cherry-pick --skip` to skip resolving conflicts for patches that are no longer required + +# If some patches are no longer required, use an interactive rebase to remove them (or do other cleanup) +git rebase --interactive patchable/base/28.0.0 + +# Leave and export the new patch series! +popd +cargo patchable export druid 28.0.0 +git status +``` + +### Porting patches between versions + +Individual patches can also be cherry-picked across versions. + +For example, assuming we are in the Druid 28.0.0 workspace and want to port the last patch of the Druid 26.0.0 series: + +```sh +# git cherry-pick is also fine for grabbing arbitrary patches +git cherry-pick patchable/26.0.0 +``` + ## Verify Product Images To verify if Apache Zookeeper validate against OpenShift preflight, run: diff --git a/druid/stackable/patches/26.0.0/01-remove-ranger-security.patch b/druid/stackable/patches/26.0.0/0001-Removes-all-traces-of-the-druid-ranger-extension.patch similarity index 89% rename from druid/stackable/patches/26.0.0/01-remove-ranger-security.patch rename to druid/stackable/patches/26.0.0/0001-Removes-all-traces-of-the-druid-ranger-extension.patch index ad5d1a144..6823e2c61 100644 --- a/druid/stackable/patches/26.0.0/01-remove-ranger-security.patch +++ b/druid/stackable/patches/26.0.0/0001-Removes-all-traces-of-the-druid-ranger-extension.patch @@ -1,10 +1,12 @@ -Removes all traces of the druid ranger extension - +From a8bec93ee6d0a4364676333168229aa0ec56657e Mon Sep 17 00:00:00 2001 From: Lars Francke - +Date: Thu, 12 Dec 2024 17:59:17 +0100 +Subject: Removes all traces of the druid ranger extension --- - 0 files changed + distribution/pom.xml | 4 ---- + pom.xml | 1 - + 2 files changed, 5 deletions(-) diff --git a/distribution/pom.xml b/distribution/pom.xml index eec26171af..a6e72cf2c2 100644 diff --git a/druid/stackable/patches/26.0.0/02-prometheus-emitter-from-source.patch b/druid/stackable/patches/26.0.0/0002-Include-Prometheus-emitter-in-distribution.patch similarity index 91% rename from druid/stackable/patches/26.0.0/02-prometheus-emitter-from-source.patch rename to druid/stackable/patches/26.0.0/0002-Include-Prometheus-emitter-in-distribution.patch index 84c042d77..3bc040817 100644 --- a/druid/stackable/patches/26.0.0/02-prometheus-emitter-from-source.patch +++ b/druid/stackable/patches/26.0.0/0002-Include-Prometheus-emitter-in-distribution.patch @@ -1,10 +1,11 @@ -Include Prometheus emitter in distribution - +From c19288cd84492d76f924152f2d4f0d0fc0499ed6 Mon Sep 17 00:00:00 2001 From: Lars Francke - +Date: Thu, 12 Dec 2024 17:59:17 +0100 +Subject: Include Prometheus emitter in distribution --- - 0 files changed + distribution/pom.xml | 46 ++++++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 46 insertions(+) diff --git a/distribution/pom.xml b/distribution/pom.xml index a6e72cf2c2..3ab13d5d11 100644 diff --git a/druid/stackable/patches/26.0.0/03-stop-building-unused-extensions.patch b/druid/stackable/patches/26.0.0/0003-Stop-building-unused-extensions.patch similarity index 92% rename from druid/stackable/patches/26.0.0/03-stop-building-unused-extensions.patch rename to druid/stackable/patches/26.0.0/0003-Stop-building-unused-extensions.patch index 6674f07ae..722e9e42a 100644 --- a/druid/stackable/patches/26.0.0/03-stop-building-unused-extensions.patch +++ b/druid/stackable/patches/26.0.0/0003-Stop-building-unused-extensions.patch @@ -1,12 +1,14 @@ -Stop building unused extensions. - +From 85cacbcc47c88a56acd60d91fbf0412040523c8d Mon Sep 17 00:00:00 2001 From: Lars Francke +Date: Thu, 12 Dec 2024 17:59:17 +0100 +Subject: Stop building unused extensions. By default Druid builds all community extensions and then discards them while assembling the final distribution. This patch removes unused extensions from the build. --- - 0 files changed + pom.xml | 32 ++++---------------------------- + 1 file changed, 4 insertions(+), 28 deletions(-) diff --git a/pom.xml b/pom.xml index a33c6bd521..f5001910e1 100644 diff --git a/druid/stackable/patches/26.0.0/04-update-patch-dependencies.patch b/druid/stackable/patches/26.0.0/0004-Updates-all-dependencies-that-have-a-new-patch-relea.patch similarity index 90% rename from druid/stackable/patches/26.0.0/04-update-patch-dependencies.patch rename to druid/stackable/patches/26.0.0/0004-Updates-all-dependencies-that-have-a-new-patch-relea.patch index 3cc1831c9..53c20d559 100644 --- a/druid/stackable/patches/26.0.0/04-update-patch-dependencies.patch +++ b/druid/stackable/patches/26.0.0/0004-Updates-all-dependencies-that-have-a-new-patch-relea.patch @@ -1,10 +1,16 @@ -Updates all dependencies that have a new patch release available. - +From 4229d1c0d096e10dce72929224a7b4c2284fb417 Mon Sep 17 00:00:00 2001 From: Lars Francke - +Date: Thu, 12 Dec 2024 17:59:17 +0100 +Subject: Updates all dependencies that have a new patch release available. --- - 0 files changed + extensions-core/avro-extensions/pom.xml | 2 +- + extensions-core/kubernetes-extensions/pom.xml | 2 +- + extensions-core/orc-extensions/pom.xml | 2 +- + extensions-core/parquet-extensions/pom.xml | 2 +- + extensions-core/protobuf-extensions/pom.xml | 2 +- + pom.xml | 20 +++++++++---------- + 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/extensions-core/avro-extensions/pom.xml b/extensions-core/avro-extensions/pom.xml index 35b154a469..a9eb0c6851 100644 diff --git a/druid/stackable/patches/26.0.0/05-xmllayout-dependencies.patch b/druid/stackable/patches/26.0.0/0005-Include-jackson-dataformat-xml-dependency.patch similarity index 80% rename from druid/stackable/patches/26.0.0/05-xmllayout-dependencies.patch rename to druid/stackable/patches/26.0.0/0005-Include-jackson-dataformat-xml-dependency.patch index 112e3b683..4032142ab 100644 --- a/druid/stackable/patches/26.0.0/05-xmllayout-dependencies.patch +++ b/druid/stackable/patches/26.0.0/0005-Include-jackson-dataformat-xml-dependency.patch @@ -1,13 +1,15 @@ -Include jackson-dataformat-xml dependency. - +From d55895a2525286a5198a3b327c3ce503bc852ead Mon Sep 17 00:00:00 2001 From: Lars Francke +Date: Thu, 12 Dec 2024 17:59:17 +0100 +Subject: Include jackson-dataformat-xml dependency. This allows us to use XmlLayout for Log4jV2. By including it here as a dependency we can make sure that we always have the matching version and we don't need to include it manually later in the build. --- - 0 files changed + server/pom.xml | 5 +++++ + 1 file changed, 5 insertions(+) diff --git a/server/pom.xml b/server/pom.xml index fdc6f1f548..9f18e614e9 100644 diff --git a/druid/stackable/patches/26.0.0/06-dont-build-targz.patch b/druid/stackable/patches/26.0.0/0006-Stop-building-the-tar.gz-distribution.patch similarity index 78% rename from druid/stackable/patches/26.0.0/06-dont-build-targz.patch rename to druid/stackable/patches/26.0.0/0006-Stop-building-the-tar.gz-distribution.patch index 1bed79fd1..910a7a0a5 100644 --- a/druid/stackable/patches/26.0.0/06-dont-build-targz.patch +++ b/druid/stackable/patches/26.0.0/0006-Stop-building-the-tar.gz-distribution.patch @@ -1,11 +1,12 @@ -Stop building the tar.gz distribution. - +From d1ae8732e2eee44abb5c831f5363c69e75e64a9a Mon Sep 17 00:00:00 2001 From: Lars Francke +Date: Thu, 12 Dec 2024 17:59:17 +0100 +Subject: Stop building the tar.gz distribution. All we do is build Druid tar and gzip it only to immediately uncompress it again. So, instead we just skip the compression step entirely. --- - distribution/src/assembly/assembly.xml | 2 +- + distribution/src/assembly/assembly.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distribution/src/assembly/assembly.xml b/distribution/src/assembly/assembly.xml diff --git a/druid/stackable/patches/26.0.0/07-cyclonedx-plugin.patch b/druid/stackable/patches/26.0.0/0007-Update-CycloneDX-plugin.patch similarity index 58% rename from druid/stackable/patches/26.0.0/07-cyclonedx-plugin.patch rename to druid/stackable/patches/26.0.0/0007-Update-CycloneDX-plugin.patch index 1635101fa..36756ca94 100644 --- a/druid/stackable/patches/26.0.0/07-cyclonedx-plugin.patch +++ b/druid/stackable/patches/26.0.0/0007-Update-CycloneDX-plugin.patch @@ -1,13 +1,22 @@ +From ff7d6a5ea07ea30653b47f6ef6844103a7ac3349 Mon Sep 17 00:00:00 2001 +From: Lukas Voetmand +Date: Thu, 12 Dec 2024 17:59:17 +0100 +Subject: Update CycloneDX plugin + +--- + pom.xml | 6 +++++- + 1 file changed, 5 insertions(+), 1 deletion(-) + diff --git a/pom.xml b/pom.xml -index c0f0654..133cbf8 100644 +index 2364f27dc4..c902899304 100644 --- a/pom.xml +++ b/pom.xml -@@ -1558,7 +1558,11 @@ +@@ -1533,7 +1533,11 @@ org.cyclonedx cyclonedx-maven-plugin - 2.7.5 -+ 2.8.0 ++ 2.8.1 + + application + 1.5 diff --git a/druid/stackable/patches/26.0.0/08-CVE-2024-36114-bump-aircompressor-0-27.patch b/druid/stackable/patches/26.0.0/0008-Fix-CVE-2024-36114.patch similarity index 81% rename from druid/stackable/patches/26.0.0/08-CVE-2024-36114-bump-aircompressor-0-27.patch rename to druid/stackable/patches/26.0.0/0008-Fix-CVE-2024-36114.patch index b5fb91b5f..7368f95e7 100644 --- a/druid/stackable/patches/26.0.0/08-CVE-2024-36114-bump-aircompressor-0-27.patch +++ b/druid/stackable/patches/26.0.0/0008-Fix-CVE-2024-36114.patch @@ -1,5 +1,9 @@ -Fix CVE-2024-36114 -see https://github.com/stackabletech/vulnerabilities/issues/834 +From bdd52ae32874b686d6ddfa3179f6af787444662f Mon Sep 17 00:00:00 2001 +From: Malte Sander +Date: Thu, 12 Dec 2024 17:59:17 +0100 +Subject: Fix CVE-2024-36114 + +See https://github.com/stackabletech/vulnerabilities/issues/834 Aircompressor is a library with ports of the Snappy, LZO, LZ4, and Zstandard compression algorithms to Java. All decompressor @@ -17,12 +21,15 @@ have been fixed. When decompressing data from untrusted users, this can be exploited for a denial-of-service attack by crashing the JVM, or to leak other sensitive information from the Java process. There are no known workarounds for this issue. +--- + pom.xml | 6 ++++++ + 1 file changed, 6 insertions(+) diff --git a/pom.xml b/pom.xml -index c0f06547f8..f1c6e2f9ee 100644 +index c902899304..6c24bdc0b2 100644 --- a/pom.xml +++ b/pom.xml -@@ -258,6 +258,12 @@ +@@ -233,6 +233,12 @@ diff --git a/druid/stackable/patches/26.0.0/09-update-fmpp.patch b/druid/stackable/patches/26.0.0/0009-Update-FMPP-version.patch similarity index 51% rename from druid/stackable/patches/26.0.0/09-update-fmpp.patch rename to druid/stackable/patches/26.0.0/0009-Update-FMPP-version.patch index 3abb818da..67120aec1 100644 --- a/druid/stackable/patches/26.0.0/09-update-fmpp.patch +++ b/druid/stackable/patches/26.0.0/0009-Update-FMPP-version.patch @@ -1,8 +1,18 @@ -diff --git a/10-update-fmpp.patch b/10-update-fmpp.patch -new file mode 100644 -index 0000000000..e69de29bb2 +From 736165ab0fe73e0bef765f2cfd21cd800baddbc1 Mon Sep 17 00:00:00 2001 +From: Lars Francke +Date: Thu, 12 Dec 2024 06:35:21 +0100 +Subject: Update FMPP version + +This is because FMPP Maven Plugin depends on FMPP in version 0.9.14 +which itself depends on a Freemarker version that has not been pinned. +Instead it specifies a "range" which resolves to a SNAPSHOT version +which we don't want. +--- + sql/pom.xml | 7 +++++++ + 1 file changed, 7 insertions(+) + diff --git a/sql/pom.xml b/sql/pom.xml -index bdd29f3f91..e5ba89f655 100644 +index e2bbd8c7f8..a72f96a6ca 100644 --- a/sql/pom.xml +++ b/sql/pom.xml @@ -322,6 +322,13 @@ diff --git a/druid/stackable/patches/26.0.0/patchable.toml b/druid/stackable/patches/26.0.0/patchable.toml new file mode 100644 index 000000000..264c71a6a --- /dev/null +++ b/druid/stackable/patches/26.0.0/patchable.toml @@ -0,0 +1,2 @@ +upstream = "https://github.com/apache/druid.git" +base = "7cffb81a8e124d5f218f9af16ad685acf5e9c67c" diff --git a/druid/stackable/patches/26.0.0/series b/druid/stackable/patches/26.0.0/series deleted file mode 100644 index cc7008e05..000000000 --- a/druid/stackable/patches/26.0.0/series +++ /dev/null @@ -1,9 +0,0 @@ -# This series applies on Git commit 7cffb81a8e124d5f218f9af16ad685acf5e9c67c -01-remove-ranger-security.patch -02-prometheus-emitter-from-source.patch -03-stop-building-unused-extensions.patch -04-update-patch-dependencies.patch -05-xmllayout-dependencies.patch -06-dont-build-targz.patch -07-cyclonedx-plugin.patch -08-CVE-2024-36114-bump-aircompressor-0-27.patch diff --git a/rust/patchable/Cargo.toml b/rust/patchable/Cargo.toml new file mode 100644 index 000000000..69895307c --- /dev/null +++ b/rust/patchable/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "patchable" +version = "0.1.0" +edition = "2021" + +[dependencies] +clap.workspace = true +git2.workspace = true +serde.workspace = true +snafu.workspace = true +tempfile.workspace = true +time.workspace = true +toml.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true diff --git a/rust/patchable/README.md b/rust/patchable/README.md new file mode 100644 index 000000000..a683413d3 --- /dev/null +++ b/rust/patchable/README.md @@ -0,0 +1,46 @@ +# Patchable + +Patchable is a tool for managing patches for the third-party products distributed by Stackable as part of the Stackable Data Platform. + +Patchable works by keeping a series of .patch files (in `docker-images//stackable/patches/`) +as its source of truth, but using temporary Git repositories as an interface to modify those patches. +This lets us track the history of patches over time, but reuse the existing familiarity and tooling around Git. + +Patchable designates a commit as the upstream _base_ for each version, and considers each commit made on top of that +to be an individual patch. + +## Usage + +```sh +pushd $(cargo patchable checkout druid 26.0.0) +# do stuff +git commit +popd +cargo patchable export druid 26.0.0 +git status +``` + +For more details, run `cargo patchable --help`. + +## Notes + +- patchable only supports linear patch series (no merges beyond the base commit) +- patchable doesn't support support merging "materialized" trees, merge the .patch files instead, and `checkout`/`export` to update the hashes +- `patchable checkout` doesn't support resolving patch conflicts, use `git am` instead (and then `patchable export` the resolved patches) +- Always run patchable via `cargo patchable` (rather than `cargo install`ing it), to ensure that you use the correct version for a given checkout of docker-images + +## Configuration + +Patchable stores a per-version file in `docker-images//stackable/patches//patchable.toml`. +It currently recognizes the following keys: + +- `upstream` - the URL of the upstream repository (such as `https://github.com/apache/druid.git`) +- `base` - the commit hash of the upstream base commit (such as `7cffb81a8e124d5f218f9af16ad685acf5e9c67c`) + +### Template + +Instead of creating this manually, run `patchable init`: + +```toml +cargo patchable init druid 28.0.0 --upstream=https://github.com/apache/druid.git --base=druid-28.0.0 +``` diff --git a/rust/patchable/src/error.rs b/rust/patchable/src/error.rs new file mode 100644 index 000000000..16084caf5 --- /dev/null +++ b/rust/patchable/src/error.rs @@ -0,0 +1,87 @@ +//! Error type helpers. + +use std::{ + fmt::Display, + path::{Path, PathBuf}, +}; + +use git2::{Commit, Object, Oid, Reference, Repository}; + +#[derive(Debug)] +pub struct CommitId(Box); +impl Display for CommitId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} +impl From for CommitId { + fn from(value: Oid) -> Self { + Self(Box::new(value)) + } +} +impl From<&Commit<'_>> for CommitId { + fn from(value: &Commit<'_>) -> Self { + value.id().into() + } +} +impl From<&Object<'_>> for CommitId { + fn from(value: &Object<'_>) -> Self { + value.id().into() + } +} +impl From> for CommitId { + fn from(value: Object<'_>) -> Self { + (&value).into() + } +} + +#[derive(Debug)] +pub struct CommitRef(String); +impl Display for CommitRef { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.0) + } +} +impl From<&str> for CommitRef { + fn from(value: &str) -> Self { + Self(value.into()) + } +} +impl From for CommitRef { + fn from(value: Oid) -> Self { + Self(value.to_string()) + } +} +impl From> for CommitRef { + fn from(value: Object<'_>) -> Self { + value.id().into() + } +} +impl From<&Reference<'_>> for CommitRef { + fn from(value: &Reference<'_>) -> Self { + value.name().unwrap_or("").into() + } +} +impl From> for CommitRef { + fn from(value: Reference<'_>) -> Self { + (&value).into() + } +} + +#[derive(Debug)] +pub struct RepoPath(PathBuf); +impl Display for RepoPath { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.0) + } +} +impl From<&Path> for RepoPath { + fn from(value: &Path) -> Self { + Self(value.into()) + } +} +impl From<&Repository> for RepoPath { + fn from(value: &Repository) -> Self { + value.path().into() + } +} diff --git a/rust/patchable/src/main.rs b/rust/patchable/src/main.rs new file mode 100644 index 000000000..45c180842 --- /dev/null +++ b/rust/patchable/src/main.rs @@ -0,0 +1,417 @@ +mod error; +mod patch; +mod patch_mail; +mod repo; +mod utils; + +use core::str; +use std::{ + fs::File, + io::Write, + path::{Path, PathBuf}, +}; + +use git2::{Oid, Repository}; +use serde::{Deserialize, Serialize}; +use snafu::{OptionExt, ResultExt as _, Snafu}; +use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _}; + +#[derive(clap::Parser)] +struct ProductVersion { + /// The product name slug (such as druid) + product: String, + + /// The product version (such as 28.0.0) + /// + /// Should not contain a v prefix. + version: String, +} + +#[derive(Deserialize, Serialize)] +struct ProductVersionConfig { + upstream: String, + #[serde(with = "utils::oid_serde")] + base: Oid, +} + +struct ProductVersionContext<'a> { + pv: ProductVersion, + images_repo_root: &'a Path, +} + +impl ProductVersionContext<'_> { + fn load_config(&self) -> Result { + let path = &self.config_path(); + tracing::info!( + config.path = ?path, + "loading config" + ); + toml::from_str::( + &std::fs::read_to_string(path).context(LoadConfigSnafu { path })?, + ) + .context(ParseConfigSnafu { path }) + } + + /// The root directory for files related to the product (across all versions). + fn product_dir(&self) -> PathBuf { + self.images_repo_root.join(&self.pv.product) + } + + /// The directory containing patches for the product version. + fn patch_dir(&self) -> PathBuf { + self.product_dir() + .join("stackable/patches") + .join(&self.pv.version) + } + + /// The patchable configuration file for the product version. + fn config_path(&self) -> PathBuf { + self.patch_dir().join("patchable.toml") + } + + /// The directory containing all ephemeral data used by patchable for the product (across all versions). + /// + /// Should be gitignored, and can safely be deleted as long as all relevant versions have been `patchable export`ed. + fn work_root(&self) -> PathBuf { + self.product_dir().join("patchable-work") + } + + /// The repository for the product (across all versions). + fn product_repo(&self) -> PathBuf { + self.work_root().join("product-repo") + } + + /// The worktree root for the product version. + fn worktree_root(&self) -> PathBuf { + self.work_root().join("worktree").join(&self.pv.version) + } + + /// Branch pointing at the upstream base commit for the product version. + fn base_branch(&self) -> String { + format!("patchable/base/{}", self.pv.version) + } + + /// branch pointing at the last commit in the patch series for the product version. + fn worktree_branch(&self) -> String { + format!("patchable/{}", self.pv.version) + } +} + +/// Patchable is a tool for managing patches for the third-party products distributed by Stackable. +#[derive(clap::Parser)] +#[clap( + // Encourage people to let cargo decide the current version + bin_name = "cargo patchable", +)] +struct Opts { + #[clap(subcommand)] + cmd: Cmd, +} + +#[derive(clap::Parser)] +enum Cmd { + /// Check out a patched source tree to docker-images//patchable-work/worktree/ + /// + /// The patches will be pulled from docker-images//stackable/patches/ + /// + /// The source tree will be overwritten if it already exists (equivalent to `git switch`). + Checkout { + #[clap(flatten)] + pv: ProductVersion, + + /// Check out the base commit, without applying patches + #[clap(long)] + base_only: bool, + }, + + /// Export the patches from the source tree at docker-images//patchable-work/worktree/ + /// + /// The patches will be saved to docker-images//stackable/patches/ + Export { + #[clap(flatten)] + pv: ProductVersion, + }, + + /// Creates a patchable.toml for a given product version + Init { + #[clap(flatten)] + pv: ProductVersion, + + /// The upstream URL (such as https://github.com/apache/druid.git) + #[clap(long)] + upstream: String, + + /// The upstream commit-ish (such as druid-28.0.0) that the patch series applies to + /// + /// Refs (such as tags and branches) will be resolved to commit IDs. + #[clap(long)] + base: String, + }, +} + +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("failed to configure git logging"))] + ConfigureGitLogging { source: git2::Error }, + + #[snafu(display("failed to load config from {path:?}"))] + LoadConfig { + source: std::io::Error, + path: PathBuf, + }, + #[snafu(display("failed to parse config from {path:?}"))] + ParseConfig { + source: toml::de::Error, + path: PathBuf, + }, + #[snafu(display("failed to serialize config"))] + SerializeConfig { source: toml::ser::Error }, + #[snafu(display("failed to create patch dir at {path:?}"))] + CreatePatchDir { + source: std::io::Error, + path: PathBuf, + }, + #[snafu(display("failed to save config to {path:?}"))] + SaveConfig { + source: std::io::Error, + path: PathBuf, + }, + + #[snafu(display("failed to find images repository"))] + FindImagesRepo { source: git2::Error }, + #[snafu(display("images repository has no work directory"))] + NoImagesRepoWorkdir, + + #[snafu(display("failed to fetch patch series' base commit"))] + FetchBaseCommit { source: repo::Error }, + #[snafu(display("failed to apply patch series"))] + ApplyPatches { source: patch::Error }, + + #[snafu(display("failed to open product repository"))] + OpenProductRepoForCheckout { source: repo::Error }, + #[snafu(display("failed to checkout product worktree"))] + CheckoutProductWorktree { source: repo::Error }, + + #[snafu(display("failed to open product repository at {path:?}"))] + OpenProductRepo { source: git2::Error, path: PathBuf }, + #[snafu(display("failed to find head commit in repository {repo}"))] + FindHeadCommit { + source: git2::Error, + repo: error::RepoPath, + }, + #[snafu(display("failed to canonicalize history between {base}..{leaf}"))] + CanonicalizeHistory { + source: patch::Error, + base: error::CommitId, + leaf: error::CommitId, + }, + #[snafu(display( + "failed to format patches between {base}..{leaf} (canonicalized from {original_leaf})" + ))] + FormatPatches { + source: patch::Error, + base: error::CommitId, + leaf: error::CommitId, + original_leaf: error::CommitId, + }, +} +type Result = std::result::Result; + +#[snafu::report] +fn main() -> Result<()> { + tracing_subscriber::registry() + .with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr)) + .with( + tracing_subscriber::EnvFilter::builder() + .with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into()) + .from_env_lossy(), + ) + .init(); + git2::trace_set(git2::TraceLevel::Trace, |level, msg| { + let msg = String::from_utf8_lossy(msg); + match level { + git2::TraceLevel::None | git2::TraceLevel::Fatal | git2::TraceLevel::Error => { + tracing::error!(target: "git", "{msg}") + } + git2::TraceLevel::Warn => tracing::warn!(target: "git", "{msg}"), + git2::TraceLevel::Info => tracing::info!(target: "git", "{msg}"), + git2::TraceLevel::Debug => tracing::debug!(target: "git", "{msg}"), + git2::TraceLevel::Trace => tracing::trace!(target: "git", "{msg}"), + } + }) + .context(ConfigureGitLoggingSnafu)?; + + let opts = ::parse(); + let images_repo = Repository::discover(".").context(FindImagesRepoSnafu)?; + let images_repo_root = images_repo.workdir().context(NoImagesRepoWorkdirSnafu)?; + match opts.cmd { + Cmd::Checkout { pv, base_only } => { + let ctx = ProductVersionContext { + pv, + images_repo_root, + }; + let config = ctx.load_config()?; + let product_repo_root = ctx.product_repo(); + let product_repo = repo::ensure_bare_repo(&product_repo_root) + .context(OpenProductRepoForCheckoutSnafu)?; + + let base_commit = repo::resolve_and_fetch_commitish( + &product_repo, + &config.base.to_string(), + &config.upstream, + ) + .context(FetchBaseCommitSnafu)?; + let base_branch = ctx.base_branch(); + let base_branch = match product_repo + .find_commit(base_commit) + .and_then(|base| product_repo.branch(&base_branch, &base, true)) + { + Ok(_) => { + tracing::info!( + branch.base = base_branch, + branch.base.commit = %base_commit, + "updated base branch" + ); + Some(base_branch) + } + Err(err) => { + tracing::warn!( + error = &err as &dyn std::error::Error, + branch.base = base_branch, + branch.base.commit = %base_commit, + "failed to update base branch reference, ignoring..." + ); + None + } + }; + let patched_commit = if !base_only { + patch::apply_patches(&product_repo, &ctx.patch_dir(), base_commit) + .context(ApplyPatchesSnafu)? + } else { + tracing::warn!("--base-only specified, skipping patches"); + base_commit + }; + + let product_worktree_root = ctx.worktree_root(); + let worktree_branch = ctx.worktree_branch(); + repo::ensure_worktree_is_at( + &product_repo, + &ctx.pv.version, + &product_worktree_root, + &worktree_branch, + patched_commit, + ) + .context(CheckoutProductWorktreeSnafu)?; + + tracing::info!( + worktree.root = ?product_worktree_root, + branch.worktree = worktree_branch, + branch.base = base_branch, + "worktree is ready!" + ); + + // Print directory so you can run `cd $(cargo patchable checkout ...)` + println!("{}", product_worktree_root.display()); + } + + Cmd::Export { pv } => { + let ctx = ProductVersionContext { + pv, + images_repo_root, + }; + let config = ctx.load_config()?; + + let product_worktree_root = ctx.worktree_root(); + tracing::info!( + worktree.root = ?product_worktree_root, + "opening product worktree" + ); + let product_version_repo = + Repository::open(&product_worktree_root).context(OpenProductRepoSnafu { + path: product_worktree_root, + })?; + + let base_commit = config.base; + let original_leaf_commit = product_version_repo + .head() + .and_then(|c| c.peel_to_commit()) + .context(FindHeadCommitSnafu { + repo: &product_version_repo, + })? + .id(); + let canonical_leaf_commit = patch::canonicalize_commit_history( + &product_version_repo, + base_commit, + original_leaf_commit, + ) + .context(CanonicalizeHistorySnafu { + base: base_commit, + leaf: original_leaf_commit, + })?; + + let patch_dir = ctx.patch_dir(); + patch::format_patches( + &product_version_repo, + &patch_dir, + base_commit, + canonical_leaf_commit, + ) + .context(FormatPatchesSnafu { + base: base_commit, + leaf: canonical_leaf_commit, + original_leaf: original_leaf_commit, + })?; + + tracing::info!( + patch.dir = ?patch_dir, + "worktree is exported!" + ); + } + + Cmd::Init { pv, upstream, base } => { + let ctx = ProductVersionContext { + pv, + images_repo_root, + }; + + let product_repo_root = ctx.product_repo(); + let product_repo = tracing::info_span!( + "finding product repository", + product.repository = ?product_repo_root, + ) + .in_scope(|| repo::ensure_bare_repo(&product_repo_root)) + .context(OpenProductRepoForCheckoutSnafu)?; + + // --base can be a reference, but patchable.toml should always have a resolved commit id, + // so that it cannot be changed under our feet (without us knowing so, anyway...). + tracing::info!(?base, "resolving base commit-ish"); + let base_commit = repo::resolve_and_fetch_commitish(&product_repo, &base, &upstream) + .context(FetchBaseCommitSnafu)?; + tracing::info!(?base, base.commit = ?base_commit, "resolved base commit"); + + tracing::info!("saving configuration"); + let config = ProductVersionConfig { + upstream, + base: base_commit, + }; + let config_path = ctx.config_path(); + if let Some(config_dir) = config_path.parent() { + std::fs::create_dir_all(config_dir) + .context(CreatePatchDirSnafu { path: config_dir })?; + } + let config_toml = toml::to_string_pretty(&config).context(SerializeConfigSnafu)?; + File::create_new(&config_path) + .and_then(|mut f| f.write_all(config_toml.as_bytes())) + .context(SaveConfigSnafu { path: &config_path })?; + + tracing::info!( + config.path = ?config_path, + product = ctx.pv.product, + version = ctx.pv.version, + "created configuration for product version" + ); + } + } + + Ok(()) +} diff --git a/rust/patchable/src/patch.rs b/rust/patchable/src/patch.rs new file mode 100644 index 000000000..b29504c17 --- /dev/null +++ b/rust/patchable/src/patch.rs @@ -0,0 +1,362 @@ +use std::{ + path::{Path, PathBuf}, + process::ExitStatus, +}; + +use git2::{Oid, Repository}; +use snafu::{OptionExt, ResultExt as _, Snafu}; + +use crate::{ + error::{self, CommitId}, + patch_mail::{self, mailinfo, mailsplit}, + utils::raw_git_cmd, +}; + +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("failed to open stgit series file {path:?}"))] + OpenStgitSeriesFile { + source: std::io::Error, + path: PathBuf, + }, + #[snafu(display("failed to list contents of patch directory {path:?}"))] + ListPatchDirectory { + source: std::io::Error, + path: PathBuf, + }, + + #[snafu(display("failed to split patch file {patch_file:?}"))] + Mailsplit { + source: patch_mail::Error, + patch_file: PathBuf, + }, + #[snafu(display( + "failed to split patch email file {patch_email_file:?} (from {patch_file:?})" + ))] + Mailinfo { + source: patch_mail::Error, + patch_email_file: PathBuf, + patch_file: PathBuf, + }, + #[snafu(display( + "failed to parse patch email file {patch_email_file:?} (from {patch_file:?})" + ))] + ParseMailinfo { + source: patch_mail::Error, + patch_email_file: PathBuf, + patch_file: PathBuf, + }, + + #[snafu(display("failed to find parent commit {commit}"))] + FindParentCommit { + source: git2::Error, + commit: error::CommitId, + }, + #[snafu(display("failed to read tree of parent commit {parent_commit}"))] + ReadParentCommitTree { + source: git2::Error, + parent_commit: error::CommitId, + }, + #[snafu(display("failed to apply patch {patch_email_file:?} (from {patch_file:?}) to parent commit {parent_commit}"))] + ApplyPatch { + source: git2::Error, + parent_commit: error::CommitId, + patch_email_file: PathBuf, + patch_file: PathBuf, + }, + #[snafu(display("failed to write tree for patch {patch_email_file:?} (from {patch_file:?}) applied to parent commit {parent_commit}"))] + WritePatchedTree { + source: git2::Error, + parent_commit: error::CommitId, + patch_email_file: PathBuf, + patch_file: PathBuf, + }, + #[snafu(display("failed to read patched tree {tree}"))] + ReadPatchedTree { source: git2::Error, tree: Oid }, + #[snafu(display("failed to write commit for patch {patch_email_file:?} (from {patch_file:?}) applied to parent commit {parent_commit}"))] + WriteCommit { + source: git2::Error, + parent_commit: error::CommitId, + patch_email_file: PathBuf, + patch_file: PathBuf, + }, + + #[snafu(display("failed to configure revwalk for canonicalization"))] + ConfigureCanonicalizeRevwalk { source: git2::Error }, + #[snafu(display("revwalk returned invalid object"))] + CanonicalizeRevwalkObject { source: git2::Error }, + #[snafu(display("failed to find commit {commit} from canonicalization revwalk"))] + CanonicalizeRevwalkFindCommit { + source: git2::Error, + commit: CommitId, + }, + #[snafu(display("failed to find read tree from original commit {commit}"))] + CanonicalizeReadOriginalCommitTree { + source: git2::Error, + commit: CommitId, + }, + #[snafu(display( + "failed to write canonicalized commit of {original_commit} (with canonicalized parent {parent_commit})" + ))] + CanonicalizeWriteCommit { + source: git2::Error, + parent_commit: error::CommitId, + original_commit: error::CommitId, + }, + #[snafu(display("commit {commit}'s commit message is invalid UTF-8"))] + NonUtf8CommitMessage { commit: CommitId }, + + #[snafu(display("failed to delete old patch file {path:?}"))] + DeleteOldPatch { + source: std::io::Error, + path: PathBuf, + }, + #[snafu(display("failed to run git format-mail"))] + RunFormatMail { source: std::io::Error }, + #[snafu(display("git format-mail exited with status code {status}"))] + FormatMailFailed { status: ExitStatus }, +} +type Result = std::result::Result; + +/// Lists the patches to apply in `patch_dir`, in order. +fn patch_files(patch_dir: &Path) -> Result> { + let series_file = patch_dir.join("series"); + Ok(match std::fs::read_to_string(&series_file) { + Ok(file) => { + tracing::info!( + patch.series = %series_file.display(), + "series file found, treating as stgit series" + ); + file.lines() + // Skip comment lines + .filter(|line| !line.starts_with('#')) + .map(|file_name| patch_dir.join(file_name)) + .collect::>() + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + tracing::info!( + error = &err as &dyn std::error::Error, + patch.series = %series_file.display(), + "series file not found, treating as git mailbox" + ); + let mut patch_files = patch_dir + .read_dir() + .and_then(|entries| { + entries + .filter_map(|e| { + e.map(|entry| { + let path = entry.path(); + path.extension() + .is_some_and(|ext| ext == "patch") + .then_some(path) + }) + .transpose() + }) + .collect::, _>>() + }) + .context(ListPatchDirectorySnafu { path: patch_dir })?; + patch_files.sort(); + patch_files + } + Err(err) => return Err(err).context(OpenStgitSeriesFileSnafu { path: series_file }), + }) +} + +/// Apply the patches in `patch_dir` to `base_commit`. +/// +/// Does not modify the worktree or branch(es). Use [`ensure_worktree_is_at`] to check out the commit afterwards. +/// +/// This effectively reimplements git-am, but lets us: +/// 1. Fake the committer information to match author (to ensure that we get deterministic committer IDs across machines) +/// 2. Avoid touching the worktree until all patches have been applied +/// (letting us keep any dirty files in the worktree that don't conflict with the final switcheroo, +/// even if those files are modified by some of the patches) +#[tracing::instrument(skip(repo))] +pub fn apply_patches(repo: &Repository, patch_dir: &Path, base_commit: Oid) -> Result { + tracing::info!("applying patches"); + let mut last_commit_id = base_commit; + for ref patch_file in patch_files(patch_dir)? { + tracing::info!( + patch.file = ?patch_file, + "parsing patch" + ); + for ref patch_email_file in + mailsplit(repo, patch_file).context(MailsplitSnafu { patch_file })? + { + let patch = mailinfo(repo, patch_email_file) + .context(MailinfoSnafu { + patch_email_file, + patch_file, + })? + .parse() + .context(ParseMailinfoSnafu { + patch_email_file, + patch_file, + })?; + tracing::info!( + commit.base = %last_commit_id, + commit.subject = patch.subject, + "applying patch" + ); + let parent_commit = + &repo + .find_commit(last_commit_id) + .context(FindParentCommitSnafu { + commit: last_commit_id, + })?; + let patch_tree_id = repo + .apply_to_tree( + &parent_commit + .tree() + .context(ReadParentCommitTreeSnafu { parent_commit })?, + &patch.patch, + None, + ) + .context(ApplyPatchSnafu { + parent_commit, + patch_email_file, + patch_file, + })? + .write_tree_to(repo) + .context(WritePatchedTreeSnafu { + parent_commit, + patch_email_file, + patch_file, + })?; + last_commit_id = repo + .commit( + None, + &patch.author, + &patch.author, + &patch.message, + &repo + .find_tree(patch_tree_id) + .context(ReadPatchedTreeSnafu { + tree: patch_tree_id, + })?, + &[parent_commit], + ) + .context(WriteCommitSnafu { + parent_commit, + patch_email_file, + patch_file, + })?; + tracing::info!( + commit.id = %last_commit_id, + "applied patch" + ); + } + } + Ok(last_commit_id) +} + +/// Canonicalize commits for all commits between `base_commit` (exclusive) and `leaf_commit` (inclusive). +/// +/// Does not modify the worktree. +/// +/// This should generate the same commit IDs we would have loaded in [`apply_patches`]. +/// +/// This is required to avoid commit ID churn because of author/committer mismatch +/// (generated whenever a commit is modified after the initial commit), and +/// whitespace mangling in git-format-patch. +#[tracing::instrument(skip(repo))] +pub fn canonicalize_commit_history( + repo: &Repository, + base_commit: Oid, + leaf_commit: Oid, +) -> Result { + tracing::info!("canonicalizing commit history"); + let canonicalize_revwalk = repo + .revwalk() + .and_then(|mut walk| { + walk.push(leaf_commit)?; + walk.hide(base_commit)?; + walk.set_sorting(git2::Sort::TOPOLOGICAL | git2::Sort::REVERSE)?; + Ok(walk) + }) + .context(ConfigureCanonicalizeRevwalkSnafu)?; + let mut last_canonical_commit = base_commit; + for original in canonicalize_revwalk { + let original = original.context(CanonicalizeRevwalkObjectSnafu)?; + let original = &repo + .find_commit(original) + .context(CanonicalizeRevwalkFindCommitSnafu { commit: original })?; + let author = original.author(); + last_canonical_commit = repo + .commit( + None, + &author, + &author, + original + .message() + .context(NonUtf8CommitMessageSnafu { commit: original })? + .trim(), + &original + .tree() + .context(CanonicalizeReadOriginalCommitTreeSnafu { commit: original })?, + &[&repo + .find_commit(last_canonical_commit) + .context(FindParentCommitSnafu { + commit: last_canonical_commit, + })?], + ) + .context(CanonicalizeWriteCommitSnafu { + parent_commit: last_canonical_commit, + original_commit: original, + })?; + } + tracing::info!( + leaf_commit.canonical = %last_canonical_commit, + "canonicalized commit history" + ); + Ok(last_canonical_commit) +} + +/// Formats the commits between `base_commit` (exclusive) and `leaf_commit` (inclusive) as patches in `patch_dir`. +/// +/// Deletes any existing patch files in `patch_dir`. +#[tracing::instrument(skip(repo))] +pub fn format_patches( + repo: &Repository, + patch_dir: &Path, + base_commit: Oid, + leaf_commit: Oid, +) -> Result<()> { + tracing::info!("deleting existing patch files"); + // git format-patch is happy to overwrite existing files, + // but we also want to delete removed (or renamed) patch files. + for entry in patch_dir + .read_dir() + .context(ListPatchDirectorySnafu { path: patch_dir })? + { + let path = &entry + .context(ListPatchDirectorySnafu { path: patch_dir })? + .path(); + // git format-patch emits the mailbox format, not stgit(/quilt), + // so also remove markers that make it look like that + if path.file_name().is_some_and(|x| x == "series") + || path.extension().is_some_and(|x| x == "patch") + { + std::fs::remove_file(path).context(DeleteOldPatchSnafu { path })?; + } + } + + tracing::info!("exporting commits since base"); + let status = raw_git_cmd(repo) + .arg("format-patch") + .arg(format!("{base_commit}..{leaf_commit}")) + .arg("-o") + .arg(patch_dir) + .args([ + "--keep-subject", + // By default, git includes its own version number as a suffix, which makes patches unstable across git versions + "--no-signature", + ]) + .status() + .context(RunFormatMailSnafu)?; + if !status.success() { + return FormatMailFailedSnafu { status }.fail(); + } + + Ok(()) +} diff --git a/rust/patchable/src/patch_mail.rs b/rust/patchable/src/patch_mail.rs new file mode 100644 index 000000000..6ff96d8a4 --- /dev/null +++ b/rust/patchable/src/patch_mail.rs @@ -0,0 +1,194 @@ +use std::{ + ffi::OsString, + fs::File, + num::ParseIntError, + path::{Path, PathBuf}, + process::{ExitStatus, Stdio}, + str::Utf8Error, +}; + +use git2::{Diff, Repository, Signature}; +use snafu::{OptionExt as _, ResultExt, Snafu}; +use tempfile::{tempdir, NamedTempFile}; +use time::{format_description::well_known::Rfc2822, OffsetDateTime}; + +use crate::utils::raw_git_cmd; + +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("failed to create temporary directory"))] + CreateTempDir { source: std::io::Error }, + #[snafu(display("failed to create temporary file"))] + CreateTempFile { source: std::io::Error }, + + #[snafu(display("failed to run git mailsplit"))] + RunMailsplit { source: std::io::Error }, + #[snafu(display("git mailsplit exited with status code {status}"))] + MailsplitFailed { status: ExitStatus }, + #[snafu(display("git mailsplit returned invalid UTF-8"))] + MailsplitOutput { source: Utf8Error }, + #[snafu(display("git mailsplit invalid number"))] + ParseMailsplit { source: ParseIntError }, + + #[snafu(display( + "failed to open mail file at {path:?} (should have been created by git mailsplit)" + ))] + OpenMailFile { + source: std::io::Error, + path: PathBuf, + }, + #[snafu(display("failed to run git mailinfo"))] + RunMailinfo { source: std::io::Error }, + #[snafu(display("git mailinfo exited with status code {status}"))] + MailinfoFailed { status: ExitStatus }, + #[snafu(display("git mailsplit returned invalid UTF-8"))] + MailinfoOutput { source: Utf8Error }, + #[snafu(display("failed to read message file created by git mailinfo"))] + ReadMailinfoMessage { source: std::io::Error }, + #[snafu(display("failed to read patch file created by git mailinfo"))] + ReadMailinfoPatch { source: std::io::Error }, + + #[snafu(display("malformed mail header (should be separated by \": \")"))] + MalformedMailHeader, + #[snafu(display("unknown mail header type {header:?}"))] + UnknownMailHeader { header: String }, + #[snafu(display("patch mail has no \"Author\" header"))] + NoAuthorName, + #[snafu(display("patch mail has no \"Email\" header"))] + NoAuthorEmail, + #[snafu(display("patch mail has no \"Date\" header"))] + NoDate, + #[snafu(display("patch mail has no \"Subject\" header"))] + NoSubject, + #[snafu(display("failed to parse \"Date\" header (should be RFC2822)"))] + InvalidMailDate { + #[snafu(source(from(time::error::Parse, Box::new)))] + source: Box, + date: String, + }, + #[snafu(display("failed to build commit signature from headers"))] + InvalidSignature { source: git2::Error }, + #[snafu(display("patch has invalid diff"))] + InvalidDiff { source: git2::Error }, +} +type Result = std::result::Result; + +/// Splits a series of git patch emails into individual patch emails. +pub fn mailsplit(repo: &Repository, patch_file: &Path) -> Result> { + let base_dir = tempdir().context(CreateTempDirSnafu)?; + let mailsplit = raw_git_cmd(repo) + .arg("mailsplit") + // mailsplit doesn't accept split arguments ("-o dir") + .arg({ + let mut output_arg = OsString::from("-o"); + output_arg.push(base_dir.path()); + output_arg + }) + .arg("--") + .arg(patch_file) + .stderr(Stdio::inherit()) + .output() + .context(RunMailsplitSnafu)?; + if !mailsplit.status.success() { + return MailsplitFailedSnafu { + status: mailsplit.status, + } + .fail(); + } + let mailsplit_patch_count = std::str::from_utf8(&mailsplit.stdout) + .context(MailsplitOutputSnafu)? + .trim() + .parse() + .context(ParseMailsplitSnafu)?; + Ok((1..=mailsplit_patch_count).map(move |patch_i| { + base_dir.path().join( + // Matches the format emitted by git-mailsplit + format!("{patch_i:04}"), + ) + })) +} + +pub struct Mailinfo { + headers: String, + rest_of_message: String, + patch: Vec, +} + +pub struct ParsedPatch { + pub subject: String, + pub message: String, + pub author: Signature<'static>, + pub patch: Diff<'static>, +} + +impl Mailinfo { + pub fn parse(self) -> Result { + let mut author_name = None; + let mut author_email = None; + let mut date = None; + let mut subject = None; + for patch_info_line in self.headers.lines() { + if !patch_info_line.is_empty() { + match patch_info_line + .split_once(": ") + .context(MalformedMailHeaderSnafu)? + { + ("Author", x) => author_name = Some(x), + ("Email", x) => author_email = Some(x), + ("Date", x) => date = Some(x), + ("Subject", x) => subject = Some(x), + (header, _) => return UnknownMailHeaderSnafu { header }.fail(), + } + } + } + let date = date.context(NoDateSnafu)?; + let date = OffsetDateTime::parse(date, &Rfc2822).context(InvalidMailDateSnafu { date })?; + let subject = subject.context(NoSubjectSnafu)?.trim(); + let full_msg = if self.rest_of_message.is_empty() { + subject.to_string() + } else { + format!("{}\n\n{}", subject, self.rest_of_message.trim()) + }; + Ok(ParsedPatch { + subject: subject.to_string(), + message: full_msg, + author: Signature::new( + author_name.context(NoAuthorNameSnafu)?, + author_email.context(NoAuthorEmailSnafu)?, + &git2::Time::new(date.unix_timestamp(), date.offset().whole_minutes().into()), + ) + .context(InvalidSignatureSnafu)?, + patch: Diff::from_buffer(&self.patch).context(InvalidDiffSnafu)?, + }) + } +} + +pub fn mailinfo(repo: &Repository, patch_email_file: &Path) -> Result { + let msg_file = NamedTempFile::new() + .context(CreateTempFileSnafu)? + .into_temp_path(); + let patch_file = NamedTempFile::new() + .context(CreateTempFileSnafu)? + .into_temp_path(); + let mailinfo = raw_git_cmd(repo) + .arg("mailinfo") + .args([&msg_file, &patch_file]) + .stdin(File::open(patch_email_file).context(OpenMailFileSnafu { + path: patch_email_file, + })?) + .stderr(Stdio::inherit()) + .output() + .context(RunMailinfoSnafu)?; + if !mailinfo.status.success() { + return MailinfoFailedSnafu { + status: mailinfo.status, + } + .fail(); + } + let patch_info = std::str::from_utf8(&mailinfo.stdout).context(MailinfoOutputSnafu)?; + Ok(Mailinfo { + headers: patch_info.to_string(), + rest_of_message: std::fs::read_to_string(msg_file).context(ReadMailinfoMessageSnafu)?, + patch: std::fs::read(patch_file).context(ReadMailinfoPatchSnafu)?, + }) +} diff --git a/rust/patchable/src/repo.rs b/rust/patchable/src/repo.rs new file mode 100644 index 000000000..07f926ae2 --- /dev/null +++ b/rust/patchable/src/repo.rs @@ -0,0 +1,267 @@ +use std::path::{Path, PathBuf}; + +use git2::{FetchOptions, ObjectType, Oid, Repository, RepositoryInitOptions, WorktreeAddOptions}; +use snafu::{ResultExt, Snafu}; + +use crate::error::{self, CommitRef}; + +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("failed to init repository at {path:?}"))] + Init { source: git2::Error, path: PathBuf }, + #[snafu(display("failed to open repository at {path:?}"))] + Open { source: git2::Error, path: PathBuf }, + + #[snafu(display( + "failed to create worktree branch {branch:?} pointing at {commit} in {repo}" + ))] + CreateWorktreeBranch { + source: git2::Error, + repo: error::RepoPath, + branch: String, + commit: error::CommitId, + }, + #[snafu(display("failed to create worktree parent folder at {path:?}"))] + CreateWorktreePath { + source: std::io::Error, + path: PathBuf, + }, + #[snafu(display( + "failed to create worktree {path:?} pointing at {branch} in {repo} (hint: {})", + "it may have been created but deleted in the past, try `git -C {repo} worktree prune`" + ))] + CreateWorktree { + source: git2::Error, + repo: error::RepoPath, + path: PathBuf, + branch: CommitRef, + }, + + #[snafu(display("failed to detach worktree {worktree} from {old_target} to {commit}"))] + DetachWorktree { + source: git2::Error, + worktree: error::RepoPath, + old_target: error::CommitRef, + commit: error::CommitId, + }, + #[snafu(display("failed to checkout commit {commit} to worktree {worktree}"))] + CheckoutWorktree { + source: git2::Error, + worktree: error::RepoPath, + commit: error::CommitId, + }, + #[snafu(display("failed to update worktree {worktree}'s head to {target}"))] + UpdateWorktreeHead { + source: git2::Error, + worktree: error::RepoPath, + target: error::CommitRef, + }, + + #[snafu(display("failed to find commit {commit} in {repo}"))] + FindCommit { + source: git2::Error, + repo: error::RepoPath, + commit: error::CommitRef, + }, + + #[snafu(display("failed to create remote in {repo} for {url:?}"))] + CreateRemote { + source: git2::Error, + repo: error::RepoPath, + url: String, + }, + #[snafu(display("failed to fetch refs {refs:?} from {url:?} to {repo}"))] + Fetch { + source: git2::Error, + repo: error::RepoPath, + url: String, + refs: Vec, + }, +} +type Result = std::result::Result; + +/// Open the Git repository at `path`, creating it if it doesn't already exist. +#[tracing::instrument] +pub fn ensure_bare_repo(path: &Path) -> Result { + match Repository::open(path) { + Ok(repo) => { + tracing::info!("repository found, reusing"); + Ok(repo) + } + Err(err) if err.code() == git2::ErrorCode::NotFound => { + tracing::info!( + error = &err as &dyn std::error::Error, + "repository not found, initializing" + ); + Repository::init_opts( + path, + RepositoryInitOptions::new() + .bare(true) + .external_template(false), + ) + .context(InitSnafu { path }) + } + Err(err) => Err(err).context(OpenSnafu { path }), + } +} + +/// Try to resolve and fetch `commitish` from `upstream_url`. +/// +/// As an optimization, it can skip fetching if `commitish` is a literal commit ID that exists locally. +/// +/// Returns the resolved commit ID. +#[tracing::instrument(skip(repo))] +pub fn resolve_and_fetch_commitish( + repo: &Repository, + commitish: &str, + upstream_url: &str, +) -> Result { + let oid = Oid::from_str(commitish); + let commitish_is_oid = oid.is_ok(); + let local_commit = oid.and_then(|oid| repo.find_commit(oid)); + let commit = match local_commit { + Ok(commit_obj) => { + tracing::info!("literal commit exists locally, reusing"); + Ok(commit_obj) + } + Err(err) if !commitish_is_oid || err.code() == git2::ErrorCode::NotFound => { + tracing::info!( + error = &err as &dyn std::error::Error, + "base commit not found locally, fetching from upstream" + ); + repo.remote_anonymous(upstream_url) + .context(CreateRemoteSnafu { + repo, + url: upstream_url, + })? + .fetch( + &[commitish], + Some( + FetchOptions::new() + .update_fetchhead(true) + // TODO: could be 1, CLI option maybe? + .depth(0), + ), + Some("fetching patchable base commit"), + ) + .with_context(|_| FetchSnafu { + repo, + url: upstream_url, + refs: vec![commitish.to_string()], + })?; + tracing::info!("fetched base commit"); + // FETCH_HEAD is written by Remote::fetch to be the last reference fetched + repo.revparse_single("FETCH_HEAD") + .and_then(|obj| obj.peel_to_commit()) + } + Err(err) => Err(err), + } + .context(FindCommitSnafu { + repo, + commit: commitish, + })?; + Ok(commit.id()) +} + +/// Ensure that the worktree at `worktree_root` exists and is checked out at `branch`. +/// +/// The worktree will be created if necessary, and the branch will be created or reset to `commit`. +#[tracing::instrument(skip(repo))] +pub fn ensure_worktree_is_at( + repo: &Repository, + worktree_name: &str, + worktree_root: &Path, + branch: &str, + commit: Oid, +) -> Result<()> { + tracing::info!("checking out worktree"); + match Repository::open(worktree_root) { + Ok(worktree) => { + tracing::info!("worktree found, reusing"); + let commit_obj = worktree + .find_commit(commit) + .context(FindCommitSnafu { repo, commit })?; + // We can't reset the branch if it's already checked out, so detach to the commit instead for the meantime + if let Ok(head) = worktree.head() { + tracing::info!(head.old = head.name(), "detaching worktree head"); + let head_commit = head + .peel_to_commit() + .context(FindCommitSnafu { + repo: &worktree, + commit: &head, + })? + .id(); + worktree + .set_head_detached(head_commit) + .context(DetachWorktreeSnafu { + worktree: worktree_root, + old_target: head, + commit: head_commit, + })?; + } + let branch = worktree + .branch(branch, &commit_obj, true) + .context(CreateWorktreeBranchSnafu { + repo: &worktree, + branch, + commit, + })? + .into_reference(); + let commit = branch.peel(ObjectType::Commit).context(FindCommitSnafu { + repo: &worktree, + commit: &branch, + })?; + worktree + .checkout_tree(&commit, None) + .context(CheckoutWorktreeSnafu { + worktree: &worktree, + commit: &commit, + })?; + worktree + .set_head_bytes(branch.name_bytes()) + .context(UpdateWorktreeHeadSnafu { + worktree: &worktree, + target: &branch, + })?; + Ok(()) + } + Err(err) if err.code() == git2::ErrorCode::NotFound => { + tracing::info!( + error = &err as &dyn std::error::Error, + "worktree not found, creating" + ); + if let Some(parent) = worktree_root.parent() { + std::fs::create_dir_all(parent) + .context(CreateWorktreePathSnafu { path: parent })?; + } + let worktree_ref = repo + .branch( + branch, + &repo + .find_commit(commit) + .context(FindCommitSnafu { repo, commit })?, + true, + ) + .context(CreateWorktreeBranchSnafu { + repo, + branch, + commit, + })? + .into_reference(); + repo.worktree( + worktree_name, + worktree_root, + Some(WorktreeAddOptions::new().reference(Some(&worktree_ref))), + ) + .context(CreateWorktreeSnafu { + repo, + path: worktree_root, + branch: worktree_ref, + })?; + Ok(()) + } + Err(err) => Err(err).context(OpenSnafu { + path: worktree_root, + }), + } +} diff --git a/rust/patchable/src/utils.rs b/rust/patchable/src/utils.rs new file mode 100644 index 000000000..f71557a28 --- /dev/null +++ b/rust/patchable/src/utils.rs @@ -0,0 +1,32 @@ +use std::path::Path; + +use git2::Repository; + +/// Runs a raw git command in the environment of a Git repository. +/// +/// Used for functionality that is not currently implemented by libgit2/gix. +pub fn raw_git_cmd(repo: &Repository) -> std::process::Command { + let mut cmd = std::process::Command::new("git"); + cmd.env("GIT_DIR", repo.path()); + cmd.env( + "GIT_WORK_TREE", + repo.workdir().unwrap_or(Path::new("/dev/null")), + ); + cmd +} + +/// Implements (equivalents of) the [`serde`] traits over [`git2::Oid`]. +/// +/// For use with `#[serde(with = ...)]`. +pub mod oid_serde { + use git2::Oid; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub fn serialize(value: &Oid, ser: S) -> Result { + value.to_string().serialize(ser) + } + pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result { + String::deserialize(de) + .and_then(|oid| Oid::from_str(&oid).map_err(::custom)) + } +} diff --git a/shell.nix b/shell.nix index 09a26e528..1782dff73 100644 --- a/shell.nix +++ b/shell.nix @@ -7,7 +7,15 @@ let bake = pkgs.callPackage (sources.image-tools + "/image-tools.nix") { }; in pkgs.mkShell { - packages = with pkgs; [ + packages = [ bake ]; + + buildInputs = [ + # Required for libraries to be discoverable + pkgs.pkg-config + + # Required by patchable + pkgs.openssl + ]; }