]> git.ipfire.org Git - thirdparty/openvpn.git/commitdiff
dev-tools/gerrit-send-mail.py: tool to send Gerrit patchsets to Patchwork
authorFrank Lichtenheld <frank@lichtenheld.com>
Sun, 22 Oct 2023 10:59:19 +0000 (12:59 +0200)
committerGert Doering <gert@greenie.muc.de>
Sun, 22 Oct 2023 11:07:38 +0000 (13:07 +0200)
Since we're trying to use Gerrit for patch reviews, but the actual
merge process is still implemented against the ML and Patchwork,
I wrote a script that attempts to bridge the gap.

It extracts all relevant information about a patch from Gerrit
and converts it into a mail compatible to git-am. Mostly this
work is done by Gerrit already, since we can get the original
patch in git format-patch format. But we add Acked-by information
according to the approvals in Gerrit and some other metadata.

This should allow the merge to happen based on this one mail
alone.

v3:
 - handle missing display_name and email fields for reviewers
   gracefully
 - handle missing Signed-off-by line gracefully
v4:
 - use formatted string consistently

Change-Id: If4e9c2e58441efb3fd00872cd62d1cc6c607f160
Signed-off-by: Frank Lichtenheld <frank@lichtenheld.com>
Acked-by: Gert Doering <gert@greenie.muc.de>
Message-Id: <20231022105919.21779-1-gert@greenie.muc.de>
URL: https://www.mail-archive.com/openvpn-devel@lists.sourceforge.net/msg27279.html
Signed-off-by: Gert Doering <gert@greenie.muc.de>
dev-tools/gerrit-send-mail.py [new file with mode: 0755]

diff --git a/dev-tools/gerrit-send-mail.py b/dev-tools/gerrit-send-mail.py
new file mode 100755 (executable)
index 0000000..851a20a
--- /dev/null
@@ -0,0 +1,132 @@
+#!/usr/bin/env python3
+
+#  Copyright (C) 2023 OpenVPN Inc <sales@openvpn.net>
+#  Copyright (C) 2023 Frank Lichtenheld <frank.lichtenheld@openvpn.net>
+#
+#  This program is free software; you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License version 2
+#  as published by the Free Software Foundation.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License along
+#  with this program; if not, write to the Free Software Foundation, Inc.,
+#  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+# Extract a patch from Gerrit and transform it in a file suitable as input
+# for git send-email.
+
+import argparse
+import base64
+from datetime import timezone
+import json
+import sys
+from urllib.parse import urlparse
+
+import dateutil.parser
+import requests
+
+
+def get_details(args):
+    params = {"o": ["CURRENT_REVISION", "LABELS", "DETAILED_ACCOUNTS"]}
+    r = requests.get(f"{args.url}/changes/{args.changeid}", params=params)
+    print(r.url)
+    json_txt = r.text.removeprefix(")]}'\n")
+    json_data = json.loads(json_txt)
+    assert len(json_data["revisions"]) == 1  # CURRENT_REVISION works as expected
+    revision = json_data["revisions"].popitem()[1]["_number"]
+    assert "Code-Review" in json_data["labels"]
+    acked_by = []
+    for reviewer in json_data["labels"]["Code-Review"]["all"]:
+        if "value" in reviewer:
+            assert reviewer["value"] >= 0  # no NACK
+            if reviewer["value"] == 2:
+                # fall back to user name if optional fields are not set
+                reviewer_name = reviewer.get("display_name", reviewer["name"])
+                reviewer_mail = reviewer.get("email", reviewer["name"])
+                ack = f"{reviewer_name} <{reviewer_mail}>"
+                print(f"Acked-by: {ack}")
+                acked_by.append(ack)
+    change_id = json_data["change_id"]
+    # assumes that the created date in Gerrit is in UTC
+    utc_stamp = (
+        dateutil.parser.parse(json_data["created"])
+        .replace(tzinfo=timezone.utc)
+        .timestamp()
+    )
+    # convert to milliseconds as used in message id
+    created_stamp = int(utc_stamp * 1000)
+    hostname = urlparse(args.url).hostname
+    msg_id = f"gerrit.{created_stamp}.{change_id}@{hostname}"
+    return {
+        "revision": revision,
+        "project": json_data["project"],
+        "target": json_data["branch"],
+        "msg_id": msg_id,
+        "acked_by": acked_by,
+    }
+
+
+def get_patch(details, args):
+    r = requests.get(
+        f"{args.url}/changes/{args.changeid}/revisions/{details['revision']}/patch?download"
+    )
+    print(r.url)
+    patch_text = base64.b64decode(r.text).decode()
+    return patch_text
+
+
+def apply_patch_mods(patch_text, details, args):
+    comment_start = patch_text.index("\n---\n") + len("\n---\n")
+    try:
+        signed_off_start = patch_text.rindex("\nSigned-off-by: ")
+        signed_off_end = patch_text.index("\n", signed_off_start + 1) + 1
+    except ValueError:  # Signed-off missing
+        signed_off_end = patch_text.index("\n---\n") + 1
+    assert comment_start > signed_off_end
+    acked_by_text = ""
+    acked_by_names = ""
+    for ack in details["acked_by"]:
+        acked_by_text += f"Acked-by: {ack}\n"
+        acked_by_names += f"{ack}\n"
+    patch_text_mod = (
+        patch_text[:signed_off_end]
+        + acked_by_text
+        + patch_text[signed_off_end:comment_start]
+        + f"""
+This change was reviewed on Gerrit and approved by at least one
+developer. I request to merge it to {details["target"]}.
+
+Gerrit URL: {args.url}/c/{details["project"]}/+/{args.changeid}
+This mail reflects revision {details["revision"]} of this Change.
+Acked-by according to Gerrit (reflected above):
+{acked_by_names}
+        """
+        + patch_text[comment_start:]
+    )
+    filename = f"gerrit-{args.changeid}-{details['revision']}.patch"
+    with open(filename, "w") as patch_file:
+        patch_file.write(patch_text_mod)
+    print("send with:")
+    print(f"git send-email --in-reply-to {details['msg_id']} {filename}")
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        prog="gerrit-send-mail",
+        description="Send patchset from Gerrit to mailing list",
+    )
+    parser.add_argument("changeid")
+    parser.add_argument("-u", "--url", default="https://gerrit.openvpn.net")
+    args = parser.parse_args()
+
+    details = get_details(args)
+    patch = get_patch(details, args)
+    apply_patch_mods(patch, details, args)
+
+
+if __name__ == "__main__":
+    sys.exit(main())