Add "git cl cherry-pick" for cherry picking a chain of CLs via Gerrit

Users who want to upload multiple cherry picks usually run "git
cherry-pick" locally, multiple times. Gerrit does not recognize
these changes as cherry picks and neither do other services that
query cherry pick info from Gerrit, e.g. rubber stamper.

For Gerrit to identify a change as a true cherry pick, you need to
use their Cherry Pick Revision REST API endpoint. This new command
uses it to create a chain of cherry pick CLs recognized by Gerrit.

Bug: b/341792235
Change-Id: I4ba75da3901f6ea68c1debd65820e802da681798
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/5756161
Reviewed-by: Josip Sokcevic <sokcevic@chromium.org>
Commit-Queue: Gavin Mak <gavinmak@google.com>
changes/61/5756161/13
Gavin Mak 11 months ago committed by LUCI CQ
parent 75932421da
commit ec800aa077

@ -1326,6 +1326,14 @@ def RestoreChange(host, change, msg=''):
return ReadHttpJsonResponse(conn)
def RebaseChange(host, change, base=None):
"""Rebases a change."""
path = f'changes/{change}/rebase'
body = {'base': base} if base else {}
conn = CreateHttpConn(host, path, reqtype='POST', body=body)
return ReadHttpJsonResponse(conn)
def SubmitChange(host, change):
"""Submits a Gerrit change via Gerrit."""
path = 'changes/%s/submit' % change
@ -1387,12 +1395,14 @@ def DeletePendingChangeEdit(host, change):
ReadHttpResponse(conn, accept_statuses=[204, 404])
def CherryPick(host, change, destination, revision='current'):
def CherryPick(host, change, destination, revision='current', message=None):
"""Create a cherry-pick commit from the given change, onto the given
destination.
"""
path = 'changes/%s/revisions/%s/cherrypick' % (change, revision)
body = {'destination': destination}
if message:
body['message'] = message
conn = CreateHttpConn(host, path, reqtype='POST', body=body)
return ReadHttpJsonResponse(conn)

@ -4504,6 +4504,107 @@ def CMDissue(parser, args):
return 0
def _create_commit_message(orig_message, bug=None):
"""Returns a commit message for the cherry picked CL."""
orig_message_lines = orig_message.splitlines()
subj_line = orig_message_lines[0]
new_message = (f'Cherry pick "{subj_line}"\n\n'
"Original change's description:\n")
for line in orig_message_lines:
new_message += f'> {line}\n'
if bug:
new_message += f'\nBug: {bug}\n'
return new_message
# TODO(b/341792235): Add metrics.
@subcommand.usage('[revisions ...]')
def CMDcherry_pick(parser, args):
"""Upload a chain of cherry picks to Gerrit.
This must be run inside the git repo you're trying to make changes to.
"""
if gclient_utils.IsEnvCog():
print('cherry-pick command is not supported in non-git environment',
file=sys.stderr)
return 1
parser.add_option('--branch', help='Gerrit branch, e.g. refs/heads/main')
parser.add_option('--bug',
help='Bug to add to the description of each change.')
parser.add_option('--parent-change-num',
type='int',
help='The parent change of the first cherry-pick CL, '
'i.e. the start of the CL chain.')
options, args = parser.parse_args(args)
if not options.branch:
parser.error('Branch is required.')
if not args:
parser.error('No revisions to cherry pick.')
# Gerrit needs a change ID for each commit we cherry pick.
change_ids_to_message = {}
change_ids_to_commit = {}
for commit in args:
message = git_common.run('show', '-s', '--format=%B', commit).strip()
if change_id := git_footers.get_footer_change_id(message):
change_ids_to_message[change_id[0]] = message
change_ids_to_commit[change_id[0]] = commit
continue
raise RuntimeError(f'Change ID not found for {commit}')
print(f'Creating chain of {len(change_ids_to_message)} cherry pick(s)...')
# Gerrit only supports cherry picking one commit per change, so we have
# to cherry pick each commit individually and create a chain of CLs.
host = Changelist().GetGerritHost()
parent_change_num = options.parent_change_num
for change_id, orig_message in change_ids_to_message.items():
change_ids_to_commit.pop(change_id)
message = _create_commit_message(orig_message, options.bug)
# Create a cherry pick first, then rebase. If we create a chained CL
# then cherry pick, the change will lose its relation to the parent.
new_change_info = gerrit_util.CherryPick(host,
change_id,
options.branch,
message=message)
new_change_id = new_change_info['change_id']
new_change_num = new_change_info['_number']
new_change_url = gerrit_util.GetChangePageUrl(host, new_change_num)
orig_subj_line = orig_message.splitlines()[0]
print(f'Created cherry pick of "{orig_subj_line}": {new_change_url}')
if parent_change_num:
try:
# TODO(b/341792235): gerrit_util will always retry failed Gerrit
# requests 5 times. This doesn't make sense if a rebase fails
# due to a merge conflict since the result won't change. Make
# RebaseChange retry at most once.
gerrit_util.RebaseChange(host, new_change_id, parent_change_num)
except gerrit_util.GerritError as e:
parent_change_url = gerrit_util.GetChangePageUrl(
host, parent_change_num)
print(f'Failed to rebase {new_change_url} on '
f'{parent_change_url}: {e}. Please resolve any merge '
'conflicts.')
print('Once resolved, you can continue the CL chain with '
f'`--parent-change-num={new_change_num}` to specify '
'which change the chain should start with.\n')
if change_ids_to_message:
print('Remaining commit(s) to cherry pick:')
for commit in change_ids_to_commit.values():
print(f' {commit}')
return 1
parent_change_num = new_change_num
return 0
@metrics.collector.collect_metrics('git cl comments')
def CMDcomments(parser, args):
"""Shows or posts review comments for any changelist."""

@ -5415,6 +5415,57 @@ class CMDLintTestCase(CMDTestCaseBase):
git_cl.sys.stderr.getvalue())
class CMDCherryPickTestCase(CMDTestCaseBase):
def setUp(self):
super(CMDTestCaseBase, self).setUp()
def testCreateCommitMessage(self):
orig_message = """Foo the bar
This change foo's the bar.
Bug: 123456
Change-Id: I25699146b24c7ad8776f17775f489b9d41499595
"""
expected_message = """Cherry pick "Foo the bar"
Original change's description:
> Foo the bar
>
> This change foo's the bar.
>
> Bug: 123456
> Change-Id: I25699146b24c7ad8776f17775f489b9d41499595
"""
self.assertEqual(git_cl._create_commit_message(orig_message),
expected_message)
def testCreateCommitMessageWithBug(self):
bug = "987654"
orig_message = """Foo the bar
This change foo's the bar.
Bug: 123456
Change-Id: I25699146b24c7ad8776f17775f489b9d41499595
"""
expected_message = f"""Cherry pick "Foo the bar"
Original change's description:
> Foo the bar
>
> This change foo's the bar.
>
> Bug: 123456
> Change-Id: I25699146b24c7ad8776f17775f489b9d41499595
Bug: {bug}
"""
self.assertEqual(git_cl._create_commit_message(orig_message, bug),
expected_message)
if __name__ == '__main__':
logging.basicConfig(
level=logging.DEBUG if '-v' in sys.argv else logging.ERROR)

Loading…
Cancel
Save