| #!/usr/bin/python3 |
| # |
| # Copyright 2025 Valve Corporation |
| # SPDX-License-Identifier: MIT |
| |
| import argparse |
| import csv |
| import unittest |
| import sys |
| import subprocess |
| import os |
| import shlex |
| from unidecode import unidecode |
| |
| def normalize(x): |
| return unidecode(x.lower()) |
| |
| def name(row): |
| return normalize(row[0]) |
| |
| def username(row): |
| return normalize(row[2]) |
| |
| def find_person(x): |
| x = normalize(x) |
| |
| filename = 'people.csv' |
| path = os.path.join(os.path.dirname(os.path.realpath(__file__)), filename) |
| with open(path, 'r') as f: |
| people = list(csv.reader(f, skipinitialspace=True)) |
| |
| # First, try to exactly match username |
| for row in people: |
| if username(row) == x: |
| return row |
| |
| # Next, try to exactly match fullname |
| for row in people: |
| if name(row) == x: |
| return row |
| |
| # Now we get fuzzy. Try to match a first name. |
| candidates = [r for r in people if name(r).split(' ')[0] == x] |
| if len(candidates) == 1: |
| return candidates[0] |
| |
| # Or a last name? |
| candidates = [r for r in people if x in name(r).split(' ')] |
| if len(candidates) == 1: |
| return candidates[0] |
| |
| # Well, frick. |
| return None |
| |
| # Self-test... is it even worth find a unit test framework for this? |
| TEST_CASES = { |
| 'gfxstrand': 'faith.ekstrand@collabora.com', |
| 'Faith': 'faith.ekstrand@collabora.com', |
| 'faith': 'faith.ekstrand@collabora.com', |
| 'alyssa': 'alyssa@rosenzweig.io', |
| 'briano': 'ivan.briano@intel.com', |
| 'schurmann': 'daniel@schuermann.dev', |
| 'Schürmann': 'daniel@schuermann.dev', |
| } |
| |
| for test in TEST_CASES: |
| a, b = find_person(test), TEST_CASES[test] |
| if a is None or a[1] != b: |
| print(test, a, b) |
| assert(a[1] == b) |
| |
| # Now the tool itself |
| if __name__ == "__main__": |
| parser = argparse.ArgumentParser( |
| prog='rb', |
| description='Add review trailers') |
| parser.add_argument('person', nargs='+', help="Reviewer's username, first name, or full name") |
| parser.add_argument('-a', '--ack', action='store_true', help="Apply an acked-by tag") |
| parser.add_argument('-d', '--dry-run', action='store_true', |
| help="Print trailer without applying") |
| parser.add_argument('-r', '--rebase', nargs='?', |
| help="Rebase on the specified branch applying tags to all commits") |
| args = parser.parse_args() |
| |
| # If we are rebasing, let git reinvoke this script with the rebase related |
| # arguments stripped. |
| if args.rebase is not None: |
| relevant_args = [sys.argv[0]] |
| if args.ack: |
| relevant_args.append("--ack") |
| |
| relevant_args += args.person |
| |
| cmd = f"python3 {' '.join(relevant_args)}" |
| rebase = ['git', 'rebase', '--exec', cmd, args.rebase] |
| |
| if args.dry_run: |
| print(' '.join([shlex.quote(s) for s in rebase])) |
| returncode = 0 |
| else: |
| returncode = subprocess.run(rebase).returncode |
| |
| sys.exit(returncode) |
| |
| for p in args.person: |
| person = find_person(p) |
| if person is None: |
| print(f'Could not uniquely identify {p}, skipping') |
| |
| trailer = 'Acked-by' if args.ack else 'Reviewed-by' |
| trailer = f'{trailer}: {person[0]} <{person[1]}>' |
| |
| if args.dry_run: |
| print(trailer) |
| continue |
| |
| diff_index = subprocess.run("git diff-index --quiet --cached HEAD --".split(" ")) |
| if diff_index.returncode != 0: |
| print("You have staged changes.") |
| print("Please commit before applying review tags.") |
| sys.exit(1) |
| |
| env = os.environ.copy() |
| env['GIT_EDITOR'] = f'git interpret-trailers --trailer "{trailer}" --in-place' |
| subprocess.run(["git", "commit", "--amend"], env=env) |