diff --git a/Cargo.lock b/Cargo.lock index b3d2745..7374ba5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,6 +67,12 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "bitflags" version = "1.3.2" @@ -81,9 +87,9 @@ checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "cc" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e" +checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" dependencies = [ "find-msvc-tools", "jobserver", @@ -174,6 +180,17 @@ dependencies = [ "thiserror", ] +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "env_filter" version = "2.0.0" @@ -221,9 +238,18 @@ checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "find-msvc-tools" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[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 = "getrandom" @@ -246,6 +272,7 @@ dependencies = [ "libc", "libgit2-sys", "log", + "url", ] [[package]] @@ -253,12 +280,14 @@ name = "gitrs" version = "0.3.6" dependencies = [ "anyhow", + "base64", "clap", "env_logger", "git2", "home", "log", "serde", + "ssh2", "tempfile", "walkdir", "yaml_serde", @@ -266,9 +295,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -285,11 +314,114 @@ dependencies = [ "windows-sys", ] +[[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 = "indexmap" -version = "2.13.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown", @@ -367,10 +499,25 @@ checksum = "23c7391e4b9f4ffab1a624223cc1d7385ff9a678f490768add717de7ea2f4d89" dependencies = [ "cc", "libc", + "libssh2-sys", "libz-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 = "libyaml-rs" version = "0.3.0" @@ -395,6 +542,21 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.33" @@ -422,11 +584,52 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[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 = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pkg-config" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" [[package]] name = "portable-atomic" @@ -443,6 +646,15 @@ dependencies = [ "portable-atomic", ] +[[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.107" @@ -467,6 +679,15 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + [[package]] name = "regex" version = "1.13.1" @@ -524,6 +745,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.229" @@ -560,6 +787,30 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "ssh2" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c95eb3c09e378543395a3fa9796f897861862466ee331d59140ade4ea0dcfdfc" +dependencies = [ + "bitflags 2.13.1", + "libc", + "libssh2-sys", + "parking_lot", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "strsim" version = "0.11.1" @@ -588,6 +839,17 @@ dependencies = [ "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 2.0.119", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -621,12 +883,40 @@ dependencies = [ "syn 3.0.3", ] +[[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 = "utf8parse" version = "0.2.2" @@ -673,11 +963,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + [[package]] name = "yaml_serde" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6552d7b747c58f1042cb51562c172e0fad554cfc3d4965ef5e7b4bc762a086af" +checksum = "ad7a8266fc7ce9d77db272ca5b40b704a1753e2a2e2ab7b1f2da22672941bf93" dependencies = [ "indexmap", "itoa", @@ -685,3 +981,80 @@ dependencies = [ "ryu", "serde", ] + +[[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 2.0.119", + "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 2.0.119", + "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 2.0.119", +] diff --git a/Cargo.toml b/Cargo.toml index 41d2200..5a887b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,6 @@ repository = "https://github.com/mccurdyc/gitrs" license = "MIT" keywords = ["git", "repositories", "declarative"] edition = "2024" -proc-macro = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -17,10 +16,12 @@ proc-macro = true anyhow = "1.0.104" clap = { version = "4.6.6", features = ["derive"] } env_logger = "0.11.11" -git2 = "0.21.0" +git2 = { version = "0.21.0", features = ["ssh"] } home = "0.5.12" log = { version = "0.4.33", features = ["serde"] } serde = { version = "1.0.229", features = ["derive"] } yaml_serde = "0.10.5" tempfile = "3.27.0" walkdir = "2.5.0" +base64 = "0.23.1" +ssh2 = "0.9.6" diff --git a/Justfile b/Justfile index 7fe5ba0..5d8f948 100644 --- a/Justfile +++ b/Justfile @@ -6,7 +6,7 @@ log := "warn" export JUST_LOG := log build: - cargo build --release --bin app + cargo build --release --bin gitrs test: cargo test -- --nocapture diff --git a/flake.lock b/flake.lock index 696d7a0..36181ef 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "crane": { "locked": { - "lastModified": 1778106249, - "narHash": "sha256-cM/AuKy5tMhwOOQIbha8ZRRMHVfNf7cv2aljIw+qoCg=", + "lastModified": 1788465171, + "narHash": "sha256-Y1/TTVXjYXGF068IThQH9fPSZ0SIE74PABlUxnWTUH0=", "owner": "ipetkov", "repo": "crane", - "rev": "6d015ea29630b7ad2402841386da2cb617a470a7", + "rev": "eb35abda9f232cc6610b1d1e3200d15c49b7ac54", "type": "github" }, "original": { @@ -36,11 +36,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1778716662, - "narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=", + "lastModified": 1788450739, + "narHash": "sha256-glZLQlzIn1fXH6PazR2iUmTo7kzzyYSshrWhLS9TqCU=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb", + "rev": "31729ca8cbdb4fa927b34e5f4353e6a83f39e993", "type": "github" }, "original": { @@ -54,11 +54,11 @@ "nixpkgs-lib": "nixpkgs-lib_2" }, "locked": { - "lastModified": 1769996383, - "narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=", + "lastModified": 1788450739, + "narHash": "sha256-glZLQlzIn1fXH6PazR2iUmTo7kzzyYSshrWhLS9TqCU=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "57928607ea566b5db3ad13af0e57e921e6b12381", + "rev": "31729ca8cbdb4fa927b34e5f4353e6a83f39e993", "type": "github" }, "original": { @@ -70,15 +70,14 @@ "git-hooks": { "inputs": { "flake-compat": "flake-compat", - "gitignore": "gitignore", "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1778507602, - "narHash": "sha256-kTwur1wV+01SdqskVMSo6JMEpg71ps3HpbFY2GsflKs=", + "lastModified": 1788267358, + "narHash": "sha256-nt+lUqYVpc9Y6JeMd2WmXzCDojasdadKo0mWcluvY2Y=", "owner": "cachix", "repo": "git-hooks.nix", - "rev": "61ab0e80d9c7ab14c256b5b453d8b3fb0189ba0a", + "rev": "27555e2624241fb116b49095df4caaee85a25691", "type": "github" }, "original": { @@ -87,27 +86,6 @@ "type": "github" } }, - "gitignore": { - "inputs": { - "nixpkgs": [ - "git-hooks", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1709087332, - "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", - "owner": "hercules-ci", - "repo": "gitignore.nix", - "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "gitignore.nix", - "type": "github" - } - }, "mccurdyc-preferences": { "inputs": { "flake-parts": "flake-parts_2", @@ -115,11 +93,11 @@ }, "locked": { "dir": "modules", - "lastModified": 1778977811, - "narHash": "sha256-fznQAHMZe/ICBAhJ0UKQhLWT8yIaCbKy3LCtebnttrc=", + "lastModified": 1787054492, + "narHash": "sha256-qTToyqrEZ3OxwtVRnsfbb7O1t0f3pLyrJrK1GBophV8=", "owner": "mccurdyc", "repo": "nix-templates", - "rev": "97975d948f9bf3fc9afe2cea060cdf1dc244ba1a", + "rev": "f5f13bfb43cc09be6b42bef37dd907a59420a160", "type": "github" }, "original": { @@ -131,11 +109,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1770073757, - "narHash": "sha256-Vy+G+F+3E/Tl+GMNgiHl9Pah2DgShmIUBJXmbiQPHbI=", + "lastModified": 1787631388, + "narHash": "sha256-vMiXptXarfSdJb1Gkc+FYVOAibuBRj7qxGa8z68q1Uw=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "47472570b1e607482890801aeaf29bfb749884f6", + "rev": "ac6b2166e7a9375683b8e98f860f273222337b16", "type": "github" }, "original": { @@ -147,11 +125,11 @@ }, "nixpkgs-lib": { "locked": { - "lastModified": 1777168982, - "narHash": "sha256-GOkGPcboWE9BmGCRMLX3worL4EMnsnG8MyKmXNeYuhQ=", + "lastModified": 1788057806, + "narHash": "sha256-DTQSMxzDWmT0zhguthvegnVkn7CFqGCv4IHCzk5ZUpM=", "owner": "nix-community", "repo": "nixpkgs.lib", - "rev": "f5901329dade4a6ea039af1433fb087bd9c1fe14", + "rev": "596e2e3940e09b2abbeb03f75fa1828c57fcd72c", "type": "github" }, "original": { @@ -162,11 +140,11 @@ }, "nixpkgs-lib_2": { "locked": { - "lastModified": 1769909678, - "narHash": "sha256-cBEymOf4/o3FD5AZnzC3J9hLbiZ+QDT/KDuyHXVJOpM=", + "lastModified": 1788057806, + "narHash": "sha256-DTQSMxzDWmT0zhguthvegnVkn7CFqGCv4IHCzk5ZUpM=", "owner": "nix-community", "repo": "nixpkgs.lib", - "rev": "72716169fe93074c333e8d0173151350670b824c", + "rev": "596e2e3940e09b2abbeb03f75fa1828c57fcd72c", "type": "github" }, "original": { @@ -177,11 +155,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1778869304, - "narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=", + "lastModified": 1789286504, + "narHash": "sha256-eiEK7cKZORNEvX0GeF3RtNEF/JXhgf2RqSp3230q13E=", "owner": "nixos", "repo": "nixpkgs", - "rev": "d233902339c02a9c334e7e593de68855ad26c4cb", + "rev": "ef34387ddd751e1ab8857adf4676492d32eb24ec", "type": "github" }, "original": { @@ -193,11 +171,11 @@ }, "nixpkgs_3": { "locked": { - "lastModified": 1778869304, - "narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=", + "lastModified": 1789286504, + "narHash": "sha256-eiEK7cKZORNEvX0GeF3RtNEF/JXhgf2RqSp3230q13E=", "owner": "nixos", "repo": "nixpkgs", - "rev": "d233902339c02a9c334e7e593de68855ad26c4cb", + "rev": "ef34387ddd751e1ab8857adf4676492d32eb24ec", "type": "github" }, "original": { @@ -208,22 +186,6 @@ } }, "nixpkgs_4": { - "locked": { - "lastModified": 1777954456, - "narHash": "sha256-hGdgeU2Nk87RAuZyYjyDjFL6LK7dAZN5RE9+hrDTkDU=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "549bd84d6279f9852cae6225e372cc67fb91a4c1", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_5": { "locked": { "lastModified": 1770107345, "narHash": "sha256-tbS0Ebx2PiA1FRW8mt8oejR0qMXmziJmPaU1d4kYY9g=", @@ -241,47 +203,27 @@ }, "root": { "inputs": { + "crane": "crane", "flake-parts": "flake-parts", "git-hooks": "git-hooks", "mccurdyc-preferences": "mccurdyc-preferences", "nixpkgs": "nixpkgs_3", - "rust-flake": "rust-flake", + "rust-overlay": "rust-overlay", "treefmt-nix": "treefmt-nix" } }, - "rust-flake": { - "inputs": { - "crane": "crane", - "nixpkgs": "nixpkgs_4", - "rust-overlay": "rust-overlay" - }, - "locked": { - "lastModified": 1778516665, - "narHash": "sha256-qA7vCofuM3cc7Mu5ynUssI4ASjE2cPSA+9rWfZ51sSA=", - "owner": "juspay", - "repo": "rust-flake", - "rev": "aa24db73f592df78094e442ee7e6d2dff9e53fcd", - "type": "github" - }, - "original": { - "owner": "juspay", - "repo": "rust-flake", - "type": "github" - } - }, "rust-overlay": { "inputs": { "nixpkgs": [ - "rust-flake", "nixpkgs" ] }, "locked": { - "lastModified": 1778296574, - "narHash": "sha256-DVmg1JCjG0VJTa+FF6tH6tkQChaxzK1EzPL98RiRWU0=", + "lastModified": 1789457514, + "narHash": "sha256-Aggle++fTyAifBy+QBPxjM+obO5iepKW/8MDxQtgGvI=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "1d634a90dffb59ec9ba52652de311b15528c1595", + "rev": "89e99bf0778a8f2cd18c9360c3f19c1ee47fc739", "type": "github" }, "original": { @@ -292,14 +234,14 @@ }, "treefmt-nix": { "inputs": { - "nixpkgs": "nixpkgs_5" + "nixpkgs": "nixpkgs_4" }, "locked": { - "lastModified": 1775636079, - "narHash": "sha256-pc20NRoMdiar8oPQceQT47UUZMBTiMdUuWrYu2obUP0=", + "lastModified": 1786901030, + "narHash": "sha256-WSFCsDSE5ffgD2MqzkM2CYjeFiKhRF/dJUN8uedb6YE=", "owner": "numtide", "repo": "treefmt-nix", - "rev": "790751ff7fd3801feeaf96d7dc416a8d581265ba", + "rev": "27b3b12a8e6375f28ebe122f07d230ca5459bbfa", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 7c9683e..9fa859a 100644 --- a/flake.nix +++ b/flake.nix @@ -4,7 +4,11 @@ git-hooks.url = "github:cachix/git-hooks.nix"; treefmt-nix.url = "github:numtide/treefmt-nix"; flake-parts.url = "github:hercules-ci/flake-parts"; - rust-flake.url = "github:juspay/rust-flake"; + crane.url = "github:ipetkov/crane"; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; mccurdyc-preferences.url = "github:mccurdyc/nix-templates?dir=modules"; }; @@ -20,12 +24,89 @@ inputs.git-hooks.flakeModule inputs.treefmt-nix.flakeModule inputs.mccurdyc-preferences.flakeModules.default - inputs.rust-flake.flakeModules.default - inputs.rust-flake.flakeModules.nixpkgs + + # Apply rust-overlay to pkgs in a separate module so the + # perSystem below can use pkgs.rust-bin without circularity. + { + perSystem = + { system, ... }: + { + _module.args.pkgs = import inputs.nixpkgs { + inherit system; + overlays = [ + inputs.rust-overlay.overlays.default + ]; + }; + }; + } ]; - perSystem = { - mccurdyc.rust.enable = true; - }; + perSystem = + { pkgs, ... }: + let + rustToolchain = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override { + extensions = [ + "rust-src" + "rust-analyzer" + "clippy" + "rustfmt" + ]; + }; + + craneLib = (inputs.crane.mkLib pkgs).overrideToolchain rustToolchain; + + src = craneLib.cleanCargoSource ./.; + + commonArgs = { + inherit src; + strictDeps = true; + nativeBuildInputs = with pkgs; [ + pkg-config + makeWrapper + ]; + buildInputs = with pkgs; [ + openssl + ]; + }; + + cargoArtifacts = craneLib.buildDepsOnly commonArgs; + in + { + mccurdyc.rust = { + enable = true; + toolchain = rustToolchain; + }; + + mccurdyc.devshell.extraPackages = with pkgs; [ + pkg-config + openssl + ]; + + # Work around mccurdyc-preferences default formatter referencing + # config.mccurdyc.pre-commit.enable without the pre-commit module + # being imported by flakeModules.default. + mccurdyc.devshell.formatter = pkgs.nixfmt; + + packages = { + app = craneLib.buildPackage (commonArgs // { inherit cargoArtifacts; }); + app-doc = craneLib.cargoDoc ( + commonArgs + // { + inherit cargoArtifacts; + RUSTDOCFLAGS = "-D warnings"; + } + ); + }; + + checks = { + app-clippy = craneLib.cargoClippy ( + commonArgs + // { + inherit cargoArtifacts; + cargoClippyExtraArgs = "--all-targets --all-features -- " + "--deny warnings"; + } + ); + }; + }; }; } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 4821ee7..0f4e567 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.93" +channel = "stable" profile = "default" components = [ "rust-src", "rustfmt", "clippy" ] diff --git a/src/config.rs b/src/config.rs index 191a881..fc094fa 100644 --- a/src/config.rs +++ b/src/config.rs @@ -58,8 +58,8 @@ impl Config { fn write(&self) -> Result<()> { info!("Writing to config file: {:?}", self.metadata.path.as_path()); // path gets moved let f = OpenOptions::new() - .write(true) .create(true) + .append(true) .open(self.metadata.path.as_path()) .context("Couldn't open file")?; Ok(yaml_serde::to_writer(f, &self)?) @@ -153,8 +153,8 @@ mod tests { got.create().expect("expected to create test.yaml"); - assert_eq!(root.path().exists(), true); - assert_eq!(root.path().join("test.yaml").exists(), true); + assert!(root.path().exists()); + assert!(root.path().join("test.yaml").exists()); got } @@ -201,7 +201,7 @@ mod tests { let r = first.add("github.com/a/a".to_string(), false); - assert_eq!(r.err().is_none(), true); + assert!(r.err().is_none()); assert_eq!(first.repos().len(), 1); assert_eq!( first.repos().to_owned(), @@ -244,7 +244,7 @@ mod tests { let mut got = create_test_cfg(&root); let r = got.add("github.com/a/a".to_string(), false); - assert_eq!(r.err().is_none(), true); + assert!(r.err().is_none()); assert_eq!(got.repos().len(), 1); assert_eq!( got.repos().to_owned(), @@ -263,7 +263,7 @@ mod tests { // Try adding duplicate let r = got.add("github.com/a/a".to_string(), false); - assert_eq!(r.err().is_none(), true); + assert!(r.err().is_none()); assert_eq!( got.repos().to_owned(), HashMap::from([( @@ -280,7 +280,7 @@ mod tests { ); let r = got.add("github.com/b/b".to_string(), false); - assert_eq!(r.err().is_none(), true); + assert!(r.err().is_none()); assert_eq!( got.repos().to_owned(), HashMap::from([ @@ -318,7 +318,7 @@ mod tests { let mut got = create_test_cfg(&root); let r = got.add("github.com/a/a".to_string(), false); - assert_eq!(r.err().is_none(), true); + assert!(r.err().is_none()); assert_eq!(got.repos().len(), 1); assert_eq!( got.repos().to_owned(), @@ -336,13 +336,13 @@ mod tests { ); let r = got.remove("github.com/a/a".to_string()); - assert_eq!(r.err().is_none(), true); + assert!(r.err().is_none()); assert_eq!(got.repos().len(), 0); assert_eq!(got.repos().to_owned(), HashMap::new()); // Try removing twice let r = got.remove("github.com/a/a".to_string()); - assert_eq!(r.err().is_none(), true); + assert!(r.err().is_none()); assert_eq!(got.repos().len(), 0); assert_eq!(got.repos().to_owned(), HashMap::new()); diff --git a/src/fs.rs b/src/fs.rs index 1f8086b..9b254a4 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -1,10 +1,13 @@ use crate::repo; use anyhow::{Result, anyhow}; +use base64::prelude::*; use git2::{Cred, RemoteCallbacks}; use home; -use log::{debug, error}; +use log::{debug, error, info}; +use ssh2::CheckResult; use std::collections::HashMap; -use std::{env, fs, path::Path, path::PathBuf}; +use std::io; +use std::{fs, path::Path, path::PathBuf}; use walkdir::WalkDir; const GITRS_ROOT_DEFAULT: &str = "src"; @@ -31,12 +34,12 @@ fn sync_with_fn( let f = d.strip_prefix(root.as_path())?; debug!("Using directory: {:?}", d); - // TODO (mccurdyc): consider fetching updates for all repos here. - if let Some(s) = f.to_str() { - if !repos.contains_key(s) { - // TODO (mccurdyc): prompt for input if there are uncommitted changes. - fs::remove_dir_all(d)?; - } + // TODO: consider fetching updates for all repos here. + if let Some(s) = f.to_str() + && !repos.contains_key(s) + { + // TODO: prompt for input if there are uncommitted changes. + fs::remove_dir_all(d)?; }; } @@ -54,38 +57,120 @@ fn sync_with_fn( Ok(()) } -// https://docs.rs/git2/latest/git2/build/struct.RepoBuilder.html +/// clone_ssh clones a git repository to a specified path. +/// +/// One thing to note is that clone_ssh does NOT respect your SSH config because +/// clone_ssh underneath uses libssh2 which does not respect your SSH config by default. +/// +// TODO: support parsing a user's ssh config. fn clone_ssh(url: &str, dst: &Path) -> Result<()> { let mut callbacks = RemoteCallbacks::new(); - callbacks.credentials(|_url, username, _allowed_types| { - let mut ssh_privkey = PathBuf::new(); - let mut ssh_privkey_pass = String::from(""); - - // default - if let Some(h) = home::home_dir() { - ssh_privkey = h.join(".ssh/id_rsa"); - } - - // default - if let Ok(pw) = env::var("SSH_PRIVKEY_PASS") { - ssh_privkey_pass = pw; + callbacks + .credentials(|_url, username, _allowed_types| Cred::ssh_key_from_agent(username.unwrap())); + + callbacks.certificate_check(|cert, hostname| { + // GitHub serves ECDSA by "default" or in higher order because it's ECDSA is more + // widely accepted by clients than ED25519 and RSA is more legacy. + + info!("verifying ssh hostkey for: {}", hostname); + + let hostkey = cert.as_hostkey(); + let raw = hostkey + // and_then defines a new Option and "flattens" the result + // it's lazy. + .and_then(|k| k.hostkey()) + .ok_or_else(|| git2::Error::from_str("issue extracting or encoding the host key"))?; + + let s = ssh2::Session::new() + .inspect_err(|e| error!("failed to create ssh session - {}", e)) + .unwrap(); + let mut known_hosts = s + .known_hosts() + .inspect_err(|e| error!("failed to create known hosts - {}", e)) + .unwrap(); + + // Load the user's known_hosts file if it exists so we can verify against + // previously-accepted keys rather than prompting every time. + let known_hosts_path = home::home_dir() + .map(|h| h.join(".ssh/known_hosts")) + .ok_or_else(|| git2::Error::from_str("failed to determine home directory"))?; + + if known_hosts_path.exists() { + known_hosts + .read_file(&known_hosts_path, ssh2::KnownHostFileKind::OpenSSH) + .map_err(|e| { + git2::Error::from_str(&format!("failed to read known_hosts: {}", e)) + })?; } - if !ssh_privkey.exists() { - ssh_privkey = PathBuf::from(env::var("SSH_PRIVKEY_PATH").expect("$HOME/.ssh/id_rsa doesn't exists, you must specify an ssh private key path via SSH_PRIVKEY_PATH")); - if !ssh_privkey.exists() { - error!("$SSH_PRIVKEY_PATH doesn't exists"); + let t = hostkey + .and_then(|k| k.hostkey_type()) + .map(|k| match k { + git2::cert::SshHostKeyType::Unknown => ssh2::KnownHostKeyFormat::Unknown, + git2::cert::SshHostKeyType::Rsa => ssh2::KnownHostKeyFormat::SshRsa, + git2::cert::SshHostKeyType::Dss => ssh2::KnownHostKeyFormat::SshDss, + git2::cert::SshHostKeyType::Ecdsa256 => ssh2::KnownHostKeyFormat::Ecdsa256, + git2::cert::SshHostKeyType::Ecdsa384 => ssh2::KnownHostKeyFormat::Ecdsa384, + git2::cert::SshHostKeyType::Ecdsa521 => ssh2::KnownHostKeyFormat::Ecdsa521, + git2::cert::SshHostKeyType::Ed255219 => ssh2::KnownHostKeyFormat::Ed25519, + _ => ssh2::KnownHostKeyFormat::Unknown, + }) + .ok_or_else(|| git2::Error::from_str("failed to get hostkey type"))?; + + let encoded = BASE64_STANDARD.encode(raw); + + match known_hosts.check(hostname, raw) { + CheckResult::Match => Ok(git2::CertificateCheckStatus::CertificateOk), + CheckResult::NotFound => { + info!( + "Host ({}) with key type {:?} and key {} not found. Is this host known? y/n", + hostname, t, encoded + ); + + let mut buffer = String::new(); + let stdin = io::stdin(); + stdin + .read_line(&mut buffer) + .map_err(|_| git2::Error::from_str("failed to read line"))?; + + if buffer.trim_end() == "y" { + known_hosts + .add(hostname, raw, "added by gitrs", t) + .map_err(|_| git2::Error::from_str("failed to add to knownhosts"))?; + known_hosts + .write_file(&known_hosts_path, ssh2::KnownHostFileKind::OpenSSH) + .map_err(|_| git2::Error::from_str("failed to write knownhosts"))?; + Ok(git2::CertificateCheckStatus::CertificateOk) + } else { + Ok(git2::CertificateCheckStatus::CertificatePassthrough) + } + } + CheckResult::Mismatch => { + info!( + "Host ({}) key mismatch (type {:?}, key {}). Is this expected? y/n", + hostname, t, encoded + ); + let mut buffer = String::new(); + let stdin = io::stdin(); + stdin + .read_line(&mut buffer) + .map_err(|_| git2::Error::from_str("failed to read line"))?; + + if buffer.trim_end() == "y" { + known_hosts + .add(hostname, raw, "added by gitrs", t) + .map_err(|_| git2::Error::from_str("failed to add to knownhosts"))?; + known_hosts + .write_file(&known_hosts_path, ssh2::KnownHostFileKind::OpenSSH) + .map_err(|_| git2::Error::from_str("failed to write knownhosts"))?; + Ok(git2::CertificateCheckStatus::CertificateOk) + } else { + Ok(git2::CertificateCheckStatus::CertificatePassthrough) + } } + CheckResult::Failure => Err(git2::Error::from_str("failed to check the known hosts")), } - - // https://libgit2.org/libgit2/#HEAD/group/credential/git_credential_ssh_key_from_agent - Cred::ssh_key( - username.unwrap(), - None, - &ssh_privkey.as_path(), - Some(ssh_privkey_pass.as_str()), - ) }); // Prepare fetch options. @@ -97,10 +182,10 @@ fn clone_ssh(url: &str, dst: &Path) -> Result<()> { builder.fetch_options(fo); // Clone the project. - debug!("Using clone url: {}", url); + info!("cloning: {}", url); match builder.clone(url, dst) { Ok(_) => Ok(()), - Err(e) => Err(anyhow!(e)), + Err(e) => Err(anyhow!("failed to clone - {}", e)), } } @@ -119,13 +204,13 @@ fn root(p: Option) -> PathBuf { // defaults to $HOME/src let h = home::home_dir().expect("couldn't get user's HOME directory"); - return h.join(PathBuf::from(GITRS_ROOT_DEFAULT)); + h.join(PathBuf::from(GITRS_ROOT_DEFAULT)) } #[cfg(test)] mod tests { use super::*; - use repo; + use std::env; use tempfile::{TempDir, tempdir}; extern crate log; use env_logger; @@ -158,7 +243,7 @@ mod tests { let p = root.path().to_path_buf(); init(Some(p.clone())).expect("init failed"); - assert_eq!(p.exists(), true); + assert!(p.exists()); cleanup(root); } @@ -177,7 +262,7 @@ mod tests { let got = init(None).expect("init failed"); - assert_eq!(want.exists(), true); + assert!(want.exists()); assert_eq!(got, want); unsafe { @@ -201,7 +286,7 @@ mod tests { // .expect("sync url failed") // .pin(false) // .sha("".to_string()) - // .to_owned(), + // .to_owned(), // )]), // &false, // ); diff --git a/src/main.rs b/src/main.rs index 7cd5098..1f999dc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Error}; use clap::{Parser, Subcommand}; +use env_logger::{Builder, Env, Target}; extern crate log; -use env_logger; use std::env; use std::path::PathBuf; @@ -13,8 +13,10 @@ pub mod repo; #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Cli { + // Directory that you want gitrs to sync to #[arg(global = true, long, value_name = "FILE")] root: Option, + // Path to your gitrs config file. #[arg(global = true, long, value_name = "FILE")] config: Option, @@ -44,7 +46,12 @@ enum Commands { } fn main() -> anyhow::Result<(), Error> { - env_logger::init(); + let env = Env::default().default_filter_or("info"); + + Builder::from_env(env) + .default_format() + .target(Target::Stdout) + .init(); let cli = Cli::parse(); run(cli) @@ -73,8 +80,7 @@ fn run(mut c: Cli) -> anyhow::Result<(), Error> { .with_context(|| format!("failed to remove repo: {}", repo))?; } Commands::Sync { clean_only } => { - fs::sync(cfg.root(), cfg.repos(), clean_only) - .with_context(|| format!("failed to sync repos"))?; + fs::sync(cfg.root(), cfg.repos(), clean_only).context("failed to sync repos")?; } } Ok(()) diff --git a/src/repo.rs b/src/repo.rs index 92f7a16..fcf3bf3 100644 --- a/src/repo.rs +++ b/src/repo.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow}; use log::debug; use serde::{Deserialize, Serialize}; @@ -10,6 +10,12 @@ pub struct Repo { sha: String, } +impl Default for Repo { + fn default() -> Self { + Self::new() + } +} + // Modeling after OpenOptions. This is so that Repo struct fields can change, but // not affect the new() constructor interface. impl Repo { @@ -96,15 +102,15 @@ mod tests { // contains ":" let got = r.url("a:a/a/a".to_string()); - assert_eq!(got.is_err(), true); + assert!(got.is_err()); // contains "@" let got = r.url("a@a/a/a".to_string()); - assert_eq!(got.is_err(), true); + assert!(got.is_err()); // <3 let got = r.url("a/a".to_string()); - assert_eq!(got.is_err(), true); + assert!(got.is_err()); } #[test]