Initial commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
624
Cargo.lock
generated
Normal file
624
Cargo.lock
generated
Normal file
@@ -0,0 +1,624 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "0.7.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atty"
|
||||||
|
version = "0.2.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
|
||||||
|
dependencies = [
|
||||||
|
"hermit-abi",
|
||||||
|
"libc",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
|
||||||
|
|
||||||
|
[[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.72"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee"
|
||||||
|
dependencies = [
|
||||||
|
"jobserver",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap"
|
||||||
|
version = "3.0.0-beta.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "feff3878564edb93745d58cf63e17b63f24142506e7a20c87a5521ed7bfb1d63"
|
||||||
|
dependencies = [
|
||||||
|
"atty",
|
||||||
|
"bitflags",
|
||||||
|
"clap_derive",
|
||||||
|
"indexmap",
|
||||||
|
"lazy_static",
|
||||||
|
"os_str_bytes",
|
||||||
|
"strsim",
|
||||||
|
"termcolor",
|
||||||
|
"textwrap",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_derive"
|
||||||
|
version = "3.0.0-beta.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b15c6b4f786ffb6192ffe65a36855bc1fc2444bcd0945ae16748dcd6ed7d0d3"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro-error",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "console"
|
||||||
|
version = "0.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a28b32d32ca44b70c3e4acd7db1babf555fa026e385fb95f18028f88848b3c31"
|
||||||
|
dependencies = [
|
||||||
|
"encode_unicode",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"regex",
|
||||||
|
"terminal_size",
|
||||||
|
"unicode-width",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs-next"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"dirs-sys-next",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs-sys-next"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"redox_users",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encode_unicode"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "form_urlencoded"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
|
||||||
|
dependencies = [
|
||||||
|
"matches",
|
||||||
|
"percent-encoding",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "git-repo-manager"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"clap",
|
||||||
|
"console",
|
||||||
|
"git2",
|
||||||
|
"regex",
|
||||||
|
"serde",
|
||||||
|
"shellexpand",
|
||||||
|
"toml",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "git2"
|
||||||
|
version = "0.13.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2a8057932925d3a9d9e4434ea016570d37420ddb1ceed45a174d577f24ed6700"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"libc",
|
||||||
|
"libgit2-sys",
|
||||||
|
"log",
|
||||||
|
"openssl-probe",
|
||||||
|
"openssl-sys",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.11.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-segmentation",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hermit-abi"
|
||||||
|
version = "0.1.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
|
||||||
|
dependencies = [
|
||||||
|
"matches",
|
||||||
|
"unicode-bidi",
|
||||||
|
"unicode-normalization",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "1.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jobserver"
|
||||||
|
version = "0.1.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa"
|
||||||
|
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.107"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fbe5e23404da5b4f555ef85ebed98fb4083e55a00c317800bc2a50ede9f3d219"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libgit2-sys"
|
||||||
|
version = "0.12.24+1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ddbd6021eef06fb289a8f54b3c2acfdd85ff2a585dfbb24b8576325373d2152c"
|
||||||
|
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.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "de5435b8549c16d423ed0c03dbaafe57cf6c3344744f1242520d59c9d8ecec66"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matches"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-probe"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-sys"
|
||||||
|
version = "0.9.70"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c6517987b3f8226b5da3661dad65ff7f300cc59fb5ea8333ca191fc65fde3edf"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "os_str_bytes"
|
||||||
|
version = "4.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "addaa943333a514159c80c97ff4a93306530d965d27e139188283cd13e06a799"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "percent-encoding"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkg-config"
|
||||||
|
version = "0.3.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-error"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-error-attr",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-error-attr"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-xid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.2.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_users"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
"redox_syscall",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.6.25"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.130"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.130"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shellexpand"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "83bdb7831b2d85ddf4a7b148aa19d0587eddbe8671a436b7bd1182eaad0f2829"
|
||||||
|
dependencies = [
|
||||||
|
"dirs-next",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strsim"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "1.0.81"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-xid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "termcolor"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "terminal_size"
|
||||||
|
version = "0.1.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "textwrap"
|
||||||
|
version = "0.14.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinyvec"
|
||||||
|
version = "1.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2"
|
||||||
|
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.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicase"
|
||||||
|
version = "2.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
|
||||||
|
dependencies = [
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-bidi"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-normalization"
|
||||||
|
version = "0.1.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
|
||||||
|
dependencies = [
|
||||||
|
"tinyvec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-segmentation"
|
||||||
|
version = "1.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-xid"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "url"
|
||||||
|
version = "2.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
|
||||||
|
dependencies = [
|
||||||
|
"form_urlencoded",
|
||||||
|
"idna",
|
||||||
|
"matches",
|
||||||
|
"percent-encoding",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vcpkg"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version_check"
|
||||||
|
version = "0.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.10.2+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
|
||||||
|
|
||||||
|
[[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-util"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
45
Cargo.toml
Normal file
45
Cargo.toml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
[package]
|
||||||
|
name = "git-repo-manager"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
authors = [
|
||||||
|
"Hannes Körber <hannes@hkoerber.de>",
|
||||||
|
]
|
||||||
|
description = """
|
||||||
|
Manage multiple git repositories.
|
||||||
|
You configure the git repositories in a file, the program does the rest!
|
||||||
|
"""
|
||||||
|
|
||||||
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "grm"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "grm"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
|
||||||
|
[dependencies.toml]
|
||||||
|
version = "0.5"
|
||||||
|
|
||||||
|
[dependencies.serde]
|
||||||
|
version = "1.0"
|
||||||
|
features = ["derive"]
|
||||||
|
|
||||||
|
[dependencies.git2]
|
||||||
|
version = "0.13"
|
||||||
|
|
||||||
|
[dependencies.shellexpand]
|
||||||
|
version = "2.1"
|
||||||
|
|
||||||
|
[dependencies.clap]
|
||||||
|
version = "3.0.0-beta.5"
|
||||||
|
|
||||||
|
[dependencies.console]
|
||||||
|
version = "0.15.0"
|
||||||
|
|
||||||
|
[dependencies.regex]
|
||||||
|
version = "1.5"
|
||||||
8
Justfile
Normal file
8
Justfile
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
lint:
|
||||||
|
cargo clippy --no-deps
|
||||||
|
|
||||||
|
lint-fix:
|
||||||
|
cargo clippy --no-deps --fix
|
||||||
|
|
||||||
|
release:
|
||||||
|
cargo build --release
|
||||||
103
README.md
Normal file
103
README.md
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# GRM — Git Repository Manager
|
||||||
|
|
||||||
|
GRM helps you manage git repositories in a declarative way. Configure your
|
||||||
|
repositories in a [TOML](https://toml.io/) file, GRM does the rest.
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
|
See [the example configuration](example.config.toml) to get a feel for the way
|
||||||
|
you configure your repositories.
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ cargo install --git https://github.com/hakoerber/git-repo-manager.git
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get the example configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ curl --proto '=https' --tlsv1.2 -sSfO https://raw.githubusercontent.com/hakoerber/git-repo-manager/master/example.config.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run it!
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ grm sync --config example.config.toml
|
||||||
|
[⚙] Cloning into "/home/me/projects/git-repo-manager" from "https://code.hkoerber.de/hannes/git-repo-manager.git"
|
||||||
|
[✔] git-repo-manager: Repository successfully cloned
|
||||||
|
[⚙] git-repo-manager: Setting up new remote "github" to "https://github.com/hakoerber/git-repo-manager.git"
|
||||||
|
[✔] git-repo-manager: OK
|
||||||
|
[⚙] Cloning into "/home/me/projects/dotfiles" from "https://github.com/hakoerber/dotfiles.git"
|
||||||
|
[✔] dotfiles: Repository successfully cloned
|
||||||
|
[✔] dotfiles: OK
|
||||||
|
```
|
||||||
|
|
||||||
|
If you run it again, it will report no changes:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ grm sync --config example.config.toml
|
||||||
|
[✔] git-repo-manager: OK
|
||||||
|
[✔] dotfiles: OK
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generate your own configuration
|
||||||
|
|
||||||
|
Now, if you already have a few repositories, it would be quite laborious to write
|
||||||
|
a configuration from scratch. Luckily, GRM has a way to generate a configuration
|
||||||
|
from an existing file tree:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ grm find ~/your/project/root > config.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
This will detect all repositories and remotes and write them to `config.toml`.
|
||||||
|
|
||||||
|
# Why?
|
||||||
|
|
||||||
|
I have a **lot** of repositories on my machines. My own stuff, forks, quick
|
||||||
|
clones of other's repositories, projects that never went anywhere ... In short,
|
||||||
|
I lost overview.
|
||||||
|
|
||||||
|
To sync these repositories between machines, I've been using Nextcloud. The thing
|
||||||
|
is, Nextcloud is not too happy about too many small files that change all the time,
|
||||||
|
like the files inside `.git`. Git also assumes that those files are updated as
|
||||||
|
atomically as possible. Nextcloud cannot guarantee that, so when I do a `git status`
|
||||||
|
during a sync, something blows up. And resolving these conflicts is just no fun ...
|
||||||
|
|
||||||
|
In the end, I think that git repos just don't belong into something like Nextcloud.
|
||||||
|
Git is already managing the content & versions, so there is no point in having
|
||||||
|
another tool do the same. But of course, setting up all those repositories from
|
||||||
|
scratch on a new machine is too much hassle. What if there was a way to clone all
|
||||||
|
those repos in a single command?
|
||||||
|
|
||||||
|
Also, I once transferred the domain of my personal git server. I updated a few
|
||||||
|
remotes manually, but I still stumble upon old, stale remotes in projects that
|
||||||
|
I haven't touched in a while. What if there was a way to update all those remotes
|
||||||
|
in once place?
|
||||||
|
|
||||||
|
This is how GRM came to be. I'm a fan of infrastructure-as-code, and GRM is a bit
|
||||||
|
like Terraform for your local git repositories. Write a config, run the tool, and
|
||||||
|
your repos are ready. The only thing that is tracked by git it the list of
|
||||||
|
repositories itself.
|
||||||
|
|
||||||
|
# Future & Ideas
|
||||||
|
|
||||||
|
* Operations over all repos (e.g. pull)
|
||||||
|
* Show status of managed repositories (dirty, compare to remotes, ...)
|
||||||
|
|
||||||
|
# Optional Features
|
||||||
|
|
||||||
|
* Support multiple file formats (YAML, JSON).
|
||||||
|
* Add systemd timer unit to run regular syncs
|
||||||
|
|
||||||
|
# Dev Notes
|
||||||
|
|
||||||
|
It requires nightly features due to the usage of [`std::path::Path::is_symlink()`](https://doc.rust-lang.org/std/fs/struct.FileType.html#method.is_symlink). See the [tracking issue](https://github.com/rust-lang/rust/issues/85748).
|
||||||
|
|
||||||
|
# Crates
|
||||||
|
|
||||||
|
* [`toml`](https://docs.rs/toml/) for the configuration file
|
||||||
|
* [`serde`](https://docs.rs/serde/) because we're using Rust, after all
|
||||||
|
* [`git2`](https://docs.rs/git2/), a safe wrapper around `libgit2`, for all git operations
|
||||||
|
* [`clap`](https://docs.rs/clap/), [`console`](https://docs.rs/console/) and [`shellexpand`](https://docs.rs/shellexpand) for good UX
|
||||||
24
example.config.toml
Normal file
24
example.config.toml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[[trees]]
|
||||||
|
root = "~/projects/"
|
||||||
|
|
||||||
|
[[trees.repos]]
|
||||||
|
name = "git-repo-manager"
|
||||||
|
|
||||||
|
[[trees.repos.remotes]]
|
||||||
|
name = "origin"
|
||||||
|
url = "https://code.hkoerber.de/hannes/git-repo-manager.git"
|
||||||
|
type = "https"
|
||||||
|
|
||||||
|
[[trees.repos.remotes]]
|
||||||
|
name = "github"
|
||||||
|
url = "https://github.com/hakoerber/git-repo-manager.git"
|
||||||
|
type = "https"
|
||||||
|
|
||||||
|
|
||||||
|
[[trees.repos]]
|
||||||
|
name = "dotfiles"
|
||||||
|
|
||||||
|
[[trees.repos.remotes]]
|
||||||
|
name = "origin"
|
||||||
|
url = "https://github.com/hakoerber/dotfiles.git"
|
||||||
|
type = "https"
|
||||||
51
src/cmd.rs
Normal file
51
src/cmd.rs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
use clap::{AppSettings, Parser};
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[clap(
|
||||||
|
name = clap::crate_name!(),
|
||||||
|
version = clap::crate_version!(),
|
||||||
|
author = clap::crate_authors!("\n"),
|
||||||
|
about = clap::crate_description!(),
|
||||||
|
long_version = clap::crate_version!(),
|
||||||
|
license = clap::crate_license!(),
|
||||||
|
setting = AppSettings::DeriveDisplayOrder,
|
||||||
|
setting = AppSettings::PropagateVersion,
|
||||||
|
setting = AppSettings::HelpRequired,
|
||||||
|
)]
|
||||||
|
pub struct Opts {
|
||||||
|
#[clap(subcommand)]
|
||||||
|
pub subcmd: SubCommand,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
pub enum SubCommand {
|
||||||
|
#[clap(
|
||||||
|
visible_alias = "run",
|
||||||
|
about = "Synchronize the repositories to the configured values"
|
||||||
|
)]
|
||||||
|
Sync(Sync),
|
||||||
|
#[clap(about = "Generate a repository configuration from an existing file tree")]
|
||||||
|
Find(Find),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[clap()]
|
||||||
|
pub struct Sync {
|
||||||
|
#[clap(
|
||||||
|
short,
|
||||||
|
long,
|
||||||
|
default_value = "./config.toml",
|
||||||
|
about = "Path to the configuration file"
|
||||||
|
)]
|
||||||
|
pub config: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
pub struct Find {
|
||||||
|
#[clap(about = "The path to search through")]
|
||||||
|
pub path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse() -> Opts {
|
||||||
|
Opts::parse()
|
||||||
|
}
|
||||||
40
src/config.rs
Normal file
40
src/config.rs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use super::repo::Repo;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct Config {
|
||||||
|
pub trees: Vec<Tree>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct Tree {
|
||||||
|
pub root: Option<String>,
|
||||||
|
pub repos: Option<Vec<Repo>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_config(path: &str) -> Result<Config, String> {
|
||||||
|
let content = match std::fs::read_to_string(&path) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
return Err(format!(
|
||||||
|
"Error reading configuration file \"{}\": {}",
|
||||||
|
path, e
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let config: Config = match toml::from_str(&content) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
return Err(format!(
|
||||||
|
"Error parsing configuration file \"{}\": {}",
|
||||||
|
path, e
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
395
src/lib.rs
Normal file
395
src/lib.rs
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process;
|
||||||
|
|
||||||
|
mod cmd;
|
||||||
|
mod config;
|
||||||
|
mod output;
|
||||||
|
mod repo;
|
||||||
|
|
||||||
|
use config::{Config, Tree};
|
||||||
|
use output::*;
|
||||||
|
|
||||||
|
use repo::{clone_repo, detect_remote_type, init_repo, open_repo, Remote, Repo};
|
||||||
|
|
||||||
|
fn path_as_string(path: &Path) -> String {
|
||||||
|
path.to_path_buf().into_os_string().into_string().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn env_home() -> PathBuf {
|
||||||
|
match std::env::var("HOME") {
|
||||||
|
Ok(path) => Path::new(&path).to_path_buf(),
|
||||||
|
Err(e) => {
|
||||||
|
print_error(&format!("Unable to read HOME: {}", e));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sync_trees(config: Config) {
|
||||||
|
for tree in config.trees {
|
||||||
|
let repos = tree.repos.unwrap_or_default();
|
||||||
|
|
||||||
|
let root_path = match &tree.root {
|
||||||
|
Some(root) => {
|
||||||
|
fn home_dir() -> Option<PathBuf> {
|
||||||
|
Some(env_home())
|
||||||
|
}
|
||||||
|
|
||||||
|
let expanded_path = match shellexpand::full_with_context(
|
||||||
|
&root,
|
||||||
|
home_dir,
|
||||||
|
|name| -> Result<Option<String>, &'static str> {
|
||||||
|
match name {
|
||||||
|
"HOME" => Ok(Some(path_as_string(home_dir().unwrap().as_path()))),
|
||||||
|
_ => Ok(None),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Ok(std::borrow::Cow::Borrowed(path)) => path.to_owned(),
|
||||||
|
Ok(std::borrow::Cow::Owned(path)) => path,
|
||||||
|
Err(e) => {
|
||||||
|
print_error(&format!("Unable to expand root: {}", e));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Path::new(&expanded_path).to_path_buf()
|
||||||
|
}
|
||||||
|
None => std::env::current_dir().unwrap(),
|
||||||
|
};
|
||||||
|
|
||||||
|
for repo in &repos {
|
||||||
|
let name = &repo.name;
|
||||||
|
|
||||||
|
let repo_path = root_path.join(&repo.name);
|
||||||
|
|
||||||
|
let mut repo_handle = None;
|
||||||
|
|
||||||
|
if repo_path.exists() {
|
||||||
|
repo_handle = Some(open_repo(&repo_path).unwrap_or_else(|error| {
|
||||||
|
print_repo_error(name, &format!("Opening repository failed: {}", error));
|
||||||
|
process::exit(1);
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
match &repo.remotes {
|
||||||
|
None => {
|
||||||
|
print_repo_action(
|
||||||
|
name,
|
||||||
|
"Repository does not have remotes configured, initializing new",
|
||||||
|
);
|
||||||
|
repo_handle = match init_repo(&repo_path) {
|
||||||
|
Ok(r) => {
|
||||||
|
print_repo_success(name, "Repository created");
|
||||||
|
Some(r)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
print_repo_error(
|
||||||
|
name,
|
||||||
|
&format!("Repository failed during init: {}", e),
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(r) => {
|
||||||
|
let first = match r.first() {
|
||||||
|
Some(e) => e,
|
||||||
|
None => {
|
||||||
|
panic!("Repos is an empty array. This is a bug");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match clone_repo(first, &repo_path) {
|
||||||
|
Ok(_) => {
|
||||||
|
print_repo_success(name, "Repository successfully cloned");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
print_repo_error(
|
||||||
|
name,
|
||||||
|
&format!("Repository failed during clone: {}", e),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(remotes) = &repo.remotes {
|
||||||
|
let repo_handle = repo_handle
|
||||||
|
.unwrap_or_else(|| open_repo(&repo_path).unwrap_or_else(|_| process::exit(1)));
|
||||||
|
|
||||||
|
let current_remotes: Vec<String> = match repo_handle.remotes() {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
print_repo_error(
|
||||||
|
name,
|
||||||
|
&format!("Repository failed during getting the remotes: {}", e),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.iter()
|
||||||
|
.flatten()
|
||||||
|
.map(|r| r.to_owned())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for remote in remotes {
|
||||||
|
if !current_remotes.iter().any(|r| *r == remote.name) {
|
||||||
|
print_repo_action(
|
||||||
|
name,
|
||||||
|
&format!(
|
||||||
|
"Setting up new remote \"{}\" to \"{}\"",
|
||||||
|
&remote.name, &remote.url
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if let Err(e) = repo_handle.remote(&remote.name, &remote.url) {
|
||||||
|
print_repo_error(
|
||||||
|
name,
|
||||||
|
&format!("Repository failed during setting the remotes: {}", e),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let current_remote = repo_handle.find_remote(&remote.name).unwrap();
|
||||||
|
let current_url = match current_remote.url() {
|
||||||
|
Some(url) => url,
|
||||||
|
None => {
|
||||||
|
print_repo_error(name, &format!("Repository failed during getting of the remote URL for remote \"{}\". This is most likely caused by a non-utf8 remote name", remote.name));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if remote.url != current_url {
|
||||||
|
if let Err(e) = repo_handle.remote_set_url(&remote.name, &remote.url) {
|
||||||
|
print_repo_error(name, &format!("Repository failed during setting of the remote URL for remote \"{}\": {}", &remote.name, e));
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for current_remote in ¤t_remotes {
|
||||||
|
if !remotes.iter().any(|r| &r.name == current_remote) {
|
||||||
|
print_repo_action(
|
||||||
|
name,
|
||||||
|
&format!("Deleting remote \"{}\"", ¤t_remote,),
|
||||||
|
);
|
||||||
|
if let Err(e) = repo_handle.remote_delete(current_remote) {
|
||||||
|
print_repo_error(
|
||||||
|
name,
|
||||||
|
&format!(
|
||||||
|
"Repository failed during deleting remote \"{}\": {}",
|
||||||
|
¤t_remote, e
|
||||||
|
),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print_repo_success(&repo.name, "OK");
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_repos = find_repos_without_details(&root_path).unwrap();
|
||||||
|
for repo in current_repos {
|
||||||
|
let name = path_as_string(repo.strip_prefix(&root_path).unwrap());
|
||||||
|
if !repos.iter().any(|r| r.name == name) {
|
||||||
|
print_warning(&format!("Found unmanaged repository: {}", name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_repos_without_details(path: &Path) -> Option<Vec<PathBuf>> {
|
||||||
|
let mut repos: Vec<PathBuf> = Vec::new();
|
||||||
|
|
||||||
|
let git_dir = path.join(".git");
|
||||||
|
if git_dir.exists() {
|
||||||
|
repos.push(path.to_path_buf());
|
||||||
|
} else {
|
||||||
|
match fs::read_dir(path) {
|
||||||
|
Ok(contents) => {
|
||||||
|
for content in contents {
|
||||||
|
match content {
|
||||||
|
Ok(entry) => {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_symlink() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if path.is_dir() {
|
||||||
|
if let Some(mut r) = find_repos_without_details(&path) {
|
||||||
|
repos.append(&mut r);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
print_error(&format!("Error accessing directory: {}", e));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
print_error(&format!("Failed to open \"{}\": {}", &path.display(), &e));
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(repos)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_repos(root: &Path, at_root: bool) -> Option<Vec<Repo>> {
|
||||||
|
let mut repos: Vec<Repo> = Vec::new();
|
||||||
|
|
||||||
|
for path in find_repos_without_details(root).unwrap() {
|
||||||
|
let repo = match open_repo(&path) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
print_error(&format!("Error opening repo {}: {}", path.display(), e));
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let remotes = match repo.remotes() {
|
||||||
|
Ok(remotes) => {
|
||||||
|
let mut results: Vec<Remote> = Vec::new();
|
||||||
|
for remote in remotes.iter() {
|
||||||
|
match remote {
|
||||||
|
Some(remote_name) => {
|
||||||
|
match repo.find_remote(remote_name) {
|
||||||
|
Ok(remote) => {
|
||||||
|
let name = match remote.name() {
|
||||||
|
Some(name) => name.to_string(),
|
||||||
|
None => {
|
||||||
|
print_repo_error(&path_as_string(&path), &format!("Falied getting name of remote \"{}\". This is most likely caused by a non-utf8 remote name", remote_name));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let url = match remote.url() {
|
||||||
|
Some(url) => url.to_string(),
|
||||||
|
None => {
|
||||||
|
print_repo_error(&path_as_string(&path), &format!("Falied getting URL of remote \"{}\". This is most likely caused by a non-utf8 URL", name));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let remote_type = match detect_remote_type(&url) {
|
||||||
|
Some(t) => t,
|
||||||
|
None => {
|
||||||
|
print_repo_error(
|
||||||
|
&path_as_string(&path),
|
||||||
|
&format!(
|
||||||
|
"Could not detect remote type of \"{}\"",
|
||||||
|
&url
|
||||||
|
),
|
||||||
|
);
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
results.push(Remote {
|
||||||
|
name,
|
||||||
|
url,
|
||||||
|
remote_type,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
print_repo_error(
|
||||||
|
&path_as_string(&path),
|
||||||
|
&format!("Error getting remote {}: {}", remote_name, e),
|
||||||
|
);
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
print_repo_error(&path_as_string(&path), "Error getting remote. This is most likely caused by a non-utf8 remote name");
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Some(results)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
print_repo_error(
|
||||||
|
&path_as_string(&path),
|
||||||
|
&format!("Error getting remotes: {}", e),
|
||||||
|
);
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
repos.push(Repo {
|
||||||
|
name: match at_root {
|
||||||
|
true => match &root.parent() {
|
||||||
|
Some(parent) => path_as_string(path.strip_prefix(parent).unwrap()),
|
||||||
|
None => {
|
||||||
|
print_error("Getting name of the search root failed. Do you have a git repository in \"/\"?");
|
||||||
|
process::exit(1);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
false => path_as_string(path.strip_prefix(&root).unwrap()),
|
||||||
|
},
|
||||||
|
remotes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Some(repos)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_in_tree(path: &Path) -> Option<Tree> {
|
||||||
|
let repos: Vec<Repo> = match find_repos(path, true) {
|
||||||
|
Some(vec) => vec,
|
||||||
|
None => Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut root = path.to_path_buf();
|
||||||
|
let home = env_home();
|
||||||
|
if root.starts_with(&home) {
|
||||||
|
// The tilde is not handled differently, it's just a normal path component for `Path`.
|
||||||
|
// Therefore we can treat it like that during **output**.
|
||||||
|
root = Path::new("~").join(root.strip_prefix(&home).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Tree {
|
||||||
|
root: Some(root.into_os_string().into_string().unwrap()),
|
||||||
|
repos: Some(repos),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run() {
|
||||||
|
let opts = cmd::parse();
|
||||||
|
|
||||||
|
match opts.subcmd {
|
||||||
|
cmd::SubCommand::Sync(sync) => {
|
||||||
|
let config = match config::read_config(&sync.config) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
print_error(&e);
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
sync_trees(config);
|
||||||
|
}
|
||||||
|
cmd::SubCommand::Find(find) => {
|
||||||
|
let path = Path::new(&find.path);
|
||||||
|
if !path.exists() {
|
||||||
|
print_error(&format!("Path \"{}\" does not exist", path.display()));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
let path = &path.canonicalize().unwrap();
|
||||||
|
if !path.is_dir() {
|
||||||
|
print_error(&format!("Path \"{}\" is not a directory", path.display()));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = Config {
|
||||||
|
trees: vec![find_in_tree(path).unwrap()],
|
||||||
|
};
|
||||||
|
|
||||||
|
let toml = toml::to_string(&config).unwrap();
|
||||||
|
|
||||||
|
print!("{}", toml);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/main.rs
Normal file
5
src/main.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
use grm::run;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
run();
|
||||||
|
}
|
||||||
58
src/output.rs
Normal file
58
src/output.rs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
use console::{Style, Term};
|
||||||
|
|
||||||
|
pub fn print_repo_error(repo: &str, message: &str) {
|
||||||
|
print_error(&format!("{}: {}", repo, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_error(message: &str) {
|
||||||
|
let stderr = Term::stderr();
|
||||||
|
let mut style = Style::new().red();
|
||||||
|
if stderr.is_term() {
|
||||||
|
style = style.force_styling(true);
|
||||||
|
}
|
||||||
|
stderr
|
||||||
|
.write_line(&format!("[{}] {}", style.apply_to('\u{2718}'), &message))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_repo_action(repo: &str, message: &str) {
|
||||||
|
print_action(&format!("{}: {}", repo, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_action(message: &str) {
|
||||||
|
let stderr = Term::stderr();
|
||||||
|
let mut style = Style::new().yellow();
|
||||||
|
if stderr.is_term() {
|
||||||
|
style = style.force_styling(true);
|
||||||
|
}
|
||||||
|
stderr
|
||||||
|
.write_line(&format!("[{}] {}", style.apply_to('\u{2699}'), &message))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_warning(message: &str) {
|
||||||
|
let stderr = Term::stderr();
|
||||||
|
let mut style = Style::new().yellow();
|
||||||
|
if stderr.is_term() {
|
||||||
|
style = style.force_styling(true);
|
||||||
|
}
|
||||||
|
stderr
|
||||||
|
.write_line(&format!("[{}] {}", style.apply_to('!'), &message))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_repo_success(repo: &str, message: &str) {
|
||||||
|
print_success(&format!("{}: {}", repo, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_success(message: &str) {
|
||||||
|
let stderr = Term::stderr();
|
||||||
|
let mut style = Style::new().green();
|
||||||
|
if stderr.is_term() {
|
||||||
|
style = style.force_styling(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
stderr
|
||||||
|
.write_line(&format!("[{}] {}", style.apply_to('\u{2714}'), &message))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
150
src/repo.rs
Normal file
150
src/repo.rs
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use git2::{Cred, RemoteCallbacks, Repository};
|
||||||
|
|
||||||
|
use crate::output::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum RemoteType {
|
||||||
|
Ssh,
|
||||||
|
Https,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct Remote {
|
||||||
|
pub name: String,
|
||||||
|
pub url: String,
|
||||||
|
#[serde(alias = "type")]
|
||||||
|
pub remote_type: RemoteType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct Repo {
|
||||||
|
pub name: String,
|
||||||
|
pub remotes: Option<Vec<Remote>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_ssh_remote() {
|
||||||
|
assert_eq!(
|
||||||
|
detect_remote_type("ssh://git@example.com"),
|
||||||
|
Some(RemoteType::Ssh)
|
||||||
|
);
|
||||||
|
assert_eq!(detect_remote_type("git@example.git"), Some(RemoteType::Ssh));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_https_remote() {
|
||||||
|
assert_eq!(
|
||||||
|
detect_remote_type("https://example.com"),
|
||||||
|
Some(RemoteType::Https)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
detect_remote_type("https://example.com/test.git"),
|
||||||
|
Some(RemoteType::Https)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_invalid_remotes() {
|
||||||
|
assert_eq!(detect_remote_type("https//example.com"), None);
|
||||||
|
assert_eq!(detect_remote_type("https:example.com"), None);
|
||||||
|
assert_eq!(detect_remote_type("ssh//example.com"), None);
|
||||||
|
assert_eq!(detect_remote_type("ssh:example.com"), None);
|
||||||
|
assert_eq!(detect_remote_type("git@example.com"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn check_unsupported_protocol_http() {
|
||||||
|
detect_remote_type("http://example.com");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn check_unsupported_protocol_git() {
|
||||||
|
detect_remote_type("git://example.com");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn check_unsupported_protocol_file() {
|
||||||
|
detect_remote_type("file:///");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detect_remote_type(remote_url: &str) -> Option<RemoteType> {
|
||||||
|
let git_regex = regex::Regex::new(r"^[a-zA-Z]+@.*$").unwrap();
|
||||||
|
if remote_url.starts_with("ssh://") {
|
||||||
|
return Some(RemoteType::Ssh);
|
||||||
|
}
|
||||||
|
if git_regex.is_match(remote_url) && remote_url.ends_with(".git") {
|
||||||
|
return Some(RemoteType::Ssh);
|
||||||
|
}
|
||||||
|
if remote_url.starts_with("https://") {
|
||||||
|
return Some(RemoteType::Https);
|
||||||
|
}
|
||||||
|
if remote_url.starts_with("http://") {
|
||||||
|
unimplemented!("Remotes using HTTP protocol are not supported");
|
||||||
|
}
|
||||||
|
if remote_url.starts_with("git://") {
|
||||||
|
unimplemented!("Remotes using git protocol are not supported");
|
||||||
|
}
|
||||||
|
if remote_url.starts_with("file://") || remote_url.starts_with('/') {
|
||||||
|
unimplemented!("Remotes using local protocol are not supported");
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_repo(path: &Path) -> Result<Repository, Box<dyn std::error::Error>> {
|
||||||
|
match Repository::open(path) {
|
||||||
|
Ok(r) => Ok(r),
|
||||||
|
Err(e) => Err(Box::new(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_repo(path: &Path) -> Result<Repository, Box<dyn std::error::Error>> {
|
||||||
|
match Repository::init(path) {
|
||||||
|
Ok(r) => Ok(r),
|
||||||
|
Err(e) => Err(Box::new(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clone_repo(remote: &Remote, path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
print_action(&format!(
|
||||||
|
"Cloning into \"{}\" from \"{}\"",
|
||||||
|
&path.display(),
|
||||||
|
&remote.url
|
||||||
|
));
|
||||||
|
match remote.remote_type {
|
||||||
|
RemoteType::Https => match Repository::clone(&remote.url, &path) {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) => Err(Box::new(e)),
|
||||||
|
},
|
||||||
|
RemoteType::Ssh => {
|
||||||
|
let mut callbacks = RemoteCallbacks::new();
|
||||||
|
callbacks.credentials(|_url, username_from_url, _allowed_types| {
|
||||||
|
Cred::ssh_key_from_agent(username_from_url.unwrap())
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut fo = git2::FetchOptions::new();
|
||||||
|
fo.remote_callbacks(callbacks);
|
||||||
|
|
||||||
|
let mut builder = git2::build::RepoBuilder::new();
|
||||||
|
builder.fetch_options(fo);
|
||||||
|
|
||||||
|
match builder.clone(&remote.url, path) {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) => Err(Box::new(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user