Diff
Modified: trunk/Tools/ChangeLog (292484 => 292485)
--- trunk/Tools/ChangeLog 2022-04-06 18:48:46 UTC (rev 292484)
+++ trunk/Tools/ChangeLog 2022-04-06 18:51:37 UTC (rev 292485)
@@ -1,5 +1,32 @@
2022-04-06 Jonathan Bedard <jbed...@apple.com>
+ [git-webkit] Add automation to assist in branch cleanup
+ https://bugs.webkit.org/show_bug.cgi?id=238637
+ <rdar://problem/91127828>
+
+ Reviewed by Stephanie Lewis and Dewei Zhu.
+
+ * Scripts/libraries/webkitscmpy/setup.py: Bump version.
+ * Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py: Ditto.
+ * Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py: Add branch deletion and reset mocks.
+ * Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/svn.py:
+ (Svn.__init__): Add `svn revert` mock.
+ * Scripts/libraries/webkitscmpy/webkitscmpy/program/clean.py:
+ (Clean):
+ (Clean.parser): Allow caller to specify branch or pull request.
+ (Clean.cleanup): Delete a branch or branches associated with a pull request.
+ (Clean.main): Differentiate calls with branches and pull requests specified from those without.
+ * Scripts/libraries/webkitscmpy/webkitscmpy/test/clean_unittest.py: Added.
+ (TestClean):
+ (TestClean.setUp):
+ (TestClean.test_checkout_none):
+ (TestClean.test_clean_git):
+ (TestClean.test_clean_svn):
+ (TestClean.test_clean_branch):
+ (TestClean.test_clean_pr):
+
+2022-04-06 Jonathan Bedard <jbed...@apple.com>
+
[git-webkit] Apply labels based on tracker
https://bugs.webkit.org/show_bug.cgi?id=238640
<rdar://problem/91135356>
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/setup.py (292484 => 292485)
--- trunk/Tools/Scripts/libraries/webkitscmpy/setup.py 2022-04-06 18:48:46 UTC (rev 292484)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/setup.py 2022-04-06 18:51:37 UTC (rev 292485)
@@ -29,7 +29,7 @@
setup(
name='webkitscmpy',
- version='4.7.0',
+ version='4.8.0',
description='Library designed to interact with git and svn repositories.',
long_description=readme(),
classifiers=[
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py (292484 => 292485)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py 2022-04-06 18:48:46 UTC (rev 292484)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py 2022-04-06 18:51:37 UTC (rev 292485)
@@ -46,7 +46,7 @@
"Please install webkitcorepy with `pip install webkitcorepy --extra-index-url <package index URL>`"
)
-version = Version(4, 7, 0)
+version = Version(4, 8, 0)
AutoInstall.register(Package('fasteners', Version(0, 15, 0)))
AutoInstall.register(Package('jinja2', Version(2, 11, 3)))
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py (292484 => 292485)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py 2022-04-06 18:48:46 UTC (rev 292484)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py 2022-04-06 18:51:37 UTC (rev 292485)
@@ -493,6 +493,10 @@
cwd=self.path,
generator=lambda *args, **kwargs: self.move_branch(args[3], args[4]),
), mocks.Subprocess.Route(
+ self.executable, 'branch', '-D', re.compile(r'.+'),
+ cwd=self.path,
+ generator=lambda *args, **kwargs: self.delete_branch(args[3]),
+ ), mocks.Subprocess.Route(
self.executable, 'push', re.compile(r'.+'), re.compile(r'.+'),
cwd=self.path,
generator=lambda *args, **kwargs: self.push(args[2], args[3].split(':')[0]),
@@ -511,9 +515,9 @@
])
)
), mocks.Subprocess.Route(
- self.executable, 'reset', re.compile(r'HEAD~\d+'),
+ self.executable, 'reset', 'HEAD',
cwd=self.path,
- generator=lambda *args, **kwargs: self.reset(int(args[2].split('~')[-1])),
+ generator=lambda *args, **kwargs: self.reset(int(args[2].split('~')[-1]) if '~' in args[2] else None),
), mocks.Subprocess.Route(
self.executable,
cwd=self.path,
@@ -930,6 +934,15 @@
self.head = self.commits[to_be_moved][-1]
return mocks.ProcessCompletion(returncode=0)
+ def delete_branch(self, branch):
+ if branch in self.commits:
+ del self.commits[branch]
+ return mocks.ProcessCompletion(returncode=0)
+ return mocks.ProcessCompletion(
+ returncode=1,
+ stdout="error: branch '{}' not found.\n".format(branch),
+ )
+
def push(self, remote, branch):
self.remotes['{}/{}'.format(remote, branch)] = self.commits[branch][-1]
return mocks.ProcessCompletion(returncode=0)
@@ -943,6 +956,11 @@
)
def reset(self, index):
+ if index is None:
+ self.modified = {}
+ self.staged = {}
+ return mocks.ProcessCompletion(returncode=0)
+
self.head = self.commits[self.head.branch][-(index + 1)]
return mocks.ProcessCompletion(returncode=0)
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/svn.py (292484 => 292485)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/svn.py 2022-04-06 18:48:46 UTC (rev 292484)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/svn.py 2022-04-06 18:51:37 UTC (rev 292485)
@@ -150,6 +150,10 @@
cwd=self.path,
completion=mocks.ProcessCompletion(returncode=0),
), mocks.Subprocess.Route(
+ self.executable, 'revert', '-R',
+ cwd=self.path,
+ completion=mocks.ProcessCompletion(returncode=0),
+ ), mocks.Subprocess.Route(
self.executable,
cwd=self.path,
completion=mocks.ProcessCompletion(
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/clean.py (292484 => 292485)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/clean.py 2022-04-06 18:48:46 UTC (rev 292484)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/clean.py 2022-04-06 18:51:37 UTC (rev 292485)
@@ -1,4 +1,4 @@
-# Copyright (C) 2021 Apple Inc. All rights reserved.
+# Copyright (C) 2021-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
@@ -20,21 +20,88 @@
# 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 re
import sys
from .command import Command
-from webkitcorepy import arguments
-from webkitscmpy import local
+from webkitcorepy import arguments, run, Terminal
+from webkitscmpy import local, remote
class Clean(Command):
name = 'clean'
- help = 'Remove all local changes from current checkout'
+ help = 'Remove all local changes from current checkout or delete local and remote branches associated with a pull-request'
+ PR_RE = re.compile(r'^\[?[Pp][Rr][ -](?P<number>\d+)]?$')
+
@classmethod
+ def parser(cls, parser, loggers=None):
+ parser.add_argument(
+ 'arguments', nargs='*',
+ type=str, default=None,
+ help='String representation(s) of a commit or branch to be cleaned',
+ )
+
+ @classmethod
+ def cleanup(cls, repository, argument):
+ if not repository.is_git:
+ sys.stderr('Can only cleanup branches on git repositories\n')
+ return 1
+
+ rmt = repository.remote()
+ target = 'fork' if isinstance(rmt, remote.GitHub) else 'origin'
+
+ match = cls.PR_RE.match(argument)
+ if match:
+ if not rmt:
+ sys.stderr.write("'{}' doesn't have a recognized remote\n".format(repository.root_path))
+ return 1
+ if not rmt.pull_requests:
+ sys.stderr.write("'{}' cannot generate pull-requests\n".format(rmt.url))
+ return 1
+
+ pr = rmt.pull_requests.get(int(match.group('number')))
+ if not pr:
+ sys.stderr.write("No pull request found for '{}'\n".format(argument))
+ return 1
+
+ if pr.opened and Terminal.choose("'{arg}' is open, cleaning it up will close the pull request.\nAre you sure you want to close {arg}?".format(arg=argument), default='No') != 'Yes':
+ sys.stderr.write("Keeping '{}' because it is still open\n".format(argument))
+ return 0
+
+ argument = pr.head
+
+ did_delete = False
+ code = 0
+ regex = re.compile(r'^{}-(?P<count>\d+)$'.format(argument))
+ for to_delete in repository.branches_for(remote=False):
+ if to_delete == argument or regex.match(to_delete):
+ code += run([repository.executable(), 'branch', '-D', to_delete], cwd=repository.root_path).returncode
+ did_delete = True
+ for to_delete in repository.branches_for(remote=target):
+ if to_delete == argument or regex.match(to_delete) and target == 'fork':
+ code += run([repository.executable(), 'push', target, '--delete', to_delete], cwd=repository.root_path).returncode
+ did_delete = True
+
+ if not did_delete:
+ sys.stderr.write("No branches matching '{}' were found\n".format(argument))
+ return 1
+
+ return code
+
+ @classmethod
def main(cls, args, repository, **kwargs):
+ if not repository:
+ sys.stderr.write('No repository provided\n')
+ return 1
if not repository.path:
sys.stderr.write('Cannot clean on remote repository\n')
return 1
- return repository.clean()
+ if not args.arguments:
+ return repository.clean()
+
+ result = 0
+ for argument in args.arguments:
+ result += cls.cleanup(repository, argument)
+ return result
Added: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/clean_unittest.py (0 => 292485)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/clean_unittest.py (rev 0)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/clean_unittest.py 2022-04-06 18:51:37 UTC (rev 292485)
@@ -0,0 +1,86 @@
+# Copyright (C) 2020-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
+
+from webkitcorepy import OutputCapture, testing
+from webkitcorepy.mocks import Time as MockTime, Terminal as MockTerminal
+from webkitscmpy import program, mocks
+
+
+class TestClean(testing.PathTestCase):
+ basepath = 'mock/repository'
+
+ def setUp(self):
+ super(TestClean, self).setUp()
+ os.mkdir(os.path.join(self.path, '.git'))
+ os.mkdir(os.path.join(self.path, '.svn'))
+
+ def test_checkout_none(self):
+ with OutputCapture() as captured, mocks.local.Git(), mocks.local.Svn(), MockTime:
+ self.assertEqual(1, program.main(
+ args=('clean',),
+ path=self.path,
+ ))
+
+ self.assertEqual(captured.stderr.getvalue(), 'No repository provided\n')
+
+ def test_clean_git(self):
+ with OutputCapture(), mocks.local.Git(self.path) as repo, mocks.local.Svn(), MockTime:
+ repo.modified['some/file'] = 'diff contnet'
+ self.assertEqual(0, program.main(
+ args=('clean',),
+ path=self.path,
+ ))
+ self.assertDictEqual({}, repo.modified)
+
+ def test_clean_svn(self):
+ with OutputCapture(), mocks.local.Git(), mocks.local.Svn(self.path), MockTime:
+ self.assertEqual(0, program.main(
+ args=('clean',),
+ path=self.path,
+ ))
+
+ def test_clean_branch(self):
+ with OutputCapture(), mocks.local.Git(self.path) as repo, mocks.local.Svn(), MockTime:
+ self.assertIn('branch-a', repo.commits)
+ self.assertEqual(0, program.main(
+ args=('clean', 'branch-a'),
+ path=self.path,
+ ))
+ self.assertNotIn('branch-a', repo.commits)
+
+ def test_clean_pr(self):
+ with OutputCapture(), mocks.remote.GitHub() as remote, mocks.local.Git(self.path, remote='https://{}'.format(remote.remote)) as repo, mocks.local.Svn():
+ repo.staged['added.txt'] = 'added'
+ self.assertEqual(0, program.main(
+ args=('pull-request', '-i', 'pr-branch'),
+ path=self.path,
+ ))
+
+ self.assertIn('eng/pr-branch', repo.commits)
+ with MockTerminal.input('y'):
+ self.assertEqual(0, program.main(
+ args=('clean', 'pr-1'),
+ path=self.path,
+ ))
+ self.assertNotIn('eng/pr-branch', repo.commits)