diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..3dc038b --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,76 @@ +name: Rust + +on: + push: + branches: + - main + pull_request: + branches: + - main + release: + types: + - published + +env: + CARGO_TERM_COLOR: always + +jobs: + checks: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + + - uses: Swatinem/rust-cache@v2 + + - name: Install latest stable + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + components: rustfmt,clippy + + - name: Run clippy + uses: actions-rs/clippy-check@v1 + with: + token: ${{ github.token }} + args: --all-features + + - name: Run rustfmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all --check + + - name: Run tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --all-features + + deploy: + runs-on: ubuntu-22.04 + if: ${{ github.event_name == 'release' }} + needs: + - checks + steps: + - uses: actions/checkout@v3 + + - uses: Swatinem/rust-cache@v2 + + - name: Install latest stable + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + + - name: Log into crates.io + uses: actions-rs/cargo@v1 + with: + command: login + args: ${{ secrets.CRATES_IO_TOKEN }} + + - name: Publish to crates.io + uses: actions-rs/cargo@v1 + with: + command: publish + args: --allow-dirty diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..a2a17f9 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,796 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" + +[[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.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.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574" +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 = "4.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e67816e006b17427c9b4386915109b494fec2d929c63e3bd3561234cbf1bf1e" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "clap_lex", + "once_cell", + "strsim", + "termcolor", +] + +[[package]] +name = "clap_derive" +version = "4.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a1b0f6422af32d5da0c58e2703320f379216ee70198241c84173a8c5ac28f3" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "console" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c050367d967ced717c04b65d8c619d863ef9292ce0c5760028655a2fb298718c" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "terminal_size", + "unicode-width", + "winapi", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + +[[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.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "ghr" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "console", + "dirs", + "git2", + "indicatif", + "itertools", + "lazy_static", + "regex", + "serde", + "tokio", + "toml", + "tracing", + "tracing-subscriber", + "url", +] + +[[package]] +name = "git2" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2994bee4a3a6a51eb90c218523be382fd7ea09b16380b9312e9dbe955ff7c7d1" +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 = "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.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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfddc9561e8baf264e0e45e197fd7696320026eb10a8180340debc27b18f535b" +dependencies = [ + "console", + "number_prefix", + "unicode-width", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[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.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" + +[[package]] +name = "libgit2-sys" +version = "0.14.0+1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a00859c70c8a4f7218e6d1cc32875c4b55f6799445b842b0d8ed5e4c3d959b" +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 = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num_cpus" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "once_cell" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" + +[[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.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b03b84c3b2d099b81f0953422b4d4ad58761589d0229b5506356afca05a3670a" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "os_str_bytes" +version = "6.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3baf96e39c5359d2eb0dd6ccb42c62b91d9678aa68160d261b9e0ccbf9e9dea9" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pkg-config" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" + +[[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.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +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 = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" + +[[package]] +name = "serde" +version = "1.0.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[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.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +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 = "thiserror" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + +[[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 = "tokio" +version = "1.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" +dependencies = [ + "autocfg", + "num_cpus", + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-ident" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" + +[[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 = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3d084bf --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "ghr" +description = "Yet another repository management with auto-attaching profiles." +version = "0.1.0" +license = "MIT" +homepage = "https://github.com/siketyan/ghr" +repository = "https://github.com/siketyan/ghr.git" +edition = "2021" +authors = [ + "Naoki Ikeguchi ", +] + +[dependencies] +anyhow = "1.0" +clap = { version = "4.0", features = ["derive"] } +console = "0.15.2" +dirs = "4.0" +git2 = "0.15.0" +itertools = "0.10.5" +indicatif = "0.17.1" +lazy_static = "1.4" +regex = "1.6" +serde = { version = "1.0", features = ["derive"] } +tokio = { version = "1.21", features = ["macros", "rt-multi-thread"] } +toml = "0.5.9" +tracing = "0.1.37" +tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } +url = "2.3" diff --git a/README.md b/README.md new file mode 100644 index 0000000..6d2e2a8 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# 🚀 ghr +Yet another repository management with auto-attaching profiles. + +## 📦 Installation +```shell +cargo install ghr +``` + +## 💚 Usages +### Cloning a repository +ghr supports many patterns or URLs of the repository to clone: + +``` +ghr clone / +ghr clone github.com:/ +ghr clone https://github.com//.git +ghr clone ssh://git@github.com//.git +ghr clone git@github.com:/.git +``` + +Easy! + +### Attaching profiles +Create `~/.ghr/config.toml` and edit as you like: + +```toml +[profiles.default] +user.name = "Your Name" +user.email = "your_name@personal.example.com" + +[profiles.company] +user.name = "Your Name (ACME Inc.)" +user.email = "your_name@company.example.com" + +[[rules]] +profile.name = "company" +owner = "acme" # Applies company profiles to all repositories in `acme` org + +[[rules]] +profile.name = "default" +``` diff --git a/src/cmd/clone.rs b/src/cmd/clone.rs new file mode 100644 index 0000000..05f3215 --- /dev/null +++ b/src/cmd/clone.rs @@ -0,0 +1,71 @@ +use std::path::PathBuf; +use std::str::FromStr; +use std::sync::mpsc::channel; +use std::time::Duration; + +use anyhow::Result; +use clap::Parser; +use console::style; +use git2::Repository; +use indicatif::{ProgressBar, ProgressStyle}; +use tracing::info; + +use crate::config::Config; +use crate::path::Path; +use crate::root::Root; +use crate::url::Url; + +#[derive(Debug, Parser)] +pub struct Cmd { + /// URL or pattern of the repository to clone. + repo: String, +} + +impl Cmd { + pub async fn run(self) -> Result<()> { + let root = Root::find()?; + let config = Config::load_from(&root)?; + + let (tx, rx) = channel(); + let progress = tokio::spawn(async move { + let spinner = ProgressStyle::with_template("{prefix} {spinner} {wide_msg}") + .unwrap() + .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ "); + + let p = ProgressBar::new(u64::MAX) + .with_style(spinner) + .with_prefix(format!(" {}", style("WAIT").dim())) + .with_message("Cloning the repository..."); + + while rx.recv_timeout(Duration::from_millis(100)).is_err() { + p.tick(); + } + + p.finish_and_clear(); + }); + + let url = Url::from_str(&self.repo)?; + let path = Path::resolve(&root, &url); + let profile = config + .rules + .resolve(&url) + .and_then(|r| config.profiles.resolve(&r.profile)); + + let repo = Repository::clone(&url.to_string(), PathBuf::from(&path))?; + if let Some((name, p)) = profile { + p.apply(&mut repo.config()?)?; + + info!("Attached profile [{}] successfully.", style(name).bold()); + } + + tx.send(())?; + progress.await?; + + info!( + "Cloned a repository successfully to: {:?}", + repo.workdir().unwrap(), + ); + + Ok(()) + } +} diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs new file mode 100644 index 0000000..464a7e2 --- /dev/null +++ b/src/cmd/mod.rs @@ -0,0 +1,29 @@ +mod clone; +mod profile; + +use anyhow::Result; +use clap::{Parser, Subcommand}; + +#[derive(Debug, Subcommand)] +pub enum Action { + /// Clones a Git repository to local. + Clone(clone::Cmd), + /// Manages profiles to use in repositories. + Profile(profile::Cmd), +} + +#[derive(Debug, Parser)] +pub struct Cli { + #[clap(subcommand)] + action: Action, +} + +impl Cli { + pub async fn run(self) -> Result<()> { + use Action::*; + match self.action { + Clone(cmd) => cmd.run().await, + Profile(cmd) => cmd.run(), + } + } +} diff --git a/src/cmd/profile/list.rs b/src/cmd/profile/list.rs new file mode 100644 index 0000000..afbd88a --- /dev/null +++ b/src/cmd/profile/list.rs @@ -0,0 +1,40 @@ +use anyhow::Result; +use clap::Parser; +use console::style; + +use crate::config::Config; + +const INHERIT: &str = "(inherit)"; + +#[derive(Debug, Parser)] +pub struct Cmd; + +impl Cmd { + pub fn run(self) -> Result<()> { + let config = Config::load()?; + + config.profiles.iter().for_each(|(name, profile)| { + println!( + " {} - {}: {} {}", + style("OK").cyan(), + style(name).bold(), + profile + .user + .as_ref() + .and_then(|u| u.name.as_deref()) + .unwrap_or(INHERIT), + style(&format!( + "<{}>", + profile + .user + .as_ref() + .and_then(|u| u.email.as_deref()) + .unwrap_or(INHERIT), + )) + .dim(), + ); + }); + + Ok(()) + } +} diff --git a/src/cmd/profile/mod.rs b/src/cmd/profile/mod.rs new file mode 100644 index 0000000..cc4e396 --- /dev/null +++ b/src/cmd/profile/mod.rs @@ -0,0 +1,24 @@ +mod list; + +use anyhow::Result; +use clap::{Parser, Subcommand}; + +#[derive(Debug, Subcommand)] +pub enum Action { + List(list::Cmd), +} + +#[derive(Debug, Parser)] +pub struct Cmd { + #[clap(subcommand)] + action: Action, +} + +impl Cmd { + pub fn run(self) -> Result<()> { + use Action::*; + match self.action { + List(cmd) => cmd.run(), + } + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..8594321 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,31 @@ +use std::fs::read_to_string; + +use anyhow::Result; +use serde::Deserialize; + +use crate::profile::Profiles; +use crate::root::Root; +use crate::rule::Rules; + +#[derive(Debug, Default, Deserialize)] +pub struct Config { + #[serde(default)] + pub profiles: Profiles, + #[serde(default)] + pub rules: Rules, +} + +impl Config { + pub fn load_from(root: &Root) -> Result { + let path = root.path().join("config.toml"); + + Ok(match path.exists() { + true => toml::from_str(read_to_string(path)?.as_str())?, + _ => Self::default(), + }) + } + + pub fn load() -> Result { + Self::load_from(&Root::find()?) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e823598 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,34 @@ +mod cmd; +mod config; +mod path; +mod profile; +mod root; +mod rule; +mod url; + +use clap::Parser; +use std::process::exit; +use tracing::error; +use tracing_subscriber::filter::LevelFilter; +use tracing_subscriber::EnvFilter; + +use crate::cmd::Cli; + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt() + .compact() + .without_time() + .with_target(false) + .with_env_filter( + EnvFilter::builder() + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(), + ) + .init(); + + if let Err(e) = Cli::parse().run().await { + error!("{}", e); + exit(1); + } +} diff --git a/src/path.rs b/src/path.rs new file mode 100644 index 0000000..aa5d6de --- /dev/null +++ b/src/path.rs @@ -0,0 +1,33 @@ +use crate::root::Root; +use crate::url::Url; +use std::path::PathBuf; + +pub struct Path<'a> { + root: &'a Root, + host: String, + owner: String, + repo: String, +} + +impl<'a> Path<'a> { + pub fn resolve(root: &'a Root, url: &Url) -> Self { + Self { + root, + host: url.host.to_string(), + owner: url.owner.clone(), + repo: url.repo.clone(), + } + } +} + +impl<'a> From<&Path<'a>> for PathBuf { + fn from(p: &Path<'a>) -> Self { + p.root.path().join(&p.host).join(&p.owner).join(&p.repo) + } +} + +impl<'a> From> for PathBuf { + fn from(p: Path<'a>) -> Self { + (&p).into() + } +} diff --git a/src/profile.rs b/src/profile.rs new file mode 100644 index 0000000..2021015 --- /dev/null +++ b/src/profile.rs @@ -0,0 +1,55 @@ +use std::collections::HashMap; +use std::ops::Deref; + +use crate::rule::ProfileRef; +use anyhow::Result; +use git2::Config; +use serde::Deserialize; + +#[derive(Debug, Default, Deserialize)] +pub struct User { + #[serde(default)] + pub name: Option, + #[serde(default)] + pub email: Option, +} + +#[derive(Debug, Default, Deserialize)] +pub struct Profile { + #[serde(default)] + pub user: Option, +} + +impl Profile { + pub fn apply(&self, config: &mut Config) -> Result<()> { + if let Some(name) = self.user.as_ref().and_then(|u| u.name.as_deref()) { + config.set_str("user.name", name)?; + } + + if let Some(email) = self.user.as_ref().and_then(|u| u.email.as_deref()) { + config.set_str("user.email", email)?; + } + + Ok(()) + } +} + +#[derive(Debug, Default, Deserialize)] +pub struct Profiles { + #[serde(flatten)] + map: HashMap, +} + +impl Profiles { + pub fn resolve(&self, r: &ProfileRef) -> Option<(&str, &Profile)> { + self.get_key_value(&r.name).map(|(s, p)| (s.as_str(), p)) + } +} + +impl Deref for Profiles { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.map + } +} diff --git a/src/root.rs b/src/root.rs new file mode 100644 index 0000000..8d655a6 --- /dev/null +++ b/src/root.rs @@ -0,0 +1,28 @@ +use std::path::PathBuf; + +use anyhow::{anyhow, Result}; +use dirs::home_dir; +use tracing::info; + +pub struct Root { + path: PathBuf, +} + +impl Root { + pub fn find() -> Result { + let path = home_dir() + .ok_or_else(|| anyhow!("Could not find a home directory"))? + .join(".ghr"); + + info!( + "Found a root directory: {}", + path.to_str().unwrap_or_default(), + ); + + Ok(Self { path }) + } + + pub fn path(&self) -> &PathBuf { + &self.path + } +} diff --git a/src/rule.rs b/src/rule.rs new file mode 100644 index 0000000..c96711d --- /dev/null +++ b/src/rule.rs @@ -0,0 +1,39 @@ +use crate::url::Url; +use serde::Deserialize; + +#[derive(Debug, Clone, Deserialize)] +pub struct ProfileRef { + pub name: String, +} + +#[derive(Debug, Deserialize)] +pub struct Rule { + pub profile: ProfileRef, + pub host: Option, + pub owner: Option, + pub repo: Option, +} + +impl Rule { + pub fn matches(&self, url: &Url) -> bool { + self.host + .as_deref() + .map(|h| h == url.host.to_string()) + .unwrap_or(true) + && self + .owner + .as_deref() + .map(|o| o == url.owner) + .unwrap_or(true) + && self.repo.as_deref().map(|r| r == url.repo).unwrap_or(true) + } +} + +#[derive(Debug, Default, Deserialize)] +pub struct Rules(Vec); + +impl Rules { + pub fn resolve(&self, url: &Url) -> Option<&Rule> { + self.0.iter().find(|rule| rule.matches(url)) + } +} diff --git a/src/url.rs b/src/url.rs new file mode 100644 index 0000000..cf9a125 --- /dev/null +++ b/src/url.rs @@ -0,0 +1,351 @@ +use std::convert::Infallible; +use std::str::FromStr; + +use anyhow::{anyhow, bail, Error, Result}; +use itertools::FoldWhile; +use itertools::Itertools; +use lazy_static::lazy_static; +use regex::{Captures, Regex}; + +const GITHUB_COM: &str = "github.com"; + +const GIT_EXTENSION: &str = ".git"; +const EXTENSIONS: &[&str] = &[GIT_EXTENSION]; + +#[derive(Debug, Default, Eq, PartialEq)] +pub enum Vcs { + #[default] + Git, +} + +impl Vcs { + fn from_url(url: &url::Url) -> Self { + let url = url.as_str(); + if url.ends_with(GIT_EXTENSION) { + Self::Git + } else { + Default::default() + } + } + + fn extension(&self) -> &'static str { + match self { + Self::Git => GIT_EXTENSION, + } + } +} + +#[derive(Debug, Default, Eq, PartialEq)] +pub enum Scheme { + #[default] + Https, + Ssh, +} + +impl FromStr for Scheme { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(match s.to_ascii_lowercase().as_str() { + "https" => Self::Https, + "ssh" => Self::Ssh, + _ => Err(anyhow!("Unknown URL scheme found: {}", s))?, + }) + } +} + +#[derive(Debug, Default, Eq, PartialEq)] +pub enum Host { + #[default] + GitHub, + Unknown(String), +} + +impl FromStr for Host { + type Err = Infallible; + + fn from_str(s: &str) -> std::result::Result { + Ok(match s.to_ascii_lowercase().as_str() { + GITHUB_COM => Self::GitHub, + _ => Self::Unknown(s.to_string()), + }) + } +} + +impl ToString for Host { + fn to_string(&self) -> String { + match self { + Self::GitHub => GITHUB_COM.to_string(), + Self::Unknown(s) => s.clone(), + } + } +} + +#[derive(Debug, Default, Eq, PartialEq)] +pub struct Url { + pub vcs: Vcs, + pub scheme: Scheme, + pub user: Option, + pub host: Host, + pub owner: String, + pub repo: String, +} + +impl Url { + fn from_url(url: &url::Url) -> Result { + let mut segments = url + .path_segments() + .ok_or_else(|| anyhow!("Could not parse path segments from the URL: {}", url))?; + + Ok(Self { + vcs: Vcs::from_url(url), + scheme: Scheme::from_str(url.scheme())?, + user: match url.username().is_empty() { + true => None, + _ => Some(url.username().to_string()), + }, + host: Host::from_str( + url.host_str() + .ok_or_else(|| anyhow!("Could not find hostname from the URL: {}", url))?, + )?, + owner: segments + .next() + .ok_or_else(|| anyhow!("Could not find owner from the URL: {}", url))? + .to_string(), + repo: Self::remove_extensions( + segments.next().ok_or_else(|| { + anyhow!("Could not find repository name from the URL: {}", url) + })?, + ), + }) + } + + fn from_pattern(s: &str) -> Result { + lazy_static! { + static ref ORG_REPO: Regex = + Regex::new(r"^(?P[0-9A-Za-z_\.\-]+)/(?P[0-9A-Za-z_\.\-]+)$").unwrap(); + + static ref HOST_ORG_REPO: Regex = + Regex::new(r"^(?P[0-9A-Za-z\.\-]+)[:/](?P[0-9A-Za-z_\.\-]+)/(?P[0-9A-Za-z_\.\-]+)$").unwrap(); + + static ref SSH: Regex = + Regex::new(r"^(?P[0-9A-Za-z\-]+)@(?P[0-9A-Za-z\.\-]+):(?P[0-9A-Za-z_\.\-]+)/(?P[0-9A-Za-z_\.\-]+)$").unwrap(); + } + + macro_rules! pattern { + ($n: ident, $f: expr) => { + if $n.is_match(s) { + let captures: Captures = $n + .captures(s) + .ok_or_else(|| anyhow!("Could not capture from the pattern"))?; + + return $f(captures); + } + }; + } + + macro_rules! group { + ($c: expr, $n: literal) => { + $c.name($n) + .map(|o| o.as_str().to_string()) + .unwrap_or_default() + }; + } + + pattern!(ORG_REPO, |c: Captures| Ok(Self { + owner: group!(c, "org"), + repo: Self::remove_extensions(&group!(c, "repo")), + ..Default::default() + })); + + pattern!(HOST_ORG_REPO, |c: Captures| Ok(Self { + host: Host::from_str(group!(c, "host").as_str())?, + owner: group!(c, "org"), + repo: Self::remove_extensions(&group!(c, "repo")), + ..Default::default() + })); + + pattern!(SSH, |c: Captures| Ok(Self { + scheme: Scheme::Ssh, + user: Some(group!(c, "user")), + host: Host::from_str(group!(c, "host").as_str())?, + owner: group!(c, "org"), + repo: Self::remove_extensions(&group!(c, "repo")), + ..Default::default() + })); + + bail!("The input did not match any pattern: {}", s) + } + + fn remove_extensions(s: &str) -> String { + EXTENSIONS + .iter() + .fold_while(s.to_string(), |v, i| { + let trimmed = v.trim_end_matches(i); + match trimmed != v.as_str() { + true => FoldWhile::Done(trimmed.to_string()), + _ => FoldWhile::Continue(v), + } + }) + .into_inner() + } +} + +impl FromStr for Url { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s.contains("://") { + true => Self::from_url(&url::Url::from_str(s)?), + _ => Self::from_pattern(s), + } + } +} + +impl ToString for Url { + fn to_string(&self) -> String { + let authority = match &self.user { + Some(u) => format!("{}@{}", u, self.host.to_string()), + _ => self.host.to_string(), + }; + + match self.scheme { + Scheme::Https => { + format!( + "https://{}/{}/{}{}", + authority, + self.owner, + self.repo, + self.vcs.extension(), + ) + } + Scheme::Ssh => { + format!( + "{}:{}/{}{}", + authority, + self.owner, + self.repo, + self.vcs.extension(), + ) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_from_url_https() { + let url = url::Url::parse("https://github.com/siketyan/siketyan.github.io.git").unwrap(); + + assert_eq!( + Url { + vcs: Vcs::Git, + scheme: Scheme::Https, + user: None, + host: Host::GitHub, + owner: "siketyan".to_string(), + repo: "siketyan.github.io".to_string() + }, + Url::from_url(&url).unwrap(), + ) + } + + #[test] + fn parse_from_url_ssh() { + let url = url::Url::parse("ssh://git@github.com/siketyan/siketyan.github.io.git").unwrap(); + + assert_eq!( + Url { + vcs: Vcs::Git, + scheme: Scheme::Ssh, + user: Some("git".to_string()), + host: Host::GitHub, + owner: "siketyan".to_string(), + repo: "siketyan.github.io".to_string() + }, + Url::from_url(&url).unwrap(), + ) + } + + #[test] + fn parse_from_pattern_org_repo() { + assert_eq!( + Url { + vcs: Vcs::Git, + scheme: Scheme::Https, + user: None, + host: Host::GitHub, + owner: "siketyan".to_string(), + repo: "siketyan.github.io".to_string() + }, + Url::from_pattern("siketyan/siketyan.github.io").unwrap(), + ) + } + + #[test] + fn parse_from_pattern_host_org_repo() { + assert_eq!( + Url { + vcs: Vcs::Git, + scheme: Scheme::Https, + user: None, + host: Host::Unknown("gitlab.com".to_string()), + owner: "siketyan".to_string(), + repo: "siketyan.github.io".to_string() + }, + Url::from_pattern("gitlab.com:siketyan/siketyan.github.io").unwrap(), + ) + } + + #[test] + fn parse_from_pattern_ssh() { + assert_eq!( + Url { + vcs: Vcs::Git, + scheme: Scheme::Ssh, + user: Some("git".to_string()), + host: Host::GitHub, + owner: "siketyan".to_string(), + repo: "siketyan.github.io".to_string() + }, + Url::from_pattern("git@github.com:siketyan/siketyan.github.io.git").unwrap(), + ) + } + + #[test] + fn to_string_https() { + assert_eq!( + "https://github.com/siketyan/siketyan.github.io.git", + Url { + vcs: Vcs::Git, + scheme: Scheme::Https, + user: None, + host: Host::GitHub, + owner: "siketyan".to_string(), + repo: "siketyan.github.io".to_string() + } + .to_string() + .as_str(), + ) + } + + #[test] + fn to_string_ssh() { + assert_eq!( + "git@github.com:siketyan/siketyan.github.io.git", + Url { + vcs: Vcs::Git, + scheme: Scheme::Ssh, + user: Some("git".to_string()), + host: Host::GitHub, + owner: "siketyan".to_string(), + repo: "siketyan.github.io".to_string() + } + .to_string() + .as_str(), + ) + } +}