Subject: Re: Missing signoff in the rdma tree

From: Jason Gunthorpe <[email-masked]>
Date: Mon, 6 Apr 2026 14:30:18 -0300

On Mon, Apr 06, 2026 at 05:38:14PM +0100, Mark Brown wrote:

> > are missing a Signed-off-by from their committers
> 
> This issue is still present today.

When Leon fixed the previous mistake with rebase it caused all of
this. filter-repo isn't good about not changing commits it doesn't
have to change, so it isn't so useful when merges are present.
rebase -r will reset the commiter for other commits.

Couldn't find a built in alternative and got tired of this, so I ended
up using AI to build a script based on an commit editor I already had
to fix it. I've included it below incase anyone else finds it useful
someday.

$ python3 edit_committer.py refs/heads/k.o/for-next \
        "Jason Gunthorpe" "[email-masked]" \
        v7.0-rc1 \
        eb15cffa15201b 485a21c14b4133 9ed273bbe66370 \
        0edac801a2693e 4e99362444a2f6 f0579e330b7736 \
        9db6a217ba5ddf ce150a8376e1ee d59f007207b394 \
        ac4bb15208d1f0 12c73bee531336 ac593f5b66c9d1 \
        6df3a6414d7364 42d2a291ef4748 f1cac281b42456 \
        4ac738c6a7b906 715eccd4798ec7 ea08d8cb8b5203 \
        8e886928a19d70

 Processing 2536 commits from v7.0-rc1 to 69db255d5faf
       8e886928a19d -> 38a6e5579d0d (target)
       ea08d8cb8b52 -> b51caeb24aad (target)
       715eccd4798e -> 1de9287ece44 (target)
       4ac738c6a7b9 -> dbf6491bb98d (target)
       f1cac281b424 -> 14badc323ed7 (target)
       42d2a291ef47 -> 4c379ba04c11 (target)
       6df3a6414d73 -> 5ebe8832ef90 (target)
       ac593f5b66c9 -> b33d860a13b4 (target)
       12c73bee5313 -> 3f6b103c4bf2 (target)
       ac4bb15208d1 -> 0cee3acab27a (target)
       d59f007207b3 -> bc30311e492e (target)
       ce150a8376e1 -> bed686d8dcd4 (target)
       9db6a217ba5d -> 613713f251c8 (target)
       f0579e330b77 -> eee6268421a2 (target)
       4e99362444a2 -> 13f9a813eee5 (target)
       0edac801a269 -> 1234a9d8aebb (target)
       9ed273bbe663 -> 3d4a42360c33 (target)
       485a21c14b41 -> cec5157b6c73 (target)
       eb15cffa1520 -> a06165a705ee (target)
       8db7b7d9ba17 -> 1dc469f669fe (parent changed)
       33960c206db2 -> 553dfa8cbd0c (parent changed)
       e86b2bf1c311 -> 797291a66ce3 (parent changed)
       932f1eef09ec -> ff85a2ebacbd (parent changed)
       f39003e8fa8c -> 3a0b171302ee (parent changed)
       5c3f6de46d6a -> 4707bf5f6c86 (parent changed)
       353b3bbdb2c4 -> 2afa8b9f5ff8 (parent changed)
       76ae14db9058 -> 1b50f42049d8 (parent changed)
       120ad1fd5914 -> 56521f587704 (parent changed)
       ba8c700185d6 -> 786ee8ddf47a (parent changed)
       e2849b392974 -> 679eb25de4ee (parent changed)
       2ad64eeaefd2 -> 0fed679e0862 (parent changed)
       1487bad4ea51 -> 6be4ca0ab3a2 (parent changed)
       5122be2a19aa -> 5aeb6e039972 (parent changed)
       4ae130ccec08 -> 9d6ba4ced734 (parent changed)
       dfd4d5a38e65 -> f3cf74933c9c (parent changed)
       a8f6ced534c0 -> 2bb02691df65 (parent changed)
       cdb802af06d6 -> 5aa437c93d90 (parent changed)
       ffdf34ca1a47 -> a60e3f3d6fba (parent changed)
       c8cc77c8418f -> 13f2a53c2a71 (parent changed)
       e5ef99e3401c -> f1327abd6abe (parent changed)
       a7a49104859d -> e01027cab38a (parent changed)
       a2347bc64b89 -> 6c45efd8f9bb (parent changed)
       ada307fd98db -> ce68351be075 (parent changed)
       b8934c5c3f83 -> dc76086a2d94 (parent changed)
       f0c62197416b -> b247ed6f60bd (parent changed)
       c291b1133360 -> 2b2f078236a4 (parent changed)
       d8d84ea1abcc -> 345f842771ff (parent changed)
       933153ef70b8 -> e69609c5d469 (parent changed)
       f9616c93912e -> 2f49e1590344 (parent changed)
       09220f0b8388 -> adc09d7fbbb9 (parent changed)
       b5bc2b6ef6fe -> e6fd24917897 (parent changed)
       997f21ed1e93 -> 179b32095854 (parent changed)
       578cb2be0db2 -> 911e5ca3e169 (parent changed)
       f968d01f47d0 -> dbeb256e8dd8 (parent changed)
       74e2711bb2af -> cef2842c922c (parent changed)
       7a8b545a8012 -> ae638288b202 (parent changed)
       e81275b67f59 -> 8d7573b19402 (parent changed)
       092585a85a19 -> e910d98dc440 (parent changed)
       7fde9cc10f60 -> 54b3bce97211 (parent changed)
       56f6ff43f486 -> 3268330fa84f (parent changed)
       4698e4c4a163 -> c8f9a7a96e9a (parent changed)
       d21477adfa80 -> 530b251b0f7a (parent changed)
       9b4e46b8cce6 -> f899787095cd (parent changed)
       fda720ddb439 -> 0453bf09a68b (parent changed)
       ab214eefaac1 -> d5c8f2f39907 (parent changed)
       03f4c8090204 -> 604caebc7f06 (parent changed)
       7186189cfa22 -> 676b570707be (parent changed)
       16d0d9d4e2e9 -> 67820de31679 (parent changed)
       5af7526eeb73 -> 69309e17293c (parent changed)
       a9cd442a5347 -> 8e3e07cca004 (parent changed)
       69db255d5faf -> fdcbddcd3aa1 (parent changed)

     71 commits rewritten
     Updating refs/heads/k.o/for-next from 69db255d5faf to fdcbddcd3aa1

