[PATCH] checkpatch: Validate committer sign-off

Bjorn Andersson posted 1 patch an hour ago
Documentation/dev-tools/checkpatch.rst |   5 ++
scripts/checkpatch.pl                  | 112 ++++++++++++++++++++++-----------
2 files changed, 82 insertions(+), 35 deletions(-)
[PATCH] checkpatch: Validate committer sign-off
Posted by Bjorn Andersson an hour ago
checkpatch validates the Signed-off-by trailers against the patch's
author, but misses the opportunity to also validate the committer when
run with --git. Maintainers therefor need third-party scripts for the
final Signed-off-by check.

git format-patch provides the author and patch content, but not the
committer. Extend the existing git log extraction to record the
committer identity.

Extract the author sign-off comparison logic into a shared helper, and
use it to apply the same heuristics to the final Signed-off-by trailer
and committer.

Emit COMMITTER_SIGN_OFF_MISMATCH when the final trailer does not match
the committer.

Assisted-by: OpenCode:GPT-5.5
Signed-off-by: Bjorn Andersson <bjorn.andersson@oss.qualcomm.com>
---
 Documentation/dev-tools/checkpatch.rst |   5 ++
 scripts/checkpatch.pl                  | 112 ++++++++++++++++++++++-----------
 2 files changed, 82 insertions(+), 35 deletions(-)

diff --git a/Documentation/dev-tools/checkpatch.rst b/Documentation/dev-tools/checkpatch.rst
index 6139a08c34cd..7726c3863bcc 100644
--- a/Documentation/dev-tools/checkpatch.rst
+++ b/Documentation/dev-tools/checkpatch.rst
@@ -562,6 +562,11 @@ Commit message
       - The email subaddresses do not match.
       - The email comments do not match.
 
+  **COMMITTER_SIGN_OFF_MISMATCH**
+    When checking a Git commit, the final Signed-off-by: line does not match
+    the commit's committer.  The person committing a patch should add their
+    own Signed-off-by: line last.
+
   **MISSING_SIGN_OFF**
     The patch is missing a Signed-off-by line.  A signed-off-by
     line should be added according to Developer's certificate of
diff --git a/scripts/checkpatch.pl b/scripts/checkpatch.pl
index 7a846a3ea127..0e1bff241597 100755
--- a/scripts/checkpatch.pl
+++ b/scripts/checkpatch.pl
@@ -37,6 +37,7 @@ my $showfile = 0;
 my $file = 0;
 my $git = 0;
 my %git_commits = ();
+my %git_committers = ();
 my $check = 0;
 my $check_orig = 0;
 my $summary = 1;
@@ -1317,14 +1318,16 @@ if ($git) {
 		} else {
 			$git_range = "-1 $commit_expr";
 		}
-		my $lines = `${git_command} log --no-color --no-merges --pretty=format:'%H %s' $git_range`;
+		my $lines = `${git_command} log --no-color --no-merges --pretty=format:'%cn <%ce> %H %s' $git_range`;
 		foreach my $line (split(/\n/, $lines)) {
-			$line =~ /^([0-9a-fA-F]{40,40}) (.*)$/;
-			next if (!defined($1) || !defined($2));
-			my $sha1 = $1;
-			my $subject = $2;
+			$line =~ /^(.* <[^>]+>) ([0-9a-fA-F]{40,40}) (.*)$/;
+			next if (!defined($1) || !defined($2) || !defined($3));
+			my $committer = $1;
+			my $sha1 = $2;
+			my $subject = $3;
 			unshift(@commits, $sha1);
 			$git_commits{$sha1} = $subject;
+			$git_committers{$sha1} = $committer;
 		}
 	}
 	die "$P: no git commits after extraction!\n" if (@commits == 0);
@@ -1530,6 +1533,43 @@ sub same_email_addresses {
 	       $comment1 eq $comment2;
 }
 
