diff options
Diffstat (limited to 'src/policies.rs')
-rw-r--r-- | src/policies.rs | 278 |
1 files changed, 154 insertions, 124 deletions
diff --git a/src/policies.rs b/src/policies.rs index b3a6fdb..5eed65f 100644 --- a/src/policies.rs +++ b/src/policies.rs @@ -1,75 +1,23 @@ +pub mod policy_result; + use crate::config::VerifyGitCommitsConfig; use crate::fs::*; use crate::git::*; use crate::gpg::*; use crate::keyring::*; +use crate::reference_update::ReferenceUpdate; + +use self::policy_result::PolicyResult; use git2::Oid; use rayon::prelude::*; use std::collections::HashSet; use std::error::Error; -use std::fmt; -use std::iter; use std::path::PathBuf; use std::time::Instant; use log::*; -#[derive(Debug, Clone)] -pub enum PolicyResult { - Ok, - UnsignedCommit(Oid), - UnsignedMergeCommit(Oid), - NotEnoughAuthors(Oid), - InvalidAuthorEmail(Oid, String), - MissingAuthorEmail(Oid), - InvalidCommitterEmail(Oid, String), - MissingCommitterEmail(Oid), -} - -impl PolicyResult { - pub fn and(self, res: PolicyResult) -> PolicyResult { - match self { - PolicyResult::Ok => res, - x => x, - } - } - pub fn is_ok(&self) -> bool { - match self { - PolicyResult::Ok => true, - _ => false, - } - } - pub fn is_err(&self) -> bool { - !self.is_ok() - } -} - -impl fmt::Display for PolicyResult { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use PolicyResult::*; - - match self { - Ok => write!(f, "Ok"), - UnsignedCommit(id) => write!(f, "Commit does not have a valid GPG signature: {}", id), - UnsignedMergeCommit(id) => write!(f, "Commit does not have a valid GPG signature: {}. This is a merge commit, please note that if there were conflicts that needed to be resolved then the commit needs a signature.", id), - NotEnoughAuthors(id) => write!(f, "Merge commit needs to have multiple authors in the branch: {}", id), - InvalidAuthorEmail(id, email) => write!(f, "Commit has an invalid author email ({}): {}", email, id), - MissingAuthorEmail(id) => write!(f, "Commit does not have an author email: {}", id), - InvalidCommitterEmail(id, email) => write!(f, "Commit has an invalid committer email ({}): {}", email, id), - MissingCommitterEmail(id) => write!(f, "Commit does not have a committer email: {}", id), - } - } -} - -impl iter::FromIterator<PolicyResult> for PolicyResult { - fn from_iter<I: IntoIterator<Item = PolicyResult>>(iter: I) -> Self { - iter.into_iter() - .find(PolicyResult::is_err) - .unwrap_or(PolicyResult::Ok) - } -} - pub fn prepend_branch_name<F: Fs, G: Git>( git: &G, commit_file: PathBuf, @@ -85,28 +33,19 @@ pub fn verify_git_commits<G: Git, P: Gpg>( git: &G, gpg: P, config: &VerifyGitCommitsConfig, - old_value: &str, - new_value: &str, - ref_name: &str, + ref_update: &ReferenceUpdate, ) -> Result<PolicyResult, Box<dyn Error>> { info!("Executing policy: verify_git_commits"); let start = Instant::now(); - let old_commit_id = Oid::from_str(old_value)?; - let new_commit_id = Oid::from_str(new_value)?; let mut policy_result = PolicyResult::Ok; - if new_commit_id.is_zero() { + if let ReferenceUpdate::Delete { .. } = ref_update { debug!("Delete branch detected, no commits to verify.") - } else if git.is_tag(new_commit_id) { + } else if git.is_tag(ref_update.ref_name())? { debug!("Tag detected, no commits to verify.") } else { - let all_commits = commits_to_verify( - git, - old_commit_id, - new_commit_id, - &config.override_tag_pattern, - )?; + let all_commits = commits_to_verify(git, &ref_update, &config.override_tag_pattern)?; debug!("Number of commits to verify {} : ", all_commits.len()); for commit in &all_commits { @@ -125,8 +64,7 @@ pub fn verify_git_commits<G: Git, P: Gpg>( )?; let not_manually_verified_commits = commits_to_verify_excluding_manually_verified( git, - old_commit_id, - new_commit_id, + &ref_update, manually_verified_commmits, &config.override_tag_pattern, )?; @@ -152,9 +90,16 @@ pub fn verify_git_commits<G: Git, P: Gpg>( policy_result = policy_result.and(verify_different_authors::<G>( &all_commits, git, - old_commit_id, - new_commit_id, - ref_name, + &ref_update, + )?); + } + + if config.verify_rebased { + policy_result = policy_result.and(verify_rebased::<G>( + &all_commits, + git, + &ref_update, + &config.override_tag_pattern, )?); } } @@ -169,23 +114,26 @@ pub fn verify_git_commits<G: Git, P: Gpg>( fn commits_to_verify<G: Git>( git: &G, - old_commit_id: Oid, - new_commit_id: Oid, + ref_update: &ReferenceUpdate, override_tag_pattern: &Option<String>, ) -> Result<Vec<Commit>, Box<dyn Error>> { - git.find_new_commits(&[old_commit_id], &[new_commit_id], override_tag_pattern) + let to_exclude = ref_update.old_commit_id().into_iter().collect::<Vec<_>>(); + let to_include = ref_update.new_commit_id().into_iter().collect::<Vec<_>>(); + git.find_new_commits(&to_exclude, &to_include, override_tag_pattern) } fn commits_to_verify_excluding_manually_verified<G: Git>( git: &G, - old_commit_id: Oid, - new_commit_id: Oid, + ref_update: &ReferenceUpdate, manually_verified: Vec<Oid>, override_tag_pattern: &Option<String>, ) -> Result<Vec<Commit>, Box<dyn Error>> { let mut to_exclude = manually_verified; - to_exclude.push(old_commit_id); - git.find_new_commits(&to_exclude, &[new_commit_id], override_tag_pattern) + if let Some(old_commit_id) = ref_update.old_commit_id() { + to_exclude.push(old_commit_id); + } + let to_include = ref_update.new_commit_id().into_iter().collect::<Vec<_>>(); + git.find_new_commits(&to_exclude, &to_include, override_tag_pattern) } fn find_and_verify_override_tags<G: Git, P: Gpg>( @@ -303,55 +251,137 @@ fn verify_commit_signatures<G: Git, P: Gpg>( fn verify_different_authors<G: Git>( commits: &[Commit], git: &G, - old_commit_id: Oid, - new_commit_id: Oid, - ref_name: &str, + ref_update: &ReferenceUpdate, ) -> 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_mainline = git.is_mainline(ref_name)?; - - 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); - Ok(PolicyResult::Ok) - } else if new_branch { - info!("Multiple author verification passed for {}: New branch does not require multiple authors for a merge commit", new_commit_id); - Ok(PolicyResult::Ok) - } else if commits.len() == 0 { - info!("Multiple author verification passed for {}: No new commits pushed, does not require multiple authors", new_commit_id); - Ok(PolicyResult::Ok) - } else if commits.len() == 1 && commits[0].is_identical_tree_to_any_parent { - info!("Multiple author verification passed for {}: There is only one commit and it has an identical filetree to one of its parents", new_commit_id); - Ok(PolicyResult::Ok) - } else if commits.len() == 1 && git.is_trivial_merge_commit(&commits[0])? { - info!("Multiple author verification passed for {}: There is only one commit and it is a trivial merge between mainline branches", new_commit_id); - Ok(PolicyResult::Ok) - } else { - let authors: HashSet<_> = commits - .iter() - .flat_map(|c| { - c.tags + match ref_update { + ReferenceUpdate::Delete { .. } => { + info!("Multiple author verification passed: No checks required for deleting a branch"); + Ok(PolicyResult::Ok) + } + ReferenceUpdate::New { new_commit_id, .. } => { + info!("Multiple author verification passed for {}: New branch does not require multiple authors for a merge commit", new_commit_id); + Ok(PolicyResult::Ok) + } + ReferenceUpdate::Update { + new_commit_id, + ref_name, + .. + } => { + let is_merge = git.is_merge_commit(*new_commit_id); + let is_mainline = git.is_mainline(ref_name)?; + + 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); + Ok(PolicyResult::Ok) + } else if commits.len() == 0 { + info!("Multiple author verification passed for {}: No new commits pushed, does not require multiple authors", new_commit_id); + Ok(PolicyResult::Ok) + } else if commits.len() == 1 && commits[0].is_identical_tree_to_any_parent { + info!("Multiple author verification passed for {}: There is only one commit and it has an identical filetree to one of its parents", new_commit_id); + Ok(PolicyResult::Ok) + } else if commits.len() == 1 && git.is_trivial_merge_commit(&commits[0])? { + info!("Multiple author verification passed for {}: There is only one commit and it is a trivial merge between mainline branches", new_commit_id); + Ok(PolicyResult::Ok) + } else { + let authors: HashSet<_> = commits .iter() - .filter_map(|t| t.tagger_email.as_ref()) - .chain(c.author_email.as_ref()) - }) - .collect(); - if authors.len() <= 1 { - error!( + .flat_map(|c| { + c.tags + .iter() + .filter_map(|t| t.tagger_email.as_ref()) + .chain(c.author_email.as_ref()) + }) + .collect(); + if authors.len() <= 1 { + error!( "Multiple author verification failed for {}: requires multiple authors, found {:?}", new_commit_id, authors ); - Ok(PolicyResult::NotEnoughAuthors(new_commit_id)) - } else { - info!( - "Multiple author verification passed for {}: found multiple authors, {:?}", - new_commit_id, authors - ); + Ok(PolicyResult::NotEnoughAuthors(*new_commit_id)) + } else { + info!( + "Multiple author verification passed for {}: found multiple authors, {:?}", + new_commit_id, authors + ); + Ok(PolicyResult::Ok) + } + } + } + } +} + +fn verify_rebased<G: Git>( + commits: &[Commit], + git: &G, + ref_update: &ReferenceUpdate, + override_tag_pattern: &Option<String>, +) -> Result<PolicyResult, Box<dyn Error>> { + match ref_update { + ReferenceUpdate::Delete { .. } => { + info!("Rebase verification passed: No checks required for deleting a branch"); + Ok(PolicyResult::Ok) + } + ReferenceUpdate::New { new_commit_id, .. } => { + info!("Rebase verification passed for {}: New branch does not require being rebased for a merge commit", new_commit_id); Ok(PolicyResult::Ok) } + ReferenceUpdate::Update { + old_commit_id, + new_commit_id, + ref_name, + } => { + let is_merge = git.is_merge_commit(*new_commit_id); + let is_mainline = git.is_mainline(ref_name)?; + let new_commit = git.find_commit(*new_commit_id, override_tag_pattern)?; + + if !is_mainline { + info!( + "Rebase verification passed for {}: Not updating a mainline branch", + new_commit_id + ); + Ok(PolicyResult::Ok) + } else if !is_merge { + info!( + "Rebase verification passed for {}: Not a merge commit", + new_commit_id + ); + Ok(PolicyResult::Ok) + } else if commits.len() == 0 { + info!( + "Rebase verification passed for {}: No new commits pushed", + new_commit_id + ); + Ok(PolicyResult::Ok) + } else if !git.is_descendent_of(*new_commit_id, *old_commit_id)? { + info!( + "Rebase verification passed for {0}: Commit Id {0} is not a descendent of Commit Id {1}, it is most likely that a force-push has occurred", + new_commit_id, + old_commit_id + ); + Ok(PolicyResult::Ok) + } else { + let mut new_commit_is_rebased = true; + for parent_id in &new_commit.parents { + let parent_is_descendent_of_old_id = parent_id == old_commit_id + || git.is_descendent_of(*parent_id, *old_commit_id)?; + new_commit_is_rebased = new_commit_is_rebased && parent_is_descendent_of_old_id; + } + + if new_commit_is_rebased { + info!( + "Rebase verification passed for {}: Branch is up to date with the mainline it's being merged into", + new_commit_id + ); + Ok(PolicyResult::Ok) + } else { + error!("Rebase verification failed for {}: branch must be rebased before it can be merged into the mainline", new_commit_id); + Ok(PolicyResult::NotRebased(*new_commit_id)) + } + } + } } } |