Skip to content

Commit

Permalink
[git-webkit] Add cherry-pick command
Browse files Browse the repository at this point in the history
https://bugs.webkit.org/show_bug.cgi?id=244307
<rdar://problem/97399601>

Reviewed by Aakash Jain.

* Tools/Scripts/libraries/webkitscmpy/setup.py: Bump version.
* Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py: Ditto.
* Tools/Scripts/hooks/prepare-commit-msg: Format `cherry-pick` commit messages.
* Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py: Support `cherry-pick`.
* Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/__init__.py: Add CherryPick command.
* Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/cherry_pick.py: Added.
(CherryPick.parser):
(CherryPick.main): Convert argument to commit object, pass commit representation to `git cherry-pcik` command.
* Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/cherry_pick_unittest.py: Added.
(TestCherryPick.setUp):
(TestCherryPick.test_none):
(TestCherryPick.test_basic):
(TestCherryPick.test_alternate_issue):

Canonical link: https://commits.webkit.org/253927@main
  • Loading branch information
JonWBedard authored and mcatanzaro committed Dec 20, 2022
1 parent 4cafd7d commit 4287511
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 2 deletions.
39 changes: 38 additions & 1 deletion Tools/Scripts/hooks/prepare-commit-msg
@@ -1,6 +1,7 @@
#!/usr/bin/env {{ python }}

import os
import re
import subprocess
import sys

Expand Down Expand Up @@ -99,13 +100,49 @@ Reviewed by NOBODY (OOPS!).
)


def cherry_pick(content):
cherry_picked = os.environ.get('GIT_WEBKIT_CHERRY_PICKED')
bug = os.environ.get('GIT_WEBKIT_BUG')

if not cherry_picked or not bug:
LINK_RE = re.compile(r'^\S+:\/\/\S+\d+\S*$')
HASH_RE = re.compile(r'^# You are currently cherry-picking commit (?P<hash>[a-f0-9A-F]+)\.$')

for line in content.splitlines():
if not line:
continue
if not bug and LINK_RE.match(line):
bug = line
match = None if cherry_picked else HASH_RE.match(line)
if match:
cherry_picked = match.group('hash')

if bug and cherry_picked:
break

result = 'Cherry-pick {}. {}\n\n'.format(cherry_picked or '???', bug or '<bug>')
for line in content.splitlines():
if not line:
result += '\n'
continue
if line[0] != '#':
result += 4*' '
result += line + '\n'
return result


def main(file_name=None, source=None, sha=None):
with open(file_name, 'r') as commit_message_file:
content = commit_message_file.read()

if source not in (None, 'commit', 'template', 'merge'):
return 0


if source == 'merge' and os.environ.get('GIT_REFLOG_ACTION') == 'cherry-pick':
with open(file_name, 'w') as commit_message_file:
commit_message_file.write(cherry_pick(content))
return 0

if source == 'merge' and not content.startswith('Revert'):
return 0

