diff --git a/llvm/utils/llvm-patch b/llvm/utils/llvm-patch new file mode 100755 --- /dev/null +++ b/llvm/utils/llvm-patch @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 + +""" +This script uses the Phabricator API to manage patches. It is currently +work-in-progress, please double-check the results before pushing changes. + +It requires a valid ~/.arcrc file with your API token, e.g. + +{ + "hosts": { + "https://reviews.llvm.org/api/": { + "token": "cli-thisismyapikey" + } + } +} + +To get your API token, visit https://reviews.llvm.org/conduit/login/. + +TODO: + * Support uploading patches. + * Better error handling. + * Python2 support? + * Use a better way to find raw diff IDs instead of using querydiffs. + * Support applying a stack of patches. +""" + +import argparse +import json +import os.path +import os +import subprocess +import sys +import tempfile +import urllib.parse +import urllib.request + + +def get_api_token(): + """Get the Phabricator API token for reviews.llvm.org from ~/.arcrc.""" + + with open(os.path.expanduser(os.path.join('~', '.arcrc')), 'rt') as f: + d = json.loads(f.read()) + if 'hosts' not in d: + print('No hosts in ~/.arcrc found') + sys.exit(1) + d = d['hosts'] + if 'https://reviews.llvm.org/api/' not in d: + print('No entry for "https://reviews.llvm.org/api/" found in ' + '~/.arcrc') + sys.exit(1) + return d['https://reviews.llvm.org/api/']['token'] + + +def api_request(endpoint, values): + values['api.token'] = get_api_token() + data = urllib.parse.urlencode(values) + data = data.encode('ascii') # data should be bytes + req = urllib.request.Request( + 'https://reviews.llvm.org/api/' + endpoint, data) + with urllib.request.urlopen(req) as response: + data = json.loads(response.read()) + if not data['result']: + print('ERROR: {}'.format(data['error_info'])) + return data['result'] + + +def should_strip_line(s): + return s.startswith('Summary:') or s.startswith( + 'Subscribers:') or s.startswith('Tags:') + + +def apply_patch(args): + """ + Fetches the latest diff for a given differential ID and applies it to the + current working directory. + """ + revision_id = int(args.ID.replace('D', '')) + + # Make sure there are no uncommitted changes. + ret = subprocess.call(['git', 'diff-index', '--quiet', 'HEAD']) + if ret != 0: + print("ERROR: There are uncommitted changes in the repository!\n" + " Can only apply a patch to a clean working directory.") + sys.exit(1) + + # Get list of diffs for the requested revision. + print('Fetching information for {}.'.format(args.ID)) + diffquery = api_request('differential.querydiffs', { + 'revisionIDs[0]': revision_id + }) + + # Sort diff ids by creation date and get the latest diff id. + diff_ids = sorted([(d['id'], int(d['dateCreated'])) + for d in diffquery.values()], key=lambda x: x[1]) + diff_id = diff_ids[-1][0] + + # Fetch raw diff for latest diff id. + print('Fetching diff with ID {}.'.format(diff_id)) + rawdiff = api_request('differential.getrawdiff', { + 'diffID': diff_id + }) + + # Get commit message and strip unnecessary lines (Summary:, Subscribers: + # and Tags" ). + commit_msg = api_request('differential.getcommitmessage', { + 'revision_id': revision_id}) + commit_msg = '\n'.join( + s for s in commit_msg.splitlines() if not should_strip_line(s)) + + # Write diff to file. + diff_file, path = tempfile.mkstemp(prefix='phabdiff', text=True) + with open(path, 'wt') as diff_file: + diff_file.write(rawdiff) + + print('Applying diff to working directory.') + with open(path, 'rt') as diff_file: + subprocess.check_call(['git', 'apply'], stdin=diff_file) + os.remove(path) + + # Commit all changed files. + subprocess.check_call(['git', 'commit', '-a', '-m', commit_msg]) + + +def main(): + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers( + title='subcommands') + + apply_parser = subparsers.add_parser( + 'apply', + help='Fetch the latest diff for the given differential ID from ' + 'reviews.llvm.org and apply it to the working directory.') + apply_parser.add_argument('ID', help='Differential ID, e.g. D5678') + apply_parser.set_defaults(func=apply_patch) + + args = parser.parse_args() + if not hasattr(args, 'func'): + parser.print_help() + print('llvm-patch: A subcommand is required') + sys.exit(1) + + args.func(args) + print('WARNING: This script is work-in-progress. Double-check results \n' + ' before pushing changes!') + + +if __name__ == "__main__": + main()