From 1f40ae93760745ce0def45c83a3e76ee53d38a5a Mon Sep 17 00:00:00 2001 From: Mutzi Date: Fri, 20 Jan 2023 16:43:17 +0100 Subject: [PATCH] Added installer --- .gitignore | 1 + .gitlab-ci.yml | 61 +++++ .idea/.gitignore | 8 + .idea/dotfiles_loader.iml | 11 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + Cargo.lock | 531 ++++++++++++++++++++++++++++++++++++++ Cargo.toml | 15 ++ src/config.rs | 35 +++ src/main.rs | 71 +++++ src/operations.rs | 113 ++++++++ src/prompt.rs | 38 +++ src/repository.rs | 131 ++++++++++ src/utils.rs | 47 ++++ 14 files changed, 1076 insertions(+) create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 .idea/.gitignore create mode 100644 .idea/dotfiles_loader.iml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/config.rs create mode 100644 src/main.rs create mode 100644 src/operations.rs create mode 100644 src/prompt.rs create mode 100644 src/repository.rs create mode 100644 src/utils.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..8ae1ec1 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,61 @@ +stages: + - build + - release + +variables: + PACKAGE_REGISTRY_URL: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/insatller/" + +build_glibc: + stage: build + image: rust:bullseye + script: + - cargo build --release + - cp target/release/dotfiles_loader ../installer-amd64-glibc + artifacts: + paths: + - installer-amd64-glibc + +build_muslc: + stage: build + image: rust:alpine + script: + - apk add pkgconf musl-dev + - cd backend + - cargo build --release + - cp target/release/dotfiles_loader ../installer-amd64-muslc + artifacts: + paths: + - installer-amd64-muslc + + +upload_assets: + stage: release + image: curlimages/curl:latest + needs: + - job: build_glibc + artifacts: true + - job: build_muslc + artifacts: true + script: + - 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file installer-amd64-glibc "${PACKAGE_REGISTRY_URL}/dev-$CI_COMMIT_SHORT_SHA/installer-amd64-glibc"' + - 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file installer-amd64-muslc "${PACKAGE_REGISTRY_URL}/dev-$CI_COMMIT_SHORT_SHA/installer-amd64-muslc"' + +create_release: + stage: release + image: registry.gitlab.com/gitlab-org/release-cli:latest + needs: + - upload_assets + script: + - echo "running release_job" + release: # See https://docs.gitlab.com/ee/ci/yaml/#release for available properties + tag_name: 'dev-$CI_COMMIT_SHORT_SHA' + description: 'Release dev-$CI_COMMIT_SHORT_SHA' + assets: + links: + - name: 'installer-amd64-glibc' + url: '${PACKAGE_REGISTRY_URL}/dev-$CI_COMMIT_SHORT_SHA/installer-amd64-glibc' + link_type: package + - name: 'installer-amd64-muslc' + url: '${PACKAGE_REGISTRY_URL}/dev-$CI_COMMIT_SHORT_SHA/installer-amd64-muslc' + link_type: package + diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/dotfiles_loader.iml b/.idea/dotfiles_loader.iml new file mode 100644 index 0000000..c254557 --- /dev/null +++ b/.idea/dotfiles_loader.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..f75d96e --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..492b6da --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,531 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cc" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "console" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d79fbe8970a77e3e34151cc13d3b3e248aa0faaecb9f6091fa07ebefe5ad60" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys", +] + +[[package]] +name = "dialoguer" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3c796f3b0b408d9fd581611b47fa850821fcb84aa640b83a3c1a5be2d691f2" +dependencies = [ + "console", + "shell-words", + "tempfile", + "zeroize", +] + +[[package]] +name = "dotfiles_loader" +version = "0.1.0" +dependencies = [ + "dialoguer", + "fs_extra", + "git2", + "indicatif", + "serde", + "strum", + "toml", +] + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" + +[[package]] +name = "git2" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be36bc9e0546df253c0cc41fd0af34f5e92845ad8509462ec76672fac6997f5b" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "url", +] + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indicatif" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cef509aa9bc73864d6756f0d34d35504af3cf0844373afe9b8669a5b8005a729" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "jobserver" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" +dependencies = [ + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "libgit2-sys" +version = "0.14.1+1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a07fb2692bc3593bda59de45a502bb3071659f2c515e28c71e728306b038e17" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + +[[package]] +name = "libssh2-sys" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b094a36eb4b8b8c8a7b4b8ae43b2944502be3e59cd87687595cf6b0a71b3f4ca" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "pkg-config" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" + +[[package]] +name = "portable-atomic" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26f6a7b87c2e435a3241addceeeff740ff8b7e76b74c13bf9acb17fa454ea00b" + +[[package]] +name = "proc-macro2" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "rustversion" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" + +[[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0046be40136ef78dc325e0edefccf84ccddacd0afcc1ca54103fa3c61bbdab1d" + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" + +[[package]] +name = "zeroize" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..b827b33 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "dotfiles_installer" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +dialoguer = "0.10.3" +indicatif = "0.17.3" +git2 = "0.16.0" +strum = { version = "0.24.1", features = ["derive"] } +serde = { version = "1.0.152", features = ["derive"] } +toml = "0.5.10" +fs_extra = "1.2.0" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..690467d --- /dev/null +++ b/src/config.rs @@ -0,0 +1,35 @@ +use std::collections::HashMap; +use serde::Deserialize; + +#[derive(Clone, Debug, Deserialize)] +pub struct Module { + #[serde(default)] + pub ignore: Vec, + pub content: HashMap +} + +#[derive(Debug, Deserialize)] +pub struct ModToml { + pub modules: HashMap +} + +pub fn parse() -> Result { + let mut files: ModToml = toml::from_str(std::fs::read_to_string("mod.toml").unwrap().as_str()) + .map_err(|e| format!("Error while parsing files.toml:\n{}", e.to_string()))?; + + let home_path = std::env::var("HOME").unwrap(); + for (_, m) in files.modules.iter_mut() { + m.content = m.content.iter().map(|(n, p)| ( + n.clone(), + p.replace("~", home_path.as_str()) + )).collect() + } + + for (name, _path) in files.modules.iter().flat_map(|(_, m)| m.content.iter()) { + let p = std::path::Path::new(name); + if !p.exists() { + return Err(format!("Missing module entry {}", name)); + } + } + Ok(files) +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..caed001 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,71 @@ +mod prompt; +mod repository; +mod config; +mod operations; +mod utils; + +use strum::{EnumIter, IntoEnumIterator}; + +#[derive(Debug, Clone, EnumIter)] +enum MainMenu { + INSTALL, + COLLECT, + DIFF, + UPLOAD, + QUIT +} + +impl ToString for MainMenu { + fn to_string(&self) -> String { + match self { + MainMenu::INSTALL => "Pull and install files", + MainMenu::COLLECT => "Collect files", + MainMenu::DIFF => "View git diff", + MainMenu::UPLOAD => "(Commit) and push to git", + MainMenu::QUIT => "Exit" + }.to_string() + } +} + +fn main() { + let repo = match repository::open_repo() { + Ok(v) => v, + Err(err) => { + println!("Failed to open/clone repo!"); + println!("{}", err); + return; + } + }; + + if !std::path::Path::new("mod.toml").exists() { + println!("'mod.toml' doesn't exist!"); + return; + } + + let mods = match config::parse() { + Ok(v) => v, + Err(err) => { + println!("Failed to process mod.toml!"); + println!("{}", err); + return; + } + }; + + 'main_loop: loop { + let res = prompt::select(Some("What do you want to do?"), MainMenu::iter().collect()); + match res { + MainMenu::INSTALL => { + if let Err(err) = repository::pull_repo(&repo) { + println!("Failed to pull repo!"); + println!("{}", err); + return; + } + operations::install(&mods); + }, + MainMenu::COLLECT => operations::collect(&mods), + MainMenu::DIFF => { std::process::Command::new("git").arg("diff").spawn().unwrap().wait().unwrap(); }, + MainMenu::UPLOAD => operations::upload(&repo), + MainMenu::QUIT => break 'main_loop + } + } +} diff --git a/src/operations.rs b/src/operations.rs new file mode 100644 index 0000000..9defd38 --- /dev/null +++ b/src/operations.rs @@ -0,0 +1,113 @@ +use crate::config::{ModToml, Module}; +use crate::prompt::multi_select; + +#[derive(Debug)] +struct NamedModule(String, Module); +impl ToString for NamedModule { + fn to_string(&self) -> String { + self.0.clone() + } +} + +fn install_mod(m: NamedModule) { + let spinner = indicatif::ProgressBar::new_spinner().with_message(format!("Installing module {}...", m.0)); + 'content_iter: for entry in m.1.content { + let src = std::path::Path::new(entry.0.as_str()); + if src.is_file() { + if let Err(err) = std::fs::copy(src, entry.1) { + println!("Failed to copy {}:\n{}", entry.0, err); + } + spinner.tick(); + } else { + let dst = std::path::Path::new(entry.1.as_str()); + + if let Err(err) = std::fs::create_dir_all(dst) { + println!("Failed to create directory {}:\n{}", entry.1, err); + continue 'content_iter; + } + + let to_remove = match crate::utils::get_dir_content_filtered(dst, &m.1.ignore) { + Ok(v) => v, + Err(err) => { println!("Failed to get destination content:\n{}", err); continue 'content_iter; } + }; + + for file in to_remove.files { + let _ = std::fs::remove_file(file); + spinner.tick(); + } + + for dir in to_remove.directories.into_iter().rev() { + let _ = std::fs::remove_dir(dir); + spinner.tick(); + } + if let Err(err) = crate::utils::copy_dir(src, dst, &spinner) { + println!("Failed to copy directory:\n{}", err); + } + } + } +} + +fn collect_mod(m: NamedModule) { + let spinner = indicatif::ProgressBar::new_spinner().with_message(format!("Collecting module {}...", m.0)); + 'content_iter: for entry in m.1.content { + let dst = std::path::Path::new(entry.0.as_str()); + let src = std::path::Path::new(entry.1.as_str()); + if src.is_file() { + if let Err(err) = std::fs::copy(src, dst) { + println!("Failed to copy {}:\n{}", entry.1, err); + } + spinner.tick(); + } else { + let _ = std::fs::remove_dir_all(dst); + if let Err(err) = std::fs::create_dir_all(dst) { + println!("Failed to create directory {}:\n{}", entry.0, err); + continue 'content_iter; + } + + let to_copy = match crate::utils::get_dir_content_filtered(src, &m.1.ignore) { + Ok(v) => crate::utils::trim_dir_content(v, src), + Err(err) => { println!("Failed to get source content:\n{}", err); continue 'content_iter; } + }; + + for dir in to_copy.directories { + std::fs::create_dir_all(dst.join(&dir)).unwrap(); + spinner.tick(); + } + + for file in to_copy.files { + std::fs::copy(src.join(&file), dst.join(&file)).unwrap(); + spinner.tick(); + } + } + } +} + +pub fn install(config: &ModToml) { + let to_install = multi_select( + Some("Which modules do you want to install?"), + config.modules.iter().map(|v| NamedModule(v.0.clone(), v.1.clone())).collect() + ); + + for m in to_install { + install_mod(m); + } +} + +pub fn collect(config: &ModToml) { + let to_collect = multi_select( + Some("Which modules do you want to collect?"), + config.modules.iter().map(|v| NamedModule(v.0.clone(), v.1.clone())).collect() + ); + + for m in to_collect { + collect_mod(m); + } +} + +pub fn upload(repo: &git2::Repository) { + if !crate::repository::is_clean(repo) { + let msg = crate::prompt::input("Commit message"); + crate::repository::commit_changes(repo, msg); + } + crate::repository::push_repo(repo).expect("Failed to push to origin"); +} diff --git a/src/prompt.rs b/src/prompt.rs new file mode 100644 index 0000000..60f4e32 --- /dev/null +++ b/src/prompt.rs @@ -0,0 +1,38 @@ +use dialoguer::{Input, MultiSelect, Select}; + +pub fn select(title: Option<&str>, mut options: Vec) -> F + where F: ToString +{ + let mut sel = Select::new(); + sel.items(&options); + sel.default(0); + if let Some(title) = title { + sel.with_prompt(title); + } + let sel = sel.interact().expect("User didn't enter anything"); + + options.remove(sel) +} + +pub fn multi_select(title: Option<&str>, options: Vec) -> Vec + where F: ToString +{ + let mut sel = MultiSelect::new(); + sel.items(&options); + if let Some(title) = title { + sel.with_prompt(title); + } + let sel = sel.interact().expect("User didn't enter anything"); + + options.into_iter() + .enumerate() + .filter_map(|(i, v)| sel.contains(&i).then_some(v)) + .collect() +} + +pub fn input(title: &str) -> String { + Input::new() + .with_prompt(title) + .interact_text() + .expect("User didn't enter anything") +} diff --git a/src/repository.rs b/src/repository.rs new file mode 100644 index 0000000..fdc159b --- /dev/null +++ b/src/repository.rs @@ -0,0 +1,131 @@ +use std::path::Path; +use git2::Repository; + +fn generate_callbacks(use_ssh_agent: bool, pb: &mut indicatif::ProgressBar) -> git2::RemoteCallbacks { + let mut callbacks = git2::RemoteCallbacks::new(); + + if use_ssh_agent { + callbacks.credentials(|_, username, _| git2::Cred::ssh_key_from_agent(username.unwrap())); + } else { + callbacks.credentials(|_, username, _| git2::Cred::ssh_key( + username.unwrap(), + None, + Path::new(&format!("{}/.ssh/id_rsa", std::env::var("HOME").unwrap_or(String::new()))), + None + )); + } + + callbacks.transfer_progress(|progress| { + if progress.total_deltas() == 0 { + pb.set_length(progress.total_objects().max(1) as u64); + pb.set_position(progress.received_objects() as u64); + pb.set_message("Receiving objects"); + } else { + pb.set_length(progress.total_deltas().max(1) as u64); + pb.set_position(progress.total_objects() as u64); + pb.set_message("Resolving deltas"); + } + true + }); + callbacks.push_transfer_progress(|cur, tot, _| { + pb.set_length(tot.max(1) as u64); + pb.set_position(cur as u64); + }); + + callbacks +} + +fn _clone_repo(callbacks: git2::RemoteCallbacks) -> Result { + let mut fo = git2::FetchOptions::new(); + fo.remote_callbacks(callbacks); + + let mut builder = git2::build::RepoBuilder::new(); + builder.fetch_options(fo); + + builder.clone( + "git@ssh.gitlab.mattv.de:root/dotfiles.git", + Path::new(".") + ) +} + +fn clone_repo() -> Result { + println!("Cloning repo..."); + let mut pb = indicatif::ProgressBar::new(1); + pb.set_style(indicatif::ProgressStyle::with_template("{spinner} [{wide_bar}] {pos:>7}/{len:7} {msg}").unwrap().progress_chars("#>-")); + _clone_repo(generate_callbacks(true, &mut pb)) + .or_else(|_| _clone_repo(generate_callbacks(false, &mut pb))) +} + +fn _pull_repo(repo: &Repository, callbacks: git2::RemoteCallbacks) -> Result<(), git2::Error> { + let mut fo = git2::FetchOptions::new(); + fo.remote_callbacks(callbacks); + + let mut co = git2::build::CheckoutBuilder::new(); + co.force(); + + repo.find_remote("origin").unwrap().fetch(&["main"], Some(&mut fo), None)?; + + let mut commits = vec![]; + repo.fetchhead_foreach(|name, url, oid, was_merge| if was_merge { commits.push(repo.annotated_commit_from_fetchhead(name, &String::from_utf8_lossy(url), oid)); true} else { true })?; + let commits = commits.into_iter().collect::, git2::Error>>()?; + if commits.is_empty() { + panic!("Failed to get heads from origin!"); + } + + repo.merge(commits.iter().collect::>().as_slice(), None, Some(&mut co)) +} + +pub fn pull_repo(repo: &Repository) -> Result<(), git2::Error> { + println!("Pulling repo..."); + let mut pb = indicatif::ProgressBar::new(1); + pb.set_style(indicatif::ProgressStyle::with_template("{spinner} [{wide_bar}] {pos:>7}/{len:7} {msg}").unwrap().progress_chars("#>-")); + _pull_repo(repo, generate_callbacks(true, &mut pb)) + .or_else(|_| _pull_repo(repo, generate_callbacks(false, &mut pb))) +} + +fn _push_repo(repo: &Repository, callbacks: git2::RemoteCallbacks) -> Result<(), git2::Error> { + let mut pu = git2::PushOptions::new(); + pu.remote_callbacks(callbacks); + + let mut remote = repo.find_remote("origin").unwrap(); + + remote.push::<&str>(&["refs/heads/main:refs/heads/main"], Some(&mut pu)) +} + +pub fn push_repo(repo: &Repository) -> Result<(), git2::Error> { + println!("Pushing repo..."); + let mut pb = indicatif::ProgressBar::new(1); + pb.set_style(indicatif::ProgressStyle::with_template("{spinner} [{wide_bar}] {bytes}/{total_bytes} {msg}").unwrap().progress_chars("#>-")); + _push_repo(repo, generate_callbacks(true, &mut pb)) + .or_else(|_| _push_repo(repo, generate_callbacks(false, &mut pb))) +} + +pub fn open_repo() -> Result { + Repository::open(".") + .or_else(|_| clone_repo()) + .map_err(|e| e.message().to_string()) +} + +pub fn is_clean(repo: &Repository) -> bool { + let mut so = git2::StatusOptions::new(); + so.show(git2::StatusShow::IndexAndWorkdir); + so.include_untracked(true); + so.include_ignored(false); + so.include_unmodified(false); + + repo.statuses(Some(&mut so)).map_or(false, |s| s.is_empty()) +} + +pub fn commit_changes(repo: &Repository, msg: String) { + let sig = git2::Signature::now("Mutzi", "root@mattv.de").unwrap(); + + let mut index = repo.index().unwrap(); + index.add_all(["*"], git2::IndexAddOption::DEFAULT, None).unwrap(); + + let tree_id = index.write_tree().unwrap(); + let tree = repo.find_tree(tree_id).unwrap(); + let parent_id = repo.refname_to_id("HEAD").unwrap(); + let parent = repo.find_commit(parent_id).unwrap(); + + repo.commit(Some("HEAD"), &sig, &sig, msg.as_str(), &tree, &[&parent]).unwrap(); +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..594b360 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,47 @@ +use fs_extra::dir::DirContent; + +pub fn get_dir_content_filtered(p: &std::path::Path, ignored: &Vec) -> Result { + let p = p.canonicalize().map_err(|e| e.to_string())?; + let base_path_len = p.to_str().unwrap().len() + 1; + + let retain_fn = |p: &String| { + if p.len() < base_path_len { return false; } + let rel_path = p.split_at(base_path_len).1; + !ignored.iter().any(|i| rel_path.starts_with(i)) + }; + + let mut content = fs_extra::dir::get_dir_content(p).map_err(|e| e.to_string())?; + content.files.retain(retain_fn); + content.directories.retain(retain_fn); + Ok(content) +} + +pub fn trim_dir_content(mut content: DirContent, base: &std::path::Path) -> DirContent { + let base_path_len = base.canonicalize().unwrap().to_str().unwrap().len() + 1; + + let iter_fn = |p: &mut String| { + if p.len() < base_path_len { return; } + let _ = p.drain(..base_path_len).collect::>(); + }; + + content.files.iter_mut().for_each(iter_fn); + content.directories.iter_mut().for_each(iter_fn); + content +} + +pub fn copy_dir(src_dir: &std::path::Path, dst_dir: &std::path::Path, spinner: &indicatif::ProgressBar) -> Result<(), String> { + let content = src_dir.read_dir().map_err(|e| e.to_string())?; + for entry in content { + let entry = entry.map_err(|e| e.to_string())?; + let new_dst = dst_dir.join(entry.file_name()); + if entry.path().is_file() { + std::fs::copy(entry.path(), new_dst).map_err(|e| e.to_string())?; + spinner.tick(); + } else { + std::fs::create_dir_all(&new_dst).map_err(|e| e.to_string())?; + spinner.tick(); + copy_dir(&entry.path(), &new_dst, spinner)?; + } + } + Ok(()) +} \ No newline at end of file