-
Notifications
You must be signed in to change notification settings - Fork 3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
dev-tools/gerrit-send-mail.py: tool to send Gerrit patchsets to Patch…
…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
1 parent
44d5cd0
commit c827f9d
Showing
1 changed file
with
132 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) |