From 9a3fa573f2f471218247476d3b3ad5fde08df29d Mon Sep 17 00:00:00 2001 From: Mica White Date: Sun, 29 Mar 2026 15:48:31 -0400 Subject: Autosave daemon --- Cargo.lock | 454 +++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 5 + git-autosave.service | 10 + src/authenticate.rs | 93 +++++++++ src/bin/git-autosave-daemon.rs | 206 +++++++++++++++++++ src/bin/git-autosave.rs | 100 +-------- src/lib.rs | 16 +- 7 files changed, 763 insertions(+), 121 deletions(-) create mode 100644 git-autosave.service create mode 100644 src/authenticate.rs create mode 100644 src/bin/git-autosave-daemon.rs diff --git a/Cargo.lock b/Cargo.lock index 3e52ce9..7e6c496 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,65 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.102" @@ -19,6 +78,12 @@ dependencies = [ "terminal-prompt", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.11.0" @@ -49,6 +114,32 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "colog" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df62599ba6adc9c6c04a54278c8209125343dc4775f57b9d76c9a4287e58f2bd" +dependencies = [ + "colored", + "env_logger", + "log", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "colored" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "confy" version = "2.0.0" @@ -77,7 +168,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags", + "bitflags 2.11.0", "crossterm_winapi", "derive_more", "document-features", @@ -167,6 +258,29 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "env_filter" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -194,6 +308,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "file-id" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fc6a637b6dc58414714eddd9170ff187ecb0933d4c7024d1abbd23a3cc26e9" +dependencies = [ + "windows-sys 0.60.2", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -215,6 +338,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "fuzzy-matcher" version = "0.3.7" @@ -266,10 +398,15 @@ version = "0.1.0" dependencies = [ "anyhow", "auth-git2", + "colog", "confy", "git2", + "happylock", "hostname", "inquire", + "log", + "notify", + "notify-debouncer-full", "serde", "thiserror", "uuid", @@ -281,7 +418,7 @@ version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" dependencies = [ - "bitflags", + "bitflags 2.11.0", "libc", "libgit2-sys", "log", @@ -290,6 +427,17 @@ dependencies = [ "url", ] +[[package]] +name = "happylock" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8849f6e4c031a850e8273f3bcf4d3a4569a8be57810a29a7576ff37befd64e8" +dependencies = [ + "lock_api", + "mutants", + "parking_lot", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -451,13 +599,33 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inotify" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199" +dependencies = [ + "bitflags 2.11.0", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "inquire" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6654738b8024300cf062d04a1c13c10c8e2cea598ec1c47dc9b6641159429756" dependencies = [ - "bitflags", + "bitflags 2.11.0", "crossterm", "dyn-clone", "fuzzy-matcher", @@ -465,12 +633,42 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itoa" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jiff" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -491,6 +689,26 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -609,12 +827,64 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "mutants" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0287524726960e07b119cebd01678f852f147742ae0d925e6a520dca956126" + +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.11.0", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-debouncer-full" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02b49179cfebc9932238d04d6079912d26de0379328872846118a0fa0dbb302" +dependencies = [ + "file-id", + "log", + "notify", + "notify-types", + "walkdir", +] + +[[package]] +name = "notify-types" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags 2.11.0", +] + [[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "openssl-probe" version = "0.1.6" @@ -674,6 +944,21 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -729,7 +1014,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.11.0", ] [[package]] @@ -743,6 +1028,35 @@ dependencies = [ "thiserror", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + [[package]] name = "rustc_version" version = "0.4.1" @@ -758,7 +1072,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", @@ -771,6 +1085,15 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1036,6 +1359,12 @@ 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 = "uuid" version = "1.23.0" @@ -1053,6 +1382,16 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -1150,7 +1489,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags", + "bitflags 2.11.0", "hashbrown 0.15.5", "indexmap", "semver", @@ -1172,6 +1511,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -1190,7 +1538,16 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", ] [[package]] @@ -1208,14 +1565,31 @@ 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", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -1224,48 +1598,96 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.7.15" @@ -1336,7 +1758,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.11.0", "indexmap", "log", "serde", diff --git a/Cargo.toml b/Cargo.toml index d488b50..85de251 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,12 @@ serde = { version = "1.0.152", features = ["derive"] } uuid = { version = "1", features = ["v4"] } thiserror = "2" anyhow = "1" +notify = "8" +notify-debouncer-full = "0.7" confy = "2" +happylock = "0.5" +log = "0.4" +colog = "1" hostname = "0.4" inquire = "0.9" auth-git2 = "0.5" diff --git a/git-autosave.service b/git-autosave.service new file mode 100644 index 0000000..1db88d8 --- /dev/null +++ b/git-autosave.service @@ -0,0 +1,10 @@ +[Unit] +Description=Autosave git repositories +After=network.target + +[Service] +Type=exec +SyslogIdentifier=git-autosave +Restart=always + +ExecStart=/usr/bin/git autosave-daemon diff --git a/src/authenticate.rs b/src/authenticate.rs new file mode 100644 index 0000000..fab33a2 --- /dev/null +++ b/src/authenticate.rs @@ -0,0 +1,93 @@ +use std::path::Path; + +use auth_git2::Prompter; +use inquire::{InquireError, PasswordDisplayMode}; + +use crate::Config; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Inquirer<'a>(pub &'a Config); + +fn config_value(git_config: &git2::Config, name: &str) -> Option { + git_config + .get_entry(name) + .ok() + .and_then(|entry| entry.value().map(|entry| entry.to_string())) +} + +fn prompt_secret(message: &str) -> Result { + inquire::Password::new(message) + .without_confirmation() + .with_display_mode(PasswordDisplayMode::Masked) + .prompt() +} + +impl Prompter for Inquirer<'_> { + fn prompt_username_password( + &mut self, + url: &str, + git_config: &git2::Config, + ) -> Option<(String, String)> { + let username = self + .0 + .username_for_url(url) + .cloned() + .or_else(|| config_value(git_config, "autosave.username")); + let password = self + .0 + .password_for_url(url) + .cloned() + .or_else(|| config_value(git_config, "autosave.password")); + if let Some(username) = username + && let Some(password) = password + { + return Some((username, password)); + } + + println!("Authenticating to {url}"); + let username = inquire::prompt_text("Username:").ok()?; + let password = prompt_secret("Password:").ok()?; + + Some((username, password)) + } + + fn prompt_password( + &mut self, + username: &str, + url: &str, + git_config: &git2::Config, + ) -> Option { + let password = self + .0 + .password_for_url(url) + .cloned() + .or_else(|| config_value(git_config, "autosave.password")); + if let Some(password) = password { + return Some(password); + } + + println!("Authenticating to {url}"); + prompt_secret(&format!("Password for {username}:")).ok() + } + + fn prompt_ssh_key_passphrase( + &mut self, + private_key_path: &Path, + git_config: &git2::Config, + ) -> Option { + let password = self + .0 + .passphrase_for_key(private_key_path) + .cloned() + .or_else(|| config_value(git_config, "autosave.password")); + if let Some(password) = password { + return Some(password); + } + + prompt_secret(&format!( + "Passphrase for {}:", + private_key_path.to_string_lossy() + )) + .ok() + } +} diff --git a/src/bin/git-autosave-daemon.rs b/src/bin/git-autosave-daemon.rs new file mode 100644 index 0000000..05ead05 --- /dev/null +++ b/src/bin/git-autosave-daemon.rs @@ -0,0 +1,206 @@ +use std::{collections::HashSet, time::Duration}; + +use auth_git2::GitAuthenticator; +use git_autosave::{Config, authenticate::Inquirer, commit_autosave, push_autosaves}; +use git2::{RemoteCallbacks, Repository}; +use happylock::{Mutex, ThreadKey}; +use notify::{EventKind, INotifyWatcher, RecursiveMode}; +use notify_debouncer_full::{ + DebounceEventHandler, DebounceEventResult, DebouncedEvent, Debouncer, FileIdCache, +}; + +struct ConfigWatcher { + config: &'static Mutex, + repo_watcher: &'static mut Debouncer, +} + +struct Watcher(&'static Mutex); + +fn is_event_useful(events: &[DebouncedEvent]) -> bool { + events.iter().all(|event| { + event.kind == EventKind::Any + || event.kind == EventKind::Other + || matches!(event.kind, EventKind::Access(_)) + }) +} + +impl DebounceEventHandler for ConfigWatcher { + fn handle_event(&mut self, events: DebounceEventResult) { + let events = match events { + Ok(events) => events, + Err(errors) => { + for error in errors { + log::error!("Failed to load event: {error}"); + } + return; + } + }; + if !is_event_useful(&events) { + return; + } + + log::info!("The config was updated. Reloading..."); + let Some(key) = ThreadKey::get() else { + log::error!("Failed to acquire thread key when reloading config. This is a bug!"); + return; + }; + let config = match git_autosave::load_config() { + Ok(config) => config, + Err(e) => { + log::error!("Failed to reload autosave config: {e}"); + return; + } + }; + + self.config.scoped_lock(key, |old_config| { + let paths_to_unwatch = old_config.repositories().difference(config.repositories()); + let paths_to_watch = config.repositories().difference(old_config.repositories()); + for path in paths_to_unwatch { + if let Err(e) = self.repo_watcher.unwatch(path) { + log::error!("Error when removing path from being watched: {e}"); + } + log::info!("Removed {path:?} from list of repos to watch"); + } + for path in paths_to_watch { + if let Err(e) = self.repo_watcher.watch(path, RecursiveMode::Recursive) { + log::error!("Error when adding path to repositories to watch: {e}"); + } + log::info!("Added {path:?} from list of repos to watch"); + } + + *old_config = config; + }); + + log::info!("Successfully reloaded autosave config"); + } +} + +impl DebounceEventHandler for Watcher { + fn handle_event(&mut self, events: DebounceEventResult) { + let Some(mut key) = ThreadKey::get() else { + log::error!("Failed to acquire thread key when autosaving repository. This is a bug!"); + return; + }; + let events = match events { + Ok(events) => events, + Err(errors) => { + for error in errors { + log::error!("Failed to get update events: {error}"); + } + return; + } + }; + if !is_event_useful(&events) { + return; + } + + let mut workdirs_to_autosave = HashSet::new(); + let mut repositories_to_autosave = Vec::new(); + for path in events.iter().flat_map(|event| &event.paths) { + if path + .components() + .any(|component| component.as_os_str() == ".git") + { + // Prevent infinite loop from commits triggering autosaves + continue; + } + + let Ok(repository) = Repository::discover(path) else { + log::warn!("Skipping non-repository: {:?}", &path); + continue; + }; + match repository.is_path_ignored(path) { + Ok(true) => { + log::trace!("Skipping event for ignored path: {:?}", path); + continue; + } + Ok(false) => {} + Err(e) => { + log::error!("Failed to determine if path is ignore: {e}"); + } + } + let Some(workdir) = repository.workdir() else { + log::warn!("Skipping bare repository: {:?}", &path); + continue; + }; + if workdirs_to_autosave.contains(workdir) { + continue; + } + + log::info!("Updated path: {:?}", &path); + workdirs_to_autosave.insert(workdir.to_path_buf()); + repositories_to_autosave.push(repository); + } + self.0.scoped_lock(&mut key, |config| { + for repository in repositories_to_autosave { + let workdir = repository + .workdir() + .map(|path| path.to_string_lossy()) + .unwrap_or_default(); + log::info!("Autosaving {:?}...", workdir); + let Ok(gitconfig) = repository.config() else { + log::error!("Failed to load gitconfig for repository: {:?}", workdir); + return; + }; + let auth = GitAuthenticator::new().set_prompter(Inquirer(&*config)); + let mut callbacks = RemoteCallbacks::new(); + callbacks.credentials(auth.credentials(&gitconfig)); + + if let Err(e) = commit_autosave(&repository) { + log::error!("Failed to commit autosave: {e}"); + } + if let Err(e) = push_autosaves(&repository, callbacks) { + log::error!("Failed to push autosaves: {e}"); + } + + log::info!("Successfully autosaved {:?}", workdir); + } + }); + } +} + +fn main() -> Result<(), anyhow::Error> { + let key = ThreadKey::get().expect("Could not get ThreadKey on startup. This is a bug!"); + colog::init(); + + log::info!("Loading autosave config..."); + let config: &'static Mutex = + Box::leak(Box::new(Mutex::new(git_autosave::load_config()?))); + log::info!("Loaded autosave config"); + + log::info!("Starting repository watcher..."); + let repo_watcher = Box::leak(Box::new(notify_debouncer_full::new_debouncer( + Duration::from_secs(1), + None, + Watcher(config), + )?)); + config.scoped_lock(key, |config| { + log::info!("Adding repositories to watch..."); + for repository in config.repositories() { + if let Err(e) = repo_watcher.watch(repository, RecursiveMode::Recursive) { + log::error!("Failed to watch {repository:?}: {e}"); + } + log::info!("Added {repository:?}"); + } + }); + log::info!("Started repository watcher"); + log::info!("Starting configuration watcher..."); + notify_debouncer_full::new_debouncer( + Duration::from_secs(1), + None, + ConfigWatcher { + config, + repo_watcher, + }, + )? + .watch( + &confy::get_configuration_file_path("git-autosave", "git-autosaved")?, + RecursiveMode::NonRecursive, + )?; + log::info!("Started configuration watcher"); + + log::info!("Initializing complete. Parking..."); + loop { + std::thread::yield_now(); + } +} diff --git a/src/bin/git-autosave.rs b/src/bin/git-autosave.rs index 3df94b6..64ffd67 100644 --- a/src/bin/git-autosave.rs +++ b/src/bin/git-autosave.rs @@ -1,105 +1,15 @@ -use std::path::Path; - -use auth_git2::{GitAuthenticator, Prompter}; -use git_autosave::{Config, commit_autosave, push_autosaves}; +use auth_git2::GitAuthenticator; +use git_autosave::{Config, authenticate::Inquirer, commit_autosave, push_autosaves}; use git2::{RemoteCallbacks, Repository}; -use inquire::{InquireError, PasswordDisplayMode}; - -#[derive(Default, Debug, Clone, PartialEq, Eq)] -struct Inquirer(Config); - -fn config_value(git_config: &git2::Config, name: &str) -> Option { - git_config - .get_entry(name) - .ok() - .and_then(|entry| entry.value().map(|entry| entry.to_string())) -} - -fn prompt_secret(message: &str) -> Result { - inquire::Password::new(message) - .without_confirmation() - .with_display_mode(PasswordDisplayMode::Masked) - .prompt() -} - -impl Prompter for Inquirer { - fn prompt_username_password( - &mut self, - url: &str, - git_config: &git2::Config, - ) -> Option<(String, String)> { - let username = self - .0 - .username_for_url(url) - .cloned() - .or_else(|| config_value(git_config, "autosave.username")); - let password = self - .0 - .password_for_url(url) - .cloned() - .or_else(|| config_value(git_config, "autosave.password")); - if let Some(username) = username - && let Some(password) = password - { - return Some((username, password)); - } - - println!("Authenticating to {url}"); - let username = inquire::prompt_text("Username:").ok()?; - let password = prompt_secret("Password:").ok()?; - - Some((username, password)) - } - - fn prompt_password( - &mut self, - username: &str, - url: &str, - git_config: &git2::Config, - ) -> Option { - let password = self - .0 - .password_for_url(url) - .cloned() - .or_else(|| config_value(git_config, "autosave.password")); - if let Some(password) = password { - return Some(password); - } - - println!("Authenticating to {url}"); - prompt_secret(&format!("Password for {username}:")).ok() - } - - fn prompt_ssh_key_passphrase( - &mut self, - private_key_path: &Path, - git_config: &git2::Config, - ) -> Option { - let password = self - .0 - .passphrase_for_key(private_key_path) - .cloned() - .or_else(|| config_value(git_config, "autosave.password")); - if let Some(password) = password { - return Some(password); - } - - prompt_secret(&format!( - "Passphrase for {}:", - private_key_path.to_string_lossy() - )) - .ok() - } -} fn main() -> Result<(), anyhow::Error> { let repository = Repository::discover(".")?; let gitconfig = repository.config()?; - let mut config = git_autosave::load_config()?; + let config: &'static mut Config = Box::leak(Box::new(git_autosave::load_config()?)); if std::env::args().any(|arg| arg == "--init") { - git_autosave::init(&repository, Some(&mut config))?; - git_autosave::save_config(&config)?; + git_autosave::init(&repository, Some(config))?; + git_autosave::save_config(config)?; } let auth = GitAuthenticator::new().set_prompter(Inquirer(config)); diff --git a/src/lib.rs b/src/lib.rs index 2882039..caa69b8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,4 @@ /* - * git-init-autosave: - * - generate local repo UUID - * - add repo to autosave configuration (optionally) - * - * git-autosave: - * - convert workdir to tree - * - note hostname - * - commit workdir to `refs/autosave/{UUID}-{branch}` - * - push autosave ref to branch upstream - * * git-autosave-daemon: * - watch configuration directory * - watch configured repositories @@ -49,6 +39,8 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use uuid::Uuid; +pub mod authenticate; + #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Config { repositories: HashSet, @@ -57,6 +49,10 @@ pub struct Config { } impl Config { + pub fn repositories(&self) -> &HashSet { + &self.repositories + } + pub fn username_for_url(&self, url: &str) -> Option<&String> { self.passwords.get(url)?.0.as_ref() } -- cgit v1.2.3