diff options
author | Justin Wernick <justin@jemstep.com> | 2020-03-16 10:02:32 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-03-16 10:02:32 +0200 |
commit | 472f52b3b78168013038c2c67ee7981684a566f9 (patch) | |
tree | 8c7bc6e3c5aa73ebdcf9e274e8f8383e15476a16 | |
parent | 9f792b1b2154dc4f0a2e54cabf7c75d7e700ce81 (diff) | |
parent | e7894ad2aaf5d512d0353048e2f2e92faf0af656 (diff) |
Merge pull request #49 from jemstep/PYKE-11910-Enforce-Rebasing
Pyke 11910 enforce rebasing
20 files changed, 654 insertions, 199 deletions
@@ -45,7 +45,7 @@ dependencies = [ [[package]] name = "capn" -version = "0.4.0" +version = "0.5.0" 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)", @@ -1,6 +1,6 @@ [package] name = "capn" -version = "0.4.0" +version = "0.5.0" authors = ["Justin Wernick <justin@jemstep.com>"] edition = "2018" @@ -238,20 +238,52 @@ To facilitate this, there is a bare Git repo, set up as a test repository, checked in to the tests folder of this repo. It is located at [[./tests/test-repo.git]]. +*** GPG keys and the test repo + +To create valid commits for these tests, you need to sign the commits +with the secret key in [[./tests/test-secret-key.asc]]. The password to +import this key is 'test'. + +You can import the key into your GPG keyring with the following command: + +#+BEGIN_SRC sh + # This command will prompt you for the key's password. The password is 'test'. + + gpg --import ./tests/test-secret-key.asc +#+END_SRC + +*** Cloning the test repo to make changes + To add extra testing scenarios, you'll probably need to add additional -commits to this bare repo. To do this, clone the repo somewhere else -on your drive with +commits to this bare repo. It's recommended to, as well as cloning the +test repo, also set your user inside the test repo to a test user. + +The test user uses the test GPG key that you imported above. #+BEGIN_SRC sh + # run this from somewhere outside of the Capn directory + git clone <path to test-repo.git> + + cd test-repo + + git config user.email "blackhole@jemstep.com" + git config user.name "Test User" + git config user.signingkey "0xE1F315E39CCCECAA" #+END_SRC Make any required commits, and push the changes back. Then commit the changes in this repo. -To create valid commits for these tests, you need to sign the commits -with the secret key in [[./tests/test-secret-key.asc]]. The password to -import this key is 'test'. +*** Visualising the test repo + +The easiest way to visualise the data in the test repo is to use =git +log=. + +#+BEGIN_SRC sh + cd <path to test-repo.git> + git log --graph --decorate --oneline --all +#+END_SRC * License diff --git a/src/config.rs b/src/config.rs index e2a983e..07ca998 100644 --- a/src/config.rs +++ b/src/config.rs @@ -38,6 +38,8 @@ pub struct VerifyGitCommitsConfig { #[serde(default = "default_false")] pub verify_different_authors: bool, + #[serde(default = "default_false")] + pub verify_rebased: bool, #[serde(default)] pub override_tag_pattern: Option<String>, diff --git a/src/error.rs b/src/error.rs index 370bb2f..189dfee 100644 --- a/src/error.rs +++ b/src/error.rs @@ -7,8 +7,10 @@ pub struct CapnError { } impl CapnError { - pub fn new(reason: String) -> CapnError { - CapnError { reason } + pub fn new(reason: impl Into<String>) -> CapnError { + CapnError { + reason: reason.into(), + } } } @@ -22,6 +22,7 @@ pub struct Commit { pub is_identical_tree_to_any_parent: bool, pub is_merge_commit: bool, pub tags: Vec<Tag>, + pub parents: Vec<Oid>, } #[derive(Debug, Clone)] @@ -40,7 +41,7 @@ pub trait Git: Sized { contents: &str, ) -> Result<(), Box<dyn Error>>; fn current_branch(&self) -> Result<String, Box<dyn Error>>; - fn is_tag(&self, id: Oid) -> bool; + fn is_tag(&self, ref_name: &str) -> Result<bool, Box<dyn Error>>; fn find_commit( &self, commit_id: Oid, @@ -72,6 +73,7 @@ pub trait Git: Sized { let config = Config::from_toml_string(&config_str)?; Ok(config) } + fn is_descendent_of(&self, commit: Oid, ancestor: Oid) -> Result<bool, Box<dyn Error>>; } pub struct LiveGit { @@ -170,6 +172,7 @@ impl Git for LiveGit { is_merge_commit: commit.parent_count() > 1, is_identical_tree_to_any_parent: Self::is_identical_tree_to_any_parent(&commit), tags: tags, + parents: commit.parent_ids().collect(), }) } @@ -384,11 +387,15 @@ impl Git for LiveGit { }) } - fn is_tag(&self, id: Oid) -> bool { - match self.repo.find_tag(id) { - Ok(_) => true, - _ => false, - } + fn is_tag(&self, ref_name: &str) -> Result<bool, Box<dyn Error>> { + let reference = self.repo.find_reference(ref_name)?; + Ok(reference.is_tag()) + } + + fn is_descendent_of(&self, commit: Oid, ancestor: Oid) -> Result<bool, Box<dyn Error>> { + self.repo + .graph_descendant_of(commit, ancestor) + .map_err(|e| e.into()) } } @@ -12,7 +12,8 @@ use crate::config::Config; use crate::fs::Fs; use crate::git::Git; use crate::gpg::Gpg; -use crate::policies::*; +use crate::policies::{policy_result::PolicyResult, *}; +use crate::reference_update::ReferenceUpdate; pub mod config; pub mod error; @@ -22,6 +23,7 @@ pub mod gpg; pub mod keyring; pub mod logger; pub mod policies; +pub mod reference_update; #[derive(Debug, StructOpt)] pub struct PrepareCommitMsg { @@ -76,10 +78,12 @@ pub fn pre_push<G: Git, P: Gpg>( _remote_ref: &str, remote_sha: &str, ) -> Result<PolicyResult, Box<dyn Error>> { + let ref_update = ReferenceUpdate::from_git_hook_format(remote_sha, local_sha, local_ref)?; + vec![config .verify_git_commits .as_ref() - .map(|c| verify_git_commits::<G, P>(git, gpg, c, remote_sha, local_sha, local_ref))] + .map(|c| verify_git_commits::<G, P>(git, gpg, c, &ref_update))] .into_iter() .flatten() .collect() @@ -93,10 +97,12 @@ pub fn pre_receive<G: Git, P: Gpg>( new_value: &str, ref_name: &str, ) -> Result<PolicyResult, Box<dyn Error>> { + let ref_update = ReferenceUpdate::from_git_hook_format(old_value, new_value, ref_name)?; + vec![config .verify_git_commits .as_ref() - .map(|c| verify_git_commits::<G, P>(git, gpg, c, old_value, new_value, ref_name))] + .map(|c| verify_git_commits::<G, P>(git, gpg, c, &ref_update))] .into_iter() .flatten() .collect() diff --git a/src/main.rs b/src/main.rs index e31a274..b1f0374 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ use capn::git::{Git, LiveGit}; use capn::gpg::{Gpg, LiveGpg}; use capn::logger; use capn::logger::{Logger, LoggingOpt}; -use capn::policies::PolicyResult; +use capn::policies::policy_result::PolicyResult; use capn::*; use log::*; 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)) + } + } + } } } diff --git a/src/policies/policy_result.rs b/src/policies/policy_result.rs new file mode 100644 index 0000000..e12ff42 --- /dev/null +++ b/src/policies/policy_result.rs @@ -0,0 +1,60 @@ +use git2::Oid; +use std::fmt; +use std::iter; + +#[derive(Debug, Clone)] +pub enum PolicyResult { + Ok, + UnsignedCommit(Oid), + UnsignedMergeCommit(Oid), + NotEnoughAuthors(Oid), + InvalidAuthorEmail(Oid, String), + MissingAuthorEmail(Oid), + InvalidCommitterEmail(Oid, String), + MissingCommitterEmail(Oid), + NotRebased(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), + NotRebased(id) => write!(f, "Merge commit needs to be rebased on the mainline before it can be merged: {}", 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) + } +} diff --git a/src/reference_update.rs b/src/reference_update.rs new file mode 100644 index 0000000..3f072b1 --- /dev/null +++ b/src/reference_update.rs @@ -0,0 +1,146 @@ +use crate::error::CapnError; +use git2::Oid; +use std::error::Error; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ReferenceUpdate { + New { + new_commit_id: Oid, + ref_name: String, + }, + Delete { + old_commit_id: Oid, + ref_name: String, + }, + Update { + old_commit_id: Oid, + new_commit_id: Oid, + ref_name: String, + }, +} + +impl ReferenceUpdate { + pub fn from_git_hook_format( + old_commit_id: &str, + new_commit_id: &str, + ref_name: &str, + ) -> Result<ReferenceUpdate, Box<dyn Error>> { + let old_commit_id = Oid::from_str(old_commit_id)?; + let new_commit_id = Oid::from_str(new_commit_id)?; + let ref_name = ref_name.to_owned(); + match (old_commit_id.is_zero(), new_commit_id.is_zero()) { + (false, false) => Ok(ReferenceUpdate::Update { + old_commit_id, + new_commit_id, + ref_name, + }), + (false, true) => Ok(ReferenceUpdate::Delete { + old_commit_id, + ref_name, + }), + (true, false) => Ok(ReferenceUpdate::New { + new_commit_id, + ref_name, + }), + (true, true) => Err(Box::new(CapnError::new("Invalid reference update specification, trying to update from a zero commit to another zero commit"))) + } + } + + pub fn old_commit_id(&self) -> Option<Oid> { + use self::ReferenceUpdate::*; + match self { + Delete { old_commit_id, .. } | Update { old_commit_id, .. } => Some(*old_commit_id), + _ => None, + } + } + + pub fn new_commit_id(&self) -> Option<Oid> { + use self::ReferenceUpdate::*; + match self { + New { new_commit_id, .. } | Update { new_commit_id, .. } => Some(*new_commit_id), + _ => None, + } + } + + pub fn ref_name(&self) -> &str { + use self::ReferenceUpdate::*; + match self { + New { ref_name, .. } | Delete { ref_name, .. } | Update { ref_name, .. } => ref_name, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use git2::Oid; + + #[test] + fn example_deleting_branch() { + let old_commit_id = Oid::from_str("eb5e0185546b0bb1a13feec6b9ee8b39985fea42").unwrap(); + let ref_name = "refs/heads/master"; + let ref_update = ReferenceUpdate::from_git_hook_format( + &format!("{}", old_commit_id), + "0000000000000000000000000000000000000000", + ref_name, + ) + .unwrap(); + assert_eq!( + ref_update, + ReferenceUpdate::Delete { + old_commit_id, + ref_name: ref_name.to_string() + } + ); + assert_eq!(ref_update.old_commit_id(), Some(old_commit_id)); + assert_eq!(ref_update.new_commit_id(), None); + assert_eq!(ref_update.ref_name(), ref_name); + } + + #[test] + fn example_creating_branch() { + let new_commit_id = Oid::from_str("eb5e0185546b0bb1a13feec6b9ee8b39985fea42").unwrap(); + let ref_name = "refs/heads/master"; + let ref_update = ReferenceUpdate::from_git_hook_format( + "0000000000000000000000000000000000000000", + &format!("{}", new_commit_id), + ref_name, + ) + .unwrap(); + assert_eq!( + ref_update, + ReferenceUpdate::New { + new_commit_id, + ref_name: ref_name.to_string() + } + ); + assert_eq!(ref_update.old_commit_id(), None); + assert_eq!(ref_update.new_commit_id(), Some(new_commit_id)); + assert_eq!(ref_update.ref_name(), ref_name); + } + + #[test] + fn example_updating_branch() { + let old_commit_id = Oid::from_str("eb5e0185546b0bb1a13feec6b9ee8b39985fea42").unwrap(); + let new_commit_id = Oid::from_str("6004dfdb071c71e5e76ad55b924b576487e1c485").unwrap(); + + let ref_name = "refs/heads/master"; + let ref_update = ReferenceUpdate::from_git_hook_format( + &format!("{}", old_commit_id), + &format!("{}", new_commit_id), + ref_name, + ) + .unwrap(); + assert_eq!( + ref_update, + ReferenceUpdate::Update { + old_commit_id, + new_commit_id, + ref_name: ref_name.to_string() + } + ); + assert_eq!(ref_update.old_commit_id(), Some(old_commit_id)); + assert_eq!(ref_update.new_commit_id(), Some(new_commit_id)); + assert_eq!(ref_update.ref_name(), ref_name); + } +} diff --git a/tests/policies_test.rs b/tests/policies_test.rs index bff0834..f4299ba 100644 --- a/tests/policies_test.rs +++ b/tests/policies_test.rs @@ -1,6 +1,7 @@ use capn; use capn::config::{Config, GitConfig, VerifyGitCommitsConfig}; use capn::policies; +use capn::reference_update::ReferenceUpdate; use capn::git::LiveGit; use capn::gpg::test::MockGpg; @@ -54,6 +55,7 @@ fn verify_commits_config() -> VerifyGitCommitsConfig { verify_email_addresses: true, verify_commit_signatures: true, verify_different_authors: true, + verify_rebased: false, override_tag_pattern: Some("capn-override-*".to_string()), override_tags_required: 1, } @@ -86,9 +88,12 @@ fn verify_git_commits_happy_path_from_empty() { &LiveGit::default("./").unwrap(), MockGpg, &verify_commits_config(), - "0000000000000000000000000000000000000000", - "7f9763e189ade34345e683ab7e0c22d164280452", - "refs/heads/master", + &ReferenceUpdate::from_git_hook_format( + "0000000000000000000000000000000000000000", + "7f9763e189ade34345e683ab7e0c22d164280452", + "refs/heads/master", + ) + .unwrap(), ) .unwrap(); assert!(result.is_ok(), "Error: {:?}", result); @@ -101,9 +106,12 @@ fn verify_git_commits_happy_path_from_existing() { &LiveGit::default("./").unwrap(), MockGpg, &verify_commits_config(), - "7f9763e189ade34345e683ab7e0c22d164280452", - "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", - "refs/heads/master", + &ReferenceUpdate::from_git_hook_format( + "7f9763e189ade34345e683ab7e0c22d164280452", + "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", + "refs/heads/master", + ) + .unwrap(), ) .unwrap(); assert!(result.is_ok(), "Error: {:?}", result); @@ -116,9 +124,12 @@ fn verify_git_commits_happy_path_unsigned_trivial_no_fast_forward_merge() { &LiveGit::default("./").unwrap(), MockGpg, &verify_commits_config(), - "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", - "3eb315d10e2ad89555d7bfc78a1db1ce07bce434", - "refs/heads/master", + &ReferenceUpdate::from_git_hook_format( + "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", + "3eb315d10e2ad89555d7bfc78a1db1ce07bce434", + "refs/heads/master", + ) + .unwrap(), ) .unwrap(); assert!(result.is_ok(), "Error: {:?}", result); @@ -131,9 +142,12 @@ fn verify_git_commits_happy_path_unsigned_trivial_merge() { &LiveGit::default("./").unwrap(), MockGpg, &verify_commits_config(), - "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", - "6754e4ec9b2dec567190d5a7f0be18b1a23d632a", - "refs/heads/master", + &ReferenceUpdate::from_git_hook_format( + "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", + "6754e4ec9b2dec567190d5a7f0be18b1a23d632a", + "refs/heads/master", + ) + .unwrap(), ) .unwrap(); assert!(result.is_ok(), "Error: {:?}", result); @@ -146,9 +160,12 @@ fn verify_git_commits_single_unsigned_commit() { &LiveGit::default("./").unwrap(), MockGpg, &verify_commits_config(), - "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", - "d2e3bfdc923986d04e7a6368b5fdd78b1ddf84f1", - "refs/heads/master", + &ReferenceUpdate::from_git_hook_format( + "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", + "d2e3bfdc923986d04e7a6368b5fdd78b1ddf84f1", + "refs/heads/master", + ) + .unwrap(), ) .unwrap(); assert!(result.is_err()); @@ -161,9 +178,12 @@ fn verify_git_commits_single_unsigned_commit_new_branch() { &LiveGit::default("./").unwrap(), MockGpg, &verify_commits_config(), - "0000000000000000000000000000000000000000", - "d2e3bfdc923986d04e7a6368b5fdd78b1ddf84f1", - "refs/heads/master", + &ReferenceUpdate::from_git_hook_format( + "0000000000000000000000000000000000000000", + "d2e3bfdc923986d04e7a6368b5fdd78b1ddf84f1", + "refs/heads/master", + ) + .unwrap(), ) .unwrap(); assert!(result.is_err()); @@ -176,9 +196,12 @@ fn verify_git_commits_unsigned_commit_being_merged_in() { &LiveGit::default("./").unwrap(), MockGpg, &verify_commits_config(), - "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", - "ef1710ba8bd1f5ed0eec7883af30fca732d39afd", - "refs/heads/master", + &ReferenceUpdate::from_git_hook_format( + "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", + "ef1710ba8bd1f5ed0eec7883af30fca732d39afd", + "refs/heads/master", + ) + .unwrap(), ) .unwrap(); assert!(result.is_err()); @@ -191,9 +214,12 @@ fn verify_git_commits_unsigned_commit_behind_a_merge_commit() { &LiveGit::default("./").unwrap(), MockGpg, &verify_commits_config(), - "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", - "e9752e78505f3c9bcec15d4bef4299caf0538388", - "refs/heads/master", + &ReferenceUpdate::from_git_hook_format( + "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", + "e9752e78505f3c9bcec15d4bef4299caf0538388", + "refs/heads/master", + ) + .unwrap(), ) .unwrap(); assert!(result.is_err()); @@ -206,9 +232,12 @@ fn verify_git_commits_invalid_author() { &LiveGit::default("./").unwrap(), MockGpg, &verify_commits_config(), - "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", - "afe2141ef20abd098927adc66d6728821cb34f59", - "refs/heads/master", + &ReferenceUpdate::from_git_hook_format( + "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", + "afe2141ef20abd098927adc66d6728821cb34f59", + "refs/heads/master", + ) + .unwrap(), ) .unwrap(); assert!(result.is_err()); @@ -221,9 +250,12 @@ fn verify_git_commits_code_injected_into_unsigned_merge() { &LiveGit::default("./").unwrap(), MockGpg, &verify_commits_config(), - "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", - "eef93e7f977c125f92fc78116fc9b881e4055ae8", - "refs/heads/master", + &ReferenceUpdate::from_git_hook_format( + "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", + "eef93e7f977c125f92fc78116fc9b881e4055ae8", + "refs/heads/master", + ) + .unwrap(), ) .unwrap(); assert!(result.is_err()); @@ -237,9 +269,12 @@ fn verify_git_commits_happy_path_pushing_previously_checked_merge_commit() { &LiveGit::default("./").unwrap(), MockGpg, &verify_commits_config(), - "3eb315d10e2ad89555d7bfc78a1db1ce07bce434", - "3eb315d10e2ad89555d7bfc78a1db1ce07bce434", - "refs/heads/master", + &ReferenceUpdate::from_git_hook_format( + "3eb315d10e2ad89555d7bfc78a1db1ce07bce434", + "3eb315d10e2ad89555d7bfc78a1db1ce07bce434", + "refs/heads/master", + ) + .unwrap(), ) .unwrap(); assert!(result.is_ok(), "Error: {:?}", result); @@ -252,9 +287,12 @@ fn verify_git_commits_author_merged_own_code_not_on_head() { &LiveGit::default("./").unwrap(), MockGpg, &verify_commits_config(), - "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", - "6004dfdb071c71e5e76ad55b924b576487e1c485", - "refs/heads/valid-branch", + &ReferenceUpdate::from_git_hook_format( + "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", + "6004dfdb071c71e5e76ad55b924b576487e1c485", + "refs/heads/valid-branch", + ) + .unwrap(), ) .unwrap(); assert!(result.is_ok(), "Error: {:?}", result); @@ -267,15 +305,18 @@ fn verify_git_commits_author_merged_own_code_on_configured_mainline() { &LiveGit::new( "./", GitConfig { - mainlines: vec!["m*".into()], + mainlines: vec!["master".into()], }, ) .unwrap(), MockGpg, &verify_commits_config(), - "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", - "6004dfdb071c71e5e76ad55b924b576487e1c485", - "refs/heads/master", + &ReferenceUpdate::from_git_hook_format( + "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", + "6004dfdb071c71e5e76ad55b924b576487e1c485", + "refs/heads/master", + ) + .unwrap(), ) .unwrap(); assert!(result.is_err()); @@ -294,9 +335,12 @@ fn verify_git_commits_author_trivial_merge_between_mainlines() { .unwrap(), MockGpg, &verify_commits_config(), - "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", - "6004dfdb071c71e5e76ad55b924b576487e1c485", - "refs/heads/valid-branch", + &ReferenceUpdate::from_git_hook_format( + "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", + "6004dfdb071c71e5e76ad55b924b576487e1c485", + "refs/heads/valid-branch", + ) + .unwrap(), ) .unwrap(); assert!(result.is_ok(), "Error: {:?}", result); @@ -309,9 +353,12 @@ fn verify_git_commits_author_merged_own_code_on_head() { &LiveGit::default("./").unwrap(), MockGpg, &verify_commits_config(), - "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", - "6004dfdb071c71e5e76ad55b924b576487e1c485", - "refs/heads/master", + &ReferenceUpdate::from_git_hook_format( + "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", + "6004dfdb071c71e5e76ad55b924b576487e1c485", + "refs/heads/master", + ) + .unwrap(), ) .unwrap(); assert!(result.is_err()); @@ -324,9 +371,12 @@ fn verify_git_commits_author_merged_own_code_on_head_with_tag() { &LiveGit::default("./").unwrap(), MockGpg, &verify_commits_config(), - "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", - "e5924d0748c8852d74049679b34ca4b3b0570d0d", - "refs/heads/master", + &ReferenceUpdate::from_git_hook_format( + "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", + "e5924d0748c8852d74049679b34ca4b3b0570d0d", + "refs/heads/master", + ) + .unwrap(), ) .unwrap(); assert!(result.is_ok()); @@ -339,9 +389,12 @@ fn verify_tagged_git_commits_override_rules() { &LiveGit::default("./").unwrap(), MockGpg, &verify_commits_config(), - "7f9763e189ade34345e683ab7e0c22d164280452", - "6f00838625cd1b7dc0acc66e43fee5594f0f124c", - "refs/heads/master", + &ReferenceUpdate::from_git_hook_format( + "7f9763e189ade34345e683ab7e0c22d164280452", + "6f00838625cd1b7dc0acc66e43fee5594f0f124c", + "refs/heads/master", + ) + .unwrap(), ) .unwrap(); assert!(result.is_ok(), "Error: {:?}", result); @@ -357,10 +410,118 @@ fn verify_tagged_git_commits_not_overridden_if_not_enough_tags() { override_tags_required: 2, ..verify_commits_config() }, - "7f9763e189ade34345e683ab7e0c22d164280452", - "6f00838625cd1b7dc0acc66e43fee5594f0f124c", - "refs/heads/master", + &ReferenceUpdate::from_git_hook_format( + "7f9763e189ade34345e683ab7e0c22d164280452", + "6f00838625cd1b7dc0acc66e43fee5594f0f124c", + "refs/heads/master", + ) + .unwrap(), ) .unwrap(); assert!(result.is_err()); } + +#[test] +fn verify_unrebased_branch_is_allowed_if_not_required() { + before_all(); + let result = policies::verify_git_commits::<LiveGit, MockGpg>( + &LiveGit::default("./").unwrap(), + MockGpg, + &VerifyGitCommitsConfig { + verify_rebased: false, + ..verify_commits_config() + }, + &ReferenceUpdate::from_git_hook_format( + "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", + "6eea56095f7498043f1d3d74bad46056b92675ea", + "refs/heads/master", + ) + .unwrap(), + ) + .unwrap(); + assert!(result.is_ok(), "Error: {:?}", result); +} + +#[test] +fn verify_unrebased_branch_is_blocked_if_required() { + before_all(); + let result = policies::verify_git_commits::<LiveGit, MockGpg>( + &LiveGit::default("./").unwrap(), + MockGpg, + &VerifyGitCommitsConfig { + verify_rebased: true, + ..verify_commits_config() + }, + &ReferenceUpdate::from_git_hook_format( + "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", + "6eea56095f7498043f1d3d74bad46056b92675ea", + "refs/heads/master", + ) + .unwrap(), + ) + .unwrap(); + assert!(result.is_err()); +} + +#[test] +fn verify_rebased_branch_is_allowed_when_required() { + before_all(); + let result = policies::verify_git_commits::<LiveGit, MockGpg>( + &LiveGit::default("./").unwrap(), + MockGpg, + &VerifyGitCommitsConfig { + verify_rebased: true, + ..verify_commits_config() + }, + &ReferenceUpdate::from_git_hook_format( + "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", + "3eb315d10e2ad89555d7bfc78a1db1ce07bce434", + "refs/heads/master", + ) + .unwrap(), + ) + .unwrap(); + assert!(result.is_ok(), "Error: {:?}", result); +} + +#[test] +fn verify_unrebased_branch_is_blocked_when_mainline_has_reverts() { + before_all(); + let result = policies::verify_git_commits::<LiveGit, MockGpg>( + &LiveGit::default("./").unwrap(), + MockGpg, + &VerifyGitCommitsConfig { + verify_rebased: true, + ..verify_commits_config() + }, + &ReferenceUpdate::from_git_hook_format( + "3f48e07e5f8d5ab932a0a298ba3dd52809eef6d2", + "3911fdcbba1621d39096cef1bddbf4b35be2aeb6", + "refs/heads/master", + ) + .unwrap(), + ) + .unwrap(); + assert!(result.is_err()); +} + +#[test] +fn verify_rebased_branch_is_allowed_when_a_force_push_has_occurred() { + before_all(); + let result = policies::verify_git_commits::<LiveGit, MockGpg>( + &LiveGit::default("./").unwrap(), + MockGpg, + &VerifyGitCommitsConfig { + verify_rebased: true, + ..verify_commits_config() + }, + &ReferenceUpdate::from_git_hook_format( + "eb5e0185546b0bb1a13feec6b9ee8b39985fea42", + "02bb68e637bda7667428f8fb3b709be5720fe76a", + "refs/heads/master", + ) + .unwrap(), + ) + .unwrap(); + assert!(result.is_ok(), "Error: {:?}", result); +} diff --git a/tests/test-repo.git/info/refs b/tests/test-repo.git/info/refs index 9220d69..cce2551 100644 --- a/tests/test-repo.git/info/refs +++ b/tests/test-repo.git/info/refs @@ -1,9 +1,13 @@ afe2141ef20abd098927adc66d6728821cb34f59 refs/heads/invalid-author eef93e7f977c125f92fc78116fc9b881e4055ae8 refs/heads/invalid-merge-with-additions eb5e0185546b0bb1a13feec6b9ee8b39985fea42 refs/heads/master +3f48e07e5f8d5ab932a0a298ba3dd52809eef6d2 refs/heads/master-with-revert +3911fdcbba1621d39096cef1bddbf4b35be2aeb6 refs/heads/merged-with-unrebased-branch e5924d0748c8852d74049679b34ca4b3b0570d0d refs/heads/multiple-author-in-tag-trunk 6004dfdb071c71e5e76ad55b924b576487e1c485 refs/heads/same-author 6f00838625cd1b7dc0acc66e43fee5594f0f124c refs/heads/tagged-branch +318fae0d25bceeb2c5d19a966c4de4c2d9fceae2 refs/heads/unrebased +6eea56095f7498043f1d3d74bad46056b92675ea refs/heads/unrebased-merge d2e3bfdc923986d04e7a6368b5fdd78b1ddf84f1 refs/heads/unsigned e9752e78505f3c9bcec15d4bef4299caf0538388 refs/heads/unsigned-buried-behind-merge 6754e4ec9b2dec567190d5a7f0be18b1a23d632a refs/heads/valid-automerge-commit diff --git a/tests/test-repo.git/objects/info/commit-graph b/tests/test-repo.git/objects/info/commit-graph Binary files differindex 91f5394..666d43b 100644 --- a/tests/test-repo.git/objects/info/commit-graph +++ b/tests/test-repo.git/objects/info/commit-graph diff --git a/tests/test-repo.git/objects/info/packs b/tests/test-repo.git/objects/info/packs index 0c7c399..d9f9067 100644 --- a/tests/test-repo.git/objects/info/packs +++ b/tests/test-repo.git/objects/info/packs @@ -1,2 +1,3 @@ -P pack-bb0116e43e397fd86cb8f3e00d9d709c11fa3611.pack +P pack-ce13b28bc6aaf0c8e45f7fd81b8c8239ede8e231.pack +P pack-9555b08a13142095d06c21a03e799895c34ca04e.pack diff --git a/tests/test-repo.git/objects/pack/pack-9555b08a13142095d06c21a03e799895c34ca04e.idx b/tests/test-repo.git/objects/pack/pack-9555b08a13142095d06c21a03e799895c34ca04e.idx Binary files differnew file mode 100644 index 0000000..fd93e47 --- /dev/null +++ b/tests/test-repo.git/objects/pack/pack-9555b08a13142095d06c21a03e799895c34ca04e.idx diff --git a/tests/test-repo.git/objects/pack/pack-bb0116e43e397fd86cb8f3e00d9d709c11fa3611.pack b/tests/test-repo.git/objects/pack/pack-9555b08a13142095d06c21a03e799895c34ca04e.pack Binary files differindex 91db953..ef6dd69 100644 --- a/tests/test-repo.git/objects/pack/pack-bb0116e43e397fd86cb8f3e00d9d709c11fa3611.pack +++ b/tests/test-repo.git/objects/pack/pack-9555b08a13142095d06c21a03e799895c34ca04e.pack diff --git a/tests/test-repo.git/objects/pack/pack-bb0116e43e397fd86cb8f3e00d9d709c11fa3611.bitmap b/tests/test-repo.git/objects/pack/pack-bb0116e43e397fd86cb8f3e00d9d709c11fa3611.bitmap Binary files differdeleted file mode 100644 index e4d677a..0000000 --- a/tests/test-repo.git/objects/pack/pack-bb0116e43e397fd86cb8f3e00d9d709c11fa3611.bitmap +++ /dev/null diff --git a/tests/test-repo.git/objects/pack/pack-bb0116e43e397fd86cb8f3e00d9d709c11fa3611.idx b/tests/test-repo.git/objects/pack/pack-bb0116e43e397fd86cb8f3e00d9d709c11fa3611.idx Binary files differdeleted file mode 100644 index 786adde..0000000 --- a/tests/test-repo.git/objects/pack/pack-bb0116e43e397fd86cb8f3e00d9d709c11fa3611.idx +++ /dev/null diff --git a/tests/test-repo.git/packed-refs b/tests/test-repo.git/packed-refs index e752539..fd29b87 100644 --- a/tests/test-repo.git/packed-refs +++ b/tests/test-repo.git/packed-refs @@ -2,9 +2,13 @@ afe2141ef20abd098927adc66d6728821cb34f59 refs/heads/invalid-author eef93e7f977c125f92fc78116fc9b881e4055ae8 refs/heads/invalid-merge-with-additions eb5e0185546b0bb1a13feec6b9ee8b39985fea42 refs/heads/master +3f48e07e5f8d5ab932a0a298ba3dd52809eef6d2 refs/heads/master-with-revert +3911fdcbba1621d39096cef1bddbf4b35be2aeb6 refs/heads/merged-with-unrebased-branch e5924d0748c8852d74049679b34ca4b3b0570d0d refs/heads/multiple-author-in-tag-trunk 6004dfdb071c71e5e76ad55b924b576487e1c485 refs/heads/same-author 6f00838625cd1b7dc0acc66e43fee5594f0f124c refs/heads/tagged-branch +318fae0d25bceeb2c5d19a966c4de4c2d9fceae2 refs/heads/unrebased +6eea56095f7498043f1d3d74bad46056b92675ea refs/heads/unrebased-merge d2e3bfdc923986d04e7a6368b5fdd78b1ddf84f1 refs/heads/unsigned e9752e78505f3c9bcec15d4bef4299caf0538388 refs/heads/unsigned-buried-behind-merge 6754e4ec9b2dec567190d5a7f0be18b1a23d632a refs/heads/valid-automerge-commit |