+sub signoff_match_status {
+	my ($signoff, $identity) = @_;
+	my $authorsignoff = 0;
+
+	if (same_email_addresses($signoff, $identity)) {
+		$authorsignoff = 1;
+	} else {
+		my ($signoff_name, $signoff_comment, $signoff_address, $comment1) = parse_email($signoff);
+		my ($identity_name, $identity_comment, $identity_address, $comment2) = parse_email($identity);
+
+		if (lc $signoff_address eq lc $identity_address &&
+		    $signoff_name eq $identity_name) {
+			$authorsignoff = 2;
+		} elsif (lc $signoff_address eq lc $identity_address) {
+			$authorsignoff = 3;
+		} elsif ($signoff_name eq $identity_name) {
+			$authorsignoff = 4;
+
+			my $address1 = $signoff_address;
+			my $address2 = $identity_address;
+
+			if ($address1 =~ /(\S+)\+\S+(\@.*)/) {
+				$address1 = "$1$2";
+			}
+			if ($address2 =~ /(\S+)\+\S+(\@.*)/) {
+				$address2 = "$1$2";
+			}
+
+			if ($address1 eq $address2) {
+				$authorsignoff = 5;
+			}
+		}
+	}
+
+	return $authorsignoff;
+}
+
 sub which {
 	my ($bin) = @_;
 
@@ -2690,6 +2730,9 @@ sub process {
 	my $author = '';
 	my $authorsignoff = 0;
 	my $author_sob = '';
+	my $last_signoff = '';
+	my $committer = $git_committers{$filename} // '';
+	my $committersignoff = 0;
 	my $is_patch = 0;
 	my $is_binding_patch = -1;
 	my $in_header_lines = $file ? 0 : 1;
@@ -3018,40 +3061,16 @@ sub process {
 # Check the patch for a signoff:
 		if ($line =~ /^\s*signed-off-by:\s*(.*)/i) {
 			$signoff++;
+			$last_signoff = $1;
 			$in_commit_log = 0;
 			if ($author ne ''  && $authorsignoff != 1) {
-				if (same_email_addresses($1, $author)) {
-					$authorsignoff = 1;
-				} else {
-					my $ctx = $1;
-					my ($email_name, $email_comment, $email_address, $comment1) = parse_email($ctx);
-					my ($author_name, $author_comment, $author_address, $comment2) = parse_email($author);
-
-					if (lc $email_address eq lc $author_address && $email_name eq $author_name) {
-						$author_sob = $ctx;
-						$authorsignoff = 2;
-					} elsif (lc $email_address eq lc $author_address) {
-						$author_sob = $ctx;
-						$authorsignoff = 3;
-					} elsif ($email_name eq $author_name) {
-						$author_sob = $ctx;
-						$authorsignoff = 4;
-
-						my $address1 = $email_address;
-						my $address2 = $author_address;
-
-						if ($address1 =~ /(\S+)\+\S+(\@.*)/) {
-							$address1 = "$1$2";
-						}
-						if ($address2 =~ /(\S+)\+\S+(\@.*)/) {
-							$address2 = "$1$2";
-						}
-						if ($address1 eq $address2) {
-							$authorsignoff = 5;
-						}
-					}
+				my $signoff_status = signoff_match_status($1, $author);
+				if ($signoff_status != 0) {
+					$authorsignoff = $signoff_status;
+					$author_sob = $1 if ($signoff_status != 1);
 				}
 			}
+			$committersignoff = signoff_match_status($1, $committer) if ($committer ne '');
 		}
 
 # Check for invalid patch separator
@@ -7901,6 +7920,29 @@ sub process {
 			}
 		}
 	}
+	if ($is_patch && $has_commit_log && $chk_signoff &&
+	    $committer ne '' &&
+	    $last_signoff ne '' &&
+	    $committersignoff != 1) {
+		my $sob_msg = "'Committer: $committer' != 'Signed-off-by: $last_signoff'";
+
+		if ($committersignoff == 2) {
+			CHK("COMMITTER_SIGN_OFF_MISMATCH",
+			    "Committer:/Signed-off-by: email comments mismatch: $sob_msg\n");
+		} elsif ($committersignoff == 3) {
+			WARN("COMMITTER_SIGN_OFF_MISMATCH",
+			     "Committer:/Signed-off-by: email name mismatch: $sob_msg\n");
+		} elsif ($committersignoff == 4) {
+			WARN("COMMITTER_SIGN_OFF_MISMATCH",
+			     "Committer:/Signed-off-by: email address mismatch: $sob_msg\n");
+		} elsif ($committersignoff == 5) {
+			WARN("COMMITTER_SIGN_OFF_MISMATCH",
+			     "Committer:/Signed-off-by: email subaddress mismatch: $sob_msg\n");
+		} else {
+			WARN("COMMITTER_SIGN_OFF_MISMATCH",
+			     "Last Signed-off-by: '$last_signoff' != commit committer '$committer'\n");
+		}
+	}
 
 	print report_dump();
 	if ($summary && !($clean == 1 && $quiet == 1)) {

---
base-commit: 290aaf24a551d5a0dce037e3fab30820f9113a10
change-id: 20260723-checkpatch-committer-sob-10fcbd636d43

Best regards,
--  
Bjorn Andersson <bjorn.andersson@oss.qualcomm.com>