summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJustin Wernick <justin@jemstep.com>2020-03-16 10:02:32 +0200
committerGitHub <noreply@github.com>2020-03-16 10:02:32 +0200
commit472f52b3b78168013038c2c67ee7981684a566f9 (patch)
tree8c7bc6e3c5aa73ebdcf9e274e8f8383e15476a16
parent9f792b1b2154dc4f0a2e54cabf7c75d7e700ce81 (diff)
parente7894ad2aaf5d512d0353048e2f2e92faf0af656 (diff)
Merge pull request #49 from jemstep/PYKE-11910-Enforce-Rebasing
Pyke 11910 enforce rebasing
-rw-r--r--Cargo.lock2
-rw-r--r--Cargo.toml2
-rw-r--r--readme.org42
-rw-r--r--src/config.rs2
-rw-r--r--src/error.rs6
-rw-r--r--src/git.rs19
-rw-r--r--src/lib.rs12
-rw-r--r--src/main.rs2
-rw-r--r--src/policies.rs278
-rw-r--r--src/policies/policy_result.rs60
-rw-r--r--src/reference_update.rs146
-rw-r--r--tests/policies_test.rs271
-rw-r--r--tests/test-repo.git/info/refs4
-rw-r--r--tests/test-repo.git/objects/info/commit-graphbin1940 -> 2052 bytes
-rw-r--r--tests/test-repo.git/objects/info/packs3
-rw-r--r--tests/test-repo.git/objects/pack/pack-9555b08a13142095d06c21a03e799895c34ca04e.idxbin0 -> 2080 bytes
-rw-r--r--tests/test-repo.git/objects/pack/pack-9555b08a13142095d06c21a03e799895c34ca04e.pack (renamed from tests/test-repo.git/objects/pack/pack-bb0116e43e397fd86cb8f3e00d9d709c11fa3611.pack)bin6318 -> 8613 bytes
-rw-r--r--tests/test-repo.git/objects/pack/pack-bb0116e43e397fd86cb8f3e00d9d709c11fa3611.bitmapbin786 -> 0 bytes
-rw-r--r--tests/test-repo.git/objects/pack/pack-bb0116e43e397fd86cb8f3e00d9d709c11fa3611.idxbin1856 -> 0 bytes
-rw-r--r--tests/test-repo.git/packed-refs4
20 files changed, 654 insertions, 199 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 06c1cc3..7a76226 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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)",
diff --git a/Cargo.toml b/Cargo.toml
index 0a152d8..e577f73 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "capn"
-version = "0.4.0"
+version = "0.5.0"
authors = ["Justin Wernick <justin@jemstep.com>"]
edition = "2018"
diff --git a/readme.org b/readme.org
index 035c2dd..5d6238b 100644
--- a/readme.org
+++ b/readme.org
@@ -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(),
+ }
}
}
diff --git a/src/git.rs b/src/git.rs
index 91902a3..de0854c 100644
--- a/src/git.rs
+++ b/src/git.rs
@@ -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())
}
}
diff --git a/src/lib.rs b/src/lib.rs
index 8b7b247..57b9d41 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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
index 91f5394..666d43b 100644
--- a/tests/test-repo.git/objects/info/commit-graph
+++ b/tests/test-repo.git/objects/info/commit-graph
Binary files differ
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
new file mode 100644
index 0000000..fd93e47
--- /dev/null
+++ b/tests/test-repo.git/objects/pack/pack-9555b08a13142095d06c21a03e799895c34ca04e.idx
Binary files differ
diff --git a/tests/test-repo.git/objects/pack/pack-bb0116e43e397fd86cb8f3e00d9d709c11fa3611.pack b/tests/test-repo.git/objects/pack/pack-9555b08a13142095d06c21a03e799895c34ca04e.pack
index 91db953..ef6dd69 100644
--- a/tests/test-repo.git/objects/pack/pack-bb0116e43e397fd86cb8f3e00d9d709c11fa3611.pack
+++ b/tests/test-repo.git/objects/pack/pack-9555b08a13142095d06c21a03e799895c34ca04e.pack
Binary files differ
diff --git a/tests/test-repo.git/objects/pack/pack-bb0116e43e397fd86cb8f3e00d9d709c11fa3611.bitmap b/tests/test-repo.git/objects/pack/pack-bb0116e43e397fd86cb8f3e00d9d709c11fa3611.bitmap
deleted file mode 100644
index e4d677a..0000000
--- a/tests/test-repo.git/objects/pack/pack-bb0116e43e397fd86cb8f3e00d9d709c11fa3611.bitmap
+++ /dev/null
Binary files differ
diff --git a/tests/test-repo.git/objects/pack/pack-bb0116e43e397fd86cb8f3e00d9d709c11fa3611.idx b/tests/test-repo.git/objects/pack/pack-bb0116e43e397fd86cb8f3e00d9d709c11fa3611.idx
deleted file mode 100644
index 786adde..0000000
--- a/tests/test-repo.git/objects/pack/pack-bb0116e43e397fd86cb8f3e00d9d709c11fa3611.idx
+++ /dev/null
Binary files differ
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