Skip to content

Commit

Permalink
dev-tools/gerrit-send-mail.py: tool to send Gerrit patchsets to Patch…
Browse files Browse the repository at this point in the history
…work

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 <[email protected]>
Acked-by: Gert Doering <[email protected]>
Message-Id: <[email protected]>
URL: https://www.mail-archive.com/[email protected]/msg27279.html
Signed-off-by: Gert Doering <[email protected]>
  • Loading branch information
flichtenheld authored and cron2 committed Oct 22, 2023
1 parent 44d5cd0 commit c827f9d
Showing 1 changed file with 132 additions and 0 deletions.
132 changes: 132 additions & 0 deletions dev-tools/gerrit-send-mail.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
#!/usr/bin/env python3

# Copyright (C) 2023 OpenVPN Inc <[email protected]>
# Copyright (C) 2023 Frank Lichtenheld <[email protected]>
#
# 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())

0 comments on commit c827f9d

Please sign in to comment.