Expand Down
30 changes: 30 additions & 0 deletions Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py
Expand Up @@ -523,6 +523,10 @@ def __init__(
self.executable, 'revert', '--abort',
cwd=self.path,
generator=lambda *args, **kwargs: self.revert(revert_abort=True),
), mocks.Subprocess.Route(
self.executable, 'cherry-pick', '-e',
cwd=self.path,
generator=lambda *args, **kwargs: self.cherry_pick(args[3], env=kwargs.get('env', dict())),
), mocks.Subprocess.Route(
self.executable, 'restore', '--staged', re.compile(r'.+'),
cwd=self.path,
Expand Down Expand Up @@ -968,6 +972,32 @@ def revert(self, commit_hashes=[], no_commit=False, revert_continue=False, rever

return mocks.ProcessCompletion(returncode=0)

def cherry_pick(self, hash, env=None):
commit = self.find(hash)
env = env or dict()

if self.staged:
return mocks.ProcessCompletion(returncode=1, stdout='error: your local changes would be overwritten by cherry-pick.\nfatal: cherry-pick failed\n')
if not commit:
return mocks.ProcessCompletion(returncode=128, stdout="fatal: bad revision '{}'\n".format(hash))

self.head = Commit(
branch=self.branch, repository_id=self.head.repository_id,
timestamp=int(time.time()),
identifier=self.head.identifier + 1 if self.head.branch_point else 1,
branch_point=self.head.branch_point or self.head.identifier,
message='Cherry-pick {}. {}\n {}\n'.format(
env.get('GIT_WEBKIT_CHERRY_PICKED', '') or commit.hash,
env.get('GIT_WEBKIT_BUG', '') or '<bug>',
'\n '.join(commit.message.splitlines()),
),
)
self.head.author = Contributor(self.config()['user.name'], [self.config()['user.email']])
self.head.hash = hashlib.sha256(string_utils.encode(self.head.message)).hexdigest()[:40]
self.commits[self.branch].append(self.head)

return mocks.ProcessCompletion(returncode=0)

def restore(self, file, staged=False):
if staged:
if file in self.staged:
Expand Down
Expand Up @@ -28,6 +28,7 @@
from .blame import Blame
from .branch import Branch
from .canonicalize import Canonicalize
from .cherry_pick import CherryPick
from .clean import Clean, DeletePRBranches
from .command import Command
from .commit import Commit
Expand Down Expand Up @@ -80,7 +81,8 @@ def main(
Blame, Branch, Canonicalize, Checkout,
Clean, Find, Info, Land, Log, Pull,
PullRequest, Revert, Setup, InstallGitLFS,
Credentials, Commit, DeletePRBranches, Squash
Credentials, Commit, DeletePRBranches, Squash,
CherryPick,
]
if subversion:
programs.append(SetupGitSvn)
Expand Down
@@ -0,0 +1,75 @@
# Copyright (C) 2022 Apple Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import sys

from .branch import Branch
from .command import Command
from webkitbugspy import Tracker
from webkitcorepy import run
from webkitscmpy import local


class CherryPick(Command):
name = 'cherry-pick'
help = 'Cherry-pick a commit with an annotated commit message'

@classmethod
def parser(cls, parser, loggers=None):
Branch.parser(parser, loggers=loggers)
parser.add_argument(
'argument', nargs=1,
type=str, default=None,
help='String representation of a commit to be cherry-picked',
)

@classmethod
def main(cls, args, repository, **kwargs):
if not repository:
sys.stderr.write('No repository provided\n')
return 1
if not isinstance(repository, local.Git):
sys.stderr.write("Can only cherry-pick from local 'git' repository\n")
return 1

try:
commit = repository.find(args.argument[0], include_log=False)
except (local.Scm.Exception, TypeError, ValueError) as exception:
# ValueErrors and Scm exceptions usually contain enough information to be displayed
# to the user as an error
sys.stderr.write(str(exception) + '\n')
return 1

issue = Tracker.from_string(args.issue) if args.issue else None
if str(commit) == commit.hash[:commit.HASH_LABEL_SIZE]:
cherry_pick_string = str(commit)
else:
cherry_pick_string = '{} ({})'.format(commit, commit.hash[:commit.HASH_LABEL_SIZE])

return run(
[repository.executable(), 'cherry-pick', '-e', commit.hash],
cwd=repository.root_path,
env=dict(
GIT_WEBKIT_CHERRY_PICKED=cherry_pick_string,
GIT_WEBKIT_BUG=issue.link if issue else '',
), capture_output=False,
).returncode
@@ -0,0 +1,76 @@
# Copyright (C) 2022 Apple Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import os
import shutil
import tempfile

from mock import patch
from webkitbugspy import Tracker, radar, mocks as bmocks
from webkitcorepy import OutputCapture, testing
from webkitcorepy.mocks import Time as MockTime
from webkitscmpy import program, mocks


class TestCherryPick(testing.PathTestCase):
basepath = 'mock/repository'

def setUp(self):
super(TestCherryPick, self).setUp()
os.mkdir(os.path.join(self.path, '.git'))
os.mkdir(os.path.join(self.path, '.svn'))

def test_none(self):
with OutputCapture() as captured, mocks.local.Git(), mocks.local.Svn(), MockTime:
self.assertEqual(1, program.main(
args=('cherry-pick', 'd8bce26fa65c'),
path=self.path,
))
self.assertEqual(captured.stderr.getvalue(), 'No repository provided\n')

def test_basic(self):
with OutputCapture() as captured, mocks.local.Git(self.path) as repo, mocks.local.Svn(), MockTime:
repo.head = repo.commits['branch-a'][-1]
self.assertEqual(0, program.main(
args=('cherry-pick', 'd8bce26fa65c'),
path=self.path,
))
self.assertEqual(repo.head.hash, '5848f06de77d306791b7410ff2197bf3dd82b9e9')
self.assertEqual(repo.head.message, 'Cherry-pick 5@main (d8bce26fa65c). <bug>\n Patch Series\n')

self.assertEqual(captured.stdout.getvalue(), '')
self.assertEqual(captured.stderr.getvalue(), '')

def test_alternate_issue(self):
with OutputCapture() as captured, mocks.local.Git(self.path) as repo, mocks.local.Svn(), bmocks.Radar(
issues=bmocks.ISSUES,
), patch('webkitbugspy.Tracker._trackers', [radar.Tracker()]), MockTime:
repo.head = repo.commits['branch-a'][-1]
self.assertEqual(0, program.main(
args=('cherry-pick', 'd8bce26fa65c', '-i', '<rdar://problem/123>'),
path=self.path,
))
self.assertEqual(repo.head.hash, '200db1e4faae82ff005f1b826a12ad8c8260b179')
self.assertEqual(repo.head.message, 'Cherry-pick 5@main (d8bce26fa65c). <rdar://123>\n Patch Series\n')

self.assertEqual(captured.stdout.getvalue(), '')
self.assertEqual(captured.stderr.getvalue(), '')

0 comments on commit 4287511

Please sign in to comment.