#!/usr/bin/env python3
"""Change the committer on specific commits without altering unmodified commits.
"""
import re
import subprocess
import sys
import tempfile

IDRE = re.compile(rb"^[0-9a-fA-F]{40}$")

def git_output(args, mode=None):
    o = subprocess.check_output(["git"] + args)
    if mode == "lines":
        return o.splitlines()
    return o.strip()

def git_read_commit(commit_id):
    """Read a commit object and return (raw_keys, desc_lines)"""
    lines = git_output(["cat-file", "commit", commit_id], mode="lines")

    keys = []
    itr = iter(lines)
    for line in itr:
        stripped = line.rstrip()
        if not stripped:
            break
        # Indented lines (e.g. gpgsig continuation) belong to previous key
        if line.startswith(b" "):
            keys[-1] = (keys[-1][0], keys[-1][1] + b"\n" + line)
            continue
        kv = stripped.partition(b" ")
        keys.append((kv[0], kv[2]))

    desc = list(itr)
    return keys, desc

def rebuild_commit(keys, desc):
    """Rebuild a commit object and return its new hash"""
    with tempfile.NamedTemporaryFile() as f:
        for k, v in keys:
            f.write(k + b" " + v + b"\n")
        f.write(b"\n")
        for line in desc:
            f.write(line + b"\n")
        f.flush()

        return git_output(["hash-object", "-t", "commit", "-w", f.name]).decode()

def get_commit_list(ancestor, newest):
    """Return commit IDs from newest to ancestor (exclusive), topological order"""
    ids = git_output(["rev-list", "--topo-order", newest, "^" + ancestor],
                     mode="lines")
    return [i.decode() for i in ids]

def main():
    ref = sys.argv[1]
    new_committer_name = sys.argv[2]
    new_committer_email = sys.argv[3]
    ancestor = sys.argv[4]
    target_prefixes = sys.argv[5:]

    if not target_prefixes:
        print("Usage: edit_committer.py <ref> <name> <email> <ancestor> <commit-prefix>...")
        sys.exit(1)

    newest = git_output(["rev-parse", ref]).decode()
    commit_ids = get_commit_list(ancestor, newest)

    print(f"Processing {len(commit_ids)} commits from {ancestor[:12]} to {newest[:12]}")

    # Process oldest first
    commit_ids.reverse()

    commit_map = {}
    changed = 0

    for cid in commit_ids:
        keys, desc = git_read_commit(cid)

        # Check if this commit is a target for committer rewrite
        is_target = any(cid.startswith(prefix) for prefix in target_prefixes)

        # Update parent pointers if any parents were rewritten
        new_keys = []
        parents_changed = False
        for k, v in keys:
            if k == b"parent":
                old_parent = v.decode()
                new_parent = commit_map.get(old_parent, old_parent)
                if new_parent != old_parent:
                    parents_changed = True
                new_keys.append((k, new_parent.encode()))
            elif is_target and k == b"committer":
                # Rewrite committer, preserving the date portion
                old_val = v.decode()
                # Format: "Name <email> timestamp timezone"
                m = re.match(r".*>(.+)$", old_val)
                date_part = m.group(1) if m else ""
                new_val = f"{new_committer_name} <{new_committer_email}>{date_part}"
                new_keys.append((k, new_val.encode()))
                parents_changed = True  # force rebuild
            else:
                new_keys.append((k, v))

        if not parents_changed and not is_target:
            commit_map[cid] = cid
            continue

        new_id = rebuild_commit(new_keys, desc)
        commit_map[cid] = new_id
        if new_id != cid:
            changed += 1
            label = "(target)" if is_target else "(parent changed)"
            print(f"  {cid[:12]} -> {new_id[:12]} {label}")

    new_head = commit_map.get(newest, newest)
    if new_head == newest:
        print("No changes needed.")
    else:
        # Verify trees match
        diff = git_output(["diff-tree", "-r", newest, new_head])
        assert diff == b"", f"Tree mismatch!\n{diff}"
        print(f"\n{changed} commits rewritten")
        print(f"Updating {ref} from {newest[:12]} to {new_head[:12]}")
        subprocess.check_call(
            ["git", "update-ref", "-m", "edit_committer", ref, new_head, newest])

    return 0

if __name__ == "__main__":
    sys.exit(main())