Skip to content

Instantly share code, notes, and snippets.

@karolherbst
Last active October 26, 2021 23:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save karolherbst/d3589dedc76759e18cdf406dd1d71572 to your computer and use it in GitHub Desktop.
Save karolherbst/d3589dedc76759e18cdf406dd1d71572 to your computer and use it in GitHub Desktop.
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