feat(spikes): add Spike 7 desktop git push smoke test
Proves add -> commit -> push over HTTPS+PAT via libgit2 (git2), the ADR-004 fallback after gitoxide was found to lack HTTP(S) push. Host crate on stable, kept out of the xtensa firmware tree.
This commit is contained in:
19
spikes/spike7-git-push/.env.example
Normal file
19
spikes/spike7-git-push/.env.example
Normal file
@@ -0,0 +1,19 @@
|
||||
# Spike 7 (desktop) config. Copy to `.env` and `set -a; . ./.env; set +a` before
|
||||
# running against a real GitHub repo. `.env` is gitignored.
|
||||
#
|
||||
# The working copy to publish. On device this is /sd/repo; on desktop, any
|
||||
# clone. Pass as argv[1] or here.
|
||||
TW_REPO_PATH=/tmp/spike7-work
|
||||
|
||||
# HTTPS + PAT auth (ADR-005). Leave both empty for a local file:// remote —
|
||||
# no credentials are needed there. Never logged.
|
||||
TW_GH_USER=your-github-username
|
||||
TW_PAT=ghp_xxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
# Commit author (ADR: author from config). Defaults applied if unset.
|
||||
TW_AUTHOR_NAME=Typoena
|
||||
TW_AUTHOR_EMAIL=typoena@example.com
|
||||
|
||||
# Optional: force origin's URL (e.g. the HTTPS remote baked in at build time).
|
||||
# If unset, the working copy's existing `origin` is used as-is.
|
||||
# TW_REMOTE_URL=https://github.com/you/typoena-test.git
|
||||
2
spikes/spike7-git-push/.gitignore
vendored
Normal file
2
spikes/spike7-git-push/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
.env
|
||||
770
spikes/spike7-git-push/Cargo.lock
generated
Normal file
770
spikes/spike7-git-push/Cargo.lock
generated
Normal file
@@ -0,0 +1,770 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.66"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"num-traits",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "git2"
|
||||
version = "0.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
"libgit2-sys",
|
||||
"log",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"potential_utf",
|
||||
"utf8_iter",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locale_core"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"litemap",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
|
||||
dependencies = [
|
||||
"icu_collections",
|
||||
"icu_normalizer_data",
|
||||
"icu_properties",
|
||||
"icu_provider",
|
||||
"smallvec",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer_data"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
|
||||
dependencies = [
|
||||
"icu_collections",
|
||||
"icu_locale_core",
|
||||
"icu_properties_data",
|
||||
"icu_provider",
|
||||
"zerotrie",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties_data"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locale_core",
|
||||
"writeable",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerotrie",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
|
||||
dependencies = [
|
||||
"idna_adapter",
|
||||
"smallvec",
|
||||
"utf8_iter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna_adapter"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
|
||||
dependencies = [
|
||||
"icu_normalizer",
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "libgit2-sys"
|
||||
version = "0.18.5+1.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "005d6ae6eac1912906073e069f7db60b1fa98e052a68227824afe3e3a1c59ca2"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"libssh2-sys",
|
||||
"libz-sys",
|
||||
"openssl-sys",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libssh2-sys"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c04141a07bb0c0bc461cb657808764de571702a59bc5c726c400ac9a7625e3ab"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"libz-sys",
|
||||
"openssl-sys",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libz-sys"
|
||||
version = "1.1.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[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.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
|
||||
dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
||||
|
||||
[[package]]
|
||||
name = "spike7-git-push"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"git2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.4+wasi-0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
|
||||
dependencies = [
|
||||
"stable_deref_trait",
|
||||
"yoke-derive",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke-derive"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
|
||||
dependencies = [
|
||||
"zerofrom-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom-derive"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.11.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
|
||||
dependencies = [
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec-derive"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
16
spikes/spike7-git-push/Cargo.toml
Normal file
16
spikes/spike7-git-push/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "spike7-git-push"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
description = "Spike 7 (desktop half) — libgit2 add/commit/push over HTTPS+PAT"
|
||||
|
||||
[dependencies]
|
||||
# libgit2 via the vendored C build — this is the ADR-004 kill-switch path.
|
||||
# gix was the first choice but cannot push over HTTPS yet (see README), so
|
||||
# Spike 7 proves the fallback the risk table names: libgit2-sys / git2.
|
||||
git2 = "0.20"
|
||||
anyhow = "1"
|
||||
# Only for the ISO-8601 commit-message timestamp (the message *is* the time,
|
||||
# per the git module spec). On device this comes from SNTP instead.
|
||||
chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
||||
92
spikes/spike7-git-push/README.md
Normal file
92
spikes/spike7-git-push/README.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Spike 7 — git push (desktop half)
|
||||
|
||||
> Bring-up spike 7 in
|
||||
> [`../../docs/v0.1-mvp-technical.md`](../../docs/v0.1-mvp-technical.md#hardware-bring-up-order).
|
||||
> Decision context: [ADR-004](../../docs/adr.md#adr-004-git-implementation--gitoxide-gix)
|
||||
> (git impl) and [ADR-005](../../docs/adr.md#adr-005-auth--https--github-personal-access-token)
|
||||
> (auth). Full write-up:
|
||||
> [`../../docs/postmortems/2026-07-05-spike7-gix-https-push.md`](../../docs/postmortems/2026-07-05-spike7-gix-https-push.md).
|
||||
|
||||
Spike 7 proves the `add → commit → push` sequence the on-device `git` module
|
||||
will run. Per the technical doc it's **desktop-Rust first, then on device** —
|
||||
this crate is the desktop half. It is a **host** program (plain `stable`
|
||||
toolchain), deliberately kept out of the xtensa-pinned `firmware/` crate.
|
||||
|
||||
## Headline finding: the ADR-004 kill-switch fired
|
||||
|
||||
Spike 7 is the documented kill-switch for [ADR-004](../../docs/adr.md): *"if
|
||||
[gix smart-HTTP push] fails on the device, we fall back to `libgit2-sys`."*
|
||||
It fires at the **library level**, before any device work: gitoxide's own
|
||||
crate-status doc states `gix` supports push only over `file://` and `ssh://` —
|
||||
**push over HTTP(S) is not implemented** (only clone/fetch are). Since
|
||||
[ADR-005](../../docs/adr.md) fixes auth as HTTPS + PAT, `gix` cannot satisfy the
|
||||
push path today. So this spike uses the fallback the risk table names:
|
||||
`libgit2` via the [`git2`](https://docs.rs/git2) crate.
|
||||
|
||||
## What it does
|
||||
|
||||
Mirrors the v0.1 `git` module contract:
|
||||
|
||||
1. open the working copy
|
||||
2. stage with `git add --all` semantics (**deletions propagate** — needed for
|
||||
v0.5 file-delete)
|
||||
3. short-circuit when nothing is staged → "nothing to publish"
|
||||
4. commit; author from config, message = an ISO-8601 timestamp (the time *is*
|
||||
the message)
|
||||
5. push `HEAD` to `origin/<branch>` over HTTPS, PAT in the credential callback
|
||||
(**never logged**)
|
||||
6. on push rejection (remote moved): fetch + `pull --no-edit` (fast-forward or a
|
||||
clean merge), then retry the push once; merge conflicts are fatal (surfaced,
|
||||
never auto-resolved)
|
||||
|
||||
## Verified (2026-07-05)
|
||||
|
||||
Run live against a local `file://` bare remote (no credentials):
|
||||
|
||||
- **first commit + push** from an unborn `HEAD` → lands in origin ✅
|
||||
- **nothing to publish** short-circuits when the index matches `HEAD` ✅
|
||||
- **divergence**: a second clone advances origin → push rejected → `pull
|
||||
--no-edit` merges cleanly → retry push succeeds, origin gets a two-parent
|
||||
merge commit ✅
|
||||
|
||||
- **real HTTPS + PAT push to github.com** — confirmed 2026-07-05 against
|
||||
`jcalixte/typoena-test`: `committed → push accepted by remote`, the commit
|
||||
landed on GitHub. The `git2` build links `openssl-sys` for the TLS transport.
|
||||
|
||||
Still not exercised: a **non-fast-forward rejection over HTTPS** (the
|
||||
`push_update_reference` callback path) — the local `file://` transport surfaced
|
||||
rejection as a `push()` error instead, and the GitHub push above was a clean
|
||||
fast-forward. The callback path is coded for but unproven live.
|
||||
|
||||
## Not proven here (the next gate)
|
||||
|
||||
The risk moved *with* the kill-switch: **can `libgit2` cross-compile to
|
||||
`xtensa-esp32s3-espidf` against esp-idf's mbedtls?** — the exact C cross-compile
|
||||
pain gix was chosen to avoid. That is the next on-device spike, and it also
|
||||
needs PSRAM (`CONFIG_SPIRAM`) enabled and a working SD card (Spike 3) for the
|
||||
`/sd/repo` working copy. This desktop pass de-risks the API + git mechanics
|
||||
only.
|
||||
|
||||
## Run
|
||||
|
||||
Local remote — proves the mechanics, no secrets:
|
||||
|
||||
```sh
|
||||
mkdir -p /tmp/s7 && cd /tmp/s7
|
||||
git init -q --bare origin.git && git clone -q origin.git work
|
||||
echo hi > work/notes.md
|
||||
cargo run --manifest-path <this-crate>/Cargo.toml -- "$PWD/work"
|
||||
```
|
||||
|
||||
Real GitHub repo — proves HTTPS + PAT (use a throwaway repo + a `repo`-scoped
|
||||
fine-grained PAT):
|
||||
|
||||
```sh
|
||||
cp .env.example .env # fill TW_GH_USER / TW_PAT / TW_REPO_PATH
|
||||
set -a; . ./.env; set +a
|
||||
cargo run -- "$TW_REPO_PATH"
|
||||
```
|
||||
|
||||
`TW_REPO_PATH` must be a clone whose `origin` is the HTTPS URL (or set
|
||||
`TW_REMOTE_URL` to point `origin` there). The PAT is passed to libgit2's
|
||||
credential callback and never printed.
|
||||
328
spikes/spike7-git-push/src/main.rs
Normal file
328
spikes/spike7-git-push/src/main.rs
Normal file
@@ -0,0 +1,328 @@
|
||||
//! Spike 7 (desktop half) — `libgit2` add → commit → push over HTTPS + PAT.
|
||||
//!
|
||||
//! ## Why libgit2 and not gitoxide
|
||||
//!
|
||||
//! [ADR-004] chose `gix` and named Spike 7 its kill-switch: "if [gix smart-HTTP
|
||||
//! push] fails on the device, we fall back to `libgit2-sys`." Before writing a
|
||||
//! line of gix code, gitoxide's own crate-status doc settles it — gix has
|
||||
//! send-pack *plumbing* but supports push only over `file://` and `ssh://`;
|
||||
//! **push over HTTP(S) is not implemented**. Since [ADR-005] fixes auth as
|
||||
//! HTTPS + PAT, the kill-switch condition is met at the library level, so this
|
||||
//! spike proves the fallback: `libgit2` via the `git2` crate.
|
||||
//!
|
||||
//! ## What this proves (desktop)
|
||||
//!
|
||||
//! The exact sequence the on-device `git` module will run (v0.1 technical →
|
||||
//! `git` module), against a real repository:
|
||||
//!
|
||||
//! 1. open the working copy
|
||||
//! 2. stage everything — `git add --all` semantics, so deletions propagate
|
||||
//! too (matters for v0.5 file-delete → next Publish's staged set)
|
||||
//! 3. short-circuit when nothing is staged ("nothing to publish")
|
||||
//! 4. commit with the configured author, message = an ISO-8601 timestamp
|
||||
//! 5. push HEAD to `origin/<branch>` over HTTPS with a PAT in the credential
|
||||
//! callback (never logged)
|
||||
//! 6. on push rejection (remote moved): fetch + `pull --no-edit`
|
||||
//! (fast-forward or a clean merge), then retry the push once
|
||||
//!
|
||||
//! ## What it does NOT prove
|
||||
//!
|
||||
//! The real remaining risk moved with the kill-switch: **can libgit2
|
||||
//! cross-compile to `xtensa-esp32s3-espidf` against esp-idf's mbedtls?** That
|
||||
//! is the next on-device gate — the very cross-compile pain gix was picked to
|
||||
//! avoid. This desktop pass de-risks the *API and the git mechanics*; the
|
||||
//! device build is a separate spike.
|
||||
//!
|
||||
//! ## Run
|
||||
//!
|
||||
//! Local remote (no credentials — proves the mechanics end to end):
|
||||
//! cargo run -- /path/to/working-copy # origin is a file:// bare repo
|
||||
//!
|
||||
//! Real GitHub repo (proves HTTPS + PAT):
|
||||
//! set -a; . ./.env; set +a
|
||||
//! cargo run -- "$TW_REPO_PATH"
|
||||
//!
|
||||
//! [ADR-004]: ../../docs/adr.md
|
||||
//! [ADR-005]: ../../docs/adr.md
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use chrono::Utc;
|
||||
use git2::{
|
||||
build::CheckoutBuilder, Commit, Cred, CredentialType, FetchOptions, IndexAddOption,
|
||||
PushOptions, RemoteCallbacks, Repository, Signature,
|
||||
};
|
||||
|
||||
/// Runtime config, mirroring the build-time `TW_*` env the firmware bakes in.
|
||||
struct Config {
|
||||
repo_path: String,
|
||||
/// GitHub username + PAT for HTTPS basic auth. Both `None` → a local
|
||||
/// (`file://`) remote that needs no credentials.
|
||||
gh_user: Option<String>,
|
||||
pat: Option<String>,
|
||||
author_name: String,
|
||||
author_email: String,
|
||||
/// Optional override for `origin`'s URL (the HTTPS remote baked in at build
|
||||
/// time). Unset → use the working copy's existing `origin` as-is.
|
||||
remote_url: Option<String>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
fn from_env_and_args() -> Result<Self> {
|
||||
let repo_path = std::env::args()
|
||||
.nth(1)
|
||||
.or_else(|| non_empty("TW_REPO_PATH"))
|
||||
.context("no working copy: pass a path as argv[1] or set TW_REPO_PATH")?;
|
||||
Ok(Self {
|
||||
repo_path,
|
||||
gh_user: non_empty("TW_GH_USER"),
|
||||
pat: non_empty("TW_PAT"),
|
||||
author_name: non_empty("TW_AUTHOR_NAME").unwrap_or_else(|| "Typoena".into()),
|
||||
author_email: non_empty("TW_AUTHOR_EMAIL")
|
||||
.unwrap_or_else(|| "typoena@example.com".into()),
|
||||
remote_url: non_empty("TW_REMOTE_URL"),
|
||||
})
|
||||
}
|
||||
|
||||
fn signature(&self) -> Result<Signature<'static>> {
|
||||
Signature::now(&self.author_name, &self.author_email).context("building commit signature")
|
||||
}
|
||||
}
|
||||
|
||||
/// An env var, treated as absent when empty (build.rs emits "" for unset vars,
|
||||
/// so the firmware uses the same convention).
|
||||
fn non_empty(key: &str) -> Option<String> {
|
||||
std::env::var(key).ok().filter(|v| !v.is_empty())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
match run() {
|
||||
Ok(summary) => println!("✅ Spike 7 (desktop) complete — {summary}"),
|
||||
Err(e) => {
|
||||
eprintln!("❌ Spike 7 (desktop) failed: {e:?}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<String> {
|
||||
let cfg = Config::from_env_and_args()?;
|
||||
let repo = Repository::open(&cfg.repo_path)
|
||||
.with_context(|| format!("opening working copy at {}", cfg.repo_path))?;
|
||||
log(&format!("opened {}", cfg.repo_path));
|
||||
|
||||
if let Some(url) = &cfg.remote_url {
|
||||
ensure_origin(&repo, url)?;
|
||||
}
|
||||
|
||||
// Stage + commit, or short-circuit.
|
||||
let branch = match commit_if_changes(&repo, &cfg)? {
|
||||
Some(b) => b,
|
||||
None => return Ok("nothing to publish (index matches HEAD)".into()),
|
||||
};
|
||||
let refspec = format!("refs/heads/{branch}:refs/heads/{branch}");
|
||||
|
||||
// Push, with one pull-and-retry on rejection (the remote-moved case).
|
||||
match push_once(&repo, &cfg, &refspec) {
|
||||
Ok(()) => Ok(format!("pushed {branch} to origin")),
|
||||
Err(first) => {
|
||||
log(&format!(
|
||||
"push rejected ({first}); pull --no-edit then retry once"
|
||||
));
|
||||
pull_no_edit(&repo, &cfg, &branch).context("pull --no-edit after rejected push")?;
|
||||
push_once(&repo, &cfg, &refspec).context("retry push after pull")?;
|
||||
Ok(format!("pushed {branch} to origin (after pull + merge)"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Point `origin` at `url`, creating the remote if the working copy has none.
|
||||
fn ensure_origin(repo: &Repository, url: &str) -> Result<()> {
|
||||
match repo.find_remote("origin") {
|
||||
Ok(_) => repo
|
||||
.remote_set_url("origin", url)
|
||||
.context("remote_set_url origin")?,
|
||||
Err(_) => {
|
||||
repo.remote("origin", url)
|
||||
.context("creating origin remote")?;
|
||||
}
|
||||
}
|
||||
log(&format!("origin → {url}"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stage everything and commit, or return `None` if the index already matches
|
||||
/// HEAD (nothing to publish). Returns the branch shorthand on commit.
|
||||
fn commit_if_changes(repo: &Repository, cfg: &Config) -> Result<Option<String>> {
|
||||
// `add_all(["*"], …)` is libgit2's `git add --all <pathspec>`: it stages
|
||||
// new + modified + **deleted** paths, which plain `git add .` would miss on
|
||||
// removals. The git module needs this for v0.5 file-delete.
|
||||
let mut index = repo.index().context("opening index")?;
|
||||
index
|
||||
.add_all(["*"], IndexAddOption::DEFAULT, None)
|
||||
.context("staging (add --all)")?;
|
||||
index.write().context("writing index")?;
|
||||
let tree_oid = index.write_tree().context("writing tree from index")?;
|
||||
|
||||
// Parent = current HEAD, or None on an unborn branch (fresh repo).
|
||||
let parent: Option<Commit> = match repo.head() {
|
||||
Ok(h) => Some(h.peel_to_commit().context("resolving HEAD to a commit")?),
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
// Nothing to publish: index tree == HEAD tree (or empty tree, unborn repo).
|
||||
match &parent {
|
||||
Some(p) if p.tree_id() == tree_oid => return Ok(None),
|
||||
None if repo.find_tree(tree_oid)?.is_empty() => return Ok(None),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let tree = repo.find_tree(tree_oid)?;
|
||||
let sig = cfg.signature()?;
|
||||
let message = Utc::now().to_rfc3339(); // the ISO-8601 timestamp *is* the message
|
||||
let parents: Vec<&Commit> = parent.iter().collect();
|
||||
repo.commit(
|
||||
Some("HEAD"),
|
||||
&sig,
|
||||
&sig,
|
||||
&message,
|
||||
&tree,
|
||||
parents.as_slice(),
|
||||
)
|
||||
.context("committing")?;
|
||||
|
||||
let branch = repo
|
||||
.head()?
|
||||
.shorthand()
|
||||
.context("HEAD has no branch shorthand (detached?)")?
|
||||
.to_string();
|
||||
log(&format!("committed to {branch}: \"{message}\""));
|
||||
Ok(Some(branch))
|
||||
}
|
||||
|
||||
/// Push `refspec` to `origin`. Errors both on transport failure and on a
|
||||
/// server-side ref rejection (non-fast-forward) — the caller pulls and retries.
|
||||
fn push_once(repo: &Repository, cfg: &Config, refspec: &str) -> Result<()> {
|
||||
let mut remote = repo
|
||||
.find_remote("origin")
|
||||
.context("remote 'origin' not found — is the working copy a clone?")?;
|
||||
|
||||
// Server-side per-ref status arrives via this callback, NOT as a push()
|
||||
// error, so record any non-None status and treat it as a failure below.
|
||||
let rejections = Rc::new(RefCell::new(Vec::<String>::new()));
|
||||
let mut cbs = RemoteCallbacks::new();
|
||||
bind_credentials(&mut cbs, cfg);
|
||||
{
|
||||
let rejections = rejections.clone();
|
||||
cbs.push_update_reference(move |refname, status| {
|
||||
if let Some(msg) = status {
|
||||
rejections.borrow_mut().push(format!("{refname}: {msg}"));
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
let mut opts = PushOptions::new();
|
||||
opts.remote_callbacks(cbs);
|
||||
remote
|
||||
.push(&[refspec], Some(&mut opts))
|
||||
.context("push transport")?;
|
||||
|
||||
let rej = rejections.borrow();
|
||||
if !rej.is_empty() {
|
||||
bail!("remote rejected: {}", rej.join("; "));
|
||||
}
|
||||
log("push accepted by remote");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `git pull --no-edit`: fetch `origin/<branch>`, then fast-forward or make a
|
||||
/// merge commit. Bails on conflicts — the device surfaces that, never auto-
|
||||
/// resolves.
|
||||
fn pull_no_edit(repo: &Repository, cfg: &Config, branch: &str) -> Result<()> {
|
||||
let mut remote = repo.find_remote("origin")?;
|
||||
let mut cbs = RemoteCallbacks::new();
|
||||
bind_credentials(&mut cbs, cfg);
|
||||
let mut fo = FetchOptions::new();
|
||||
fo.remote_callbacks(cbs);
|
||||
remote
|
||||
.fetch(&[branch], Some(&mut fo), None)
|
||||
.context("fetch origin")?;
|
||||
|
||||
let fetch_head = repo
|
||||
.find_reference("FETCH_HEAD")
|
||||
.context("no FETCH_HEAD after fetch")?;
|
||||
let fetched = repo.reference_to_annotated_commit(&fetch_head)?;
|
||||
let (analysis, _) = repo.merge_analysis(&[&fetched])?;
|
||||
|
||||
if analysis.is_up_to_date() {
|
||||
log("already up to date with origin");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if analysis.is_fast_forward() {
|
||||
let refname = format!("refs/heads/{branch}");
|
||||
repo.find_reference(&refname)?
|
||||
.set_target(fetched.id(), "spike7: fast-forward")?;
|
||||
repo.set_head(&refname)?;
|
||||
repo.checkout_head(Some(CheckoutBuilder::new().force()))?;
|
||||
log("fast-forwarded to origin");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// True divergence → merge. Conflicts are fatal here (no auto-resolve).
|
||||
repo.merge(&[&fetched], None, None).context("merge")?;
|
||||
let mut idx = repo.index()?;
|
||||
if idx.has_conflicts() {
|
||||
repo.cleanup_state().ok();
|
||||
bail!("merge conflicts on pull — resolve manually; Publish will not auto-resolve");
|
||||
}
|
||||
let tree = repo.find_tree(idx.write_tree()?)?;
|
||||
let sig = cfg.signature()?;
|
||||
let head_commit = repo.head()?.peel_to_commit()?;
|
||||
let their_commit = repo.find_commit(fetched.id())?;
|
||||
repo.commit(
|
||||
Some("HEAD"),
|
||||
&sig,
|
||||
&sig,
|
||||
&format!("Merge origin/{branch}"),
|
||||
&tree,
|
||||
&[&head_commit, &their_commit],
|
||||
)
|
||||
.context("creating merge commit")?;
|
||||
repo.cleanup_state()?;
|
||||
log("merged origin (no conflicts)");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Wire the credential callback: HTTPS → PAT (user/pass plaintext), local
|
||||
/// remotes → default. The PAT is handed to libgit2 but never printed.
|
||||
fn bind_credentials<'a>(cbs: &mut RemoteCallbacks<'a>, cfg: &'a Config) {
|
||||
cbs.credentials(move |_url, username_from_url, allowed| {
|
||||
// GitHub over HTTPS asks for USER_PASS_PLAINTEXT; the PAT is the
|
||||
// password (any non-empty username works, we send the configured one).
|
||||
if allowed.contains(CredentialType::USER_PASS_PLAINTEXT) {
|
||||
if let (Some(user), Some(pat)) = (cfg.gh_user.as_deref(), cfg.pat.as_deref()) {
|
||||
return Cred::userpass_plaintext(user, pat);
|
||||
}
|
||||
}
|
||||
// file:// / already-authenticated transports need nothing.
|
||||
if allowed.contains(CredentialType::DEFAULT) {
|
||||
return Cred::default();
|
||||
}
|
||||
if let Some(user) = username_from_url {
|
||||
return Cred::username(user);
|
||||
}
|
||||
Err(git2::Error::from_str(
|
||||
"no usable credentials — set TW_GH_USER and TW_PAT for an HTTPS remote",
|
||||
))
|
||||
});
|
||||
}
|
||||
|
||||
/// Step log to stdout. Deliberately plain (this is a bench tool, not firmware);
|
||||
/// it never touches `cfg.pat`.
|
||||
fn log(msg: &str) {
|
||||
println!("spike7: {msg}");
|
||||
}
|
||||
Reference in New Issue
Block a user