diff --git a/Cargo.lock b/Cargo.lock index 72920f45..42e94747 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,6 +72,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", + "getrandom", "once_cell", "version_check", "zerocopy", @@ -171,6 +172,12 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "arrayref" version = "0.3.8" @@ -365,6 +372,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bstr" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" +dependencies = [ + "memchr", + "regex-automata 0.4.8", + "serde", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -518,6 +536,21 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +[[package]] +name = "clru" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd0f76e066e64fdc5631e3bb46381254deab9ef1158292f27c8c57e3bf3fe59" + +[[package]] +name = "cmake" +version = "0.1.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c682c223677e0e5b6b7f63a64b9351844c3f1b1678a68b7ee617e30fb082620e" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" version = "1.0.1" @@ -623,6 +656,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.5" @@ -865,6 +907,12 @@ dependencies = [ "tokio", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "dyn-clone" version = "1.0.17" @@ -883,6 +931,15 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "entropy" version = "0.1.0" @@ -944,6 +1001,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "faster-hex" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183" +dependencies = [ + "serde", +] + [[package]] name = "fastrand" version = "2.1.1" @@ -981,6 +1047,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", + "libz-ng-sys", "miniz_oxide 0.8.0", ] @@ -1147,10 +1214,10 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", + "gix", "hipcheck-sdk", "jiff", "log", - "nom", "once_cell", "schemars", "semver", @@ -1193,6 +1260,644 @@ dependencies = [ "url", ] +[[package]] +name = "gix" +version = "0.68.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b04c66359b5e17f92395abc433861df0edf48f39f3f590818d1d7217327dd6a1" +dependencies = [ + "gix-actor", + "gix-attributes", + "gix-command", + "gix-commitgraph", + "gix-config", + "gix-date", + "gix-diff", + "gix-discover", + "gix-features", + "gix-filter", + "gix-fs", + "gix-glob", + "gix-hash", + "gix-hashtable", + "gix-ignore", + "gix-index", + "gix-lock", + "gix-object", + "gix-odb", + "gix-pack", + "gix-path", + "gix-pathspec", + "gix-ref", + "gix-refspec", + "gix-revision", + "gix-revwalk", + "gix-sec", + "gix-submodule", + "gix-tempfile", + "gix-trace", + "gix-traverse", + "gix-url", + "gix-utils", + "gix-validate", + "gix-worktree", + "once_cell", + "smallvec", + "thiserror 2.0.3", +] + +[[package]] +name = "gix-actor" +version = "0.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b24171f514cef7bb4dfb72a0b06dacf609b33ba8ad2489d4c4559a03b7afb3" +dependencies = [ + "bstr", + "gix-date", + "gix-utils", + "itoa", + "thiserror 2.0.3", + "winnow 0.6.18", +] + +[[package]] +name = "gix-attributes" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf9bf852194c0edfe699a2d36422d2c1f28f73b7c6d446c3f0ccd3ba232cadc" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-quote", + "gix-trace", + "kstring", + "smallvec", + "thiserror 2.0.3", + "unicode-bom", +] + +[[package]] +name = "gix-bitmap" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d48b897b4bbc881aea994b4a5bbb340a04979d7be9089791304e04a9fbc66b53" +dependencies = [ + "thiserror 2.0.3", +] + +[[package]] +name = "gix-chunk" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6ffbeb3a5c0b8b84c3fe4133a6f8c82fa962f4caefe8d0762eced025d3eb4f7" +dependencies = [ + "thiserror 2.0.3", +] + +[[package]] +name = "gix-command" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7d6b8f3a64453fd7e8191eb80b351eb7ac0839b40a1237cd2c137d5079fe53" +dependencies = [ + "bstr", + "gix-path", + "gix-trace", + "shell-words", +] + +[[package]] +name = "gix-commitgraph" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8da6591a7868fb2b6dabddea6b09988b0b05e0213f938dbaa11a03dd7a48d85" +dependencies = [ + "bstr", + "gix-chunk", + "gix-features", + "gix-hash", + "memmap2", + "thiserror 2.0.3", +] + +[[package]] +name = "gix-config" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6649b406ca1f99cb148959cf00468b231f07950f8ec438cc0903cda563606f19" +dependencies = [ + "bstr", + "gix-config-value", + "gix-features", + "gix-glob", + "gix-path", + "gix-ref", + "gix-sec", + "memchr", + "once_cell", + "smallvec", + "thiserror 2.0.3", + "unicode-bom", + "winnow 0.6.18", +] + +[[package]] +name = "gix-config-value" +version = "0.14.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aaeef5d98390a3bcf9dbc6440b520b793d1bf3ed99317dc407b02be995b28e" +dependencies = [ + "bitflags 2.6.0", + "bstr", + "gix-path", + "libc", + "thiserror 2.0.3", +] + +[[package]] +name = "gix-date" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "691142b1a34d18e8ed6e6114bc1a2736516c5ad60ef3aa9bd1b694886e3ca92d" +dependencies = [ + "bstr", + "itoa", + "jiff", + "thiserror 2.0.3", +] + +[[package]] +name = "gix-diff" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a327be31a392144b60ab0b1c863362c32a1c8f7effdfa2141d5d5b6b916ef3bf" +dependencies = [ + "bstr", + "gix-command", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-object", + "gix-path", + "gix-tempfile", + "gix-trace", + "gix-traverse", + "gix-worktree", + "imara-diff", + "thiserror 2.0.3", +] + +[[package]] +name = "gix-discover" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83bf6dfa4e266a4a9becb4d18fc801f92c3f7cc6c433dd86fdadbcf315ffb6ef" +dependencies = [ + "bstr", + "dunce", + "gix-fs", + "gix-hash", + "gix-path", + "gix-ref", + "gix-sec", + "thiserror 2.0.3", +] + +[[package]] +name = "gix-features" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d85d673f2e022a340dba4713bed77ef2cf4cd737d2f3e0f159d45e0935fd81f" +dependencies = [ + "crc32fast", + "crossbeam-channel", + "flate2", + "gix-hash", + "gix-trace", + "gix-utils", + "libc", + "once_cell", + "parking_lot 0.12.3", + "prodash", + "sha1", + "sha1_smol", + "thiserror 2.0.3", + "walkdir", +] + +[[package]] +name = "gix-filter" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5108cc58d58b27df10ac4de7f31b2eb96d588a33e5eba23739b865f5d8db7995" +dependencies = [ + "bstr", + "encoding_rs", + "gix-attributes", + "gix-command", + "gix-hash", + "gix-object", + "gix-packetline-blocking", + "gix-path", + "gix-quote", + "gix-trace", + "gix-utils", + "smallvec", + "thiserror 2.0.3", +] + +[[package]] +name = "gix-fs" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34740384d8d763975858fa2c176b68652a6fcc09f616e24e3ce967b0d370e4d8" +dependencies = [ + "fastrand", + "gix-features", + "gix-utils", +] + +[[package]] +name = "gix-glob" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf69a6bec0a3581567484bf99a4003afcaf6c469fd4214352517ea355cf3435" +dependencies = [ + "bitflags 2.6.0", + "bstr", + "gix-features", + "gix-path", +] + +[[package]] +name = "gix-hash" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5eccc17194ed0e67d49285e4853307e4147e95407f91c1c3e4a13ba9f4e4ce" +dependencies = [ + "faster-hex", + "thiserror 2.0.3", +] + +[[package]] +name = "gix-hashtable" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef65b256631078ef733bc5530c4e6b1c2e7d5c2830b75d4e9034ab3997d18fe" +dependencies = [ + "gix-hash", + "hashbrown 0.14.5", + "parking_lot 0.12.3", +] + +[[package]] +name = "gix-ignore" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b1fb24d2a4af0aa7438e2771d60c14a80cf2c9bd55c29cf1712b841f05bb8a" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-trace", + "unicode-bom", +] + +[[package]] +name = "gix-index" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "270645fd20556b64c8ffa1540d921b281e6994413a0ca068596f97e9367a257a" +dependencies = [ + "bitflags 2.6.0", + "bstr", + "filetime", + "fnv", + "gix-bitmap", + "gix-features", + "gix-fs", + "gix-hash", + "gix-lock", + "gix-object", + "gix-traverse", + "gix-utils", + "gix-validate", + "hashbrown 0.14.5", + "itoa", + "libc", + "memmap2", + "rustix", + "smallvec", + "thiserror 2.0.3", +] + +[[package]] +name = "gix-lock" +version = "15.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd3ab68a452db63d9f3ebdacb10f30dba1fa0d31ac64f4203d395ed1102d940" +dependencies = [ + "gix-tempfile", + "gix-utils", + "thiserror 2.0.3", +] + +[[package]] +name = "gix-object" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65d93e2bbfa83a307e47f45e45de7b6c04d7375a8bd5907b215f4bf45237d879" +dependencies = [ + "bstr", + "gix-actor", + "gix-date", + "gix-features", + "gix-hash", + "gix-hashtable", + "gix-utils", + "gix-validate", + "itoa", + "smallvec", + "thiserror 2.0.3", + "winnow 0.6.18", +] + +[[package]] +name = "gix-odb" +version = "0.65.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93bed6e1b577c25a6bb8e6ecbf4df525f29a671ddf5f2221821a56a8dbeec4e3" +dependencies = [ + "arc-swap", + "gix-date", + "gix-features", + "gix-fs", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-pack", + "gix-path", + "gix-quote", + "parking_lot 0.12.3", + "tempfile", + "thiserror 2.0.3", +] + +[[package]] +name = "gix-pack" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b91fec04d359544fecbb8e85117ec746fbaa9046ebafcefb58cb74f20dc76d4" +dependencies = [ + "clru", + "gix-chunk", + "gix-features", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-path", + "memmap2", + "smallvec", + "thiserror 2.0.3", + "uluru", +] + +[[package]] +name = "gix-packetline-blocking" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce9004ce1bc00fd538b11c1ec8141a1558fb3af3d2b7ac1ac5c41881f9e42d2a" +dependencies = [ + "bstr", + "faster-hex", + "gix-trace", + "thiserror 2.0.3", +] + +[[package]] +name = "gix-path" +version = "0.10.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc292ef1a51e340aeb0e720800338c805975724c1dfbd243185452efd8645b7" +dependencies = [ + "bstr", + "gix-trace", + "home", + "once_cell", + "thiserror 2.0.3", +] + +[[package]] +name = "gix-pathspec" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c472dfbe4a4e96fcf7efddcd4771c9037bb4fdea2faaabf2f4888210c75b81e" +dependencies = [ + "bitflags 2.6.0", + "bstr", + "gix-attributes", + "gix-config-value", + "gix-glob", + "gix-path", + "thiserror 2.0.3", +] + +[[package]] +name = "gix-quote" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a1e282216ec2ab2816cd57e6ed88f8009e634aec47562883c05ac8a7009a63" +dependencies = [ + "bstr", + "gix-utils", + "thiserror 2.0.3", +] + +[[package]] +name = "gix-ref" +version = "0.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1eae462723686272a58f49501015ef7c0d67c3e042c20049d8dd9c7eff92efde" +dependencies = [ + "gix-actor", + "gix-features", + "gix-fs", + "gix-hash", + "gix-lock", + "gix-object", + "gix-path", + "gix-tempfile", + "gix-utils", + "gix-validate", + "memmap2", + "thiserror 2.0.3", + "winnow 0.6.18", +] + +[[package]] +name = "gix-refspec" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00c056bb747868c7eb0aeb352c9f9181ab8ca3d0a2550f16470803500c6c413d" +dependencies = [ + "bstr", + "gix-hash", + "gix-revision", + "gix-validate", + "smallvec", + "thiserror 2.0.3", +] + +[[package]] +name = "gix-revision" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44488e0380847967bc3e3cacd8b22652e02ea1eb58afb60edd91847695cd2d8d" +dependencies = [ + "bitflags 2.6.0", + "bstr", + "gix-commitgraph", + "gix-date", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-revwalk", + "gix-trace", + "thiserror 2.0.3", +] + +[[package]] +name = "gix-revwalk" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "510026fc32f456f8f067d8f37c34088b97a36b2229d88a6a5023ef179fcb109d" +dependencies = [ + "gix-commitgraph", + "gix-date", + "gix-hash", + "gix-hashtable", + "gix-object", + "smallvec", + "thiserror 2.0.3", +] + +[[package]] +name = "gix-sec" +version = "0.10.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8b876ef997a955397809a2ec398d6a45b7a55b4918f2446344330f778d14fd6" +dependencies = [ + "bitflags 2.6.0", + "gix-path", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "gix-submodule" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2455f8c0fcb6ebe2a6e83c8f522d30615d763eb2ef7a23c7d929f9476e89f5c" +dependencies = [ + "bstr", + "gix-config", + "gix-path", + "gix-pathspec", + "gix-refspec", + "gix-url", + "thiserror 2.0.3", +] + +[[package]] +name = "gix-tempfile" +version = "15.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2feb86ef094cc77a4a9a5afbfe5de626897351bbbd0de3cb9314baf3049adb82" +dependencies = [ + "dashmap", + "gix-fs", + "libc", + "once_cell", + "parking_lot 0.12.3", + "tempfile", +] + +[[package]] +name = "gix-trace" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04bdde120c29f1fc23a24d3e115aeeea3d60d8e65bab92cc5f9d90d9302eb952" + +[[package]] +name = "gix-traverse" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff2ec9f779680f795363db1c563168b32b8d6728ec58564c628e85c92d29faf" +dependencies = [ + "bitflags 2.6.0", + "gix-commitgraph", + "gix-date", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-revwalk", + "smallvec", + "thiserror 2.0.3", +] + +[[package]] +name = "gix-url" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e09f97db3618fb8e473d7d97e77296b50aaee0ddcd6a867f07443e3e87391099" +dependencies = [ + "bstr", + "gix-features", + "gix-path", + "thiserror 2.0.3", + "url", +] + +[[package]] +name = "gix-utils" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba427e3e9599508ed98a6ddf8ed05493db114564e338e41f6a996d2e4790335f" +dependencies = [ + "fastrand", + "unicode-normalization", +] + +[[package]] +name = "gix-validate" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd520d09f9f585b34b32aba1d0b36ada89ab7fefb54a8ca3fe37fc482a750937" +dependencies = [ + "bstr", + "thiserror 2.0.3", +] + +[[package]] +name = "gix-worktree" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756dbbe15188fa22540d5eab941f8f9cf511a5364d5aec34c88083c09f4bea13" +dependencies = [ + "bstr", + "gix-attributes", + "gix-features", + "gix-fs", + "gix-glob", + "gix-hash", + "gix-ignore", + "gix-index", + "gix-object", + "gix-path", + "gix-validate", +] + [[package]] name = "glob" version = "0.3.1" @@ -1782,6 +2487,16 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "imara-diff" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc9da1a252bd44cd341657203722352efc9bc0c847d06ea6d2dc1cd1135e0a01" +dependencies = [ + "ahash", + "hashbrown 0.14.5", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -1936,6 +2651,15 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "kstring" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" +dependencies = [ + "static_assertions", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1986,6 +2710,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libz-ng-sys" +version = "1.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f0f7295a34685977acb2e8cc8b08ee4a8dffd6cf278eeccddbe1ed55ba815d5" +dependencies = [ + "cmake", + "libc", +] + [[package]] name = "libz-sys" version = "1.1.18" @@ -2128,6 +2862,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memmap2" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +dependencies = [ + "libc", +] + [[package]] name = "miette" version = "5.10.0" @@ -2393,6 +3136,16 @@ dependencies = [ "parking_lot_core 0.8.6", ] +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.10", +] + [[package]] name = "parking_lot_core" version = "0.8.6" @@ -2566,6 +3319,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prodash" +version = "29.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a266d8d6020c61a437be704c5e618037588e1985c7dbb7bf8d265db84cffe325" +dependencies = [ + "log", + "parking_lot 0.12.3", +] + [[package]] name = "prost" version = "0.13.3" @@ -2905,7 +3668,7 @@ dependencies = [ "lock_api", "log", "oorandom", - "parking_lot", + "parking_lot 0.11.2", "rustc-hash", "salsa-macros", "smallvec", @@ -3109,8 +3872,24 @@ dependencies = [ "cfg-if", "cpufeatures", "digest", + "sha1-asm", ] +[[package]] +name = "sha1-asm" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "286acebaf8b67c1130aedffad26f594eff0c1292389158135327d2e23aed582b" +dependencies = [ + "cc", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.10.8" @@ -3257,6 +4036,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" @@ -3838,6 +4623,21 @@ dependencies = [ "url", ] +[[package]] +name = "uluru" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c8a2469e56e6e5095c82ccd3afb98dad95f7af7929aab6d8ba8d6e0f73657da" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "unicode-bom" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" + [[package]] name = "unicode-ident" version = "1.0.13" diff --git a/hipcheck-common/src/chunk.rs b/hipcheck-common/src/chunk.rs index 6cd3dfa4..7d7967ab 100644 --- a/hipcheck-common/src/chunk.rs +++ b/hipcheck-common/src/chunk.rs @@ -223,9 +223,7 @@ mod test { let res = match chunk_with_size(query, 10) { Ok(r) => r, Err(e) => { - println!("{e}"); - assert!(false); - return; + panic!("{e}"); } }; assert_eq!(res.len(), 4); diff --git a/hipcheck/src/policy_exprs/mod.rs b/hipcheck/src/policy_exprs/mod.rs index 16094c92..9d654327 100644 --- a/hipcheck/src/policy_exprs/mod.rs +++ b/hipcheck/src/policy_exprs/mod.rs @@ -291,7 +291,7 @@ mod tests { let program = "(eq 3 (count (filter (gt 8.0) (foreach (sub 1.0) [1.0 2.0 10.0 20.0 30.0]))))"; let context = Value::Null; - let expr = parse(&program).unwrap(); + let expr = parse(program).unwrap(); println!("EXPR: {:?}", &expr); let expr = FunctionResolver::std().run(expr).unwrap(); let expr = TypeFixer::std().run(expr).unwrap(); @@ -352,13 +352,12 @@ mod tests { #[test] fn type_lambda() { let program = "(gt #t)"; - let expr = parse(&program).unwrap(); + let expr = parse(program).unwrap(); let expr = FunctionResolver::std().run(expr).unwrap(); let expr = TypeFixer::std().run(expr).unwrap(); let res_ty = TypeChecker::default().run(&expr); let Ok(Type::Lambda(l_ty)) = res_ty else { - assert!(false); - return; + panic!(); }; let ret_ty = l_ty.get_return_type(); assert_eq!(ret_ty, Ok(ReturnableType::Primitive(PrimitiveType::Bool))); @@ -368,7 +367,7 @@ mod tests { fn type_filter_bad_lambda_array() { // Should fail because can't compare ints and bools let program = "(filter (gt #t) [1 2])"; - let expr = parse(&program).unwrap(); + let expr = parse(program).unwrap(); let expr = FunctionResolver::std().run(expr).unwrap(); let expr = TypeFixer::std().run(expr).unwrap(); let res_ty = TypeChecker::default().run(&expr); @@ -386,7 +385,7 @@ mod tests { fn type_array_mixed_types() { // Should fail because array elts must have one primitive type let program = "(count [#t 2])"; - let mut expr = parse(&program).unwrap(); + let mut expr = parse(program).unwrap(); expr = FunctionResolver::std().run(expr).unwrap(); let res_ty = TypeChecker::default().run(&expr); assert_eq!( @@ -403,12 +402,11 @@ mod tests { fn type_propagate_unknown() { // Type for array should be unknown because we can't know ident type let program = "(max [])"; - let mut expr = parse(&program).unwrap(); + let mut expr = parse(program).unwrap(); expr = FunctionResolver::std().run(expr).unwrap(); let res_ty = TypeChecker::default().run(&expr); let Ok(Type::Function(f_ty)) = res_ty else { - assert!(false); - return; + panic!() }; assert_eq!(f_ty.get_return_type(), Ok(ReturnableType::Unknown)); } @@ -416,13 +414,12 @@ mod tests { #[test] fn type_not() { let program = "(not $)"; - let mut expr = parse(&program).unwrap(); + let mut expr = parse(program).unwrap(); expr = FunctionResolver::std().run(expr).unwrap(); let res_ty = TypeChecker::default().run(&expr); println!("RESTY: {res_ty:?}"); let Ok(Type::Function(f_ty)) = res_ty else { - assert!(false); - return; + panic!() }; let ret_ty = f_ty.get_return_type(); assert_eq!(ret_ty, Ok(ReturnableType::Primitive(PrimitiveType::Bool))); @@ -433,7 +430,7 @@ mod tests { let programs = vec!["(not $)", "(gt 0)", "(filter (gt 0) $/alpha)"]; for program in programs { - let mut expr = parse(&program).unwrap(); + let mut expr = parse(program).unwrap(); expr = FunctionResolver::std().run(expr).unwrap(); expr = TypeFixer::std().run(expr).unwrap(); let string = expr.to_string(); diff --git a/plugins/git/Cargo.toml b/plugins/git/Cargo.toml index 9bf3a43d..ae5bdf6b 100644 --- a/plugins/git/Cargo.toml +++ b/plugins/git/Cargo.toml @@ -9,10 +9,13 @@ publish = false [dependencies] anyhow = "1.0.91" clap = { version = "4.5.23", features = ["derive"] } +gix = { version = "0.68.0", default-features = false, features = [ + "basic", + "max-performance", +] } hipcheck-sdk = { path = "../../sdk/rust", features = ["macros"] } jiff = { version = "0.1.14", features = ["serde"] } log = "0.4.22" -nom = "7.1.3" once_cell = "1.10.0" schemars = { version = "0.8.21", features = ["url"] } semver = "1.0.9" diff --git a/plugins/git/src/data.rs b/plugins/git/src/data.rs index 7e475785..1d06da17 100644 --- a/plugins/git/src/data.rs +++ b/plugins/git/src/data.rs @@ -6,7 +6,6 @@ use serde::{Deserialize, Serialize}; use std::{ fmt::{self, Display, Formatter}, hash::Hash, - sync::Arc, }; /// A locally stored git repo, with optional additional details @@ -21,15 +20,44 @@ pub struct DetailedGitRepo { } /// Commits as they come directly out of `git log`. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct RawCommit { pub hash: String, pub author: Contributor, - pub written_on: Result, + pub written_on: Result, pub committer: Contributor, - pub committed_on: Result, + pub committed_on: Result, +} + +impl TryFrom> for RawCommit { + type Error = anyhow::Error; + + fn try_from(value: gix::Commit<'_>) -> Result { + let commit_author = value.author()?; + let author = Contributor { + name: commit_author.name.to_string(), + email: commit_author.email.to_string(), + }; + let written_on = + jiff::Timestamp::from_second(commit_author.time.seconds).map_err(|x| x.to_string()); + let commit_committer = value.committer()?; + let committer = Contributor { + name: commit_committer.name.to_string(), + email: commit_committer.email.to_string(), + }; + let committed_on = + jiff::Timestamp::from_second(commit_committer.time.seconds).map_err(|x| x.to_string()); + + Ok(Self { + hash: value.id().to_string(), + author, + written_on, + committer, + committed_on, + }) + } } /// Commits as understood in Hipcheck's data model. @@ -39,12 +67,20 @@ pub struct RawCommit { #[derive(Debug, Serialize, Clone, PartialEq, Eq, Hash, JsonSchema)] pub struct Commit { pub hash: String, - pub written_on: Result, - pub committed_on: Result, } +impl From for Commit { + fn from(value: RawCommit) -> Self { + Self { + hash: value.hash, + written_on: value.written_on.map(|x| x.to_string()), + committed_on: value.committed_on.map(|x| x.to_string()), + } + } +} + impl Display for Commit { fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!(f, "{}", self.hash) @@ -98,22 +134,18 @@ pub struct CommitDiff { pub diff: Diff, } +impl CommitDiff { + pub fn new(commit: Commit, diff: Diff) -> Self { + Self { commit, diff } + } +} + impl Display for CommitDiff { fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!( f, "{} +{} -{}", - self.commit, - self.diff - .additions - .map(|n| n.to_string()) - .as_deref() - .unwrap_or(""), - self.diff - .deletions - .map(|n| n.to_string()) - .as_deref() - .unwrap_or("") + self.commit, self.diff.additions, self.diff.deletions ) } } @@ -121,16 +153,39 @@ impl Display for CommitDiff { /// A set of changes in a commit. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] pub struct Diff { - pub additions: Option, - pub deletions: Option, + pub additions: i64, + pub deletions: i64, pub file_diffs: Vec, } /// A set of changes to a specific file in a commit. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] pub struct FileDiff { - pub file_name: Arc, - pub additions: Option, - pub deletions: Option, + pub file_name: String, + pub additions: i64, + pub deletions: i64, pub patch: String, } + +impl FileDiff { + pub fn new(file_name: String) -> Self { + Self { + file_name, + additions: 0, + deletions: 0, + patch: String::new(), + } + } + + pub fn increment_additions(&mut self, additions: i64) { + self.additions += additions + } + + pub fn increment_deletions(&mut self, deletions: i64) { + self.deletions += deletions + } + + pub fn set_patch(&mut self, patch_data: String) { + self.patch = patch_data; + } +} diff --git a/plugins/git/src/git.rs b/plugins/git/src/git.rs new file mode 100644 index 00000000..89290ef1 --- /dev/null +++ b/plugins/git/src/git.rs @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: Apache-2.0 + +use crate::data::*; + +use anyhow::Context; +use anyhow::Result; +use gix::bstr::ByteSlice; +use gix::diff::blob::intern::InternedInput; +use gix::diff::blob::sink::Counter; +use gix::diff::blob::sources::lines_with_terminator; +use gix::diff::blob::Algorithm; +use gix::diff::blob::UnifiedDiffBuilder; +use gix::object; +use gix::objs::tree::EntryKind; +use gix::revision::walk::Sorting; +use gix::revision::Walk; +use gix::traverse::commit::simple::CommitTimeOrder; +use gix::Repository; +use jiff::Timestamp; +use std::path::Path; +use std::sync::OnceLock; + +// a lot of the data related to commits can be derived from a Vec, so cache this and reuse it when possible +static ALL_RAW_COMMITS: OnceLock>> = OnceLock::new(); + +// initialize the repo so only one repository is used +fn initialize_repo

(repo_path: P) -> Result +where + P: AsRef, +{ + gix::discover(repo_path).context("failed to find repo") +} + +/// Retrieves an iterator that walks the repo's commits +/// +/// Commits are sorted by commit time and the newest commit (HEAD) is seen first +fn get_commit_walker(repo: &Repository) -> Result> { + let head_commit_id = repo.head_commit()?.id(); + let repo_walker = repo + .rev_walk(Some(head_commit_id)) + .sorting(Sorting::ByCommitTime(CommitTimeOrder::NewestFirst)) + .all()?; + Ok(repo_walker) +} + +/// Function to call on a commit in the repo to attempt to convert it to type `T` +type MapFn<'a, T> = &'a dyn Fn(&Repository, gix::Commit<'_>) -> Result; +/// Function to call on a commit to determine if iteration should be halted (if Ok(true) is returned) +type BreakNowFn<'a> = &'a dyn Fn(&gix::Commit<'_>) -> Result; + +/// Utility function for walking all of the commits in a git repo and running a function on each commit to generate some result and breaking out of the walk if `break_now` is true +fn walk_commits<'repo, T>( + repo: &'repo Repository, + repo_walker: Walk<'repo>, + transform_fn: MapFn, + break_now_fn: Option, +) -> Result> { + // since we are walking commit by commit, 5,000 was arbitrarily chosen to reduce allocations for small/medium repo sizes + let mut results = Vec::with_capacity(5_000); + for object in repo_walker { + let commit = object?.object()?; + if let Some(ref break_now_fn) = break_now_fn { + if let Ok(true) = break_now_fn(&commit) { + break; + } + } + let res = transform_fn(repo, commit)?; + results.push(res); + } + Ok(results) +} + +pub fn get_latest_commit(repo: &str) -> Result> { + let repo = initialize_repo(repo)?; + let mut commit_walker = get_commit_walker(&repo)?; + match commit_walker.next() { + Some(object) => { + let info = object?; + let commit = info.object()?; + let raw_commit = RawCommit::try_from(commit)?; + Ok(Some(raw_commit)) + } + None => Ok(None), + } +} + +/// Convert a `gix::Commit` into a `RawCommit` +fn get_raw_commit(_repo: &Repository, commit: gix::Commit) -> Result { + let raw_commit = RawCommit::try_from(commit)?; + Ok(raw_commit) +} + +/// get all of the `RawCommit` from a repo **sorted from newest to oldest** +/// +/// This contains a cache of all `RawCommit` to avoid needing to recompute this and should be used as the starting point for a +/// function if all of the desired data can be derived from all of the `RawCommit` in the repo +pub fn get_all_raw_commits

(repo: P) -> Result> +where + P: AsRef, +{ + let commits = ALL_RAW_COMMITS.get_or_init(|| { + let repo = initialize_repo(repo)?; + let commit_walker = get_commit_walker(&repo)?; + let commits = walk_commits(&repo, commit_walker, &get_raw_commit, None)?; + Ok(commits) + }); + match commits { + Ok(commits) => Ok(commits.clone()), + Err(e) => Err(anyhow::Error::msg(e.to_string())), + } +} + +pub fn get_commits_from_date(repo: &str, cutoff_date: Timestamp) -> Result> { + let raw_commits = get_all_raw_commits(repo)?; + let raw_commits_since_cutoff: Vec = raw_commits + .into_iter() + .filter(|raw_commit| { + if let Ok(commit_timestamp) = &raw_commit.committed_on { + return *commit_timestamp > cutoff_date; + } + false + }) + .collect(); + Ok(raw_commits_since_cutoff) +} + +fn diff_objects(old_object: Option<&str>, new_object: Option<&str>) -> Counter { + let input = InternedInput::new( + lines_with_terminator(old_object.unwrap_or_default()), + lines_with_terminator(new_object.unwrap_or_default()), + ); + gix::diff::blob::diff( + Algorithm::Myers, + &input, + Counter::new(UnifiedDiffBuilder::new(&input)), + ) +} + +fn get_diff(repo: &Repository, commit: gix::Commit) -> Result { + let current_tree = commit.tree()?; + let parent_tree = match commit.parent_ids().next() { + Some(id) => repo.find_commit(id)?.tree()?, + // if there is no parent, then this must be the first commit which can be represented with an empty tree + None => repo.empty_tree(), + }; + + let changes = repo.diff_tree_to_tree( + Some(&parent_tree), + Some(¤t_tree), + // this is recommended to increase performance! + gix::diff::Options::default().with_rewrites(None), + )?; + + let mut file_diffs = Vec::with_capacity(changes.len()); + + for change in changes { + let change_kind = EntryKind::from(change.entry_mode()); + // check to see if the given change is a file that we can diff + if !matches!(change_kind, EntryKind::Blob | EntryKind::BlobExecutable) { + continue; + } + match change { + object::tree::diff::ChangeDetached::Addition { + location, + relation: _, + entry_mode: _, + id, + } => { + let mut file_diff = FileDiff::new(location.to_string()); + let blob = repo.find_object(id)?; + let new_hunk = String::from_utf8_lossy(&blob.data); + + let diff = diff_objects(None, Some(&new_hunk)); + file_diff.increment_additions(diff.insertions as i64); + file_diff.set_patch(diff.wrapped); + file_diffs.push(file_diff); + } + object::tree::diff::ChangeDetached::Deletion { + location, + relation: _, + entry_mode: _, + id, + } => { + let mut file_diff = FileDiff::new(location.to_string()); + let object = repo.find_object(id)?; + let deleted_hunk = String::from_utf8_lossy(&object.data); + + let diff = diff_objects(Some(&deleted_hunk), None); + file_diff.increment_deletions(diff.removals as i64); + file_diff.set_patch(diff.wrapped); + file_diffs.push(file_diff); + } + object::tree::diff::ChangeDetached::Modification { + location, + previous_entry_mode: _, + previous_id, + entry_mode: _, + id, + } => { + let mut file_diff = FileDiff::new(location.to_string()); + let current_blob = repo.find_blob(id)?; + let previous_blob = repo.find_blob(previous_id)?; + let current_blob_data = current_blob.data.to_str_lossy(); + let previous_blob_data = previous_blob.data.to_str_lossy(); + + let diff = diff_objects(Some(&previous_blob_data), Some(¤t_blob_data)); + file_diff.increment_additions(diff.insertions as i64); + file_diff.increment_deletions(diff.removals as i64); + file_diff.set_patch(diff.wrapped); + file_diffs.push(file_diff); + } + // because we are not tracking rewrites (for performance reasons), this branch will never get hit + object::tree::diff::ChangeDetached::Rewrite { + source_location: _, + source_entry_mode: _, + source_relation: _, + source_id: _, + diff: _, + entry_mode: _, + id: _, + location: _, + relation: _, + copy: _, + } => {} + } + } + + let diff = Diff { + additions: file_diffs.iter().map(|x| x.additions).sum(), + deletions: file_diffs.iter().map(|x| x.deletions).sum(), + file_diffs, + }; + Ok(diff) +} + +pub fn get_diffs(repo: &str) -> Result> { + let repo = initialize_repo(repo)?; + let commit_walker = get_commit_walker(&repo)?; + walk_commits(&repo, commit_walker, &get_diff, None) +} + +/// Get all of the contributors (committers and authors) in a repo's history +pub fn get_contributors(repo: &str) -> Result> { + let commits = get_all_raw_commits(repo)?; + let mut contributors: Vec = commits + .into_iter() + .map(|raw_commit| [raw_commit.author, raw_commit.committer]) + .flat_map(|contributors| contributors.into_iter()) + .collect(); + contributors.sort(); + contributors.dedup(); + Ok(contributors) +} + +/// Get the `CommitDiff` for a commit +fn get_commit_diff(repo: &Repository, commit: gix::Commit) -> Result { + let raw_commit = get_raw_commit(repo, commit.clone())?; + let diff = get_diff(repo, commit)?; + Ok(CommitDiff::new(raw_commit.into(), diff)) +} + +pub fn get_commit_diffs(repo: &str) -> Result> { + let repo = initialize_repo(repo)?; + let commit_walker = get_commit_walker(&repo)?; + let commit_diffs = walk_commits(&repo, commit_walker, &get_commit_diff, None)?; + Ok(commit_diffs) +} diff --git a/plugins/git/src/main.rs b/plugins/git/src/main.rs index ab31f0d4..24c8cb45 100644 --- a/plugins/git/src/main.rs +++ b/plugins/git/src/main.rs @@ -3,18 +3,21 @@ //! Plugin containing secondary queries that return information about a Git repo to another query mod data; -mod parse; -mod util; +mod git; use crate::{ data::{ Commit, CommitContributor, CommitContributorView, CommitDiff, Contributor, ContributorView, DetailedGitRepo, Diff, RawCommit, }, - util::git_command::{get_commits, get_commits_from_date, get_diffs}, + git::{ + get_all_raw_commits, get_commit_diffs, get_commits_from_date, get_contributors, get_diffs, + get_latest_commit, + }, }; use clap::Parser; use hipcheck_sdk::{prelude::*, types::LocalGitRepo}; +use jiff::Timestamp; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -34,7 +37,7 @@ pub struct BatchGitRepo { /// Returns all raw commits extracted from the repository fn local_raw_commits(repo: LocalGitRepo) -> Result> { - get_commits(&repo.path).map_err(|e| { + get_all_raw_commits(&repo.path).map_err(|e| { log::error!("failed to get raw commits: {}", e); Error::UnspecifiedQueryState }) @@ -45,20 +48,20 @@ fn local_raw_commits(repo: LocalGitRepo) -> Result> { #[query] async fn last_commit_date(_engine: &mut PluginEngine, repo: LocalGitRepo) -> Result { let path = &repo.path; - let commits = get_commits(path).map_err(|e| { - log::error!("failed to get raw commits: {}", e); - Error::UnspecifiedQueryState - })?; - - let first = commits.first().ok_or_else(|| { - log::error!("no commits"); - Error::UnspecifiedQueryState - })?; - - first.written_on.clone().map_err(|e| { - log::error!("{}", e); - Error::UnspecifiedQueryState - }) + let last_commit = get_latest_commit(path)?; + match last_commit { + Some(commit) => match commit.written_on { + Ok(date) => Ok(date.to_string()), + Err(e) => { + log::error!("{}", e); + Err(Error::UnspecifiedQueryState) + } + }, + None => { + log::error!("no commits"); + Err(Error::UnspecifiedQueryState) + } + } } /// Returns all diffs extracted from the repository @@ -76,18 +79,11 @@ async fn diffs(_engine: &mut PluginEngine, repo: LocalGitRepo) -> Result Result> { let path = &repo.path; - let raw_commits = get_commits(path).map_err(|e| { + let raw_commits = get_all_raw_commits(path).map_err(|e| { log::error!("failed to get raw commits: {}", e); Error::UnspecifiedQueryState })?; - let commits = raw_commits - .iter() - .map(|raw| Commit { - hash: raw.hash.to_owned(), - written_on: raw.written_on.to_owned(), - committed_on: raw.committed_on.to_owned(), - }) - .collect(); + let commits = raw_commits.into_iter().map(Commit::from).collect(); Ok(commits) } @@ -101,24 +97,23 @@ async fn commits_from_date( ) -> Result> { let path = &repo.local.path; let date = match repo.details { - Some(date) => date, + Some(date) => Timestamp::from_str(&date).map_err(|e| { + log::error!("Failed to convert to jiff::Timestamp: {}", e); + Error::UnspecifiedQueryState + })?, None => { log::error!("No date provided"); return Err(Error::UnspecifiedQueryState); } }; // The called function will return an error if the date is not formatted correctly, so we do not need to check for ahead of time - let raw_commits_from_date = get_commits_from_date(path, &date).map_err(|e| { + let raw_commits_from_date = get_commits_from_date(path, date).map_err(|e| { log::error!("failed to get raw commits from date: {}", e); Error::UnspecifiedQueryState })?; let commits = raw_commits_from_date - .iter() - .map(|raw| Commit { - hash: raw.hash.to_owned(), - written_on: raw.written_on.to_owned(), - committed_on: raw.committed_on.to_owned(), - }) + .into_iter() + .map(Commit::from) .collect(); Ok(commits) @@ -128,50 +123,20 @@ async fn commits_from_date( #[query] async fn contributors(_engine: &mut PluginEngine, repo: LocalGitRepo) -> Result> { let path = &repo.path; - let raw_commits = get_commits(path).map_err(|e| { - log::error!("failed to get raw commits: {}", e); + let contributors = get_contributors(path).map_err(|e| { + log::error!("failed to get contributors: {}", e); Error::UnspecifiedQueryState })?; - - let mut contributors: Vec<_> = raw_commits - .iter() - .flat_map(|raw| [raw.author.to_owned(), raw.committer.to_owned()]) - .collect(); - - contributors.sort(); - contributors.dedup(); - Ok(contributors) } /// Returns all commit-diff pairs #[query] -async fn commit_diffs(engine: &mut PluginEngine, repo: LocalGitRepo) -> Result> { - let commits = commits(engine, repo.clone()).await.map_err(|e| { - log::error!("failed to get commits: {}", e); - Error::UnspecifiedQueryState - })?; - let diffs = diffs(engine, repo).await.map_err(|e| { - log::error!("failed to get diffs: {}", e); +async fn commit_diffs(_engine: &mut PluginEngine, repo: LocalGitRepo) -> Result> { + let commit_diffs = get_commit_diffs(&repo.path).map_err(|e| { + log::error!("Error finding commit diffs: {}", e); Error::UnspecifiedQueryState })?; - - if commits.len() != diffs.len() { - log::error!( - "parsed {} diffs but there are {} commits", - diffs.len(), - commits.len() - ); - return Err(Error::UnspecifiedQueryState); - } - - let commit_diffs = Iterator::zip(commits.iter(), diffs.iter()) - .map(|(commit, diff)| CommitDiff { - commit: commit.clone(), - diff: diff.clone(), - }) - .collect(); - Ok(commit_diffs) } @@ -185,7 +150,7 @@ async fn commits_for_contributor( let email = match repo.details { Some(ref email) => email.clone(), None => { - log::error!("No contributor e-maill address provided"); + log::error!("No contributor e-mail address provided"); return Err(Error::UnspecifiedQueryState); } }; @@ -238,7 +203,10 @@ async fn commits_for_contributor( }) } -use std::collections::{HashMap, HashSet}; +use std::{ + collections::{HashMap, HashSet}, + str::FromStr, +}; // Temporary query to call multiple commits_for_contributors() queries until we implement batching // TODO: Remove this query once batching works @@ -258,11 +226,7 @@ async fn batch_commits_for_contributor( })?; let commits: Vec = raw_commits .iter() - .map(|raw| Commit { - hash: raw.hash.to_owned(), - written_on: raw.written_on.to_owned(), - committed_on: raw.committed_on.to_owned(), - }) + .map(|raw| Commit::from(raw.clone())) .collect(); // @Assert - raw_commit and commits idxes correspond @@ -401,14 +365,10 @@ async fn batch_contributors_for_commit( .into_iter() .enumerate() .map(|(i, raw)| { - let commit = Commit { - hash: raw.hash.to_owned(), - written_on: raw.written_on.to_owned(), - committed_on: raw.committed_on.to_owned(), - }; - let author = raw.author; - let committer = raw.committer; - hash_to_idx.insert(raw.hash.clone(), i); + let author = raw.author.clone(); + let committer = raw.committer.clone(); + let commit = Commit::from(raw); + hash_to_idx.insert(commit.hash.clone(), i); CommitContributorView { commit, author, @@ -440,7 +400,7 @@ async fn commit_contributors( log::error!("failed to get contributors: {}", e); Error::UnspecifiedQueryState })?; - let raw_commits = get_commits(path).map_err(|e| { + let raw_commits = get_all_raw_commits(path).map_err(|e| { log::error!("failed to get raw commits: {}", e); Error::UnspecifiedQueryState })?; @@ -502,36 +462,3 @@ async fn main() -> Result<()> { let args = Args::try_parse().unwrap(); PluginServer::register(GitPlugin {}).listen(args.port).await } - -#[cfg(test)] -mod test { - #[test] - fn test_no_newline_before_end_of_chunk() { - let input = "diff --git a/plugins/review/plugin.kdl b/plugins/review/plugin.kdl\nindex 83f0355..9fa8e47 100644\n--- a/plugins/review/plugin.kdl\n+++ b/plugins/review/plugin.kdl\n@@ -6,4 +6,4 @@ entrypoint {\n- on arch=\"aarch64-apple-darwin\" \"./hc-mitre-review\"\n- on arch=\"x86_64-apple-darwin\" \"./hc-mitre-review\"\n- on arch=\"x86_64-unknown-linux-gnu\" \"./hc-mitre-review\"\n- on arch=\"x86_64-pc-windows-msvc\" \"./hc-mitre-review\"\n+ on arch=\"aarch64-apple-darwin\" \"./target/debug/review_sdk\"\n+ on arch=\"x86_64-apple-darwin\" \"./target/debug/review_sdk\"\n+ on arch=\"x86_64-unknown-linux-gnu\" \"./target/debug/review_sdk\"\n+ on arch=\"x86_64-pc-windows-msvc\" \"./target/debug/review_sdk\"\n@@ -14 +14 @@ dependencies {\n-}\n\\ No newline at end of file\n+}\n"; - - let (leftover, _parsed) = crate::parse::patch(input).unwrap(); - assert!(leftover.is_empty()); - } - - #[test] - fn test_hyphens_in_diff_stats() { - let input = "0\t4\tsite/content/_index.md\n136\t2\tsite/content/install/_index.md\n-\t-\tsite/static/images/homepage-bg.png\n2\t2\tsite/tailwind.config.js\n2\t0\tsite/templates/bases/base.tera.html\n82\t1\tsite/templates/index.html\n3\t3\tsite/templates/shortcodes/info.html\n15\t14\txtask/src/task/site/serve.rs\n"; - let (leftover, _) = crate::parse::stats(input).unwrap(); - assert!(leftover.is_empty()); - } - - #[test] - fn test_patch_with_only_meta() { - let input = "diff --git a/hipcheck/src/analysis/session/spdx.rs b/hipcheck/src/session/spdx.rs\nsimilarity index 100%\nrename from hipcheck/src/analysis/session/spdx.rs\nrename to hipcheck/src/session/spdx.rs\n"; - let (leftover, _) = crate::parse::patch(input).unwrap(); - assert!(leftover.is_empty()); - } - - #[test] - fn test_patch_without_triple_plus_minus() { - let input = "~~~\n\n0\t0\tmy_test_.py\n\ndiff --git a/my_test_.py b/my_test_.py\ndeleted file mode 100644\nindex e69de29bb2..0000000000\n~~~\n\n33\t3\tnumpy/_core/src/umath/string_fastsearch.h\n\ndiff --git a/numpy/_core/src/umath/string_fastsearch.h b/numpy/_core/src/umath/string_fastsearch.h\nindex 2a778bb86f..1f2d47e8f1 100644\n--- a/numpy/_core/src/umath/string_fastsearch.h\n+++ b/numpy/_core/src/umath/string_fastsearch.h\n@@ -35,0 +36 @@\n+ * @internal\n"; - let (leftover, diffs) = crate::parse::diffs(input).unwrap(); - assert!(leftover.is_empty()); - assert!(diffs.len() == 2); - } -} diff --git a/plugins/git/src/parse.rs b/plugins/git/src/parse.rs deleted file mode 100644 index 6ce4b1aa..00000000 --- a/plugins/git/src/parse.rs +++ /dev/null @@ -1,1276 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#![allow(dead_code)] - -use crate::data::{Contributor, Diff, FileDiff, RawCommit}; -use anyhow::{Context as _, Error, Result}; -use jiff::Timestamp; -use nom::{ - branch::alt, - bytes::complete::tag, - character::complete::{char as character, digit1, newline, not_line_ending, one_of, space1}, - combinator::{map, opt, peek, recognize}, - error::{Error as NomError, ErrorKind}, - multi::{fold_many0, many0, many1, many_m_n}, - sequence::{preceded, terminated, tuple}, - IResult, -}; -use std::{iter::Iterator, result::Result as StdResult, sync::Arc}; - -const HEX_CHARS: &str = "0123456789abcdef"; -const GIT_HASH_MIN_LEN: usize = 5; -const GIT_HASH_MAX_LEN: usize = 40; - -/// Parse a complete git log. -pub fn git_log(input: &str) -> Result> { - let (_, commits) = commits(input) - .map_err(|e| Error::msg(e.to_string())) - .context("can't parse git log")?; - log::trace!("parsed git commits [commits='{:#?}']", commits); - Ok(commits) -} - -/// Parse a complete set of git diffs. -pub fn git_diff(input: &str) -> Result> { - let (_, diffs) = diffs(input) - .map_err(|e| Error::msg(e.to_string())) - .context("can't parse git diff")?; - log::trace!("parsed git diffs [diffs='{:#?}']", diffs); - Ok(diffs) -} - -/// Parse a complete set of GitHub diffs. -pub fn github_diff(input: &str) -> Result> { - let (_, diffs) = gh_diffs(input) - .map_err(|e| Error::msg(e.to_string())) - .context("can't parse GitHub diff")?; - log::trace!("parsed GitHub diffs [diffs='{:#?}']", diffs); - Ok(diffs) -} - -fn hash(input: &str) -> IResult<&str, &str> { - recognize(many_m_n(GIT_HASH_MIN_LEN, GIT_HASH_MAX_LEN, hex_char))(input) -} - -fn hex_char(input: &str) -> IResult<&str, char> { - one_of(HEX_CHARS)(input) -} - -fn date(input: &str) -> StdResult { - let ts: StdResult = input.parse().map_err(|e| { - format!( - "Could not parse git commit timestamp as RFC3339: '{}'\ - \nCaused by: {}", - input, e - ) - }); - ts.map(|t| t.to_string()) -} - -fn commit(input: &str) -> IResult<&str, RawCommit> { - let (input, hash_str) = line(input)?; - let (input, author_name) = line(input)?; - let (input, author_email) = line(input)?; - let (input, written_on_str) = line(input)?; - let (input, committer_name) = line(input)?; - let (input, committer_email) = line(input)?; - let (input, committed_on_str) = line(input)?; - // At one point our `git log` invocation was configured - // to return GPG key info, but that was leading to errors - // with format and GPG key validation, so we removed it - // from the print specifier - - // There is always an empty line here; ignore it - let (input, _empty_line) = line(input)?; - - let (_, hash) = hash(hash_str).map_err(|e| { - log::error!("failed to parse git commit hash [err='{}']", e); - e - })?; - - let written_on = date(written_on_str); - let committed_on = date(committed_on_str); - - let short_hash = &hash[..8]; - - if let Err(e) = &written_on { - log::error!( - "git commit has invalid written_on timestamp [commit={}, error=\"{}\"]", - short_hash, - e - ); - } - - if let Err(e) = &committed_on { - log::error!( - "git commit has invalid committed_on timestamp [commit={}, error=\"{}\"]", - short_hash, - e - ); - } - - let commit = RawCommit { - hash: hash.to_owned(), - author: Contributor { - name: author_name.to_owned(), - email: author_email.to_owned(), - }, - written_on, - committer: Contributor { - name: committer_name.to_owned(), - email: committer_email.to_owned(), - }, - committed_on, - }; - - Ok((input, commit)) -} - -fn commits(input: &str) -> IResult<&str, Vec> { - many0(commit)(input) -} - -fn line_ending(input: &str) -> IResult<&str, &str> { - recognize(alt(( - recognize(character('\n')), - recognize(tuple((character('\r'), character('\n')))), - )))(input) -} - -fn line(input: &str) -> IResult<&str, &str> { - terminated(not_line_ending, line_ending)(input) -} - -fn num(input: &str) -> IResult<&str, i64> { - digit1(input).map(|(input, output)| { - // Unwrap here is fine because we know it's only going to be - // a bunch of digits. Overflow is possible but we're choosing - // not to worry about it for now, because if a commit is large - // enough that the number of lines added or deleted - // in a single file overflows an i64 we have bigger problems. - (input, output.parse().unwrap()) - }) -} - -fn num_or_dash(input: &str) -> IResult<&str, Option> { - let some_num = map(num, Some); - let dash = map(character('-'), |_| None); - alt((some_num, dash))(input) -} - -fn stat(input: &str) -> IResult<&str, Option>> { - tuple((num_or_dash, space1, num_or_dash, space1, line))(input).map( - |(i, (lines_added, _, lines_deleted, _, file_name))| { - let Some(lines_added) = lines_added else { - return (i, None); - }; - - let Some(lines_deleted) = lines_deleted else { - return (i, None); - }; - - let stat = Stat { - lines_added, - lines_deleted, - file_name, - }; - - (i, Some(stat)) - }, - ) -} - -pub(crate) fn stats(input: &str) -> IResult<&str, Vec>> { - map(many0(stat), |vec| { - vec.into_iter().flatten().collect::>() - })(input) -} - -pub(crate) fn opt_rest_diff_header(input: &str) -> IResult<&str, Diff> { - opt(tuple((newline, diff)))(input).map(|(i, x)| { - if let Some((_, d)) = x { - (i, d) - } else { - ( - i, - Diff { - additions: None, - deletions: None, - file_diffs: vec![], - }, - ) - } - }) -} - -// Some empty commits have no output in the corresponding `git log` command, so we had to add a -// special header to be able to parse and recognize empty diffs and thus make the number of diffs -// and commits equal -pub(crate) fn diff_header(input: &str) -> IResult<&str, Diff> { - tuple((tag("~~~\n"), opt_rest_diff_header))(input).map(|(i, (_, diff))| (i, diff)) -} - -pub(crate) fn diff(input: &str) -> IResult<&str, Diff> { - log::trace!("input is {:#?}", input); - tuple((stats, line, patches))(input).map(|(i, (stats, _, patches))| { - log::trace!("patches are {:#?}", patches); - let mut additions = Some(0); - let mut deletions = Some(0); - - let file_diffs = Iterator::zip(stats.into_iter(), patches) - .map(|(stat, patch)| { - log::trace!( - "stat is {:#?} added and {:#?} deleted", - stat.lines_added, - stat.lines_deleted - ); - additions = additions.map(|a| a + stat.lines_added); - deletions = deletions.map(|d| d + stat.lines_deleted); - - FileDiff { - file_name: Arc::new(stat.file_name.to_owned()), - additions: Some(stat.lines_added), - deletions: Some(stat.lines_deleted), - patch, - } - }) - .collect::>(); - - let diff = Diff { - additions, - deletions, - file_diffs, - }; - - (i, diff) - }) -} - -fn gh_diff(input: &str) -> IResult<&str, Diff> { - // Handle reaching the end of the diff text without causing map0 to error - if input.is_empty() { - return Err(nom::Err::Error(NomError::new(input, ErrorKind::Many0))); - } - - patches_with_context(input).map(|(i, patches)| { - log::trace!("patches are {:#?}", patches); - - // GitHub diffs don't provide these. - let additions = None; - let deletions = None; - - let file_diffs = patches - .into_iter() - .map(|patch| FileDiff { - file_name: Arc::new(patch.file_name), - additions: None, - deletions: None, - patch: patch.content, - }) - .collect(); - log::trace!("file_diffs are {:#?}", file_diffs); - - let diff = Diff { - additions, - deletions, - file_diffs, - }; - log::trace!("diff is {:#?}", diff); - - (i, diff) - }) -} - -pub(crate) fn diffs(input: &str) -> IResult<&str, Vec> { - many0(diff_header)(input) -} - -fn gh_diffs(input: &str) -> IResult<&str, Vec> { - log::trace!("input is {}", input); - many0(gh_diff)(input) -} - -fn meta(input: &str) -> IResult<&str, &str> { - recognize(tuple((single_alpha, line)))(input) -} - -pub(crate) fn metas(input: &str) -> IResult<&str, Vec<&str>> { - many1(meta)(input) -} - -fn single_alpha(input: &str) -> IResult<&str, &str> { - recognize(one_of( - "qwertyuioplokjhgfdsazxcvbnmQWERTYUIOPLOKJHGFDSAZXCVBNM", - ))(input) -} - -fn triple_plus_minus_line(input: &str) -> IResult<&str, &str> { - recognize(tuple((alt((tag("+++"), tag("---"))), line)))(input) -} - -pub(crate) fn patch_header(input: &str) -> IResult<&str, &str> { - recognize(tuple(( - metas, - opt(triple_plus_minus_line), - opt(triple_plus_minus_line), - )))(input) -} - -fn chunk_prefix(input: &str) -> IResult<&str, &str> { - recognize(one_of("+-\\"))(input) -} - -fn line_with_ending(input: &str) -> IResult<&str, &str> { - recognize(tuple((not_line_ending, line_ending)))(input) -} - -fn chunk_line(input: &str) -> IResult<&str, &str> { - preceded(chunk_prefix, line_with_ending)(input) -} - -fn chunk_body(input: &str) -> IResult<&str, String> { - fold_many0(chunk_line, String::new, |mut patch, line| { - if line == " No newline at end of file\n" { - return patch; - } - - patch.push_str(line); - patch - })(input) -} - -fn chunk_header(input: &str) -> IResult<&str, &str> { - recognize(tuple((peek(character('@')), line)))(input) -} - -fn chunk(input: &str) -> IResult<&str, String> { - preceded(chunk_header, chunk_body)(input) -} - -fn chunks(input: &str) -> IResult<&str, String> { - fold_many0(chunk, String::new, |mut patch, line| { - patch.push_str(&line); - patch - })(input) -} - -fn no_newline(input: &str) -> IResult<&str, &str> { - recognize(tuple((peek(character('\\')), line)))(input) -} - -fn patch_footer(input: &str) -> IResult<&str, Option<&str>> { - opt(no_newline)(input) -} - -pub(crate) fn patch(input: &str) -> IResult<&str, String> { - tuple((patch_header, opt(chunks), patch_footer))(input) - .map(|(i, (_, chunks, _))| (i, chunks.unwrap_or_else(String::new))) -} - -fn gh_meta(input: &str) -> IResult<&str, &str> { - recognize(tuple((single_alpha, line)))(input) -} - -fn gh_metas(input: &str) -> IResult<&str, Vec<&str>> { - many1(gh_meta)(input) -} - -fn gh_patch_header(input: &str) -> IResult<&str, &str> { - recognize(tuple((gh_metas, line, line)))(input) -} - -fn chunk_line_with_context(input: &str) -> IResult<&str, &str> { - recognize(tuple((not_line_ending, line_ending)))(input).and_then(|(i, parsed)| { - if parsed.starts_with("diff --git") { - Err(nom::Err::Error(NomError::new(i, ErrorKind::Many0))) - } else { - Ok((i, parsed)) - } - }) -} - -fn chunk_body_with_context(input: &str) -> IResult<&str, String> { - fold_many0(chunk_line_with_context, String::new, |mut patch, line| { - if line.starts_with('+') || line.starts_with('-') { - // Omit the first character. - patch.push_str(&line[1..]); - } - - patch - })(input) -} - -fn chunk_with_context(input: &str) -> IResult<&str, String> { - preceded(chunk_header, chunk_body_with_context)(input) -} - -fn chunks_with_context(input: &str) -> IResult<&str, String> { - fold_many0(chunk_with_context, String::new, |mut patch, line| { - patch.push_str(&line); - patch - })(input) -} - -fn patch_with_context(input: &str) -> IResult<&str, GhPatch> { - tuple((gh_patch_header, chunks_with_context, patch_footer))(input).map( - |(i, (header, content, _))| { - let file_name = file_name_from_header(header); - - let gh_patch = GhPatch { file_name, content }; - - (i, gh_patch) - }, - ) -} - -fn file_name_from_header(header: &str) -> String { - let uf = ""; - - // Extract the file name from a known-valid diff header. - // - // Example: diff --git a/README.md b/README.md - header - .split_whitespace() - .nth(3) - .unwrap_or(uf) - .strip_prefix("b/") - .unwrap_or(uf) - .trim() - .into() -} - -fn patches_with_context(input: &str) -> IResult<&str, Vec> { - many0(patch_with_context)(input) -} - -fn patches(input: &str) -> IResult<&str, Vec> { - many0(patch)(input) -} - -#[derive(Debug)] -struct GhPatch { - file_name: String, - content: String, -} - -pub struct Stat<'a> { - pub lines_added: i64, - pub lines_deleted: i64, - pub file_name: &'a str, -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn parse_diff_header() { - let input = "\ -~~~\n\ -~~~\n\ -\n\ -1\t0\trequirements/test_requirements.txt\n\ -\n\ -diff --git a/requirements/test_requirements.txt b/requirements/test_requirements.txt\n\ -index 4e53f86d35..856ecf115e 100644\n\ ---- a/requirements/test_requirements.txt\n\ -+++ b/requirements/test_requirements.txt\n\ -@@ -7,0 +8 @@ pytest==7.4.0\n\ -+scipy-doctest\n"; - let (leftover, diffs) = diffs(input).unwrap(); - assert!(leftover.is_empty()); - assert_eq!(diffs.len(), 2); - assert!(diffs.get(0).unwrap().file_diffs.is_empty()); - assert!(!(diffs.get(1).unwrap().file_diffs.is_empty())); - } - - #[test] - fn parse_stat() { - let line = "7 0 Cargo.toml\n"; - - let (remaining, stat) = stat(line).unwrap(); - let stat = stat.unwrap(); - - assert_eq!("", remaining); - assert_eq!(7, stat.lines_added); - assert_eq!(0, stat.lines_deleted); - assert_eq!("Cargo.toml", stat.file_name); - } - - #[test] - fn parse_stats() { - let input = "\ -7 0 Cargo.toml\n\ -18 0 README.md\n\ -3 0 src/main.rs\n"; - - let (remaining, stats) = stats(input).unwrap(); - - assert_eq!("", remaining); - - assert_eq!(7, stats[0].lines_added); - assert_eq!(0, stats[0].lines_deleted); - assert_eq!("Cargo.toml", stats[0].file_name); - - assert_eq!(18, stats[1].lines_added); - assert_eq!(0, stats[1].lines_deleted); - assert_eq!("README.md", stats[1].file_name); - - assert_eq!(3, stats[2].lines_added); - assert_eq!(0, stats[2].lines_deleted); - assert_eq!("src/main.rs", stats[2].file_name); - } - - #[test] - fn parse_patch_header() { - let input = "\ -diff --git a/src/main.rs b/src/main.rs\n\ -new file mode 100644\n\ -index 0000000..e7a11a9\n\ ---- /dev/null\n\ -+++ b/src/main.rs\n"; - - let (remaining, header) = patch_header(input).unwrap(); - - assert_eq!("", remaining); - assert_eq!(input, header); - } - - #[test] - fn parse_patches() { - let input = "\ -diff --git a/src/main.rs b/src/main.rs\n\ -new file mode 100644\n\ -index 0000000..e7a11a9\n\ ---- /dev/null\n\ -+++ b/src/main.rs\n\ -@@ -0,0 +1,116 @@\n\ -+use clap::{Arg, App, SubCommand};\n\ -+use serde::{Serialize, Deserialize};\n\ -@@ -0,0 +1,116 @@\n\ -+use clap::{Arg, App, SubCommand};\n\ -+use serde::{Serialize, Deserialize};\n\ -diff --git a/src/main.rs b/src/main.rs\n\ -new file mode 100644\n\ -index 0000000..e7a11a9\n\ ---- /dev/null\n\ -+++ b/src/main.rs\n\ -@@ -0,0 +1,116 @@\n\ -+use clap::{Arg, App, SubCommand};\n\ -+use serde::{Serialize, Deserialize};\n\ -@@ -0,0 +1,116 @@\n\ -+use clap::{Arg, App, SubCommand};\n\ -+use serde::{Serialize, Deserialize};\n"; - - let expected_1 = "\ -use clap::{Arg, App, SubCommand};\n\ -use serde::{Serialize, Deserialize};\n\ -use clap::{Arg, App, SubCommand};\n\ -use serde::{Serialize, Deserialize};\n"; - - let expected_2 = "\ -use clap::{Arg, App, SubCommand};\n\ -use serde::{Serialize, Deserialize};\n\ -use clap::{Arg, App, SubCommand};\n\ -use serde::{Serialize, Deserialize};\n"; - - let (remaining, patches) = patches(input).unwrap(); - - assert_eq!("", remaining); - assert_eq!(expected_1, patches[0]); - assert_eq!(expected_2, patches[1]); - } - - #[test] - fn parse_patch() { - let input = "\ -diff --git a/src/main.rs b/src/main.rs\n\ -new file mode 100644\n\ -index 0000000..e7a11a9\n\ ---- /dev/null\n\ -+++ b/src/main.rs\n\ -@@ -0,0 +1,116 @@\n\ -+use clap::{Arg, App, SubCommand};\n\ -+use serde::{Serialize, Deserialize};\n\ -@@ -0,0 +1,116 @@\n\ -+use clap::{Arg, App, SubCommand};\n\ -+use serde::{Serialize, Deserialize};\n"; - - let expected = "\ -use clap::{Arg, App, SubCommand};\n\ -use serde::{Serialize, Deserialize};\n\ -use clap::{Arg, App, SubCommand};\n\ -use serde::{Serialize, Deserialize};\n"; - - let (remaining, patch) = patch(input).unwrap(); - - assert_eq!("", remaining); - assert_eq!(expected, patch); - } - - #[test] - fn parse_chunks() { - let input = "\ -@@ -0,0 +1,116 @@\n\ -+use clap::{Arg, App, SubCommand};\n\ -+use serde::{Serialize, Deserialize};\n\ -@@ -0,0 +1,116 @@\n\ -+use clap::{Arg, App, SubCommand};\n\ -+use serde::{Serialize, Deserialize};\n"; - - let expected = "\ -use clap::{Arg, App, SubCommand};\n\ -use serde::{Serialize, Deserialize};\n\ -use clap::{Arg, App, SubCommand};\n\ -use serde::{Serialize, Deserialize};\n"; - - let (remaining, patch) = chunks(input).unwrap(); - - assert_eq!("", remaining); - assert_eq!(expected, patch); - } - - #[test] - fn parse_chunk() { - let input = "\ -@@ -0,0 +1,116 @@\n\ -+use clap::{Arg, App, SubCommand};\n\ -+use serde::{Serialize, Deserialize};\n"; - - let expected = "\ -use clap::{Arg, App, SubCommand};\n\ -use serde::{Serialize, Deserialize};\n"; - - let (remaining, patch) = chunk(input).unwrap(); - - assert_eq!("", remaining); - assert_eq!(expected, patch); - } - - #[test] - fn parse_chunk_header() { - let input = "@@ -0,0 +1,116 @@\n"; - let expected = "@@ -0,0 +1,116 @@\n"; - - let (remaining, header) = chunk_header(input).unwrap(); - - assert_eq!("", remaining); - assert_eq!(expected, header); - } - - #[test] - fn parse_chunk_body() { - let input = "\ -+use clap::{Arg, App, SubCommand};\n\ -+use serde::{Serialize, Deserialize};\n"; - - let expected = "\ -use clap::{Arg, App, SubCommand};\n\ -use serde::{Serialize, Deserialize};\n"; - - let (remaining, body) = chunk_body(input).unwrap(); - - assert_eq!("", remaining); - assert_eq!(expected, body); - } - - #[test] - fn parse_chunk_line() { - let input = "+use clap::{Arg, App, SubCommand};\n"; - let expected = "use clap::{Arg, App, SubCommand};\n"; - - let (remaining, line) = chunk_line(input).unwrap(); - - assert_eq!("", remaining); - assert_eq!(expected, line); - } - - #[test] - fn parse_plus_or_minus() { - let input_plus = "+"; - let expected_plus = "+"; - - let (remaining, c) = chunk_prefix(input_plus).unwrap(); - assert_eq!("", remaining); - assert_eq!(expected_plus, c); - - let input_minus = "-"; - let expected_minus = "-"; - let (remaining, c) = chunk_prefix(input_minus).unwrap(); - assert_eq!("", remaining); - assert_eq!(expected_minus, c); - } - - #[test] - fn parse_line_with_ending() { - let input = "use clap::{Arg, App, SubCommand};\n"; - let expected = "use clap::{Arg, App, SubCommand};\n"; - - let (remaining, line) = line_with_ending(input).unwrap(); - assert_eq!("", remaining); - assert_eq!(expected, line); - } - - #[test] - fn parse_diff() { - let input = r#"10 0 .gitignore -4 0 Cargo.toml -127 1 src/main.rs - -diff --git a/.gitignore b/.gitignore -new file mode 100644 -index 0000000..50c8301 ---- /dev/null -+++ b/.gitignore -@@ -0,0 +1,10 @@ -+# Generated by Cargo -+# will have compiled files and executables -+/target/ -+ -+# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -+# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -+Cargo.lock -+ -+# These are backup files generated by rustfmt -+**/*.rs.bk -\ No newline at end of file -diff --git a/Cargo.toml b/Cargo.toml -index 191135b..d91dabb 100644 ---- a/Cargo.toml -+++ b/Cargo.toml -@@ -7,0 +8,4 @@ edition = "2018" -+clap = "2.33.0" -+petgraph = "0.4.13" -+serde = { version = "1.0.91", features = ["derive"] } -+serde_json = "1.0.39" -\ No newline at end of file -diff --git a/src/main.rs b/src/main.rs -index e7a11a9..4894a2e 100644 ---- a/src/main.rs -+++ b/src/main.rs -@@ -0,0 +1,116 @@ -+use clap::{Arg, App, SubCommand}; -+use serde::{Serialize, Deserialize}; -+// use petgraph::{Graph, Directed}; -+// use std::collections::Vec; -+use std::process::Command; -+use std::str; -+ -+// 1. Check that you're in a Git repo. -+// * If not, error out. -+// 2. Run a command to get the Git log data. -+// 3. Deserialize that data with Serde, into a GitLog data structure. -+// 4. Convert the GitLog data structure into a CommitGraph. -+// 5. Run analyses on the CommitGraph. -+ -+/* -+struct CommitGraph { -+ graph: Graph, -+} -+ -+struct AnalysisReport { -+ records: Vec, -+} -+ -+trait Analysis { -+ fn analyze(commit_graph: &CommitGraph) -> AnalysisReport; -+} -+*/ -+ -+#[derive(Deserialize, Debug)] -+struct GitContributor { -+ name: String, -+ email: String, -+ date: String, -+} -+ -+#[derive(Deserialize, Debug)] -+struct GitCommit { -+ commit: String, -+ abbreviated_commit: String, -+ tree: String, -+ abbreviated_tree: String, -+ parent: String, -+ abbreviated_parent: String, -+ refs: String, -+ encoding: String, -+ subject: String, -+ sanitized_subject_line: String, -+ body: String, -+ commit_notes: String, -+ verification_flag: String, -+ signer: String, -+ signer_key: String, -+ author: GitContributor, -+ committer: GitContributor, -+} -+ -+#[derive(Deserialize, Debug)] -+struct GitLog { -+ commits: Vec, -+} -+ -+fn strip_characters(original: &str, to_strip: &str) -> String { -+ original.chars().filter(|&c| !to_strip.contains(c)).collect() -+} -+ -+fn get_git_log() -> String { -+ // The format string being passed to Git, to get commit data. -+ // Note that this matches the GitLog struct above. -+ let format = " \ -+ --pretty=format: \ -+ { %n \ -+ \"commit\": \"%H\", %n \ -+ \"abbreviated_commit\": \"%h\", %n \ -+ \"tree\": \"%T\", %n \ -+ \"abbreviated_tree\": \"%t\", %n \ -+ \"parent\": \"%P\", %n \ -+ \"abbreviated_parent\": \"%p\", %n \ -+ \"refs\": \"%D\", %n \ -+ \"encoding\": \"%e\", %n \ -+ \"subject\": \"%s\", %n \ -+ \"sanitized_subject_line\": \"%f\", %n \ -+ \"body\": \"%b\", %n \ -+ \"commit_notes\": \"%N\", %n \ -+ \"verification_flag\": \"%G?\", %n \ -+ \"signer\": \"%GS\", %n \ -+ \"signer_key\": \"%GK\", %n \ -+ \"author\": { %n \ -+ \"name\": \"%aN\", %n \ -+ \"email\": \"%aE\", %n \ -+ \"date\": \"%aD\" %n \ -+ }, %n \ -+ \"commiter\": { %n \ -+ \"name\": \"%cN\", %n \ -+ \"email\": \"%cE\", %n \ -+ \"date\": \"%cD\" %n \ -+ } %n \ -+ },"; -+ let format = strip_characters(format, " "); -+ -+ // Run the git command and extract the stdout as a string, stripping the trailing comma. -+ let output = Command::new("git") -+ .args(&["log", &format]) -+ .output() -+ .expect("failed to execute process"); -+ let output = str::from_utf8(&output.stdout).unwrap().to_string(); -+ let output = (&output[0..output.len() - 2]).to_string(); // Remove trailing comma. -+ -+ // Wrap the result in brackets. -+ let mut result = String::new(); -+ result.push('['); -+ result.push_str(&output); -+ result.push(']'); -+ -+ result -+} -+ -@@ -2 +118,11 @@ fn main() { -- println!("Hello, world!"); -+ let matches = App::new("hipcheck") -+ .version("0.1") -+ .author("Andrew Lilley Brinker ") -+ .about("Check Git history for concerning patterns") -+ .get_matches(); -+ -+ let log_string = get_git_log(); -+ -+ let gl: GitLog = serde_json::from_str(&log_string).unwrap(); -+ -+ println!("{:?}", gl); -"#; - - let expected = Diff { - additions: Some(141), - deletions: Some(1), - file_diffs: vec![ - FileDiff { - file_name: Arc::new(String::from(".gitignore")), - additions: Some(10), - deletions: Some(0), - patch: String::from( - r#"# Generated by Cargo -# will have compiled files and executables -/target/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk -"#, - ), - }, - FileDiff { - file_name: Arc::new(String::from("Cargo.toml")), - additions: Some(4), - deletions: Some(0), - patch: String::from( - r#"clap = "2.33.0" -petgraph = "0.4.13" -serde = { version = "1.0.91", features = ["derive"] } -serde_json = "1.0.39" -"#, - ), - }, - FileDiff { - file_name: Arc::new(String::from("src/main.rs")), - additions: Some(127), - deletions: Some(1), - patch: String::from( - r#"use clap::{Arg, App, SubCommand}; -use serde::{Serialize, Deserialize}; -// use petgraph::{Graph, Directed}; -// use std::collections::Vec; -use std::process::Command; -use std::str; - -// 1. Check that you're in a Git repo. -// * If not, error out. -// 2. Run a command to get the Git log data. -// 3. Deserialize that data with Serde, into a GitLog data structure. -// 4. Convert the GitLog data structure into a CommitGraph. -// 5. Run analyses on the CommitGraph. - -/* -struct CommitGraph { - graph: Graph, -} - -struct AnalysisReport { - records: Vec, -} - -trait Analysis { - fn analyze(commit_graph: &CommitGraph) -> AnalysisReport; -} -*/ - -#[derive(Deserialize, Debug)] -struct GitContributor { - name: String, - email: String, - date: String, -} - -#[derive(Deserialize, Debug)] -struct GitCommit { - commit: String, - abbreviated_commit: String, - tree: String, - abbreviated_tree: String, - parent: String, - abbreviated_parent: String, - refs: String, - encoding: String, - subject: String, - sanitized_subject_line: String, - body: String, - commit_notes: String, - verification_flag: String, - signer: String, - signer_key: String, - author: GitContributor, - committer: GitContributor, -} - -#[derive(Deserialize, Debug)] -struct GitLog { - commits: Vec, -} - -fn strip_characters(original: &str, to_strip: &str) -> String { - original.chars().filter(|&c| !to_strip.contains(c)).collect() -} - -fn get_git_log() -> String { - // The format string being passed to Git, to get commit data. - // Note that this matches the GitLog struct above. - let format = " \ - --pretty=format: \ - { %n \ - \"commit\": \"%H\", %n \ - \"abbreviated_commit\": \"%h\", %n \ - \"tree\": \"%T\", %n \ - \"abbreviated_tree\": \"%t\", %n \ - \"parent\": \"%P\", %n \ - \"abbreviated_parent\": \"%p\", %n \ - \"refs\": \"%D\", %n \ - \"encoding\": \"%e\", %n \ - \"subject\": \"%s\", %n \ - \"sanitized_subject_line\": \"%f\", %n \ - \"body\": \"%b\", %n \ - \"commit_notes\": \"%N\", %n \ - \"verification_flag\": \"%G?\", %n \ - \"signer\": \"%GS\", %n \ - \"signer_key\": \"%GK\", %n \ - \"author\": { %n \ - \"name\": \"%aN\", %n \ - \"email\": \"%aE\", %n \ - \"date\": \"%aD\" %n \ - }, %n \ - \"commiter\": { %n \ - \"name\": \"%cN\", %n \ - \"email\": \"%cE\", %n \ - \"date\": \"%cD\" %n \ - } %n \ - },"; - let format = strip_characters(format, " "); - - // Run the git command and extract the stdout as a string, stripping the trailing comma. - let output = Command::new("git") - .args(&["log", &format]) - .output() - .expect("failed to execute process"); - let output = str::from_utf8(&output.stdout).unwrap().to_string(); - let output = (&output[0..output.len() - 2]).to_string(); // Remove trailing comma. - - // Wrap the result in brackets. - let mut result = String::new(); - result.push('['); - result.push_str(&output); - result.push(']'); - - result -} - - println!("Hello, world!"); - let matches = App::new("hipcheck") - .version("0.1") - .author("Andrew Lilley Brinker ") - .about("Check Git history for concerning patterns") - .get_matches(); - - let log_string = get_git_log(); - - let gl: GitLog = serde_json::from_str(&log_string).unwrap(); - - println!("{:?}", gl); -"#, - ), - }, - ], - }; - - let (remaining, diff) = diff(input).unwrap(); - - assert_eq!("", remaining); - assert_eq!(expected, diff); - } - - #[test] - fn parse_patch_with_context() { - let input = r#"diff --git a/README.md b/README.md -index 20b42ecfdf..b0f30e8e35 100644 ---- a/README.md -+++ b/README.md -@@ -432,24 +432,31 @@ Other Style Guides - }); - - // bad -- inbox.filter((msg) => { -- const { subject, author } = msg; -- if (subject === 'Mockingbird') { -- return author === 'Harper Lee'; -- } else { -- return false; -- } -- }); -+ var indexMap = myArray.reduce(function(memo, item, index) { -+ memo[item] = index; -+ }, {}); - -- // good -- inbox.filter((msg) => { -- const { subject, author } = msg; -- if (subject === 'Mockingbird') { -- return author === 'Harper Lee'; -- } - -- return false; -+ // good -+ var indexMap = myArray.reduce(function(memo, item, index) { -+ memo[item] = index; -+ return memo; -+ }, {}); -+ -+ -+ // bad -+ const alpha = people.sort((lastOne, nextOne) => { -+ const [aLast, aFirst] = lastOne.split(', '); -+ const [bLast, bFirst] = nextOne.split(', '); - }); -+ -+ // good -+ const alpha = people.sort((lastOne, nextOne) => { -+ const [aLast, aFirst] = lastOne.split(', '); -+ const [bLast, bFirst] = nextOne.split(', '); -+ return aLast > bLast ? 1 : -1; -+ }); -+ - ``` - - -"#; - - let expected = r#" inbox.filter((msg) => { - const { subject, author } = msg; - if (subject === 'Mockingbird') { - return author === 'Harper Lee'; - } else { - return false; - } - }); - var indexMap = myArray.reduce(function(memo, item, index) { - memo[item] = index; - }, {}); - // good - inbox.filter((msg) => { - const { subject, author } = msg; - if (subject === 'Mockingbird') { - return author === 'Harper Lee'; - } - return false; - // good - var indexMap = myArray.reduce(function(memo, item, index) { - memo[item] = index; - return memo; - }, {}); - - - // bad - const alpha = people.sort((lastOne, nextOne) => { - const [aLast, aFirst] = lastOne.split(', '); - const [bLast, bFirst] = nextOne.split(', '); - - // good - const alpha = people.sort((lastOne, nextOne) => { - const [aLast, aFirst] = lastOne.split(', '); - const [bLast, bFirst] = nextOne.split(', '); - return aLast > bLast ? 1 : -1; - }); - -"#; - - let (remaining, patch) = patch_with_context(input).unwrap(); - - assert_eq!( - "", remaining, - "expected nothing remaining, got '{}'", - remaining - ); - assert_eq!(expected, patch.content); - } - - #[test] - fn parse_gh_diff() { - let input = r#"diff --git a/README.md b/README.md -index 20b42ecfdf..b0f30e8e35 100644 ---- a/README.md -+++ b/README.md -@@ -432,24 +432,31 @@ Other Style Guides - }); - - // bad -- inbox.filter((msg) => { -- const { subject, author } = msg; -- if (subject === 'Mockingbird') { -- return author === 'Harper Lee'; -- } else { -- return false; -- } -- }); -+ var indexMap = myArray.reduce(function(memo, item, index) { -+ memo[item] = index; -+ }, {}); - -- // good -- inbox.filter((msg) => { -- const { subject, author } = msg; -- if (subject === 'Mockingbird') { -- return author === 'Harper Lee'; -- } - -- return false; -+ // good -+ var indexMap = myArray.reduce(function(memo, item, index) { -+ memo[item] = index; -+ return memo; -+ }, {}); -+ -+ -+ // bad -+ const alpha = people.sort((lastOne, nextOne) => { -+ const [aLast, aFirst] = lastOne.split(', '); -+ const [bLast, bFirst] = nextOne.split(', '); - }); -+ -+ // good -+ const alpha = people.sort((lastOne, nextOne) => { -+ const [aLast, aFirst] = lastOne.split(', '); -+ const [bLast, bFirst] = nextOne.split(', '); -+ return aLast > bLast ? 1 : -1; -+ }); -+ - ``` - - -"#; - - let expected = r#" inbox.filter((msg) => { - const { subject, author } = msg; - if (subject === 'Mockingbird') { - return author === 'Harper Lee'; - } else { - return false; - } - }); - var indexMap = myArray.reduce(function(memo, item, index) { - memo[item] = index; - }, {}); - // good - inbox.filter((msg) => { - const { subject, author } = msg; - if (subject === 'Mockingbird') { - return author === 'Harper Lee'; - } - return false; - // good - var indexMap = myArray.reduce(function(memo, item, index) { - memo[item] = index; - return memo; - }, {}); - - - // bad - const alpha = people.sort((lastOne, nextOne) => { - const [aLast, aFirst] = lastOne.split(', '); - const [bLast, bFirst] = nextOne.split(', '); - - // good - const alpha = people.sort((lastOne, nextOne) => { - const [aLast, aFirst] = lastOne.split(', '); - const [bLast, bFirst] = nextOne.split(', '); - return aLast > bLast ? 1 : -1; - }); - -"#; - - let (remaining, diff) = gh_diff(input).unwrap(); - - assert_eq!( - "", remaining, - "expected nothing remaining, got '{}'", - remaining - ); - - assert_eq!(None, diff.additions); - assert_eq!(None, diff.deletions); - - assert_eq!("README.md", diff.file_diffs[0].file_name.as_ref()); - assert_eq!(None, diff.file_diffs[0].additions); - assert_eq!(None, diff.file_diffs[0].deletions); - assert_eq!(expected, diff.file_diffs[0].patch) - } -} diff --git a/plugins/git/src/util/command.rs b/plugins/git/src/util/command.rs deleted file mode 100644 index bd289bed..00000000 --- a/plugins/git/src/util/command.rs +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -use std::{convert::AsRef, env, ffi::OsStr, iter::IntoIterator}; - -/// Print command line args as well as commands and args for git commands -pub fn log_git_args(repo_path: &str, args: I, git_path: &str) -where - I: IntoIterator + Copy, - S: AsRef, -{ - log::debug!("logging git CLI args"); - - for arg in env::args() { - log::debug!("git CLI environment arg [arg='{}']", arg); - } - - log::debug!("git CLI executable location [path='{}']", git_path); - - log::debug!("git CLI repository location [path='{}']", repo_path); - - log_each_git_arg(args); - - log::debug!("done logging git CLI args"); -} - -pub fn log_each_git_arg(args: I) -where - I: IntoIterator, - S: AsRef, -{ - for (index, val) in args.into_iter().enumerate() { - let arg_val = val - .as_ref() - .to_str() - .unwrap_or("argument for command could not be logged."); - - log::debug!("git CLI argument [name='{}', value='{}']", index, arg_val); - } -} diff --git a/plugins/git/src/util/git_command.rs b/plugins/git/src/util/git_command.rs deleted file mode 100644 index 1bbecec1..00000000 --- a/plugins/git/src/util/git_command.rs +++ /dev/null @@ -1,129 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -use crate::data::*; -use crate::parse::*; -use crate::util::command::log_git_args; - -use anyhow::{anyhow, Context as _, Result}; -use std::{ - convert::AsRef, ffi::OsStr, iter::IntoIterator, ops::Not as _, path::Path, process::Command, -}; - -#[derive(Debug)] -pub struct GitCommand { - command: Command, -} - -impl GitCommand { - pub fn for_repo(repo_path: &Path, args: I) -> Result - where - I: IntoIterator + Copy, - S: AsRef, - { - GitCommand::internal(Some(repo_path), args) - } - - fn internal(repo_path: Option<&Path>, args: I) -> Result - where - I: IntoIterator + Copy, - S: AsRef, - { - // Init the command. - let git_path = which::which("git").context("can't find git command")?; - let no_repo_found = Path::new("no_repo_found"); - let repo = repo_path.unwrap_or(no_repo_found).display().to_string(); - let path = git_path.display().to_string(); - log_git_args(&repo, args, &path); - let mut command = Command::new(&git_path); - command.args(args); - - // Set the path if necessary - if let Some(repo_path) = repo_path { - command.current_dir(repo_path); - } - - if cfg!(windows) { - // this method is broken on Windows. See: https://github.com/rust-lang/rust/issues/31259 - //command.env_clear() - } else { - command.env_clear(); - }; - - Ok(GitCommand { command }) - } - - pub fn output(&mut self) -> Result { - let output = self.command.output()?; - - if output.status.success() { - let output_text = String::from_utf8_lossy(&output.stdout).to_string(); - return Ok(output_text); - } - - match String::from_utf8(output.stderr) { - Ok(msg) if msg.is_empty().not() => { - Err(anyhow!("(from git) {} [{}]", msg.trim(), output.status)) - } - _ => Err(anyhow!("git failed [{}]", output.status)), - } - } -} - -pub fn get_commits(repo: &str) -> Result> { - let path = Path::new(repo); - let raw_output = GitCommand::for_repo( - path, - [ - "--no-pager", - "log", - "--no-merges", - "--date=iso-strict", - "--pretty=tformat:%H%n%aN%n%aE%n%ad%n%cN%n%cE%n%cd%n", - ], - )? - .output() - .context("git log command failed")?; - - git_log(&raw_output) -} - -pub fn get_commits_from_date(repo: &str, date: &str) -> Result> { - let path = Path::new(repo); - let since_date = format!("--since='{} month ago'", date); - let msg = format!("git log from date {} command failed", &date); - let raw_output = GitCommand::for_repo( - path, - [ - "--no-pager", - "log", - "--no-merges", - "--date=iso-strict", - "--pretty=tformat:%H%n%aN%n%aE%n%ad%n%cN%n%cE%n%cd%n%GS%n%GK%n", - "--all", - &since_date, - ], - )? - .output() - .context(msg)?; - - git_log(&raw_output) -} - -pub fn get_diffs(repo: &str) -> Result> { - let path = Path::new(repo); - let output = GitCommand::for_repo( - path, - [ - "--no-pager", - "log", - "--no-merges", - "--numstat", - "--pretty=tformat:~~~", - "-U0", - ], - )? - .output() - .context("git diff command failed")?; - - git_diff(&output) -} diff --git a/plugins/git/src/util/mod.rs b/plugins/git/src/util/mod.rs deleted file mode 100644 index ec532257..00000000 --- a/plugins/git/src/util/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -pub mod command; -pub mod git_command;