diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml new file mode 100644 index 0000000..7e412f1 --- /dev/null +++ b/.github/workflows/push.yaml @@ -0,0 +1,40 @@ +name: Push + +on: [push] + +jobs: + test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v1 + with: + persist-credentials: false + + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + components: clippy, rustfmt + + - name: cargo fmt + run: cargo fmt --all -- --check + shell: bash + + - name: cargo clippy + run: cargo clippy -- -D clippy::all + shell: bash + + - name: cargo build + run: cargo build + shell: bash + + - name: cargo test + run: cargo test -- --nocapture + shell: bash + + - name: cargo package + run: cargo package + shell: bash diff --git a/.gitignore b/.gitignore index ee2a7eb..a3adb21 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .* !/.*ignore +!/.github /target/ diff --git a/Cargo.lock b/Cargo.lock index 58090ae..a0691ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,43 @@ # 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 = "assert_cmd" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e996dc7940838b7ef1096b882e29ec30a3149a3a443cdc8dba19ed382eca1fe2" +dependencies = [ + "bstr", + "doc-comment", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "assert_fs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633ff1df0788db09e2087fb93d05974e93acb886ac3aec4e67be1d6932e360e4" +dependencies = [ + "doc-comment", + "globwalk", + "predicates", + "predicates-core", + "predicates-tree", + "tempfile", +] + [[package]] name = "autocfg" version = "1.0.1" @@ -26,6 +63,12 @@ dependencies = [ "serde", ] +[[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" @@ -73,6 +116,16 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" +dependencies = [ + "cfg-if", + "lazy_static", +] + [[package]] name = "csv" version = "1.1.6" @@ -95,6 +148,74 @@ dependencies = [ "memchr", ] +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[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 = "globset" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags", + "ignore", + "walkdir", +] + [[package]] name = "hashbrown" version = "0.11.2" @@ -110,6 +231,24 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "ignore" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d" +dependencies = [ + "crossbeam-utils", + "globset", + "lazy_static", + "log", + "memchr", + "regex", + "same-file", + "thread_local", + "walkdir", + "winapi-util", +] + [[package]] name = "indexmap" version = "1.7.0" @@ -120,6 +259,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "itertools" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.4.8" @@ -132,12 +280,48 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "libc" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119" + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + [[package]] name = "memchr" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + [[package]] name = "os_str_bytes" version = "4.2.0" @@ -147,6 +331,86 @@ dependencies = [ "memchr", ] +[[package]] +name = "phf" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9fc3db1018c4b59d7d582a739436478b6035138b6aecbce989fc91c3e98409f" +dependencies = [ + "phf_macros", + "phf_shared", + "proc-macro-hack", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" + +[[package]] +name = "predicates" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6ce811d0b2e103743eec01db1c50612221f173084ce2f7941053e94b6bb474" +dependencies = [ + "difflib", + "float-cmp", + "itertools", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57e35a3326b75e49aa85f5dc6ec15b41108cf5aee58eabb1f274dd18b73c2451" + +[[package]] +name = "predicates-tree" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338c7be2905b732ae3984a2f40032b5e94fd8f52505b186c7d4d68d193445df7" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -171,6 +435,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + [[package]] name = "proc-macro2" version = "1.0.32" @@ -189,18 +459,102 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +dependencies = [ + "bitflags", +] + +[[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-automata" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "ryu" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "serde" version = "1.0.130" @@ -221,13 +575,23 @@ dependencies = [ "syn", ] +[[package]] +name = "siphasher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533494a8f9b724d33625ab53c6c4800f7cc445895924a8ef649222dcb76e938b" + [[package]] name = "stc" -version = "0.0.8" +version = "0.1.0" dependencies = [ + "assert_cmd", + "assert_fs", "clap", "const_format", "csv", + "phf", + "predicates", "serde", ] @@ -242,12 +606,41 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tempfile" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +dependencies = [ + "cfg-if", + "libc", + "rand", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "termtree" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13a4ec180a2de59b57434704ccfad967f789b12737738798fa08798cd5824c16" + [[package]] name = "textwrap" version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" +[[package]] +name = "thread_local" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" +dependencies = [ + "once_cell", +] + [[package]] name = "unicode-segmentation" version = "1.8.0" @@ -265,3 +658,60 @@ name = "version_check" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[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" diff --git a/Cargo.toml b/Cargo.toml index 39c3a57..29c8212 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,15 +1,18 @@ [package] name = "stc" description = "[WIP] Easy stacking of dev branches in git repositories." -version = "0.0.8" +version = "0.1.0" authors = ["Folke Behrens "] edition = "2021" repository = "https://github.com/cloneable/stc/" license = "Apache-2.0" +categories = ["command-line-utilities", "development-tools"] +keywords = ["git"] +exclude = [".gitignore", ".github", "target"] [dependencies] const_format = "0.2.22" -serde = {version = "1.0.130", features = ["derive"]} +serde = { version = "1.0.130", features = ["derive"] } csv = "1.1.6" [dependencies.clap] @@ -17,6 +20,12 @@ version = "3.0.0-beta.5" default-features = false features = ["std", "derive"] +[dev-dependencies] +assert_cmd = "2.0.2" +assert_fs = "1.0.6" +phf = { version = "0.10.0", features = ["macros"] } +predicates = "2.0.3" + [profile.release] lto = true opt-level = 3 diff --git a/README.md b/README.md index f61fb63..11007c8 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ cargo install --git https://github.com/cloneable/stc ## Usage ``` -stc 0.0.8 +stc 0.1.0 [WIP] Easy stacking of dev branches in git repositories. diff --git a/src/git.rs b/src/git.rs index 93e88e0..22d942f 100644 --- a/src/git.rs +++ b/src/git.rs @@ -1,32 +1,31 @@ use ::const_format::concatcp; use ::csv::ReaderBuilder; use ::serde::Deserialize; -use ::std::borrow::Cow; -use ::std::clone::Clone; -use ::std::collections::HashMap; -use ::std::convert::From; -use ::std::default::Default; -use ::std::error::Error; -use ::std::format; -use ::std::iter::IntoIterator; -use ::std::iter::Iterator; -use ::std::option::Option::{self, None}; -use ::std::result::Result::{self, Err, Ok}; -use ::std::string::String; -use ::std::string::ToString; -use ::std::todo; -use ::std::vec::Vec; -use ::std::write; - -// TODO: use ObjectName as type for const if possibe -pub const NON_EXISTANT_OBJECT: &'static str = "0000000000000000000000000000000000000000"; - -pub const STC_REF_PREFIX: &'static str = "refs/stc/"; -pub const STC_BASE_REF_PREFIX: &'static str = concatcp!(STC_REF_PREFIX, "base/"); -pub const STC_START_REF_PREFIX: &'static str = concatcp!(STC_REF_PREFIX, "start/"); -pub const STC_REMOTE_REF_PREFIX: &'static str = concatcp!(STC_REF_PREFIX, "remote/"); - -pub const BRANCH_REF_PREFIX: &'static str = "refs/heads/"; +use ::std::{ + borrow::{Cow, ToOwned}, + clone::Clone, + collections::{BTreeSet, HashMap}, + convert::AsRef, + default::Default, + error::Error, + format, + iter::{IntoIterator, Iterator}, + option::Option::{self, None}, + result::Result::{self, Err, Ok}, + string::{String, ToString}, + vec::Vec, + write, +}; + +pub const NON_EXISTANT_OBJECT: ObjectName<'static> = + ObjectName::new("0000000000000000000000000000000000000000"); + +pub const STC_REF_PREFIX: &str = "refs/stc/"; +pub const STC_BASE_REF_PREFIX: &str = concatcp!(STC_REF_PREFIX, "base/"); +pub const STC_START_REF_PREFIX: &str = concatcp!(STC_REF_PREFIX, "start/"); +pub const STC_REMOTE_REF_PREFIX: &str = concatcp!(STC_REF_PREFIX, "remote/"); + +pub const BRANCH_REF_PREFIX: &str = "refs/heads/"; #[derive(Debug)] pub struct Status { @@ -71,27 +70,26 @@ pub trait Git { fn snapshot(&self) -> Result { let status = self.exec(&["for-each-ref", "--format", FIELD_FORMATS.join(",").as_str()])?; - let refs = parse_ref(status.stdout.as_slice()).map_err(move |_err| Status::with(1))?; + let refs = parse_ref(status.stdout.as_slice()).map_err(|_err| Status::with(1))?; let head = refs .values() - .find(move |r| r.head) - .map(move |r| r.name.branchname()); + .find(|r| r.head) + .map(|r| r.name.branchname().owning_clone()); Ok(Repository { refs, head }) } - fn check_branchname<'a>(&self, name: &'a String) -> Result, Status> { + fn check_branchname<'a>(&self, name: &'a str) -> Result, Status> { self.exec(&["check-ref-format", "--branch", name])?; - Ok(BranchName(Cow::Borrowed(name))) + Ok(BranchName(Cow::Owned(name.to_string()))) } fn create_branch(&self, name: &BranchName, base: &BranchName) -> Result<(), Status> { self.exec(&["branch", "--create-reflog", name.as_str(), base.as_str()]) - .map(move |_| -> () {}) + .map(|_| {}) } fn switch_branch(&self, b: &BranchName) -> Result<(), Status> { - self.exec(&["switch", "--no-guess", b.as_str()]) - .map(move |_| -> () {}) + self.exec(&["switch", "--no-guess", b.as_str()]).map(|_| {}) } fn create_symref( @@ -101,12 +99,12 @@ pub trait Git { reason: &'static str, ) -> Result<(), Status> { self.exec(&["symbolic-ref", "-m", reason, name.as_str(), target.as_str()]) - .map(move |_| -> () {}) + .map(|_| {}) } fn delete_symref(&self, name: &RefName) -> Result<(), Status> { self.exec(&["symbolic-ref", "--delete", name.as_str()]) - .map(move |_| -> () {}) + .map(|_| {}) } fn create_ref(&self, name: &RefName, commit: &ObjectName) -> Result<(), Status> { @@ -116,9 +114,9 @@ pub trait Git { "--create-reflog", name.as_str(), commit.as_str(), - NON_EXISTANT_OBJECT, + NON_EXISTANT_OBJECT.as_str(), ]) - .map(move |_| -> () {}) + .map(|_| {}) } fn update_ref( @@ -135,7 +133,7 @@ pub trait Git { new_commit.as_str(), cur_commit.as_str(), ]) - .map(move |_| -> () {}) + .map(|_| {}) } fn delete_ref(&self, name: &RefName, cur_commit: &ObjectName) -> Result<(), Status> { @@ -146,7 +144,7 @@ pub trait Git { name.as_str(), cur_commit.as_str(), ]) - .map(move |_| -> () {}) + .map(|_| {}) } fn rebase_onto(&self, name: &BranchName) -> Result<(), Status> { @@ -158,7 +156,7 @@ pub trait Git { name.stc_start_refname().as_str(), name.as_str(), ]) - .map(move |_| -> () {}) + .map(|_| {}) } fn push( @@ -174,17 +172,16 @@ pub trait Git { remote.as_str(), format!("{}:{}", name.as_str(), name.as_str()).as_str(), ]) - .map(move |_| -> () {}) + .map(|_| {}) } fn config_set(&self, key: &str, value: &str) -> Result<(), Status> { - self.exec(&["config", "--local", key, value]) - .map(move |_| -> () {}) + self.exec(&["config", "--local", key, value]).map(|_| {}) } fn config_add(&self, key: &str, value: &str) -> Result<(), Status> { self.exec(&["config", "--local", "--add", key, value]) - .map(move |_| -> () {}) + .map(|_| {}) } fn config_unset_pattern(&self, key: &str, pattern: &str) -> Result<(), Status> { @@ -203,12 +200,7 @@ pub trait Git { } fn fetch_all_prune(&self) -> Result<(), Status> { - self.exec(&["fetch", "--all", "--prune"]) - .map(move |_| -> () {}) - } - - fn tracked_branches(&self) -> Result, Status> { - todo!() + self.exec(&["fetch", "--all", "--prune"]).map(|_| {}) } fn forkpoint(&self, base: &RefName, branch: &RefName) -> Result { @@ -235,18 +227,32 @@ impl<'a> Repository<'a> { pub fn head(&self) -> Option<&'a BranchName> { self.head.as_ref() } + + pub fn tracked_branches(&self) -> Vec { + self.refs + .iter() + .filter(|(name, _)| name.0.starts_with(STC_REF_PREFIX)) + .map(|(name, _)| name.branchname()) + .collect::>() + .into_iter() + .collect() + } } -#[derive(PartialEq, PartialOrd, Debug)] -pub struct BranchName<'a>(Cow<'a, String>); +#[derive(Eq, PartialEq, Ord, PartialOrd, Debug)] +pub struct BranchName<'a>(Cow<'a, str>); impl<'a> BranchName<'a> { - const fn new(name: String) -> Self { - BranchName(Cow::Owned(name)) + pub const fn new(name: &'a str) -> Self { + BranchName(Cow::Borrowed(name)) + } + + pub fn owning_clone<'b: 'a>(&'a self) -> BranchName<'b> { + BranchName(Cow::Owned(self.0.as_ref().to_owned())) } pub fn as_str(&self) -> &str { - self.0.as_str() + &self.0 } pub fn refname(&self) -> RefName { @@ -267,46 +273,58 @@ impl<'a> BranchName<'a> { } #[derive(Deserialize, Clone, Hash, Eq, PartialEq, Ord, PartialOrd, Debug)] -pub struct RefName<'a>(Cow<'a, String>); +pub struct RefName<'a>(Cow<'a, str>); impl<'a> RefName<'a> { - const fn new(name: String) -> Self { - RefName(Cow::Owned(name)) + pub const fn new(name: &'a str) -> Self { + RefName(Cow::Borrowed(name)) + } + + pub fn owning_clone<'b: 'a>(&'a self) -> RefName<'b> { + RefName(Cow::Owned(self.0.as_ref().to_owned())) } pub fn as_str(&self) -> &str { - self.0.as_str() + &self.0 } - pub fn branchname(&self) -> BranchName<'a> { + pub fn branchname(&'a self) -> BranchName<'a> { let (_, branchname) = self.0.rsplit_once("/").unwrap(); - BranchName::new(branchname.to_string()) + BranchName::new(branchname) } } #[derive(Deserialize, PartialEq, PartialOrd, Clone, Debug)] -pub struct ObjectName<'a>(pub Cow<'a, String>); +pub struct ObjectName<'a>(pub Cow<'a, str>); impl<'a> ObjectName<'a> { - pub const fn new(value: String) -> Self { - ObjectName(Cow::Owned(value)) + pub const fn new(name: &'a str) -> Self { + ObjectName(Cow::Borrowed(name)) + } + + pub fn owning_clone<'b: 'a>(&'a self) -> ObjectName<'b> { + ObjectName(Cow::Owned(self.0.as_ref().to_owned())) } pub fn as_str(&self) -> &str { - self.0.as_str() + &self.0 } } #[derive(Deserialize, PartialEq, PartialOrd, Debug)] -pub struct RemoteName<'a>(Cow<'a, String>); +pub struct RemoteName<'a>(Cow<'a, str>); impl<'a> RemoteName<'a> { - const fn new(value: String) -> Self { - RemoteName(Cow::Owned(value)) + pub const fn new(name: &'a str) -> Self { + RemoteName(Cow::Borrowed(name)) + } + + pub fn owning_clone<'b: 'a>(&'a self) -> RemoteName<'b> { + RemoteName(Cow::Owned(self.0.as_ref().to_owned())) } pub fn as_str(&self) -> &str { - self.0.as_str() + &self.0 } } @@ -332,7 +350,7 @@ pub struct Ref<'a> { pub upstream_refname: RefName<'a>, } -const FIELD_FORMATS: [&'static str; 9] = [ +const FIELD_FORMATS: [&str; 9] = [ "%(refname)", // name "%(if)%(HEAD)%(then)true%(else)false%(end)", // head "%(objectname)", // objectname @@ -365,6 +383,7 @@ fn parse_ref<'a, R: ::std::io::Read + ::std::fmt::Debug>( mod tests { use super::*; use ::std::assert_eq; + use ::std::convert::From; #[test] fn test_parse_ref() { @@ -374,18 +393,17 @@ refs/heads/moo1,true,123abc,commit,<>,origin,refs/heads/moo,,refs/remotes/origin let refs = parse_ref(csv.as_bytes()).expect("cannot parse"); assert_eq!(refs.len(), 1); assert_eq!( - refs.get(&RefName::new("refs/heads/moo1".to_string())) - .unwrap(), + refs.get(&RefName::new("refs/heads/moo1")).unwrap(), &Ref { - name: RefName::new("refs/heads/moo1".to_string()), + name: RefName::new("refs/heads/moo1"), head: true, - objectname: ObjectName::new("123abc".to_string()), + objectname: ObjectName::new("123abc"), objecttype: RefType::Commit, track: String::from("<>"), - remote: RemoteName::new("origin".to_string()), - remote_refname: RefName::new("refs/heads/moo".to_string()), - symref_target: RefName::new("".to_string()), - upstream_refname: RefName::new("refs/remotes/origin/moo".to_string()), + remote: RemoteName::new("origin"), + remote_refname: RefName::new("refs/heads/moo"), + symref_target: RefName::new(""), + upstream_refname: RefName::new("refs/remotes/origin/moo"), } ) } diff --git a/src/main.rs b/src/main.rs index 7e9c097..14be093 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,9 +3,11 @@ #![allow(dead_code)] // TODO: remove use ::clap::{self, Parser, Subcommand}; -use ::std::option::Option::{self, None, Some}; -use ::std::result::Result; -use ::std::string::String; +use ::std::{ + option::Option::{self, None, Some}, + result::Result, + string::String, +}; mod git; mod runner; @@ -82,7 +84,7 @@ enum Command { fn main() -> Result<(), git::Status> { let root = Root::parse(); let runner = runner::Runner::new("git"); - let stc = stc::STC::new(runner); + let stc = stc::Stc::new(runner); match root.subcommand { Command::Clean => stc.clean(), Command::Fix { branch, base } => stc.fix(branch, base), diff --git a/src/runner.rs b/src/runner.rs index 8846b22..2a5c474 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -1,43 +1,52 @@ -use crate::git::{Git, Status}; -use ::std::assert_ne; -use ::std::option::Option::Some; -use ::std::process::Command; -use ::std::process::Stdio; -use ::std::result::Result::{self, Err, Ok}; +use crate::git; +use ::std::{ + assert_ne, + collections::HashMap, + option::Option::Some, + path::PathBuf, + process::{Command, Stdio}, + result::Result::{self, Err, Ok}, +}; pub struct Runner<'a> { gitpath: &'a str, + workdir: PathBuf, + env: HashMap<&'a str, &'a str>, } impl<'a> Runner<'a> { pub fn new(gitpath: &'a str) -> Self { - Runner { gitpath } + Runner { + gitpath, + workdir: ::std::env::current_dir().expect("cannot determine current working directory"), + env: HashMap::<&'a str, &'a str>::new(), + } } } -impl<'a> Git for Runner<'a> { - fn exec(&self, args: &[&str]) -> Result { +impl<'a> git::Git for Runner<'a> { + fn exec(&self, args: &[&str]) -> Result { let cmd = Command::new(self.gitpath) .args(args) + .current_dir(&self.workdir) + .envs(self.env.iter()) .stdin(Stdio::null()) - .stdin(Stdio::piped()) .stdout(Stdio::piped()) + .stderr(Stdio::piped()) .spawn() .expect("failed to start git"); let output = cmd.wait_with_output().expect("failed to wait on git"); if output.status.success() { ::std::eprintln!("[OK] git {:?}", args); - Ok(Status::new(0, output.stdout, output.stderr)) + Ok(git::Status::new(0, output.stdout, output.stderr)) + } else if let Some(code) = output.status.code() { + assert_ne!(code, 0); + ::std::eprintln!("[ERR {:?}] git {:?}", code, args); + Err(git::Status::new(code, output.stdout, output.stderr)) } else { - if let Some(code) = output.status.code() { - assert_ne!(code, 0); - ::std::eprintln!("[ERR {:?}] git {:?}", code, args); - Err(Status::new(code, output.stdout, output.stderr)) - } else { - ::std::eprintln!("[ERR] git {:?}", args); - Err(Status::new(1, output.stdout, output.stderr)) - } + ::std::eprintln!("[ERR] git {:?}", args); + Err(git::Status::new(1, output.stdout, output.stderr)) } } } diff --git a/src/stc.rs b/src/stc.rs index 0a33622..6d90186 100644 --- a/src/stc.rs +++ b/src/stc.rs @@ -1,23 +1,24 @@ -use crate::git::*; -use ::std::option::Option::{self, Some}; -use ::std::result::Result::{self, Err, Ok}; -use ::std::string::String; -use ::std::string::ToString; -use ::std::vec::Vec; - -pub struct STC { +use crate::git; +use ::std::{ + option::Option::{self, Some}, + result::Result::{self, Err, Ok}, + string::String, + vec::Vec, +}; + +pub struct Stc { git: G, } -impl STC { +impl Stc { pub fn new(git: G) -> Self { - STC { git } + Stc { git } } - pub fn init(&self) -> Result<(), Status> { + pub fn init(&self) -> Result<(), git::Status> { let g = &self.git; - g.config_add("transfer.hideRefs", STC_REF_PREFIX)?; - g.config_add("log.excludeDecoration", STC_REF_PREFIX)?; + g.config_add("transfer.hideRefs", git::STC_REF_PREFIX)?; + g.config_add("log.excludeDecoration", git::STC_REF_PREFIX)?; // TODO: read refs, branches, remotes // TODO: validate stc refs against branches @@ -27,10 +28,10 @@ impl STC { Ok(()) } - pub fn clean(&self) -> Result<(), Status> { + pub fn clean(&self) -> Result<(), git::Status> { let g = &self.git; - g.config_unset_pattern("transfer.hideRefs", STC_REF_PREFIX)?; - g.config_unset_pattern("log.excludeDecoration", STC_REF_PREFIX)?; + g.config_unset_pattern("transfer.hideRefs", git::STC_REF_PREFIX)?; + g.config_unset_pattern("log.excludeDecoration", git::STC_REF_PREFIX)?; // TODO: for each branch // TODO: ... check if fully merged @@ -41,10 +42,10 @@ impl STC { Ok(()) } - pub fn start(&self, name: String) -> Result<(), Status> { + pub fn start(&self, name: String) -> Result<(), git::Status> { let g = &self.git; let repo = g.snapshot()?; - let base_branch = repo.head().ok_or(Status::with(1))?; + let base_branch = repo.head().ok_or_else(|| git::Status::with(1))?; let new_name = g.check_branchname(&name)?; g.create_branch(&new_name, base_branch)?; g.switch_branch(&new_name)?; @@ -54,38 +55,42 @@ impl STC { "stc: base branch marker", )?; let base_refname = base_branch.refname(); - let base_ref = repo.get_ref(&base_refname).ok_or(Status::with(1))?; + let base_ref = repo + .get_ref(&base_refname) + .ok_or_else(|| git::Status::with(1))?; g.create_ref(&new_name.stc_start_refname(), &base_ref.objectname)?; Ok(()) } - pub fn push(&self) -> Result<(), Status> { + pub fn push(&self) -> Result<(), git::Status> { let g = &self.git; - let expected_commit: ObjectName; + let expected_commit: git::ObjectName; { let repo = g.snapshot()?; - let cur_branch = repo.head().ok_or(Status::with(1))?; + let cur_branch = repo.head().ok_or_else(|| git::Status::with(1))?; let stc_base_refname = cur_branch.stc_base_refname(); - let base_symref = repo.get_ref(&stc_base_refname).ok_or(Status::with(1))?; + let base_symref = repo + .get_ref(&stc_base_refname) + .ok_or_else(|| git::Status::with(1))?; let base_ref = repo .get_ref(&base_symref.symref_target) - .ok_or(Status::with(1))?; + .ok_or_else(|| git::Status::with(1))?; if let Some(remote_ref) = repo.get_ref(&cur_branch.stc_remote_refname()) { - // use ::std::borrow::ToOwned; - expected_commit = ObjectName::new(remote_ref.objectname.0.to_string()); - // TODO: clean clone + expected_commit = remote_ref.objectname.owning_clone(); } else { - expected_commit = ObjectName::new(NON_EXISTANT_OBJECT.to_string()); + expected_commit = git::NON_EXISTANT_OBJECT; } - g.push(&cur_branch, &base_ref.remote, &expected_commit)?; + g.push(cur_branch, &base_ref.remote, &expected_commit)?; } { let repo = g.snapshot()?; - let cur_branch = repo.head().ok_or(Status::with(1))?; + let cur_branch = repo.head().ok_or_else(|| git::Status::with(1))?; let cur_refname = cur_branch.refname(); - let cur_ref = repo.get_ref(&cur_refname).ok_or(Status::with(1))?; + let cur_ref = repo + .get_ref(&cur_refname) + .ok_or_else(|| git::Status::with(1))?; g.update_ref( &cur_branch.stc_remote_refname(), &cur_ref.objectname, @@ -96,16 +101,20 @@ impl STC { Ok(()) } - pub fn rebase(&self) -> Result<(), Status> { + pub fn rebase(&self) -> Result<(), git::Status> { let g = &self.git; let repo = g.snapshot()?; - let branch = repo.head().ok_or(Status::with(1))?; + let branch = repo.head().ok_or_else(|| git::Status::with(1))?; let stc_base_refname = branch.stc_base_refname(); let stc_start_refname = branch.stc_start_refname(); - let base_ref = repo.get_ref(&stc_base_refname).ok_or(Status::with(1))?; - let start_ref = repo.get_ref(&stc_start_refname).ok_or(Status::with(1))?; - g.rebase_onto(&branch)?; + let base_ref = repo + .get_ref(&stc_base_refname) + .ok_or_else(|| git::Status::with(1))?; + let start_ref = repo + .get_ref(&stc_start_refname) + .ok_or_else(|| git::Status::with(1))?; + g.rebase_onto(branch)?; g.update_ref( &branch.stc_start_refname(), &base_ref.objectname, @@ -115,7 +124,7 @@ impl STC { Ok(()) } - pub fn sync(&self) -> Result<(), Status> { + pub fn sync(&self) -> Result<(), git::Status> { let g = &self.git; g.fetch_all_prune()?; @@ -123,7 +132,7 @@ impl STC { Ok(()) } - pub fn fix(&self, branch: Option, base: Option) -> Result<(), Status> { + pub fn fix(&self, branch: Option, base: Option) -> Result<(), git::Status> { let g = &self.git; let repo = g.snapshot()?; @@ -134,7 +143,7 @@ impl STC { let base_branch = g.check_branchname(&base_branchname)?; if let Some(base_symref) = repo.get_ref(&branch.stc_base_refname()) { if base_symref.symref_target != base_branch.refname() { - return Err(Status::new( + return Err(git::Status::new( 1, Vec::::new(), "base branch already defined".as_bytes().to_vec(), @@ -154,7 +163,7 @@ impl STC { g.create_ref(&branch.stc_start_refname(), &forkpoint)?; } } else { - return Err(Status::new( + return Err(git::Status::new( 1, Vec::::new(), "base not specified".as_bytes().to_vec(), @@ -163,7 +172,7 @@ impl STC { } let repo = g.snapshot()?; - for branch in g.tracked_branches()? { + for branch in repo.tracked_branches() { if repo.get_ref(&branch.refname()).is_none() { if let Some(r) = repo.get_ref(&branch.stc_base_refname()) { g.delete_symref(&r.name)?; @@ -178,7 +187,7 @@ impl STC { } let repo = g.snapshot()?; - for branch in g.tracked_branches()? { + for branch in repo.tracked_branches() { // for each existing branch that's somehow still being tracked: let base_symref_name = branch.stc_base_refname(); let start_refname = branch.stc_start_refname(); diff --git a/tests/integration_test.rs b/tests/integration_test.rs new file mode 100644 index 0000000..5965551 --- /dev/null +++ b/tests/integration_test.rs @@ -0,0 +1,187 @@ +use ::assert_fs::{assert::PathAssert, fixture::PathChild}; +use ::phf::{phf_map, Map}; +use ::predicates::prelude::*; +use ::std::{ + env, + fs::File, + io::{Read, Write}, + path::{Path, PathBuf}, + process::{Command, Output, Stdio}, +}; + +// Make commits reproducible. +static TEST_ENV: Map<&'static str, &'static str> = phf_map! { + "GIT_CONFIG_GLOBAL" => "/dev/null", + "GIT_CONFIG_SYSTEM" => "/dev/null", + "GIT_AUTHOR_NAME" => "tester", + "GIT_AUTHOR_EMAIL" => "tester@example.com", + "GIT_AUTHOR_DATE" => "1600000000 +0000", + "GIT_COMMITTER_NAME" => "tester", + "GIT_COMMITTER_EMAIL" => "tester@example.com", + "GIT_COMMITTER_DATE" => "1600000000 +0000", +}; + +fn run_git(repo_dir: &Path, args: I) -> Output +where + I: IntoIterator, + S: AsRef<::std::ffi::OsStr>, +{ + let output = Command::new("git") + .args(args) + .current_dir(&repo_dir) + .env_clear() + .env("PATH", env::var("PATH").expect("$PATH not defined")) + .envs(TEST_ENV.entries()) + .stdin(Stdio::null()) + .output() + .expect("failed to run git command"); + assert!(output.status.success()); + output +} + +fn write_file(repo_dir: &Path, file_path: &str, content: &str) { + let mut f = File::create(repo_dir.join(file_path)).expect("cannot create file"); + f.write_all(content.as_bytes()) + .expect("cannot write content"); + f.flush().expect("cannot flush content"); +} + +fn read_file(p: &Path) -> String { + let mut buf = String::new(); + ::std::fs::File::open(p) + .expect("cannot open file") + .read_to_string(&mut buf) + .expect("cannot read file"); + buf +} + +fn new_stc_cmd(repo_dir: &Path) -> ::assert_cmd::Command { + let mut cmd = ::assert_cmd::Command::cargo_bin("stc").expect("cannot resolve stc binary"); + cmd.current_dir(repo_dir.to_path_buf()) + .env_clear() + .env("PATH", env::var("PATH").expect("$PATH not defined")) + .envs(TEST_ENV.entries()); + cmd +} + +#[allow(dead_code)] +struct TestRepo { + tempdir: ::assert_fs::TempDir, + origin_dir: PathBuf, + clone_dir: PathBuf, +} + +fn new_test_repo(persistent: bool) -> TestRepo { + let tempdir = ::assert_fs::TempDir::new() + .expect("cannot create test tempdir") + .into_persistent_if(persistent); + let origin_dir = tempdir.path().join("origin.git"); + let clone_dir = tempdir.path().join("clone"); + + run_git( + tempdir.path(), + [ + "init", + "--bare", + "--initial-branch=main", + origin_dir.as_os_str().to_str().unwrap(), + ], + ); + run_git( + tempdir.path(), + [ + "clone", + "--origin=origin", + origin_dir.as_os_str().to_str().unwrap(), + clone_dir.as_os_str().to_str().unwrap(), + ], + ); + + write_file(&clone_dir, "README.md", "# test repo\n"); + run_git(&clone_dir, ["add", "README.md"]); + run_git(&clone_dir, ["commit", "-m", "Initial commit"]); + run_git(&clone_dir, ["push", "origin"]); + + TestRepo { + tempdir, + origin_dir, + clone_dir, + } +} + +#[test] +fn test_stc_init() { + let repo = new_test_repo(false); + let mut stc = new_stc_cmd(&repo.clone_dir); + + stc.arg("init").assert().success(); + + // TODO: check init settings + // TODO: run again, check idempotency +} + +#[test] +fn test_stc_start() { + let repo = new_test_repo(false); + let mut stc = new_stc_cmd(&repo.clone_dir); + + repo.tempdir + .child("clone/.git/refs/stc") + .assert(predicate::path::missing()); + let main_ref = read_file(repo.tempdir.child("clone/.git/refs/heads/main").path()); + + stc.arg("start").arg("test-branch").assert().success(); + + repo.tempdir + .child("clone/.git/refs/stc/start/test-branch") + .assert(main_ref); + repo.tempdir + .child("clone/.git/refs/stc/base/test-branch") + .assert("ref: refs/heads/main\n"); + repo.tempdir + .child("clone/.git/refs/stc/remote/test-branch") + .assert(predicate::path::missing()); +} + +#[test] +fn test_stc_push() { + let repo = new_test_repo(false); + + { + let mut stc = new_stc_cmd(&repo.clone_dir); + stc.arg("start").arg("test-branch").assert().success(); + } + + write_file(&repo.clone_dir, "test-branch.txt", "test-branch #1\n"); + run_git(&repo.clone_dir, ["add", "test-branch.txt"]); + run_git(&repo.clone_dir, ["commit", "-m", "test-branch.txt #1"]); + + let branch_ref = read_file( + repo.tempdir + .child("clone/.git/refs/heads/test-branch") + .path(), + ); + + repo.tempdir + .child("clone/.git/refs/stc/remote/test-branch") + .assert(predicate::path::missing()); + + { + let mut stc = new_stc_cmd(&repo.clone_dir); + stc.arg("push").assert().success(); + } + + repo.tempdir + .child("clone/.git/refs/stc/remote/test-branch") + .assert(&branch_ref); + + { + // No-op. Test idempotency. + let mut stc = new_stc_cmd(&repo.clone_dir); + stc.arg("push").assert().success(); + } + + repo.tempdir + .child("clone/.git/refs/stc/remote/test-branch") + .assert(&branch_ref); +}