blob: 6e0de40d126a0c70f268c473c8d2e54003b5d7fb [file] [log] [blame]
#!/usr/bin/env python3
import argparse
import os
import shlex
import subprocess
import sys
def main():
parser = argparse.ArgumentParser(description='Rebase and push individual commits to studio-main', allow_abbrev=False)
parser.add_argument('commit')
parser.add_argument('-f', '--push', action='store_true', help='push to remote')
parser.add_argument('-p', '--presubmit', action='store_true')
parser.add_argument('-t', '--topic')
parser.add_argument('-ht', '--hashtag')
args = parser.parse_args()
if not args.push:
print('Dry run. Use -f to push to remote.')
# Rebase.
rebased = cherry_pick('goog/studio-main', args.commit)
# Sanity-check the number of new refs.
new_refs = run('git', 'log', '--format=%H', rebased, '^goog/studio-main').splitlines()
if len(new_refs) != 1:
sys.exit(f'ERROR: cherry-pick {rebased:.10} has too many new refs on top of studio-main')
# Push.
push_args = []
if args.presubmit: push_args.append(f'l=Presubmit-Ready+1')
if args.topic: push_args.append(f'topic={args.topic}')
if args.hashtag: push_args.append(f'hashtag={args.hashtag}')
push_args = ','.join(push_args)
push_cmd = ['git', 'push', 'goog', f'{rebased}:refs/for/studio-main%{push_args}']
print(f'Will run:', shlex.join(push_cmd))
if args.push:
run(*push_cmd)
else:
subprocess.check_call(['git', 'show', rebased])
# Half copied from 'repo-smart-rebase'.
def cherry_pick(head: str, commit: str) -> str:
"""Like git-cherry-pick but does not touch the working tree."""
new_tree, *_ = run('git', 'merge-tree', f'--merge-base={commit}^1', head, commit).splitlines()
# Preserve authorship; see https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables.
author, email, date, *msg = run('git', 'log', '-1', '--format=%an%n%ae%n%ad%n%B', commit).splitlines()
msg_args = ['-m', '\n'.join(msg)]
env = dict(GIT_AUTHOR_NAME=author, GIT_AUTHOR_EMAIL=email, GIT_AUTHOR_DATE=date, **os.environ)
cherry_pick = run('git', 'commit-tree', '-p', f'{head}^{{commit}}', *msg_args, new_tree, env=env)
# Double-check that the cherry-pick looks reasonably similar to the original commit.
# Note: we use git-diff-tree because for some reason it is way faster than git-show.
tree_diff = ['git', 'diff-tree', '-r', '--name-only', '--diff-merges=1', '--no-commit-id']
old_changed_files = run(*tree_diff, commit).splitlines()
new_changed_files = run(*tree_diff, cherry_pick).splitlines()
assert old_changed_files and new_changed_files, 'Expected a nonzero number of changed files'
if new_changed_files != old_changed_files:
sys.exit(f'ERROR: cherry-pick {cherry_pick:.10} modifies different files than its original commit {commit:.10}')
return cherry_pick
# Run a command and return stdout.
def run(*args: str, **kwargs) -> str:
return subprocess.check_output(args, **kwargs).decode().strip()
if __name__ == '__main__':
main()