summaryrefslogtreecommitdiff
path: root/src/policies.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/policies.rs')
-rw-r--r--src/policies.rs278
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))
+ }
+ }
+ }
}
}