-
-
Save karolherbst/d3589dedc76759e18cdf406dd1d71572 to your computer and use it in GitHub Desktop.
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
From bea2975a5ef99422766372b2ffb37239b9f97f5c Mon Sep 17 00:00:00 2001 | |
From: Karol Herbst <kherbst@redhat.com> | |
Date: Tue, 19 Oct 2021 17:31:20 +0200 | |
Subject: [PATCH] local changes | |
--- | |
marge/app.py | 6 +++++ | |
marge/git.py | 30 ++++++++++++++++++++++ | |
marge/job.py | 47 +++++++++++++++++++++++++++++++---- | |
marge/sobfilter.py | 62 ++++++++++++++++++++++++++++++++++++++++++++++ | |
4 files changed, 140 insertions(+), 5 deletions(-) | |
create mode 100755 marge/sobfilter.py | |
diff --git a/marge/app.py b/marge/app.py | |
index 49402d7..e3373cb 100644 | |
--- a/marge/app.py | |
+++ b/marge/app.py | |
@@ -152,6 +152,11 @@ def _parse_config(args): | |
action='store_true', | |
help='Marge-bot pushes effectively don\'t change approval status.\n', | |
) | |
+ parser.add_argument( | |
+ '--add-sob', | |
+ action='store_true', | |
+ help='Add "Signed-off-by: $maintainer" forthe person assigning to marge.\n' | |
+ ) | |
parser.add_argument( | |
'--merge-order', | |
default='created_at', | |
@@ -334,6 +339,7 @@ def main(args=None): | |
add_tested=options.add_tested, | |
add_part_of=options.add_part_of, | |
add_reviewers=options.add_reviewers, | |
+ add_sob=options.add_sob, | |
reapprove=options.impersonate_approvers, | |
approval_timeout=options.approval_reset_timeout, | |
embargo=options.embargo, | |
diff --git a/marge/git.py b/marge/git.py | |
index a46b023..be45a15 100644 | |
--- a/marge/git.py | |
+++ b/marge/git.py | |
@@ -9,6 +9,7 @@ from collections import namedtuple | |
from . import trailerfilter | |
+from . import sobfilter | |
# Turning off StrictHostKeyChecking is a nasty hack to approximate | |
# just accepting the hostkey sight unseen the first time marge | |
@@ -29,6 +30,12 @@ def _filter_branch_script(trailer_name, trailer_values): | |
) | |
return filter_script | |
+def _sob_branch_script(sob_value): | |
+ sob_script = 'SOB={sob_value} python3 {script}'.format( | |
+ sob_value=shlex.quote(sob_value), | |
+ script=sobfilter.__file__, | |
+ ) | |
+ return sob_script | |
class Repo(namedtuple('Repo', 'remote_url local_path ssh_key_file timeout reference')): | |
def clone(self): | |
@@ -74,6 +81,29 @@ class Repo(namedtuple('Repo', 'remote_url local_path ssh_key_file timeout refere | |
raise | |
return self.get_commit_hash() | |
+ def tag_with_sob(self, value, branch, start_commit): | |
+ """Adds a Signed-by-off tag with the given `value` if it's missing and removes all trailing ones. | |
+ """ | |
+ | |
+ # Strips all `$trailer_name``: lines and trailing newlines, adds an empty | |
+ # newline and tags on the `$trailer_name: $trailer_value` for each `trailer_value` in | |
+ # `trailer_values`. | |
+ sob_script = _sob_branch_script(value) | |
+ commit_range = start_commit + '..' + branch | |
+ try: | |
+ # --force = overwrite backup of last filter-branch | |
+ self.git('filter-branch', '--force', '--msg-filter', sob_script, commit_range) | |
+ except GitError: | |
+ log.warning('filter-branch failed, will try to restore') | |
+ try: | |
+ self.get_commit_hash('refs/original/refs/heads/') | |
+ except GitError: | |
+ log.warning('No changes have been effected by filter-branch') | |
+ else: | |
+ self.git('reset', '--hard', 'refs/original/refs/heads/' + branch) | |
+ raise | |
+ return self.get_commit_hash() | |
+ | |
def merge(self, source_branch, target_branch, *merge_args, source_repo_url=None, local=False): | |
"""Merge `target_branch` into `source_branch` and return the new HEAD commit id. | |
diff --git a/marge/job.py b/marge/job.py | |
index 7c0cd44..d7cc17b 100644 | |
--- a/marge/job.py | |
+++ b/marge/job.py | |
@@ -12,7 +12,9 @@ from .merge_request import MergeRequestRebaseFailed | |
from .project import Project | |
from .user import User | |
from .pipeline import Pipeline | |
+from pprint import pprint | |
+GET = gitlab.GET | |
class MergeJob: | |
@@ -126,17 +128,50 @@ class MergeJob: | |
self._options.fusion is not Fusion.gitlab_rebase | |
) | |
part_of = ( | |
- '<{0.web_url}>'.format(merge_request) | |
+ '{0.web_url}'.format(merge_request) | |
if should_add_parts_of | |
else None | |
) | |
if part_of is not None: | |
sha = self._repo.tag_with_trailer( | |
- trailer_name='Part-of', | |
+ trailer_name='Link', | |
trailer_values=[part_of], | |
branch=merge_request.source_branch, | |
start_commit='origin/' + merge_request.target_branch, | |
) | |
+ | |
+ # add Signed-off-by | |
+ should_add_sob = ( | |
+ self._options.add_sob and | |
+ self._options.fusion is not Fusion.gitlab_rebase | |
+ ) | |
+ if should_add_sob: | |
+ notes = self._api.call(GET( | |
+ '/projects/{project_id}/merge_requests/{merge_request_id}/notes'.format( | |
+ project_id=merge_request.target_project_id, | |
+ merge_request_id=merge_request.iid, | |
+ ), | |
+ { | |
+ 'order_by': 'created_at', | |
+ 'sort': 'desc', | |
+ } | |
+ )) | |
+ | |
+ maintainers = { | |
+ 'karolherbst': 'Karol Herbst <kherbst@redhat.com>', | |
+ 'skeggsb': 'Ben Skeggs <bskeggs@redhat.com>', | |
+ 'lyudess': 'Lyude Paul <lyude@redhat.com>', | |
+ } | |
+ filtered_note = [note for note in notes if note['system'] and 'assigned to @{0._user.username}'.format(self) in note['body']][0] | |
+ pprint(filtered_note) | |
+ | |
+ sob = maintainers[filtered_note['author']['username']] | |
+ sha = self._repo.tag_with_sob( | |
+ value=sob, | |
+ branch=merge_request.source_branch, | |
+ start_commit='origin/' + merge_request.target_branch, | |
+ ) | |
+ | |
return sha | |
def get_mr_ci_status(self, merge_request, commit_sha=None): | |
@@ -265,7 +300,7 @@ class MergeJob: | |
source_project = self.get_source_project(merge_request) | |
if source_project is not self._project: | |
remote = 'source' | |
- remote_url = source_project.ssh_url_to_repo | |
+ remote_url = source_project.http_url_to_repo | |
self._repo.fetch( | |
remote_name=remote, | |
remote_url=remote_url, | |
@@ -452,6 +487,7 @@ JOB_OPTIONS = [ | |
'add_tested', | |
'add_part_of', | |
'add_reviewers', | |
+ 'add_sob', | |
'reapprove', | |
'approval_timeout', | |
'embargo', | |
@@ -468,12 +504,12 @@ class MergeJobOptions(namedtuple('MergeJobOptions', JOB_OPTIONS)): | |
@property | |
def requests_commit_tagging(self): | |
- return self.add_tested or self.add_part_of or self.add_reviewers | |
+ return self.add_tested or self.add_part_of or self.add_reviewers or self.add_sob | |
@classmethod | |
def default( | |
cls, *, | |
- add_tested=False, add_part_of=False, add_reviewers=False, reapprove=False, | |
+ add_tested=False, add_part_of=False, add_reviewers=False, add_sob=False, reapprove=False, | |
approval_timeout=None, embargo=None, ci_timeout=None, fusion=Fusion.rebase, | |
use_no_ff_batches=False, use_merge_commit_batches=False, skip_ci_batches=False, | |
): | |
@@ -484,6 +520,7 @@ class MergeJobOptions(namedtuple('MergeJobOptions', JOB_OPTIONS)): | |
add_tested=add_tested, | |
add_part_of=add_part_of, | |
add_reviewers=add_reviewers, | |
+ add_sob=add_sob, | |
reapprove=reapprove, | |
approval_timeout=approval_timeout, | |
embargo=embargo, | |
diff --git a/marge/sobfilter.py b/marge/sobfilter.py | |
new file mode 100755 | |
index 0000000..abc4cd6 | |
--- /dev/null | |
+++ b/marge/sobfilter.py | |
@@ -0,0 +1,62 @@ | |
+#!/usr/bin/env python3 | |
+"""Executable script to pass to git filter-branch --msgfilter to rewrite trailers. | |
+ | |
+This treats everything (stdin, stdout, env) at the level of raw bytes which are | |
+assumed to be utf-8, or more specifically some ASCII superset, regardless of | |
+(possibly broken) LOCALE settings. | |
+ | |
+""" | |
+import collections | |
+import os | |
+import re | |
+import sys | |
+ | |
+STDIN = sys.stdin.buffer | |
+STDOUT = sys.stdout.buffer | |
+STDERR = sys.stderr.buffer | |
+ | |
+ | |
+def die(msg): | |
+ STDERR.write(b'ERROR: ') | |
+ STDERR.write(msg) | |
+ sys.exit(1) | |
+ | |
+ | |
+def drop_trailing_newlines(lines): | |
+ while lines and not lines[-1]: | |
+ del lines[-1] | |
+ | |
+ | |
+def remove_duplicates(trailers): | |
+ return list(collections.OrderedDict((t, None) for t in trailers).keys()) | |
+ | |
+ | |
+def rework_commit_message(commit_message, value): | |
+ if not commit_message: | |
+ die(b'Expected a non-empty commit message') | |
+ | |
+ trailer_name = b'Signed-off-by:' | |
+ trailer = trailer_name + b' ' + value | |
+ reworked_lines = [line.rstrip() for line in commit_message.split(b'\n')] | |
+ | |
+ drop_trailing_newlines(reworked_lines) | |
+ | |
+ if trailer_name in reworked_lines[-1]: | |
+ del reworked_lines[-1] | |
+ if trailer not in reworked_lines: | |
+ reworked_lines.append(trailer) | |
+ | |
+ drop_trailing_newlines(reworked_lines) | |
+ | |
+ return b'\n'.join(reworked_lines) | |
+ | |
+ | |
+def main(): | |
+ sob = os.environb[b'SOB'] | |
+ original_commit_message = STDIN.read().strip() | |
+ new_commit_message = rework_commit_message(original_commit_message, sob) | |
+ STDOUT.write(new_commit_message) | |
+ | |
+ | |
+if __name__ == '__main__': | |
+ main() | |
-- | |
2.31.1 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment