summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMica White <botahamec@outlook.com>2026-03-29 15:48:31 -0400
committerMica White <botahamec@outlook.com>2026-03-29 15:48:31 -0400
commit9a3fa573f2f471218247476d3b3ad5fde08df29d (patch)
tree4be264e3d6137d69043cf111a945cd10a8f7897c
parent604d3be6dc04892cdef71935d1117855c1cc7bb4 (diff)
Autosave daemon
-rw-r--r--Cargo.lock454
-rw-r--r--Cargo.toml5
-rw-r--r--git-autosave.service10
-rw-r--r--src/authenticate.rs93
-rw-r--r--src/bin/git-autosave-daemon.rs206
-rw-r--r--src/bin/git-autosave.rs100
-rw-r--r--src/lib.rs16
7 files changed, 763 insertions, 121 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 3e52ce9..7e6c496 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,6 +3,65 @@
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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -21,6 +80,12 @@ dependencies = [
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
@@ -50,6 +115,32 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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",
@@ -168,6 +259,29 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -195,6 +309,15 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -216,6 +339,15 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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",
@@ -291,6 +428,17 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -452,12 +600,32 @@ dependencies = [
]
[[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",
@@ -466,12 +634,42 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -492,6 +690,26 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -610,12 +828,64 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -675,6 +945,21 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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]]
@@ -744,6 +1029,35 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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",
@@ -772,6 +1086,15 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1037,6 +1360,12 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1054,6 +1383,16 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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",
@@ -1173,6 +1512,15 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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]]
@@ -1225,48 +1599,96 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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<String> {
+ git_config
+ .get_entry(name)
+ .ok()
+ .and_then(|entry| entry.value().map(|entry| entry.to_string()))
+}
+
+fn prompt_secret(message: &str) -> Result<String, InquireError> {
+ 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<String> {
+ 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<String> {
+ 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<Cache: FileIdCache + 'static> {
+ config: &'static Mutex<Config>,
+ repo_watcher: &'static mut Debouncer<INotifyWatcher, Cache>,
+}
+
+struct Watcher(&'static Mutex<Config>);
+
+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<Cache: FileIdCache + Send + 'static> DebounceEventHandler for ConfigWatcher<Cache> {
+ 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<Config> =
+ 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<String> {
- git_config
- .get_entry(name)
- .ok()
- .and_then(|entry| entry.value().map(|entry| entry.to_string()))
-}
-
-fn prompt_secret(message: &str) -> Result<String, InquireError> {
- 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<String> {
- 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<String> {
- 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<PathBuf>,
@@ -57,6 +49,10 @@ pub struct Config {
}
impl Config {
+ pub fn repositories(&self) -> &HashSet<PathBuf> {
+ &self.repositories
+ }
+
pub fn username_for_url(&self, url: &str) -> Option<&String> {
self.passwords.get(url)?.0.as_ref()
}