diff options
author | Daniel Paulo Pimenta Cabral <danielc777888@gmail.com> | 2020-01-30 14:57:54 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-01-30 14:57:54 +0200 |
commit | 7c198ca97edc24fe8373526ba4891430ce04b75f (patch) | |
tree | 1a6afb5250531daea0b466818a875043cd67a14d | |
parent | fe60d2a0585f7e83c3091a4a567f10a92edcaa18 (diff) | |
parent | 805dd3e4d3cba8e7a2bda92d8c2c4f38d0c4e360 (diff) |
Merge pull request #44 from jemstep/PYKE-11909-multiple-mainline-branches
Pyke 11909 multiple mainline branches
-rw-r--r-- | .capn | 3 | ||||
-rw-r--r-- | Cargo.lock | 77 | ||||
-rw-r--r-- | Cargo.toml | 6 | ||||
-rw-r--r-- | readme.org | 17 | ||||
-rw-r--r-- | src/config.rs | 22 | ||||
-rw-r--r-- | src/git.rs | 264 | ||||
-rw-r--r-- | src/lib.rs | 16 | ||||
-rw-r--r-- | src/main.rs | 31 | ||||
-rw-r--r-- | src/policies.rs | 24 | ||||
-rw-r--r-- | tests/policies_test.rs | 41 | ||||
-rw-r--r-- | tests/test-repo.git/refs/heads/.gitkeep | 0 | ||||
-rw-r--r-- | tests/test-repo.git/refs/tags/.gitkeep | 0 |
12 files changed, 441 insertions, 60 deletions
@@ -1 +1,4 @@ +[git] +mainlines = [ "master" ] + [prepend_branch_name] @@ -1,6 +1,14 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. [[package]] +name = "aho-corasick" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "ansi_term" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -42,6 +50,8 @@ dependencies = [ "chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", "git2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "quickcheck 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", + "quickcheck_macros 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", "rayon 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.44 (registry+https://github.com/rust-lang/crates.io-index)", @@ -130,6 +140,15 @@ version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "getrandom" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -229,6 +248,11 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] +name = "memchr" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] name = "memoffset" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -296,6 +320,27 @@ dependencies = [ ] [[package]] +name = "quickcheck" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "quickcheck_macros" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "quote" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -368,6 +413,22 @@ version = "0.1.56" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] +name = "regex" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "aho-corasick 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", + "thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex-syntax" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] name = "rustc_version" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -476,6 +537,14 @@ dependencies = [ ] [[package]] +name = "thread_local" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "time" version = "0.1.42" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -578,6 +647,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" [metadata] +"checksum aho-corasick 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)" = "58fb5e95d83b38284460a5fda7d6470aa0b8844d283a0b614b8535e880800d2d" "checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" "checksum atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1803c647a3ec87095e7ae7acfca019e98de5ec9a7d01343f611cf3152ed71a90" "checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" @@ -592,6 +662,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum crossbeam-queue 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "dfd6515864a82d2f877b42813d4553292c6659498c9a2aa31bab5a15243c2700" "checksum crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ce446db02cdc3165b94ae73111e570793400d0794e46125cc4056c81cbb039f4" "checksum either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" +"checksum env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" "checksum getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "e7db7ca94ed4cd01190ceee0d8a8052f08a247aa1b469a7f68c6a3b71afcf407" "checksum git2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c7339329bfa14a00223244311560d11f8f489b453fb90092af97f267a6090ab0" "checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" @@ -604,6 +675,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum libz-sys 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)" = "2eb5e43362e38e2bca2fd5f5134c4d4564a23a5c28e9b95411652021a8675ebe" "checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" "checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" +"checksum memchr 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3197e20c7edb283f87c071ddfc7a2cca8f8e0b888c242959846a6fce03c72223" "checksum memoffset 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "75189eb85871ea5c2e2c15abbdd541185f63b408415e5051f5cac122d8c774b9" "checksum num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "b85e541ef8255f6cf42bbfe4ef361305c6c135d10919ecc26126c4e5ae94bc09" "checksum num-traits 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c81ffc11c212fa327657cb19dd85eb7419e163b5b076bede2bdb5c974c07e4" @@ -613,6 +685,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" "checksum proc-macro-error 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "aeccfe4d5d8ea175d5f0e4a2ad0637e0f4121d63bd99d356fb1f39ab2e7c6097" "checksum proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "9c9e470a8dc4aeae2dee2f335e8f533e2d4b347e1434e5671afc49b054592f27" +"checksum quickcheck 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a44883e74aa97ad63db83c4bf8ca490f02b2fc02f92575e720c8551e843c945f" +"checksum quickcheck_macros 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "608c156fd8e97febc07dc9c2e2c80bf74cfc6ef26893eae3daf8bc2bc94a4b7f" "checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" "checksum rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3ae1b169243eaf61759b8475a998f0a385e42042370f3a7dbaf35246eacc8412" "checksum rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "03a2a90da8c7523f554344f921aa97283eadf6ac484a6d2a7d0212fa7f8d6853" @@ -621,6 +695,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum rayon 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "43739f8831493b276363637423d3622d4bd6394ab6f0a9c4a552e208aeb7fddd" "checksum rayon-core 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f8bf17de6f23b05473c437eb958b9c850bfc8af0961fe17b4cc92d5a627b4791" "checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" +"checksum regex 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b5508c1941e4e7cb19965abef075d35a9a8b5cdf0846f30b4050e9b55dc55e87" +"checksum regex-syntax 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "e734e891f5b408a29efbf8309e656876276f49ab6a6ac208600b4419bd893d90" "checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" "checksum ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8" "checksum scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b42e15e59b18a828bbf5c58ea01debb36b9b096346de35d941dcb89009f24a0d" @@ -635,6 +711,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum structopt-derive 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea692d40005b3ceba90a9fe7a78fa8d4b82b0ce627eebbffc329aab850f3410e" "checksum syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)" = "dff0acdb207ae2fe6d5976617f887eb1e35a2ba52c13c7234c790960cdad9238" "checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +"checksum thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" "checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" "checksum toml 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "01d1404644c8b12b16bfcffa4322403a91a451584daaaa7c28d3152e6cbc98cf" "checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" @@ -13,4 +13,8 @@ serde_json = "1.0.40" log = { version = "0.4.8", features = ["std", "serde"] } chrono = "0.4.7" rayon = "1.0.3" -uuid = { version = "0.8.1", features = ["serde", "v4"] }
\ No newline at end of file +uuid = { version = "0.8.1", features = ["serde", "v4"] } + +[dev-dependencies] +quickcheck = "0.9.2" +quickcheck_macros = "0.9.1"
\ No newline at end of file @@ -85,6 +85,21 @@ project's repo. This configuration file is in TOML format. This is an example ~.capn~ file: [[./.capn]] *** Policies +**** Git configuration +There are some properties that are common across +policies. Configuration of how certain Git conventions are followed +are grouped into the ~[git]~ section, and may affect multiple +policies. + +#+BEGIN_SRC toml + [git] + + # The set of branches and patterns that are considered the 'mainline' by other policies. + # Supports globs and the special symbolic reference "HEAD". + # Default is [ "HEAD" ] + mainlines = [ "HEAD", "develop", "RC-*" ] +#+END_SRC + **** Verify Git Commits This policy ensures that all commits come from a trusted source, using GPG keys. For this policy to work, Captain Git Hook must be installed @@ -114,7 +129,7 @@ This is the config section for this policy: recv_keys_par = true # run key requests to keyserver in parallel skip_recv_keys = false # if true, do not fetch keys from the keyserver - verify_different_authors = true # if true, merge commits to the head branch of the repo should have multiple authors in the branch + verify_different_authors = true # if true, merge commits to the mainline branch of the repo should have multiple authors in the branch override_tag_pattern = "capn-override-*" # glob used to limit tags that are considered override tags (see Override Tags docs) override_tags_required = 2 # the number of tags required to override signed commit rules diff --git a/src/config.rs b/src/config.rs index ec27ab6..e2a983e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,10 +3,26 @@ use toml; #[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct Config { + #[serde(default)] + pub git: GitConfig, pub prepend_branch_name: Option<Unit>, pub verify_git_commits: Option<VerifyGitCommitsConfig>, } +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub struct GitConfig { + #[serde(default = "GitConfig::default_mainlines")] + pub mainlines: Vec<String>, +} + +impl Default for GitConfig { + fn default() -> GitConfig { + GitConfig { + mainlines: GitConfig::default_mainlines(), + } + } +} + #[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct VerifyGitCommitsConfig { pub author_domain: String, @@ -49,3 +65,9 @@ impl Config { toml::from_str(input) } } + +impl GitConfig { + fn default_mainlines() -> Vec<String> { + vec!["HEAD".into()] + } +} @@ -1,12 +1,13 @@ use crate::error::CapnError; use crate::keyring::Keyring; use git2; -use git2::{ObjectType, Oid, Repository}; +use git2::{ErrorClass, ObjectType, Oid, Repository}; use std::cell::RefCell; use std::collections::HashMap; use std::error::Error; use std::fs::File; use std::io::prelude::*; +use std::path::Path; use std::process::*; use std::str; @@ -31,7 +32,6 @@ pub struct Tag { } pub trait Git: Sized { - fn new() -> Result<Self, Box<dyn Error>>; fn read_file(&self, path: &str) -> Result<String, Box<dyn Error>>; fn write_git_file( &self, @@ -46,7 +46,7 @@ pub trait Git: Sized { commit_id: Oid, override_tag_pattern: &Option<String>, ) -> Result<Commit, Box<dyn Error>>; - fn find_commits( + fn find_new_commits( &self, exclusions: &[Oid], inclusions: &[Oid], @@ -55,15 +55,15 @@ pub trait Git: Sized { fn is_merge_commit(&self, commit_id: Oid) -> bool; fn is_trivial_merge_commit(&self, commit: &Commit) -> Result<bool, Box<dyn Error>>; - fn is_head(&self, ref_name: &str) -> Result<bool, Box<dyn Error>>; - fn path(&self) -> &std::path::Path; + fn is_mainline(&self, ref_name: &str) -> Result<bool, Box<dyn Error>>; + fn path(&self) -> &Path; fn verify_commit_signature( - path: &std::path::Path, + path: &Path, commit: &Commit, keyring: &Keyring, ) -> Result<bool, Box<dyn Error>>; fn verify_tag_signature( - path: &std::path::Path, + path: &Path, tag: &Tag, keyring: &Keyring, ) -> Result<bool, Box<dyn Error>>; @@ -77,18 +77,11 @@ pub trait Git: Sized { pub struct LiveGit { repo: Repository, tag_cache: RefCell<HashMap<Option<String>, HashMap<Oid, Vec<Tag>>>>, + config: GitConfig, } impl Git for LiveGit { - fn new() -> Result<Self, Box<dyn Error>> { - let repo = Repository::discover("./")?; - Ok(LiveGit { - repo, - tag_cache: RefCell::new(HashMap::new()), - }) - } - - fn path(&self) -> &std::path::Path { + fn path(&self) -> &Path { self.repo.path() } @@ -180,7 +173,7 @@ impl Git for LiveGit { }) } - fn find_commits( + fn find_new_commits( &self, exclusions: &[Oid], inclusions: &[Oid], @@ -193,7 +186,21 @@ impl Git for LiveGit { for &exclusion in exclusions.iter().filter(|id| !id.is_zero()) { revwalk.hide(exclusion)?; } - revwalk.hide_head()?; + for mainline in &self.config.mainlines { + if mainline == "HEAD" { + revwalk.hide_head()?; + } else if mainline.contains(|c| c == '?' || c == '*' || c == '[') { + revwalk.hide_glob(&format!("refs/heads/{}", mainline))?; + } else { + match revwalk.hide_ref(&format!("refs/heads/{}", mainline)) { + Ok(()) => {} + Err(e) if e.class() == ErrorClass::Reference => { + warn!("Failed to exclude mainline branch {}. Error: {}.\nThis could indicate that the branch doesn't exist.", mainline, e); + } + Err(e) => return Err(e.into()), + } + } + } let commits = revwalk .into_iter() @@ -207,7 +214,7 @@ impl Git for LiveGit { } fn verify_tag_signature( - path: &std::path::Path, + path: &Path, tag: &Tag, keyring: &Keyring, ) -> Result<bool, Box<dyn Error>> { @@ -261,7 +268,7 @@ impl Git for LiveGit { } fn verify_commit_signature( - path: &std::path::Path, + path: &Path, commit: &Commit, keyring: &Keyring, ) -> Result<bool, Box<dyn Error>> { @@ -347,9 +354,34 @@ impl Git for LiveGit { } } - fn is_head(&self, ref_name: &str) -> Result<bool, Box<dyn Error>> { - let head = self.repo.head()?; - Ok(Some(ref_name) == head.name()) + fn is_mainline(&self, ref_name: &str) -> Result<bool, Box<dyn Error>> { + fn is_head(git: &LiveGit, ref_name: &str) -> Result<bool, Box<dyn Error>> { + let head = git.repo.head()?; + Ok(Some(ref_name) == head.name()) + } + fn matches_glob(git: &LiveGit, ref_name: &str, glob: &str) -> Result<bool, Box<dyn Error>> { + git.repo + .references_glob(&format!("refs/heads/{}", glob))? + .names() + .map(|name| name.map(|n| n == ref_name)) + .fold(Ok(false), |acc, next| { + acc.and_then(|a| next.map(|b| a || b).map_err(|e| e.into())) + }) + } + + self.config + .mainlines + .iter() + .map(|mainline_glob| { + if mainline_glob == "HEAD" { + is_head(self, ref_name) + } else { + matches_glob(self, ref_name, mainline_glob) + } + }) + .fold(Ok(false), |acc, next| { + acc.and_then(|a| next.map(|b| a || b)) + }) } fn is_tag(&self, id: Oid) -> bool { @@ -361,6 +393,24 @@ impl Git for LiveGit { } impl LiveGit { + pub fn default(path: impl AsRef<Path>) -> Result<Self, Box<dyn Error>> { + let repo = Repository::discover(path)?; + Ok(LiveGit { + repo, + tag_cache: RefCell::new(HashMap::new()), + config: GitConfig::default(), + }) + } + + pub fn new(path: impl AsRef<Path>, config: GitConfig) -> Result<Self, Box<dyn Error>> { + let repo = Repository::discover(path)?; + Ok(LiveGit { + repo, + tag_cache: RefCell::new(HashMap::new()), + config, + }) + } + fn is_identical_tree_to_any_parent(commit: &git2::Commit<'_>) -> bool { let tree_id = commit.tree_id(); commit.parents().any(|p| p.tree_id() == tree_id) @@ -444,3 +494,171 @@ impl Drop for TempRepo { } } } + +#[cfg(test)] +mod test { + use super::*; + use git2::{Oid, Reference}; + use quickcheck_macros::quickcheck; + + fn valid_mainlines(mainlines: &[String]) -> bool { + mainlines + .iter() + .all(|mainline| !mainline.contains('\u{0}') && Reference::is_valid_name(&mainline)) + } + + #[test] + fn is_mainline_with_default_config_only_identifies_head_branch() { + let project_root = env!("CARGO_MANIFEST_DIR"); + let git = LiveGit::default(format!("{}/tests/test-repo.git", project_root)).unwrap(); + assert_eq!(git.is_mainline("refs/heads/master").unwrap(), true); + assert_eq!(git.is_mainline("refs/heads/tagged-branch").unwrap(), false); + } + + #[test] + fn is_mainline_with_glob_config_does_not_identify_head_branch() { + let project_root = env!("CARGO_MANIFEST_DIR"); + let git = LiveGit::new( + format!("{}/tests/test-repo.git", project_root), + GitConfig { + mainlines: vec!["tagged-*".into()], + }, + ) + .unwrap(); + assert_eq!(git.is_mainline("refs/heads/master").unwrap(), false); + assert_eq!(git.is_mainline("refs/heads/tagged-branch").unwrap(), true); + } + + #[test] + fn is_mainline_with_literal_config_does_not_identify_head_branch() { + let project_root = env!("CARGO_MANIFEST_DIR"); + let git = LiveGit::new( + format!("{}/tests/test-repo.git", project_root), + GitConfig { + mainlines: vec!["tagged-branch".into()], + }, + ) + .unwrap(); + assert_eq!(git.is_mainline("refs/heads/master").unwrap(), false); + assert_eq!(git.is_mainline("refs/heads/tagged-branch").unwrap(), true); + } + + #[quickcheck] + fn is_mainline_fuzz(branch: String, mainlines: Vec<String>) { + if valid_mainlines(&mainlines) { + let project_root = env!("CARGO_MANIFEST_DIR"); + let git = LiveGit::new( + format!("{}/tests/test-repo.git", project_root), + GitConfig { mainlines }, + ) + .unwrap(); + git.is_mainline(&branch).unwrap(); + } + } + + #[test] + fn is_mainline_with_multiple_glob_config_identifies_all_matches() { + let project_root = env!("CARGO_MANIFEST_DIR"); + let git = LiveGit::new( + format!("{}/tests/test-repo.git", project_root), + GitConfig { + mainlines: vec!["HEAD".into(), "tagged-*".into()], + }, + ) + .unwrap(); + assert_eq!(git.is_mainline("refs/heads/master").unwrap(), true); + assert_eq!(git.is_mainline("refs/heads/tagged-branch").unwrap(), true); + } + + #[test] + fn new_commits_off_master_with_default_config() { + let project_root = env!("CARGO_MANIFEST_DIR"); + let git = LiveGit::default(format!("{}/tests/test-repo.git", project_root)).unwrap(); + let commits = git + .find_new_commits( + &[Oid::from_str("eb5e0185546b0bb1a13feec6b9ee8b39985fea42").unwrap()], + &[Oid::from_str("6004dfdb071c71e5e76ad55b924b576487e1c485").unwrap()], + &None, + ) + .unwrap(); + assert_eq!(commits.len(), 2) + } + + #[test] + fn new_commits_off_master_with_configured_mainline_glob() { + let project_root = env!("CARGO_MANIFEST_DIR"); + let git = LiveGit::new( + format!("{}/tests/test-repo.git", project_root), + GitConfig { + mainlines: vec!["HEAD".into(), "valid-*".into()], + }, + ) + .unwrap(); + let commits = git + .find_new_commits( + &[Oid::from_str("eb5e0185546b0bb1a13feec6b9ee8b39985fea42").unwrap()], + &[Oid::from_str("6004dfdb071c71e5e76ad55b924b576487e1c485").unwrap()], + &None, + ) + .unwrap(); + assert_eq!(commits.len(), 1) + } + + #[test] + fn new_commits_off_master_with_configured_mainline_literal_branch_name() { + let project_root = env!("CARGO_MANIFEST_DIR"); + let git = LiveGit::new( + format!("{}/tests/test-repo.git", project_root), + GitConfig { + mainlines: vec!["HEAD".into(), "valid-branch".into()], + }, + ) + .unwrap(); + let commits = git + .find_new_commits( + &[Oid::from_str("eb5e0185546b0bb1a13feec6b9ee8b39985fea42").unwrap()], + &[Oid::from_str("6004dfdb071c71e5e76ad55b924b576487e1c485").unwrap()], + &None, + ) + .unwrap(); + assert_eq!(commits.len(), 1) + } + + #[test] + fn new_commits_off_master_with_configured_mainline_literal_branch_doesnt_exist() { + let project_root = env!("CARGO_MANIFEST_DIR"); + let git = LiveGit::new( + format!("{}/tests/test-repo.git", project_root), + GitConfig { + mainlines: vec!["HEAD".into(), "this-branch-does-not-exist-asdfg".into()], + }, + ) + .unwrap(); + let commits = git + .find_new_commits( + &[Oid::from_str("eb5e0185546b0bb1a13feec6b9ee8b39985fea42").unwrap()], + &[Oid::from_str("6004dfdb071c71e5e76ad55b924b576487e1c485").unwrap()], + &None, + ) + .unwrap(); + assert_eq!(commits.len(), 2) + } + + #[quickcheck] + fn new_commits_fuzz(mainlines: Vec<String>) { + if valid_mainlines(&mainlines) { + let project_root = env!("CARGO_MANIFEST_DIR"); + let git = LiveGit::new( + format!("{}/tests/test-repo.git", project_root), + GitConfig { mainlines }, + ) + .unwrap(); + git.find_new_commits( + &[Oid::from_str("eb5e0185546b0bb1a13feec6b9ee8b39985fea42").unwrap()], + &[Oid::from_str("6004dfdb071c71e5e76ad55b924b576487e1c485").unwrap()], + &None, + ) + .unwrap(); + } + } +} @@ -47,13 +47,14 @@ pub struct PrePush { } pub fn prepare_commit_msg<F: Fs, G: Git>( + git: &G, opt: PrepareCommitMsg, config: Config, ) -> Result<PolicyResult, Box<dyn Error>> { if opt.commit_source.is_none() { vec![config .prepend_branch_name - .map(|_| prepend_branch_name::<F, G>(opt.commit_file))] + .map(|_| prepend_branch_name::<F, G>(git, opt.commit_file))] .into_iter() .flatten() .collect() @@ -66,6 +67,7 @@ pub fn prepare_commit_msg<F: Fs, G: Git>( } pub fn pre_push<G: Git, P: Gpg>( + git: &G, gpg: P, _opt: &PrePush, config: &Config, @@ -77,13 +79,14 @@ pub fn pre_push<G: Git, P: Gpg>( vec![config .verify_git_commits .as_ref() - .map(|c| verify_git_commits::<G, P>(gpg, c, remote_sha, local_sha, local_ref))] + .map(|c| verify_git_commits::<G, P>(git, gpg, c, remote_sha, local_sha, local_ref))] .into_iter() .flatten() .collect() } pub fn pre_receive<G: Git, P: Gpg>( + git: &G, gpg: P, config: &Config, old_value: &str, @@ -93,22 +96,21 @@ pub fn pre_receive<G: Git, P: Gpg>( vec![config .verify_git_commits .as_ref() - .map(|c| verify_git_commits::<G, P>(gpg, c, old_value, new_value, ref_name))] + .map(|c| verify_git_commits::<G, P>(git, gpg, c, old_value, new_value, ref_name))] .into_iter() .flatten() .collect() } -pub fn install_hooks<G: Git>() -> Result<(), Box<dyn Error>> { - let repo = G::new()?; - repo.write_git_file( +pub fn install_hooks<G: Git>(git: &G) -> Result<(), Box<dyn Error>> { + git.write_git_file( "hooks/prepare-commit-msg", 0o750, r#"#!/bin/sh capn prepare-commit-msg "$@" "#, )?; - repo.write_git_file( + git.write_git_file( "hooks/pre-push", 0o750, r#"#!/bin/sh diff --git a/src/main.rs b/src/main.rs index 550023c..e31a274 100644 --- a/src/main.rs +++ b/src/main.rs @@ -63,18 +63,10 @@ fn main() { quiet, ); - let git = match LiveGit::new() { - Ok(g) => g, + let config = match load_config() { + Ok(config) => config, Err(e) => { - error!("Failed to initialize Capn Githook. Error: {}\nPlease check that you are in a Git repo.", e); - exit(1); - } - }; - - let config = match git.read_config() { - Ok(c) => c, - Err(e) => { - error!("Failed to read the .capn config file. Error: {}.\nPlease check that you are in a Git repo that has a .capn config file in the root of the repo.", e); + error!("Failed to initialize Capn Githook. Error: {}.\nPlease check that you are in a Git repo that has a .capn config file in the root of the repo.", e); exit(1); } }; @@ -99,11 +91,20 @@ fn main() { } } +fn load_config() -> Result<Config, Box<dyn Error>> { + // This is a necessary bootstrapping step, because we need a Git + // object to load the config, which is used to initialize the Git + // object used for the rest of the run. + let default_git = LiveGit::default("./")?; + default_git.read_config() +} + fn execute_command(command: Command, config: Config) -> Result<PolicyResult, Box<dyn Error>> { + let git = LiveGit::new("./", config.git.clone())?; match command { Command::PrepareCommitMsg(args) => { info!("Calling prepare-commit-msg"); - prepare_commit_msg::<LiveFs, LiveGit>(args, config) + prepare_commit_msg::<LiveFs, LiveGit>(&git, args, config) } Command::PrePush(args) => { info!("Calling pre-push"); @@ -113,7 +114,7 @@ fn execute_command(command: Command, config: Config) -> Result<PolicyResult, Box match (fields.next(), fields.next(), fields.next(), fields.next()) { (Some(local_ref), Some(local_sha), Some(remote_ref), Some(remote_sha)) => { info!("Running pre-push for: {} {} {} {}", local_ref, local_sha, remote_ref, remote_sha); - pre_push::<LiveGit, _>(build_gpg_client(&config), &args, &config, local_ref, local_sha, remote_ref, remote_sha) + pre_push::<LiveGit, _>(&git, build_gpg_client(&config), &args, &config, local_ref, local_sha, remote_ref, remote_sha) }, _ => { warn!("Expected parameters not received on stdin. Line received was: {}", line); @@ -132,7 +133,7 @@ fn execute_command(command: Command, config: Config) -> Result<PolicyResult, Box match (fields.next(), fields.next(), fields.next()) { (Some(old_value), Some(new_value), Some(ref_name)) => { info!("Running pre-receive for: {} {} {}", old_value, new_value, ref_name); - pre_receive::<LiveGit, _>(build_gpg_client(&config), &config, old_value, new_value, ref_name) + pre_receive::<LiveGit, _>(&git, build_gpg_client(&config), &config, old_value, new_value, ref_name) }, _ => { warn!("Expected parameters not received on stdin. Line received was: {}", line); @@ -143,7 +144,7 @@ fn execute_command(command: Command, config: Config) -> Result<PolicyResult, Box .flatten() .collect() } - Command::InstallHooks => install_hooks::<LiveGit>().map(|_| PolicyResult::Ok), + Command::InstallHooks => install_hooks(&git).map(|_| PolicyResult::Ok), } } diff --git a/src/policies.rs b/src/policies.rs index 821b6f5..eb5dcee 100644 --- a/src/policies.rs +++ b/src/policies.rs @@ -71,17 +71,18 @@ impl iter::FromIterator<PolicyResult> for PolicyResult { } pub fn prepend_branch_name<F: Fs, G: Git>( + git: &G, commit_file: PathBuf, ) -> Result<PolicyResult, Box<dyn Error>> { info!("Executing policy: prepend_branch_name"); - let git = G::new()?; let branch = git.current_branch()?; F::prepend_string_to_file(branch, commit_file)?; Ok(PolicyResult::Ok) } pub fn verify_git_commits<G: Git, P: Gpg>( + git: &G, gpg: P, config: &VerifyGitCommitsConfig, old_value: &str, @@ -89,7 +90,6 @@ pub fn verify_git_commits<G: Git, P: Gpg>( ref_name: &str, ) -> Result<PolicyResult, Box<dyn Error>> { info!("Executing policy: verify_git_commits"); - let git = G::new()?; let start = Instant::now(); let old_commit_id = Oid::from_str(old_value)?; let new_commit_id = Oid::from_str(new_value)?; @@ -102,7 +102,7 @@ pub fn verify_git_commits<G: Git, P: Gpg>( debug!("Tag detected, no commits to verify.") } else { let all_commits = commits_to_verify( - &git, + git, old_commit_id, new_commit_id, &config.override_tag_pattern, @@ -117,14 +117,14 @@ pub fn verify_git_commits<G: Git, P: Gpg>( Keyring::from_team_fingerprints_file(git.read_file(&config.team_fingerprints_file)?); let manually_verified_commmits = find_and_verify_override_tags( - &git, + git, &gpg, &all_commits, config.override_tags_required, &mut keyring, )?; let not_manually_verified_commits = commits_to_verify_excluding_manually_verified( - &git, + git, old_commit_id, new_commit_id, manually_verified_commmits, @@ -141,7 +141,7 @@ pub fn verify_git_commits<G: Git, P: Gpg>( if config.verify_commit_signatures { policy_result = policy_result.and(verify_commit_signatures::<G, P>( - &git, + git, &gpg, ¬_manually_verified_commits, &mut keyring, @@ -151,7 +151,7 @@ pub fn verify_git_commits<G: Git, P: Gpg>( if config.verify_different_authors { policy_result = policy_result.and(verify_different_authors::<G>( &all_commits, - &git, + git, old_commit_id, new_commit_id, ref_name, @@ -173,7 +173,7 @@ fn commits_to_verify<G: Git>( new_commit_id: Oid, override_tag_pattern: &Option<String>, ) -> Result<Vec<Commit>, Box<dyn Error>> { - git.find_commits(&[old_commit_id], &[new_commit_id], override_tag_pattern) + git.find_new_commits(&[old_commit_id], &[new_commit_id], override_tag_pattern) } fn commits_to_verify_excluding_manually_verified<G: Git>( @@ -185,7 +185,7 @@ fn commits_to_verify_excluding_manually_verified<G: Git>( ) -> Result<Vec<Commit>, Box<dyn Error>> { let mut to_exclude = manually_verified; to_exclude.push(old_commit_id); - git.find_commits(&to_exclude, &[new_commit_id], override_tag_pattern) + git.find_new_commits(&to_exclude, &[new_commit_id], override_tag_pattern) } fn find_and_verify_override_tags<G: Git, P: Gpg>( @@ -309,10 +309,10 @@ fn verify_different_authors<G: Git>( ) -> Result<PolicyResult, Box<dyn Error>> { let new_branch = old_commit_id.is_zero(); let is_merge = git.is_merge_commit(new_commit_id); - let is_head = git.is_head(ref_name)?; + let is_mainline = git.is_mainline(ref_name)?; - if !is_head { - info!("Multiple author verification passed for {}: Not updating the head of the repo, does not require multiple authors", new_commit_id); + if !is_mainline { + info!("Multiple author verification passed for {}: Not updating a mainline branch, does not require multiple authors", new_commit_id); Ok(PolicyResult::Ok) } else if !is_merge { info!("Multiple author verification passed for {}: Not a merge commit, does not require multiple authors", new_commit_id); diff --git a/tests/policies_test.rs b/tests/policies_test.rs index cc2a5f3..b5708dd 100644 --- a/tests/policies_test.rs +++ b/tests/policies_test.rs @@ -1,5 +1,5 @@ use capn; -use capn::config::{Config, VerifyGitCommitsConfig}; +use capn::config::{Config, GitConfig, VerifyGitCommitsConfig}; use capn::policies; use capn::git::LiveGit; @@ -63,10 +63,12 @@ fn verify_commits_config() -> VerifyGitCommitsConfig { fn verify_git_commits_happy_path_from_empty_through_pre_receive() { before_all(); let config = Config { + git: GitConfig::default(), prepend_branch_name: None, verify_git_commits: Some(verify_commits_config()), }; let result = capn::pre_receive::<LiveGit, MockGpg>( + &LiveGit::default("./").unwrap(), MockGpg, &config, "0000000000000000000000000000000000000000", @@ -81,6 +83,7 @@ fn verify_git_commits_happy_path_from_empty_through_pre_receive() { fn verify_git_commits_happy_path_from_empty() { before_all(); let result = policies::verify_git_commits::<LiveGit, MockGpg>( + &LiveGit::default("./").unwrap(), MockGpg, &verify_commits_config(), "0000000000000000000000000000000000000000", @@ -95,6 +98,7 @@ fn verify_git_commits_happy_path_from_empty() { fn verify_git_commits_happy_path_from_existing() { before_all(); let result = policies::verify_git_commits::<LiveGit, MockGpg>( + &LiveGit::default("./").unwrap(), MockGpg, &verify_commits_config(), "7f9763e189ade34345e683ab7e0c22d164280452", @@ -109,6 +113,7 @@ fn verify_git_commits_happy_path_from_existing() { fn verify_git_commits_happy_path_unsigned_trivial_no_fast_forward_merge() { before_all(); let result = policies::verify_git_commits::<LiveGit, MockGpg>( + &LiveGit::default("./").unwrap(), MockGpg, &verify_commits_config(), "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", @@ -123,6 +128,7 @@ fn verify_git_commits_happy_path_unsigned_trivial_no_fast_forward_merge() { fn verify_git_commits_happy_path_unsigned_trivial_merge() { before_all(); let result = policies::verify_git_commits::<LiveGit, MockGpg>( + &LiveGit::default("./").unwrap(), MockGpg, &verify_commits_config(), "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", @@ -137,6 +143,7 @@ fn verify_git_commits_happy_path_unsigned_trivial_merge() { fn verify_git_commits_single_unsigned_commit() { before_all(); let result = policies::verify_git_commits::<LiveGit, MockGpg>( + &LiveGit::default("./").unwrap(), MockGpg, &verify_commits_config(), "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", @@ -151,6 +158,7 @@ fn verify_git_commits_single_unsigned_commit() { fn verify_git_commits_single_unsigned_commit_new_branch() { before_all(); let result = policies::verify_git_commits::<LiveGit, MockGpg>( + &LiveGit::default("./").unwrap(), MockGpg, &verify_commits_config(), "0000000000000000000000000000000000000000", @@ -165,6 +173,7 @@ fn verify_git_commits_single_unsigned_commit_new_branch() { fn verify_git_commits_unsigned_commit_being_merged_in() { before_all(); let result = policies::verify_git_commits::<LiveGit, MockGpg>( + &LiveGit::default("./").unwrap(), MockGpg, &verify_commits_config(), "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", @@ -179,6 +188,7 @@ fn verify_git_commits_unsigned_commit_being_merged_in() { fn verify_git_commits_unsigned_commit_behind_a_merge_commit() { before_all(); let result = policies::verify_git_commits::<LiveGit, MockGpg>( + &LiveGit::default("./").unwrap(), MockGpg, &verify_commits_config(), "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", @@ -193,6 +203,7 @@ fn verify_git_commits_unsigned_commit_behind_a_merge_commit() { fn verify_git_commits_invalid_author() { before_all(); let result = policies::verify_git_commits::<LiveGit, MockGpg>( + &LiveGit::default("./").unwrap(), MockGpg, &verify_commits_config(), "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", @@ -207,6 +218,7 @@ fn verify_git_commits_invalid_author() { fn verify_git_commits_code_injected_into_unsigned_merge() { before_all(); let result = policies::verify_git_commits::<LiveGit, MockGpg>( + &LiveGit::default("./").unwrap(), MockGpg, &verify_commits_config(), "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", @@ -222,6 +234,7 @@ fn verify_git_commits_happy_path_pushing_previously_checked_merge_commit() { // This is an edge case for checking that merges have multiple authors before_all(); let result = policies::verify_git_commits::<LiveGit, MockGpg>( + &LiveGit::default("./").unwrap(), MockGpg, &verify_commits_config(), "3eb315d10e2ad89555d7bfc78a1db1ce07bce434", @@ -236,6 +249,7 @@ fn verify_git_commits_happy_path_pushing_previously_checked_merge_commit() { fn verify_git_commits_author_merged_own_code_not_on_head() { before_all(); let result = policies::verify_git_commits::<LiveGit, MockGpg>( + &LiveGit::default("./").unwrap(), MockGpg, &verify_commits_config(), "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", @@ -247,9 +261,31 @@ fn verify_git_commits_author_merged_own_code_not_on_head() { } #[test] +fn verify_git_commits_author_merged_own_code_on_configured_mainline() { + before_all(); + let result = policies::verify_git_commits::<LiveGit, MockGpg>( + &LiveGit::new( + "./", + GitConfig { + mainlines: vec!["valid-*".into()], + }, + ) + .unwrap(), + MockGpg, + &verify_commits_config(), + "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", + "6004dfdb071c71e5e76ad55b924b576487e1c485", + "refs/heads/valid-branch", + ) + .unwrap(); + assert!(result.is_err()); +} + +#[test] fn verify_git_commits_author_merged_own_code_on_head() { before_all(); let result = policies::verify_git_commits::<LiveGit, MockGpg>( + &LiveGit::default("./").unwrap(), MockGpg, &verify_commits_config(), "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", @@ -264,6 +300,7 @@ fn verify_git_commits_author_merged_own_code_on_head() { fn verify_git_commits_author_merged_own_code_on_head_with_tag() { before_all(); let result = policies::verify_git_commits::<LiveGit, MockGpg>( + &LiveGit::default("./").unwrap(), MockGpg, &verify_commits_config(), "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", @@ -278,6 +315,7 @@ fn verify_git_commits_author_merged_own_code_on_head_with_tag() { fn verify_tagged_git_commits_override_rules() { before_all(); let result = policies::verify_git_commits::<LiveGit, MockGpg>( + &LiveGit::default("./").unwrap(), MockGpg, &verify_commits_config(), "7f9763e189ade34345e683ab7e0c22d164280452", @@ -292,6 +330,7 @@ fn verify_tagged_git_commits_override_rules() { fn verify_tagged_git_commits_not_overridden_if_not_enough_tags() { before_all(); let result = policies::verify_git_commits::<LiveGit, MockGpg>( + &LiveGit::default("./").unwrap(), MockGpg, &VerifyGitCommitsConfig { override_tags_required: 2, diff --git a/tests/test-repo.git/refs/heads/.gitkeep b/tests/test-repo.git/refs/heads/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/test-repo.git/refs/heads/.gitkeep diff --git a/tests/test-repo.git/refs/tags/.gitkeep b/tests/test-repo.git/refs/tags/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/test-repo.git/refs/tags/.gitkeep |