From ed3bd5d53d1248efbc117d4603014e2fc725e8be Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sergio=20Casta=C3=B1o=20Arteaga?= <tegioz@icloud.com>
Date: Wed, 4 Sep 2024 17:39:20 +0200
Subject: [PATCH] Add support for remediation commits (#27)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Sergio CastaƱo Arteaga <tegioz@icloud.com>
---
 Cargo.lock                  |   27 +-
 Cargo.toml                  |    1 +
 dco2/Cargo.toml             |    1 +
 dco2/src/dco/check/mod.rs   |  190 ++++-
 dco2/src/dco/check/tests.rs | 1522 ++++++++++++++++++++++++++++++++++-
 dco2/src/github/client.rs   |   26 +-
 6 files changed, 1730 insertions(+), 37 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index c1ae071..a437542 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -490,6 +490,7 @@ dependencies = [
  "mockall",
  "octorust",
  "pem 3.0.4",
+ "pretty_assertions",
  "regex",
  "serde",
  "serde_json",
@@ -540,6 +541,12 @@ dependencies = [
  "powerfmt",
 ]
 
+[[package]]
+name = "diff"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
+
 [[package]]
 name = "digest"
 version = "0.10.7"
@@ -1388,7 +1395,7 @@ checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467"
 dependencies = [
  "inlinable_string",
  "pear_codegen",
- "yansi",
+ "yansi 1.0.1",
 ]
 
 [[package]]
@@ -1501,6 +1508,16 @@ dependencies = [
  "termtree",
 ]
 
+[[package]]
+name = "pretty_assertions"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
+dependencies = [
+ "diff",
+ "yansi 0.5.1",
+]
+
 [[package]]
 name = "proc-macro2"
 version = "1.0.86"
@@ -1520,7 +1537,7 @@ dependencies = [
  "quote",
  "syn",
  "version_check",
- "yansi",
+ "yansi 1.0.1",
 ]
 
 [[package]]
@@ -2765,6 +2782,12 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
 
+[[package]]
+name = "yansi"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
+
 [[package]]
 name = "yansi"
 version = "1.0.1"
diff --git a/Cargo.toml b/Cargo.toml
index a492fcb..f5a07ad 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -32,6 +32,7 @@ lambda_http = "0.13.0"
 mockall = "0.13.0"
 octorust = "0.8.0-rc.1"
 pem = "3.0.4"
+pretty_assertions = "1.4.0"
 regex = "1.10.6"
 serde = { version = "1.0.209", features = ["derive"] }
 serde_json = "1.0.127"
diff --git a/dco2/Cargo.toml b/dco2/Cargo.toml
index 1747bae..41f82d9 100644
--- a/dco2/Cargo.toml
+++ b/dco2/Cargo.toml
@@ -28,3 +28,4 @@ tracing = { workspace = true }
 [dev-dependencies]
 indoc = { workspace = true }
 mockall = { workspace = true }
+pretty_assertions = { workspace = true }
diff --git a/dco2/src/dco/check/mod.rs b/dco2/src/dco/check/mod.rs
index 225a70b..a024b9d 100644
--- a/dco2/src/dco/check/mod.rs
+++ b/dco2/src/dco/check/mod.rs
@@ -1,6 +1,7 @@
 //! This module contains the DCO check logic.
 
-use crate::github::{Commit, Config};
+use crate::github::{Commit, Config, ConfigAllowRemediationCommits, GitUser};
+use anyhow::{bail, Result};
 use askama::Template;
 use email_address::EmailAddress;
 use regex::Regex;
@@ -67,14 +68,18 @@ pub(crate) enum CommitSuccessReason {
     FromBot,
     IsMerge,
     ValidSignOff,
+    ValidSignOffInRemediationCommit,
 }
 
 impl Display for CommitSuccessReason {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         match self {
-            CommitSuccessReason::FromBot => write!(f, "sign-off not required in bot commit"),
-            CommitSuccessReason::IsMerge => write!(f, "sign-off not required in merge commit"),
+            CommitSuccessReason::FromBot => write!(f, "skipped: sign-off not required in bot commit"),
+            CommitSuccessReason::IsMerge => write!(f, "skipped: sign-off not required in merge commit"),
             CommitSuccessReason::ValidSignOff => write!(f, "valid sign-off found"),
+            CommitSuccessReason::ValidSignOffInRemediationCommit => {
+                write!(f, "valid sign-off found in remediation commit")
+            }
         }
     }
 }
@@ -87,6 +92,9 @@ pub(crate) fn check(input: &CheckInput) -> CheckOutput {
         num_commits_with_errors: 0,
     };
 
+    // Get remediations from all commits
+    let remediations = get_remediations(&input.config.allow_remediation_commits, &input.commits);
+
     // Check each commit
     for commit in &input.commits {
         let mut commit_output = CommitCheckOutput::new(commit.clone());
@@ -115,14 +123,21 @@ pub(crate) fn check(input: &CheckInput) -> CheckOutput {
         }
 
         // Check if any of the sign-offs matches the author's or committer's email
-        if emails_are_valid && !signoffs.is_empty() && !signoffs_match(&signoffs, commit) {
-            commit_output.errors.push(CommitError::SignOffMismatch);
+        if emails_are_valid && !signoffs.is_empty() {
+            if signoffs_match(&signoffs, commit) {
+                commit_output.success_reason = Some(CommitSuccessReason::ValidSignOff);
+            } else {
+                commit_output.errors.push(CommitError::SignOffMismatch);
+            }
         }
 
-        // Track commit
-        if commit_output.errors.is_empty() {
-            commit_output.success_reason = Some(CommitSuccessReason::ValidSignOff);
+        // Check if the sign-off is present in a remediation commit
+        if commit_output.success_reason.is_none() && remediations_match(&remediations, commit) {
+            commit_output.errors.clear();
+            commit_output.success_reason = Some(CommitSuccessReason::ValidSignOffInRemediationCommit);
         }
+
+        // Track commit
         output.commits.push(commit_output);
     }
 
@@ -186,15 +201,18 @@ static SIGN_OFF: LazyLock<Regex> = LazyLock::new(|| {
 struct SignOff {
     name: String,
     email: String,
-    kind: SignOffKind,
 }
 
-/// Sign-off kind.
-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
-enum SignOffKind {
-    Explicit,
-    IndividualRemediation,
-    ThirdPartyRemediation,
+impl SignOff {
+    /// Check if the sign-off matches the provided user (if any).
+    fn matches_user(&self, user: &Option<GitUser>) -> bool {
+        if let Some(user) = user {
+            self.name.to_lowercase() == user.name.to_lowercase()
+                && self.email.to_lowercase() == user.email.to_lowercase()
+        } else {
+            false
+        }
+    }
 }
 
 /// Get sign-offs found in the commit message.
@@ -205,7 +223,6 @@ fn get_signoffs(commit: &Commit) -> Vec<SignOff> {
         signoffs.push(SignOff {
             name: name.to_string(),
             email: email.to_string(),
-            kind: SignOffKind::Explicit,
         });
     }
 
@@ -214,21 +231,140 @@ fn get_signoffs(commit: &Commit) -> Vec<SignOff> {
 
 /// Check if any of the sign-offs matches the author's or committer's email.
 fn signoffs_match(signoffs: &[SignOff], commit: &Commit) -> bool {
-    let signoff_matches_author = |s: &SignOff| {
-        if let Some(a) = &commit.author {
-            s.name.to_lowercase() == a.name.to_lowercase() && s.email.to_lowercase() == a.email.to_lowercase()
+    signoffs
+        .iter()
+        .any(|signoff| signoff.matches_user(&commit.author) || signoff.matches_user(&commit.committer))
+}
+
+/// Individual remediation regular expression.
+static INDIVIDUAL_REMEDIATION: LazyLock<Regex> = LazyLock::new(|| {
+    Regex::new(r"(?mi)^I, (.*) <(.*)>, hereby add my Signed-off-by to this commit: (.*)\s*$")
+        .expect("expr in INDIVIDUAL_REMEDIATION to be valid")
+});
+
+/// Third party remediation regular expression.
+static THIRD_PARTY_REMEDIATION: LazyLock<Regex> = LazyLock::new(|| {
+    Regex::new(r"(?mi)^On behalf of (.*) <(.*)>, I, (.*) <(.*)>, hereby add my Signed-off-by to this commit: (.*)\s*$")
+        .expect("expr in THIRD_PARTY_REMEDIATION to be valid")
+});
+
+/// Remediation details.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+struct Remediation {
+    pub declarant: GitUser,
+    pub target_sha: String,
+}
+
+impl Remediation {
+    /// Create a new remediation.
+    fn new(
+        declarant_name: &str,
+        declarant_email: &str,
+        representative_name: Option<&str>,
+        representative_email: Option<&str>,
+        target_sha: &str,
+        commit: &Commit,
+    ) -> Result<Self> {
+        // Prepare declarant and representative
+        let declarant = GitUser {
+            name: declarant_name.to_string(),
+            email: declarant_email.to_string(),
+            ..Default::default()
+        };
+        let representative = 'representative: {
+            let Some(name) = representative_name else {
+                break 'representative None;
+            };
+            let Some(email) = representative_email else {
+                break 'representative None;
+            };
+            Some(GitUser {
+                name: name.to_string(),
+                email: email.to_string(),
+                ..Default::default()
+            })
+        };
+
+        // If the representative is provided, it must match the author or committer
+        if let Some(representative) = &representative {
+            if !representative.matches(&commit.author) && !representative.matches(&commit.committer) {
+                bail!("representative must match the author or committer");
+            }
         } else {
-            false
+            // Otherwise, the declarant must match the author or committer
+            if !declarant.matches(&commit.author) && !declarant.matches(&commit.committer) {
+                bail!("declarant must match the author or committer");
+            }
         }
-    };
 
-    let signoff_matches_committer = |s: &SignOff| {
-        if let Some(c) = &commit.committer {
-            s.name.to_lowercase() == c.name.to_lowercase() && s.email.to_lowercase() == c.email.to_lowercase()
-        } else {
-            false
+        // Create remediation and return it
+        Ok(Remediation {
+            declarant,
+            target_sha: target_sha.to_string(),
+        })
+    }
+
+    /// Check if the remediation matches the provided commit.
+    fn matches_commit(&self, commit: &Commit) -> bool {
+        if self.target_sha != commit.sha {
+            return false;
         }
+        self.declarant.matches(&commit.author) || self.declarant.matches(&commit.committer)
+    }
+}
+
+/// Get remediations found in the list of commits provided.
+fn get_remediations(
+    allow_remediation_commits: &Option<ConfigAllowRemediationCommits>,
+    commits: &[Commit],
+) -> Vec<Remediation> {
+    let mut remediations = Vec::new();
+
+    // Nothing to do if this feature isn't enabled in the config
+    let Some(allow_remediation_commits) = allow_remediation_commits else {
+        return remediations;
     };
 
-    signoffs.iter().any(|s| signoff_matches_author(s) || signoff_matches_committer(s))
+    // Collect remediations from commits
+    for commit in commits {
+        // Collect individual remediations if this feature is enabled
+        if allow_remediation_commits.individual.unwrap_or(false) {
+            let captures = INDIVIDUAL_REMEDIATION.captures_iter(&commit.message).map(|c| c.extract());
+            for (_, [declarant_name, declarant_email, target_sha]) in captures {
+                if let Ok(remediation) =
+                    Remediation::new(declarant_name, declarant_email, None, None, target_sha, commit)
+                {
+                    remediations.push(remediation);
+                }
+            }
+
+            // Collect third-party remediations if this feature is enabled
+            if allow_remediation_commits.third_party.unwrap_or(false) {
+                let captures = THIRD_PARTY_REMEDIATION.captures_iter(&commit.message).map(|c| c.extract());
+                for (
+                    _,
+                    [declarant_name, declarant_email, representative_name, representative_email, target_sha],
+                ) in captures
+                {
+                    if let Ok(remediation) = Remediation::new(
+                        declarant_name,
+                        declarant_email,
+                        Some(representative_name),
+                        Some(representative_email),
+                        target_sha,
+                        commit,
+                    ) {
+                        remediations.push(remediation);
+                    }
+                }
+            }
+        }
+    }
+
+    remediations
+}
+
+/// Check if any of the remediations matches the provided commit.
+fn remediations_match(remediations: &[Remediation], commit: &Commit) -> bool {
+    remediations.iter().any(|remediation| remediation.matches_commit(commit))
 }
diff --git a/dco2/src/dco/check/tests.rs b/dco2/src/dco/check/tests.rs
index f41ca87..5de8412 100644
--- a/dco2/src/dco/check/tests.rs
+++ b/dco2/src/dco/check/tests.rs
@@ -1,8 +1,9 @@
 use crate::{
     dco::check::{check, CheckInput, CheckOutput, CommitCheckOutput, CommitError, CommitSuccessReason},
-    github::{Commit, GitUser},
+    github::{Commit, Config, ConfigAllowRemediationCommits, GitUser},
 };
 use indoc::indoc;
+use pretty_assertions::assert_eq;
 use std::vec;
 
 #[test]
@@ -1743,6 +1744,1332 @@ fn two_commits_invalid_signoff_in_first_no_signoff_in_second() {
     );
 }
 
+#[test]
+fn two_commits_no_signoff_in_first_valid_remediation_commit_in_second_but_not_enabled_in_config() {
+    let commit1 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: "Test commit message".to_string(),
+        sha: "sha1".to_string(),
+        ..Default::default()
+    };
+    let commit2 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: indoc! {r"
+            Test commit message
+
+            I, user1 <user1@email.test>, hereby add my Signed-off-by to this commit: sha1
+
+            Signed-off-by: user1 <user1@email.test>
+        "}
+        .to_string(),
+        ..Default::default()
+    };
+
+    let input = CheckInput {
+        commits: vec![commit1.clone(), commit2.clone()],
+        config: Default::default(),
+        head_ref: "main".to_string(),
+    };
+    let output = check(&input);
+
+    assert_eq!(
+        output,
+        CheckOutput {
+            commits: vec![
+                CommitCheckOutput {
+                    commit: commit1,
+                    errors: vec![CommitError::SignOffNotFound],
+                    success_reason: None,
+                },
+                CommitCheckOutput {
+                    commit: commit2,
+                    errors: vec![],
+                    success_reason: Some(CommitSuccessReason::ValidSignOff),
+                }
+            ],
+            head_ref: "main".to_string(),
+            num_commits_with_errors: 1,
+        }
+    );
+}
+
+#[test]
+fn two_commits_no_signoff_in_first_valid_remediation_commit_matching_author_in_second() {
+    let commit1 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user2".to_string(),
+            email: "user2@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: "Test commit message".to_string(),
+        sha: "sha1".to_string(),
+        ..Default::default()
+    };
+    let commit2 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: indoc! {r"
+            Test commit message
+
+            I, user1 <user1@email.test>, hereby add my Signed-off-by to this commit: sha1
+
+            Signed-off-by: user1 <user1@email.test>
+        "}
+        .to_string(),
+        ..Default::default()
+    };
+
+    let input = CheckInput {
+        commits: vec![commit1.clone(), commit2.clone()],
+        config: Config {
+            allow_remediation_commits: Some(ConfigAllowRemediationCommits {
+                individual: Some(true),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        head_ref: "main".to_string(),
+    };
+    let output = check(&input);
+
+    assert_eq!(
+        output,
+        CheckOutput {
+            commits: vec![
+                CommitCheckOutput {
+                    commit: commit1,
+                    errors: vec![],
+                    success_reason: Some(CommitSuccessReason::ValidSignOffInRemediationCommit),
+                },
+                CommitCheckOutput {
+                    commit: commit2,
+                    errors: vec![],
+                    success_reason: Some(CommitSuccessReason::ValidSignOff),
+                }
+            ],
+            head_ref: "main".to_string(),
+            num_commits_with_errors: 0,
+        }
+    );
+}
+
+#[test]
+fn two_commits_no_signoff_in_first_valid_remediation_commit_matching_committer_in_second() {
+    let commit1 = Commit {
+        author: Some(GitUser {
+            name: "user2".to_string(),
+            email: "user2@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: "Test commit message".to_string(),
+        sha: "sha1".to_string(),
+        ..Default::default()
+    };
+    let commit2 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: indoc! {r"
+            Test commit message
+
+            I, user1 <user1@email.test>, hereby add my Signed-off-by to this commit: sha1
+
+            Signed-off-by: user1 <user1@email.test>
+        "}
+        .to_string(),
+        ..Default::default()
+    };
+
+    let input = CheckInput {
+        commits: vec![commit1.clone(), commit2.clone()],
+        config: Config {
+            allow_remediation_commits: Some(ConfigAllowRemediationCommits {
+                individual: Some(true),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        head_ref: "main".to_string(),
+    };
+    let output = check(&input);
+
+    assert_eq!(
+        output,
+        CheckOutput {
+            commits: vec![
+                CommitCheckOutput {
+                    commit: commit1,
+                    errors: vec![],
+                    success_reason: Some(CommitSuccessReason::ValidSignOffInRemediationCommit),
+                },
+                CommitCheckOutput {
+                    commit: commit2,
+                    errors: vec![],
+                    success_reason: Some(CommitSuccessReason::ValidSignOff),
+                }
+            ],
+            head_ref: "main".to_string(),
+            num_commits_with_errors: 0,
+        }
+    );
+}
+
+#[test]
+fn two_commits_invalid_signoff_incorrect_name_in_first_valid_remediation_commit_in_second() {
+    let commit1 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: indoc! {r"
+            Test commit message
+
+            Signed-off-by: userx <user1@email.test>
+        "}
+        .to_string(),
+        sha: "sha1".to_string(),
+        ..Default::default()
+    };
+    let commit2 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: indoc! {r"
+            Test commit message
+
+            I, user1 <user1@email.test>, hereby add my Signed-off-by to this commit: sha1
+
+            Signed-off-by: user1 <user1@email.test>
+        "}
+        .to_string(),
+        ..Default::default()
+    };
+
+    let input = CheckInput {
+        commits: vec![commit1.clone(), commit2.clone()],
+        config: Config {
+            allow_remediation_commits: Some(ConfigAllowRemediationCommits {
+                individual: Some(true),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        head_ref: "main".to_string(),
+    };
+    let output = check(&input);
+
+    assert_eq!(
+        output,
+        CheckOutput {
+            commits: vec![
+                CommitCheckOutput {
+                    commit: commit1,
+                    errors: vec![],
+                    success_reason: Some(CommitSuccessReason::ValidSignOffInRemediationCommit),
+                },
+                CommitCheckOutput {
+                    commit: commit2,
+                    errors: vec![],
+                    success_reason: Some(CommitSuccessReason::ValidSignOff),
+                }
+            ],
+            head_ref: "main".to_string(),
+            num_commits_with_errors: 0,
+        }
+    );
+}
+
+#[test]
+fn two_commits_invalid_signoff_incorrect_email_in_first_valid_remediation_commit_in_second() {
+    let commit1 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: indoc! {r"
+            Test commit message
+
+            Signed-off-by: user1 <userx@email.test>
+        "}
+        .to_string(),
+        sha: "sha1".to_string(),
+        ..Default::default()
+    };
+    let commit2 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: indoc! {r"
+            Test commit message
+
+            I, user1 <user1@email.test>, hereby add my Signed-off-by to this commit: sha1
+
+            Signed-off-by: user1 <user1@email.test>
+        "}
+        .to_string(),
+        ..Default::default()
+    };
+
+    let input = CheckInput {
+        commits: vec![commit1.clone(), commit2.clone()],
+        config: Config {
+            allow_remediation_commits: Some(ConfigAllowRemediationCommits {
+                individual: Some(true),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        head_ref: "main".to_string(),
+    };
+    let output = check(&input);
+
+    assert_eq!(
+        output,
+        CheckOutput {
+            commits: vec![
+                CommitCheckOutput {
+                    commit: commit1,
+                    errors: vec![],
+                    success_reason: Some(CommitSuccessReason::ValidSignOffInRemediationCommit),
+                },
+                CommitCheckOutput {
+                    commit: commit2,
+                    errors: vec![],
+                    success_reason: Some(CommitSuccessReason::ValidSignOff),
+                }
+            ],
+            head_ref: "main".to_string(),
+            num_commits_with_errors: 0,
+        }
+    );
+}
+
+#[test]
+fn two_commits_valid_signoff_in_first_redundant_remediation_commit_in_second() {
+    let commit1 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: indoc! {r"
+            Test commit message
+
+            Signed-off-by: user1 <user1@email.test>
+        "}
+        .to_string(),
+        sha: "sha1".to_string(),
+        ..Default::default()
+    };
+    let commit2 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: indoc! {r"
+            Test commit message
+
+            I, user1 <user1@email.test>, hereby add my Signed-off-by to this commit: sha1
+
+            Signed-off-by: user1 <user1@email.test>
+        "}
+        .to_string(),
+        ..Default::default()
+    };
+
+    let input = CheckInput {
+        commits: vec![commit1.clone(), commit2.clone()],
+        config: Config {
+            allow_remediation_commits: Some(ConfigAllowRemediationCommits {
+                individual: Some(true),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        head_ref: "main".to_string(),
+    };
+    let output = check(&input);
+
+    assert_eq!(
+        output,
+        CheckOutput {
+            commits: vec![
+                CommitCheckOutput {
+                    commit: commit1,
+                    errors: vec![],
+                    success_reason: Some(CommitSuccessReason::ValidSignOff),
+                },
+                CommitCheckOutput {
+                    commit: commit2,
+                    errors: vec![],
+                    success_reason: Some(CommitSuccessReason::ValidSignOff),
+                }
+            ],
+            head_ref: "main".to_string(),
+            num_commits_with_errors: 0,
+        }
+    );
+}
+
+#[test]
+fn two_commits_valid_signoff_in_first_remediation_commit_non_existent_sha_in_second() {
+    let commit1 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: indoc! {r"
+            Test commit message
+
+            Signed-off-by: user1 <user1@email.test>
+        "}
+        .to_string(),
+        sha: "sha1".to_string(),
+        ..Default::default()
+    };
+    let commit2 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: indoc! {r"
+            Test commit message
+
+            I, user1 <user1@email.test>, hereby add my Signed-off-by to this commit: non-existent
+
+            Signed-off-by: user1 <user1@email.test>
+        "}
+        .to_string(),
+        ..Default::default()
+    };
+
+    let input = CheckInput {
+        commits: vec![commit1.clone(), commit2.clone()],
+        config: Config {
+            allow_remediation_commits: Some(ConfigAllowRemediationCommits {
+                individual: Some(true),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        head_ref: "main".to_string(),
+    };
+    let output = check(&input);
+
+    assert_eq!(
+        output,
+        CheckOutput {
+            commits: vec![
+                CommitCheckOutput {
+                    commit: commit1,
+                    errors: vec![],
+                    success_reason: Some(CommitSuccessReason::ValidSignOff),
+                },
+                CommitCheckOutput {
+                    commit: commit2,
+                    errors: vec![],
+                    success_reason: Some(CommitSuccessReason::ValidSignOff),
+                }
+            ],
+            head_ref: "main".to_string(),
+            num_commits_with_errors: 0,
+        }
+    );
+}
+
+#[test]
+fn two_commits_no_signoff_in_first_remediation_commit_no_signoff_in_second() {
+    let commit1 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: "Test commit message".to_string(),
+        sha: "sha1".to_string(),
+        ..Default::default()
+    };
+    let commit2 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: indoc! {r"
+            Test commit message
+
+            I, user1 <user1@email.test>, hereby add my Signed-off-by to this commit: sha1
+        "}
+        .to_string(),
+        ..Default::default()
+    };
+
+    let input = CheckInput {
+        commits: vec![commit1.clone(), commit2.clone()],
+        config: Config {
+            allow_remediation_commits: Some(ConfigAllowRemediationCommits {
+                individual: Some(true),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        head_ref: "main".to_string(),
+    };
+    let output = check(&input);
+
+    assert_eq!(
+        output,
+        CheckOutput {
+            commits: vec![
+                CommitCheckOutput {
+                    commit: commit1,
+                    errors: vec![],
+                    success_reason: Some(CommitSuccessReason::ValidSignOffInRemediationCommit),
+                },
+                CommitCheckOutput {
+                    commit: commit2,
+                    errors: vec![CommitError::SignOffNotFound],
+                    success_reason: None,
+                }
+            ],
+            head_ref: "main".to_string(),
+            num_commits_with_errors: 1,
+        }
+    );
+}
+
+#[test]
+fn two_commits_no_signoff_in_first_remediation_commit_different_name_in_second() {
+    let commit1 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: "Test commit message".to_string(),
+        sha: "sha1".to_string(),
+        ..Default::default()
+    };
+    let commit2 = Commit {
+        author: Some(GitUser {
+            name: "userx".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "userx".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: indoc! {r"
+            Test commit message
+
+            I, userx <user1@email.test>, hereby add my Signed-off-by to this commit: sha1
+
+            Signed-off-by: userx <user1@email.test>
+        "}
+        .to_string(),
+        ..Default::default()
+    };
+
+    let input = CheckInput {
+        commits: vec![commit1.clone(), commit2.clone()],
+        config: Config {
+            allow_remediation_commits: Some(ConfigAllowRemediationCommits {
+                individual: Some(true),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        head_ref: "main".to_string(),
+    };
+    let output = check(&input);
+
+    assert_eq!(
+        output,
+        CheckOutput {
+            commits: vec![
+                CommitCheckOutput {
+                    commit: commit1,
+                    errors: vec![CommitError::SignOffNotFound],
+                    success_reason: None,
+                },
+                CommitCheckOutput {
+                    commit: commit2,
+                    errors: vec![],
+                    success_reason: Some(CommitSuccessReason::ValidSignOff),
+                }
+            ],
+            head_ref: "main".to_string(),
+            num_commits_with_errors: 1,
+        }
+    );
+}
+
+#[test]
+fn two_commits_no_signoff_in_first_remediation_commit_different_email_in_second() {
+    let commit1 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: "Test commit message".to_string(),
+        sha: "sha1".to_string(),
+        ..Default::default()
+    };
+    let commit2 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "userx@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "userx@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: indoc! {r"
+            Test commit message
+
+            I, user1 <userx@email.test>, hereby add my Signed-off-by to this commit: sha1
+
+            Signed-off-by: user1 <userx@email.test>
+        "}
+        .to_string(),
+        ..Default::default()
+    };
+
+    let input = CheckInput {
+        commits: vec![commit1.clone(), commit2.clone()],
+        config: Config {
+            allow_remediation_commits: Some(ConfigAllowRemediationCommits {
+                individual: Some(true),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        head_ref: "main".to_string(),
+    };
+    let output = check(&input);
+
+    assert_eq!(
+        output,
+        CheckOutput {
+            commits: vec![
+                CommitCheckOutput {
+                    commit: commit1,
+                    errors: vec![CommitError::SignOffNotFound],
+                    success_reason: None,
+                },
+                CommitCheckOutput {
+                    commit: commit2,
+                    errors: vec![],
+                    success_reason: Some(CommitSuccessReason::ValidSignOff),
+                }
+            ],
+            head_ref: "main".to_string(),
+            num_commits_with_errors: 1,
+        }
+    );
+}
+
+#[test]
+fn two_commits_no_signoff_in_first_remediation_commit_different_name_and_email_in_second() {
+    let commit1 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: "Test commit message".to_string(),
+        sha: "sha1".to_string(),
+        ..Default::default()
+    };
+    let commit2 = Commit {
+        author: Some(GitUser {
+            name: "userx".to_string(),
+            email: "userx@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "userx".to_string(),
+            email: "userx@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: indoc! {r"
+            Test commit message
+
+            I, userx <userx@email.test>, hereby add my Signed-off-by to this commit: sha1
+
+            Signed-off-by: userx <userx@email.test>
+        "}
+        .to_string(),
+        ..Default::default()
+    };
+
+    let input = CheckInput {
+        commits: vec![commit1.clone(), commit2.clone()],
+        config: Config {
+            allow_remediation_commits: Some(ConfigAllowRemediationCommits {
+                individual: Some(true),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        head_ref: "main".to_string(),
+    };
+    let output = check(&input);
+
+    assert_eq!(
+        output,
+        CheckOutput {
+            commits: vec![
+                CommitCheckOutput {
+                    commit: commit1,
+                    errors: vec![CommitError::SignOffNotFound],
+                    success_reason: None,
+                },
+                CommitCheckOutput {
+                    commit: commit2,
+                    errors: vec![],
+                    success_reason: Some(CommitSuccessReason::ValidSignOff),
+                }
+            ],
+            head_ref: "main".to_string(),
+            num_commits_with_errors: 1,
+        }
+    );
+}
+
+#[test]
+fn two_commits_no_signoff_in_first_remediation_commit_different_name_in_signoff_in_second() {
+    let commit1 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: "Test commit message".to_string(),
+        sha: "sha1".to_string(),
+        ..Default::default()
+    };
+    let commit2 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: indoc! {r"
+            Test commit message
+
+            I, user1 <user1@email.test>, hereby add my Signed-off-by to this commit: sha1
+
+            Signed-off-by: userx <user1@email.test>
+        "}
+        .to_string(),
+        ..Default::default()
+    };
+
+    let input = CheckInput {
+        commits: vec![commit1.clone(), commit2.clone()],
+        config: Config {
+            allow_remediation_commits: Some(ConfigAllowRemediationCommits {
+                individual: Some(true),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        head_ref: "main".to_string(),
+    };
+    let output = check(&input);
+
+    assert_eq!(
+        output,
+        CheckOutput {
+            commits: vec![
+                CommitCheckOutput {
+                    commit: commit1,
+                    errors: vec![],
+                    success_reason: Some(CommitSuccessReason::ValidSignOffInRemediationCommit),
+                },
+                CommitCheckOutput {
+                    commit: commit2,
+                    errors: vec![CommitError::SignOffMismatch],
+                    success_reason: None,
+                }
+            ],
+            head_ref: "main".to_string(),
+            num_commits_with_errors: 1,
+        }
+    );
+}
+
+#[test]
+fn two_commits_no_signoff_in_first_remediation_commit_different_email_in_signoff_in_second() {
+    let commit1 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: "Test commit message".to_string(),
+        sha: "sha1".to_string(),
+        ..Default::default()
+    };
+    let commit2 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: indoc! {r"
+            Test commit message
+
+            I, user1 <user1@email.test>, hereby add my Signed-off-by to this commit: sha1
+
+            Signed-off-by: user1 <userx@email.test>
+        "}
+        .to_string(),
+        ..Default::default()
+    };
+
+    let input = CheckInput {
+        commits: vec![commit1.clone(), commit2.clone()],
+        config: Config {
+            allow_remediation_commits: Some(ConfigAllowRemediationCommits {
+                individual: Some(true),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        head_ref: "main".to_string(),
+    };
+    let output = check(&input);
+
+    assert_eq!(
+        output,
+        CheckOutput {
+            commits: vec![
+                CommitCheckOutput {
+                    commit: commit1,
+                    errors: vec![],
+                    success_reason: Some(CommitSuccessReason::ValidSignOffInRemediationCommit),
+                },
+                CommitCheckOutput {
+                    commit: commit2,
+                    errors: vec![CommitError::SignOffMismatch],
+                    success_reason: None,
+                }
+            ],
+            head_ref: "main".to_string(),
+            num_commits_with_errors: 1,
+        }
+    );
+}
+
+#[test]
+fn two_commits_no_signoff_in_first_remediation_commit_different_name_and_email_in_signoff_in_second() {
+    let commit1 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: "Test commit message".to_string(),
+        sha: "sha1".to_string(),
+        ..Default::default()
+    };
+    let commit2 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: indoc! {r"
+            Test commit message
+
+            I, user1 <user1@email.test>, hereby add my Signed-off-by to this commit: sha1
+
+            Signed-off-by: userx <userx@email.test>
+        "}
+        .to_string(),
+        ..Default::default()
+    };
+
+    let input = CheckInput {
+        commits: vec![commit1.clone(), commit2.clone()],
+        config: Config {
+            allow_remediation_commits: Some(ConfigAllowRemediationCommits {
+                individual: Some(true),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        head_ref: "main".to_string(),
+    };
+    let output = check(&input);
+
+    assert_eq!(
+        output,
+        CheckOutput {
+            commits: vec![
+                CommitCheckOutput {
+                    commit: commit1,
+                    errors: vec![],
+                    success_reason: Some(CommitSuccessReason::ValidSignOffInRemediationCommit),
+                },
+                CommitCheckOutput {
+                    commit: commit2,
+                    errors: vec![CommitError::SignOffMismatch],
+                    success_reason: None,
+                }
+            ],
+            head_ref: "main".to_string(),
+            num_commits_with_errors: 1,
+        }
+    );
+}
+
+#[test]
+fn two_commits_no_signoff_in_first_remediation_commit_different_name_in_remediation_in_second() {
+    let commit1 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: "Test commit message".to_string(),
+        sha: "sha1".to_string(),
+        ..Default::default()
+    };
+    let commit2 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: indoc! {r"
+            Test commit message
+
+            I, userx <user1@email.test>, hereby add my Signed-off-by to this commit: sha1
+
+            Signed-off-by: user1 <user1@email.test>
+        "}
+        .to_string(),
+        ..Default::default()
+    };
+
+    let input = CheckInput {
+        commits: vec![commit1.clone(), commit2.clone()],
+        config: Config {
+            allow_remediation_commits: Some(ConfigAllowRemediationCommits {
+                individual: Some(true),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        head_ref: "main".to_string(),
+    };
+    let output = check(&input);
+
+    assert_eq!(
+        output,
+        CheckOutput {
+            commits: vec![
+                CommitCheckOutput {
+                    commit: commit1,
+                    errors: vec![CommitError::SignOffNotFound],
+                    success_reason: None,
+                },
+                CommitCheckOutput {
+                    commit: commit2,
+                    errors: vec![],
+                    success_reason: Some(CommitSuccessReason::ValidSignOff),
+                }
+            ],
+            head_ref: "main".to_string(),
+            num_commits_with_errors: 1,
+        }
+    );
+}
+
+#[test]
+fn two_commits_no_signoff_in_first_remediation_commit_different_email_in_remediation_in_second() {
+    let commit1 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: "Test commit message".to_string(),
+        sha: "sha1".to_string(),
+        ..Default::default()
+    };
+    let commit2 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: indoc! {r"
+            Test commit message
+
+            I, user1 <userx@email.test>, hereby add my Signed-off-by to this commit: sha1
+
+            Signed-off-by: user1 <user1@email.test>
+        "}
+        .to_string(),
+        ..Default::default()
+    };
+
+    let input = CheckInput {
+        commits: vec![commit1.clone(), commit2.clone()],
+        config: Config {
+            allow_remediation_commits: Some(ConfigAllowRemediationCommits {
+                individual: Some(true),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        head_ref: "main".to_string(),
+    };
+    let output = check(&input);
+
+    assert_eq!(
+        output,
+        CheckOutput {
+            commits: vec![
+                CommitCheckOutput {
+                    commit: commit1,
+                    errors: vec![CommitError::SignOffNotFound],
+                    success_reason: None,
+                },
+                CommitCheckOutput {
+                    commit: commit2,
+                    errors: vec![],
+                    success_reason: Some(CommitSuccessReason::ValidSignOff),
+                }
+            ],
+            head_ref: "main".to_string(),
+            num_commits_with_errors: 1,
+        }
+    );
+}
+
+#[test]
+fn two_commits_no_signoff_in_first_remediation_commit_different_name_and_email_in_remediation_in_second() {
+    let commit1 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: "Test commit message".to_string(),
+        sha: "sha1".to_string(),
+        ..Default::default()
+    };
+    let commit2 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: indoc! {r"
+            Test commit message
+
+            I, userx <userx@email.test>, hereby add my Signed-off-by to this commit: sha1
+
+            Signed-off-by: user1 <user1@email.test>
+        "}
+        .to_string(),
+        ..Default::default()
+    };
+
+    let input = CheckInput {
+        commits: vec![commit1.clone(), commit2.clone()],
+        config: Config {
+            allow_remediation_commits: Some(ConfigAllowRemediationCommits {
+                individual: Some(true),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        head_ref: "main".to_string(),
+    };
+    let output = check(&input);
+
+    assert_eq!(
+        output,
+        CheckOutput {
+            commits: vec![
+                CommitCheckOutput {
+                    commit: commit1,
+                    errors: vec![CommitError::SignOffNotFound],
+                    success_reason: None,
+                },
+                CommitCheckOutput {
+                    commit: commit2,
+                    errors: vec![],
+                    success_reason: Some(CommitSuccessReason::ValidSignOff),
+                }
+            ],
+            head_ref: "main".to_string(),
+            num_commits_with_errors: 1,
+        }
+    );
+}
+
+#[test]
+fn two_commits_no_signoff_in_first_remediation_commit_different_sha_in_second() {
+    let commit1 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: "Test commit message".to_string(),
+        sha: "sha1".to_string(),
+        ..Default::default()
+    };
+    let commit2 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: indoc! {r"
+            Test commit message
+
+            I, user1 <user1@email.test>, hereby add my Signed-off-by to this commit: sha2
+
+            Signed-off-by: user1 <user1@email.test>
+        "}
+        .to_string(),
+        ..Default::default()
+    };
+
+    let input = CheckInput {
+        commits: vec![commit1.clone(), commit2.clone()],
+        config: Config {
+            allow_remediation_commits: Some(ConfigAllowRemediationCommits {
+                individual: Some(true),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        head_ref: "main".to_string(),
+    };
+    let output = check(&input);
+
+    assert_eq!(
+        output,
+        CheckOutput {
+            commits: vec![
+                CommitCheckOutput {
+                    commit: commit1,
+                    errors: vec![CommitError::SignOffNotFound],
+                    success_reason: None,
+                },
+                CommitCheckOutput {
+                    commit: commit2,
+                    errors: vec![],
+                    success_reason: Some(CommitSuccessReason::ValidSignOff),
+                }
+            ],
+            head_ref: "main".to_string(),
+            num_commits_with_errors: 1,
+        }
+    );
+}
+
 #[test]
 fn three_commits_valid_signoff_in_all() {
     let commit1 = Commit {
@@ -2032,3 +3359,196 @@ fn three_commits_valid_signoff_first_invalid_signoff_second_valid_signoff_third(
         }
     );
 }
+
+#[test]
+fn three_commits_no_signoff_in_first_remediation_commit_without_signoff_in_second_valid_remediation_commit_in_third(
+) {
+    let commit1 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: "Test commit message".to_string(),
+        sha: "sha1".to_string(),
+        ..Default::default()
+    };
+    let commit2 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: indoc! {r"
+            Test commit message
+
+            I, user1 <user1@email.test>, hereby add my Signed-off-by to this commit: sha1
+        "}
+        .to_string(),
+        sha: "sha2".to_string(),
+        ..Default::default()
+    };
+    let commit3 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: indoc! {r"
+            Test commit message
+
+            I, user1 <user1@email.test>, hereby add my Signed-off-by to this commit: sha2
+
+            Signed-off-by: user1 <user1@email.test>
+        "}
+        .to_string(),
+        ..Default::default()
+    };
+
+    let input = CheckInput {
+        commits: vec![commit1.clone(), commit2.clone(), commit3.clone()],
+        config: Config {
+            allow_remediation_commits: Some(ConfigAllowRemediationCommits {
+                individual: Some(true),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        head_ref: "main".to_string(),
+    };
+    let output = check(&input);
+
+    assert_eq!(
+        output,
+        CheckOutput {
+            commits: vec![
+                CommitCheckOutput {
+                    commit: commit1,
+                    errors: vec![],
+                    success_reason: Some(CommitSuccessReason::ValidSignOffInRemediationCommit),
+                },
+                CommitCheckOutput {
+                    commit: commit2,
+                    errors: vec![],
+                    success_reason: Some(CommitSuccessReason::ValidSignOffInRemediationCommit),
+                },
+                CommitCheckOutput {
+                    commit: commit3,
+                    errors: vec![],
+                    success_reason: Some(CommitSuccessReason::ValidSignOff),
+                }
+            ],
+            head_ref: "main".to_string(),
+            num_commits_with_errors: 0,
+        }
+    );
+}
+
+#[test]
+fn three_commits_no_signoff_in_first_no_signoff_in_second_valid_remediation_commit_for_both_in_third() {
+    let commit1 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: "Test commit message".to_string(),
+        sha: "sha1".to_string(),
+        ..Default::default()
+    };
+    let commit2 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: "Test commit message".to_string(),
+        sha: "sha2".to_string(),
+        ..Default::default()
+    };
+    let commit3 = Commit {
+        author: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        committer: Some(GitUser {
+            name: "user1".to_string(),
+            email: "user1@email.test".to_string(),
+            ..Default::default()
+        }),
+        message: indoc! {r"
+            Test commit message
+
+            I, user1 <user1@email.test>, hereby add my Signed-off-by to this commit: sha1
+            I, user1 <user1@email.test>, hereby add my Signed-off-by to this commit: sha2
+
+            Signed-off-by: user1 <user1@email.test>
+        "}
+        .to_string(),
+        ..Default::default()
+    };
+
+    let input = CheckInput {
+        commits: vec![commit1.clone(), commit2.clone(), commit3.clone()],
+        config: Config {
+            allow_remediation_commits: Some(ConfigAllowRemediationCommits {
+                individual: Some(true),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        head_ref: "main".to_string(),
+    };
+    let output = check(&input);
+
+    assert_eq!(
+        output,
+        CheckOutput {
+            commits: vec![
+                CommitCheckOutput {
+                    commit: commit1,
+                    errors: vec![],
+                    success_reason: Some(CommitSuccessReason::ValidSignOffInRemediationCommit),
+                },
+                CommitCheckOutput {
+                    commit: commit2,
+                    errors: vec![],
+                    success_reason: Some(CommitSuccessReason::ValidSignOffInRemediationCommit),
+                },
+                CommitCheckOutput {
+                    commit: commit3,
+                    errors: vec![],
+                    success_reason: Some(CommitSuccessReason::ValidSignOff),
+                }
+            ],
+            head_ref: "main".to_string(),
+            num_commits_with_errors: 0,
+        }
+    );
+}
diff --git a/dco2/src/github/client.rs b/dco2/src/github/client.rs
index 7e519c4..e8cec59 100644
--- a/dco2/src/github/client.rs
+++ b/dco2/src/github/client.rs
@@ -357,8 +357,8 @@ impl From<octorust::types::CommitDataType> for Commit {
 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
 #[serde(rename_all(deserialize = "camelCase"))]
 pub struct Config {
-    allow_remediation_commits: Option<ConfigAllowRemediationCommits>,
-    require: Option<ConfigRequire>,
+    pub allow_remediation_commits: Option<ConfigAllowRemediationCommits>,
+    pub require: Option<ConfigRequire>,
 }
 
 impl Default for Config {
@@ -376,25 +376,25 @@ impl Default for Config {
 }
 
 /// Allow remediation commits section of the configuration.
-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
 #[serde(rename_all(deserialize = "camelCase"))]
 pub struct ConfigAllowRemediationCommits {
     /// Indicates whether individual remediation commits are allowed or not.
     /// (default: false)
-    individual: Option<bool>,
+    pub individual: Option<bool>,
 
     /// Indicates whether third party remediation commits are allowed or not.
     /// (default: false)
-    third_party: Option<bool>,
+    pub third_party: Option<bool>,
 }
 
 /// Require section of the configuration.
-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
 #[serde(rename_all(deserialize = "camelCase"))]
 pub struct ConfigRequire {
     /// Indicates whether members are required to sign-off or not.
     /// (default: true)
-    members: Option<bool>,
+    pub members: Option<bool>,
 }
 
 /// Git user information.
@@ -405,6 +405,18 @@ pub struct GitUser {
     pub is_bot: bool,
 }
 
+impl GitUser {
+    /// Check if the user matches the provided user (if any).
+    pub fn matches(&self, user: &Option<GitUser>) -> bool {
+        if let Some(user) = user {
+            self.name.to_lowercase() == user.name.to_lowercase()
+                && self.email.to_lowercase() == user.email.to_lowercase()
+        } else {
+            false
+        }
+    }
+}
+
 /// Input used to create a new check run.
 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
 pub struct NewCheckRunInput {