Diff
Modified: trunk/Tools/ChangeLog (271181 => 271182)
--- trunk/Tools/ChangeLog 2021-01-06 00:33:17 UTC (rev 271181)
+++ trunk/Tools/ChangeLog 2021-01-06 00:35:52 UTC (rev 271182)
@@ -1,3 +1,45 @@
+2021-01-05 Jonathan Bedard <[email protected]>
+
+ [webkitscmpy] Add command to canonicalize unpushed commits
+ https://bugs.webkit.org/show_bug.cgi?id=219982
+ <rdar://problem/72427536>
+
+ Reviewed by Dewei Zhu.
+
+ * Scripts/git-webkit: Specify web-service for canonical identifier translation.
+ * Scripts/libraries/webkitscmpy/setup.py: Add canonicalize directory to package.
+ * Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py: Bump version.
+ * Scripts/libraries/webkitscmpy/webkitscmpy/canonicalize: Added.
+ * Scripts/libraries/webkitscmpy/webkitscmpy/canonicalize/__init__.py: Added.
+ (Canonicalize): Command to edit history of unpushed commits on a branch.
+ (Canonicalize.parser):
+ (Canonicalize.main): Call `git filter-branch` to edit commit message and authorship
+ of unpushed commits.
+ * Scripts/libraries/webkitscmpy/webkitscmpy/canonicalize/committer.py: Added.
+ (canonicalize): Given a name, email and a contributor mapping, return a canonical name
+ and email for the given author or committer.
+ (main): Print out the canonical author and committer to be parsed by `git filter-branch`.
+ * Scripts/libraries/webkitscmpy/webkitscmpy/canonicalize/message.py: Added.
+ (main): Add the canonical identifier to a commit message.
+ * Scripts/libraries/webkitscmpy/webkitscmpy/contributor.py:
+ (Contributor): Add unknown author regex.
+ (Contributor.from_scm_log):
+ * Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py:
+ (Git.__init__): Add `git filter-branch` mock. Note that this mock makes an effort to test
+ message.py and contributor.py, but not the shell script passed to the command.
+ * Scripts/libraries/webkitscmpy/webkitscmpy/program.py:
+ (Command.main): Accept arbitrary **kwargs.
+ (Find.main): Ditto.
+ (Checkout.main): Ditto.
+ (main): Allow caller to specify a string template for canonical identifiers in commit messages.
+ * Scripts/libraries/webkitscmpy/webkitscmpy/test/canonicalize_unittest.py: Added.
+ (TestCanonicalize):
+ (TestCanonicalize.test_invalid):
+ (TestCanonicalize.test_no_commits):
+ (TestCanonicalize.test_formated_identifier):
+ (TestCanonicalize.test_git_svn):
+ (TestCanonicalize.test_branch_commits):
+
2021-01-05 Angelos Oikonomopoulos <[email protected]>
[JSC] allow stress tests to opt out of parallel execution
Modified: trunk/Tools/Scripts/git-webkit (271181 => 271182)
--- trunk/Tools/Scripts/git-webkit 2021-01-06 00:33:17 UTC (rev 271181)
+++ trunk/Tools/Scripts/git-webkit 2021-01-06 00:35:52 UTC (rev 271182)
@@ -42,5 +42,9 @@
if nick not in contributors:
contributors[nick] = c
- sys.exit(program.main(path=os.path.dirname(__file__), contributors=contributors))
+ sys.exit(program.main(
+ path=os.path.dirname(__file__),
+ contributors=contributors,
+ identifier_template='Canonical link: https://commits.webkit.org/{}',
+ ))
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/setup.py (271181 => 271182)
--- trunk/Tools/Scripts/libraries/webkitscmpy/setup.py 2021-01-06 00:33:17 UTC (rev 271181)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/setup.py 2021-01-06 00:35:52 UTC (rev 271182)
@@ -50,6 +50,7 @@
license='Modified BSD',
packages=[
'webkitscmpy',
+ 'webkitscmpy.canonicalize',
'webkitscmpy.local',
'webkitscmpy.mocks',
'webkitscmpy.mocks.local',
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py (271181 => 271182)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py 2021-01-06 00:33:17 UTC (rev 271181)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py 2021-01-06 00:35:52 UTC (rev 271182)
@@ -46,7 +46,7 @@
"Please install webkitcorepy with `pip install webkitcorepy --extra-index-url <package index URL>`"
)
-version = Version(0, 6, 4)
+version = Version(0, 7, 0)
AutoInstall.register(Package('fasteners', Version(0, 15, 0)))
AutoInstall.register(Package('monotonic', Version(1, 5)))
Added: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/canonicalize/__init__.py (0 => 271182)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/canonicalize/__init__.py (rev 0)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/canonicalize/__init__.py 2021-01-06 00:35:52 UTC (rev 271182)
@@ -0,0 +1,153 @@
+# Copyright (C) 2020 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 logging
+import os
+import tempfile
+import subprocess
+import sys
+
+from webkitcorepy import arguments, run, string_utils
+from webkitscmpy import log
+from webkitscmpy.program import Command
+
+
+class Canonicalize(Command):
+ name = 'canonicalize'
+ help = 'Take the set of commits which have not yet been pushed and edit history to normalize the ' +\
+ 'committers with existing contributor mapping and add identifiers to commit messages'
+
+ @classmethod
+ def parser(cls, parser, loggers=None):
+ output_args = arguments.LoggingGroup(
+ parser,
+ loggers=loggers,
+ help='{} amount of logging and `git rebase` information displayed'
+ )
+ output_args.add_argument(
+ '--identifier', '--no-identifier',
+ help='Add in the identifier to commit messages, true by default',
+ action=""
+ dest='identifier',
+ default=True,
+ )
+ output_args.add_argument(
+ '--remote',
+ help='Compare against a different remote',
+ dest='remote',
+ default='origin',
+ )
+
+ @classmethod
+ def main(cls, args, repository, identifier_template=None, **kwargs):
+ if not repository.path:
+ sys.stderr.write('Cannot canonicalize commits on a remote repository\n')
+ return 1
+ if not repository.is_git:
+ sys.stderr.write('Commits can only be canonicalized on a Git repository\n')
+ return 1
+
+ branch = repository.branch
+ if not branch:
+ sys.stderr.write('Failed to determine current branch\n')
+ return -1
+ result = run([
+ repository.executable(), 'rev-list',
+ '--count', '--no-merges',
+ '{remote}/{branch}..{branch}'.format(remote=args.remote, branch=branch),
+ ], capture_output=True, cwd=repository.path)
+ if result.returncode:
+ sys.stderr.write('Failed to find local commits\n')
+ return -1
+ difference = int(result.stdout.rstrip())
+ if difference <= 0:
+ print('No local commits to be edited')
+ return 0
+ log.warning('{} to be editted...'.format(string_utils.pluralize(difference, 'commit')))
+
+ base = repository.find('{}~{}'.format(branch, difference))
+ log.info('Base commit is {} (ref {})'.format(base, base.hash))
+
+ log.debug('Saving contributors to temp file to be picked up by child processes')
+ contributors = os.path.join(tempfile.gettempdir(), '{}-contributors.json'.format(os.getpid()))
+ try:
+ with open(contributors, 'w') as file:
+ repository.contributors.save(file)
+
+ message_filter = [
+ '--msg-filter',
+ "{} {} '{}'".format(
+ sys.executable,
+ os.path.join(os.path.dirname(__file__), 'message.py'),
+ identifier_template or 'Identifier: {}',
+ ),
+ ] if args.identifier else []
+
+ with open(os.devnull, 'w') as devnull:
+ subprocess.check_call([
+ repository.executable(), 'filter-branch', '-f',
+ '--env-filter', '''{overwrite_message}
+committerOutput=$({python} {committer_py} {contributor_json})
+KEY=''
+VALUE=''
+for word in $committerOutput; do
+ if [[ $word == GIT_* ]] ; then
+ if [[ $KEY == GIT_* ]] ; then
+ {setting_message}
+ printf -v $KEY "${{VALUE::$((${{#VALUE}} - 1))}}"
+ KEY=''
+ VALUE=''
+ fi
+ fi
+ if [[ "$KEY" == "" ]] ; then
+ KEY="$word"
+ else
+ VALUE="$VALUE$word "
+ fi
+done
+if [[ $KEY == GIT_* ]] ; then
+ {setting_message}
+ printf -v $KEY "${{VALUE::$((${{#VALUE}} - 1))}}"
+fi'''.format(
+ overwrite_message='' if log.level > logging.INFO else 'echo "Overwriting $GIT_COMMIT"',
+ python=sys.executable,
+ committer_py=os.path.join(os.path.dirname(__file__), 'committer.py'),
+ contributor_json=contributors,
+ setting_message='' if log.level > logging.DEBUG else 'echo " $KEY=$VALUE"',
+ ),
+ ] + message_filter + ['{}...{}'.format(branch, base.hash)],
+ cwd=repository.path,
+ env={'FILTER_BRANCH_SQUELCH_WARNING': '1', 'PYTHONPATH': ':'.join(sys.path)},
+ stdout=devnull if log.level > logging.WARNING else None,
+ stderr=devnull if log.level > logging.WARNING else None,
+ )
+
+ except subprocess.CalledProcessError:
+ sys.stderr.write('Failed to modify local commit messages\n')
+ return -1
+
+ finally:
+ os.remove(contributors)
+
+ print('{} successfully canonicalized!'.format(string_utils.pluralize(difference, 'commit')))
+
+ return 0
Added: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/canonicalize/committer.py (0 => 271182)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/canonicalize/committer.py (rev 0)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/canonicalize/committer.py 2021-01-06 00:35:52 UTC (rev 271182)
@@ -0,0 +1,92 @@
+#!/usr/bin/env python
+
+# Copyright (C) 2020 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 re
+import sys
+
+from webkitcorepy import run
+from webkitscmpy import Contributor, local
+
+EMAIL_RE = re.compile(r'(?P<email>[^@]+@[^@]+)(@.*)?')
+
+
+def canonicalize(name, email, contributors):
+ match = EMAIL_RE.match(email)
+ if match:
+ email = match.group('email')
+
+ contributor = contributors.get(email, contributors.get(email.lower()))
+ if contributor:
+ return contributor.name, email if match else contributor.email
+ contributor = contributors.get(name)
+ if contributor:
+ return contributor.name, contributor.email
+ return name, email
+
+
+def main(contributor_file):
+ REPOSITORY_PATH = os.environ.get('OLDPWD')
+ GIT_COMMIT = os.environ.get('GIT_COMMIT')
+
+ with open(contributor_file, 'r') as file:
+ contributors = Contributor.Mapping.load(file)
+
+ author, author_email = canonicalize(
+ os.environ.get('GIT_AUTHOR_NAME', 'Unknown'),
+ os.environ.get('GIT_AUTHOR_EMAIL', 'null'),
+ contributors,
+ )
+ committer, committer_email = canonicalize(
+ os.environ.get('GIT_COMMITTER_NAME', 'Unknown'),
+ os.environ.get('GIT_COMMITTER_EMAIL', 'null'),
+ contributors,
+ )
+
+ # Attempt to extract patch-by
+ if author_email == committer_email and REPOSITORY_PATH and GIT_COMMIT:
+ log = run(
+ [local.Git.executable(), 'log', GIT_COMMIT, '-1'],
+ cwd=REPOSITORY_PATH, capture_output=True, encoding='utf-8',
+ )
+
+ patch_by = re.search(
+ r'\s+Patch by (?P<author>[^<]+) \<(?P<email>[^<]+)\>.* on \d+-\d+-\d+',
+ log.stdout,
+ )
+ if patch_by:
+ author, author_email = canonicalize(
+ patch_by.group('author'),
+ patch_by.group('email'),
+ contributors,
+ )
+
+ print(u'GIT_AUTHOR_NAME {}'.format(author))
+ print(u'GIT_AUTHOR_EMAIL {}'.format(author_email))
+ print(u'GIT_COMMITTER_NAME {}'.format(committer))
+ print(u'GIT_COMMITTER_EMAIL {}'.format(committer_email))
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1]))
Added: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/canonicalize/message.py (0 => 271182)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/canonicalize/message.py (rev 0)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/canonicalize/message.py 2021-01-06 00:35:52 UTC (rev 271182)
@@ -0,0 +1,69 @@
+#!/usr/bin/env python
+
+# Copyright (C) 2020 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 sys
+
+from webkitscmpy.local import Git
+
+
+def main(inputfile, identifier_template):
+ REPOSITORY_PATH = os.environ.get('OLDPWD')
+ GIT_COMMIT = os.environ.get('GIT_COMMIT')
+
+ if not REPOSITORY_PATH:
+ sys.stderr.write('Failed to retrieve repository path\n')
+ return -1
+ if not GIT_COMMIT:
+ sys.stderr.write('Failed to retrieve git hash\n')
+ return -1
+
+ repository = Git(REPOSITORY_PATH)
+ commit = repository.commit(hash=GIT_COMMIT)
+ if not commit:
+ sys.stderr.write("Failed to find '{}' in the repository".format(GIT_COMMIT))
+ return -1
+ if not commit.identifier or not commit.branch:
+ sys.stderr.write("Failed to compute the identifier for '{}'".format(GIT_COMMIT))
+ return -1
+
+ lines = []
+ for line in inputfile.readlines():
+ lines.append(line.rstrip())
+
+ identifier_index = len(lines)
+ if identifier_index and repository.GIT_SVN_REVISION.match(lines[-1]):
+ identifier_index -= 1
+
+ if identifier_index and lines[identifier_index - 1].startswith(identifier_template.format('').split(':')[0]):
+ lines[identifier_index - 1] = identifier_template.format(commit)
+ else:
+ lines.insert(identifier_index, identifier_template.format(commit))
+
+ for line in lines:
+ print(line)
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.stdin, sys.argv[1]))
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/contributor.py (271181 => 271182)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/contributor.py 2021-01-06 00:33:17 UTC (rev 271181)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/contributor.py 2021-01-06 00:35:52 UTC (rev 271182)
@@ -30,6 +30,7 @@
class Contributor(object):
GIT_AUTHOR_RE = re.compile(r'Author: (?P<author>.*) <(?P<email>[^@]+@[^@]+)(@.*)?>')
AUTOMATED_CHECKIN_RE = re.compile(r'Author: (?P<author>.*) <devnull>')
+ UNKNOWN_AUTHOR = re.compile(r'Author: (?P<author>.*) <None>')
SVN_AUTHOR_RE = re.compile(r'r\d+ \| (?P<email>.*) \| (?P<date>.*) \| \d+ lines?')
SVN_PATCH_FROM_RE = re.compile(r'Patch by (?P<author>.*) <(?P<email>.*)> on \d+-\d+-\d+')
@@ -111,12 +112,12 @@
email = None
author = None
- for _expression_ in [cls.GIT_AUTHOR_RE, cls.SVN_AUTHOR_RE, cls.SVN_PATCH_FROM_RE, cls.AUTOMATED_CHECKIN_RE]:
+ for _expression_ in [cls.GIT_AUTHOR_RE, cls.SVN_AUTHOR_RE, cls.SVN_PATCH_FROM_RE, cls.AUTOMATED_CHECKIN_RE, cls.UNKNOWN_AUTHOR]:
match = _expression_.match(line)
if match:
if 'author' in _expression_.groupindex:
author = match.group('author')
- if '(no author)' in author or 'Automated Checkin' in author:
+ if '(no author)' in author or 'Automated Checkin' in author or 'Unknown' in author:
author = None
if 'email' in _expression_.groupindex:
email = match.group('email')
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py (271181 => 271182)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py 2021-01-06 00:33:17 UTC (rev 271181)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py 2021-01-06 00:35:52 UTC (rev 271182)
@@ -25,8 +25,10 @@
import re
from datetime import datetime
-from webkitcorepy import mocks
+from webkitcorepy import mocks, OutputCapture, StringIO
from webkitscmpy import local, Commit, Contributor
+from webkitscmpy.canonicalize.committer import main as committer_main
+from webkitscmpy.canonicalize.message import main as message_main
class Git(mocks.Subprocess):
@@ -58,6 +60,7 @@
commit.revision = None
self.head = self.commits[self.default_branch][-1]
+ self.remotes = {'origin/{}'.format(branch): commits[-1] for branch, commits in self.commits.items()}
self.tags = {}
if git_svn:
@@ -233,6 +236,14 @@
generator=lambda *args, **kwargs:
mocks.ProcessCompletion(returncode=0) if self.checkout(args[2]) else mocks.ProcessCompletion(returncode=1)
), mocks.Subprocess.Route(
+ self.executable, 'filter-branch', '-f',
+ cwd=self.path,
+ generator=lambda *args, **kwargs: self.filter_branch(
+ args[-1],
+ identifier_template=args[-2].split("'")[-2] if args[-3] == '--msg-filter' else None,
+ environment_shell=args[4] if args[3] == '--env-filter' and args[4] else None,
+ )
+ ), mocks.Subprocess.Route(
self.executable,
cwd=self.path,
completion=mocks.ProcessCompletion(
@@ -265,6 +276,13 @@
return self.commits[self.default_branch][found.branch_point - difference - 1]
return None
+ something = str(something)
+ if '..' in something:
+ a, b = something.split('..')
+ a = self.find(a)
+ b = self.find(b)
+ return b if a and b else None
+
if something == 'HEAD':
return self.head
if something in self.commits.keys():
@@ -271,10 +289,9 @@
return self.commits[something][-1]
if something in self.tags.keys():
return self.tags[something]
+ if something in self.remotes.keys():
+ return self.remotes[something]
- something = str(something)
- if '..' in something:
- something = something.split('..')[1]
for branch, commits in self.commits.items():
if branch == something:
return commits[-1]
@@ -286,11 +303,17 @@
return None
def count(self, something):
- match = self.find(something)
- if '..' in something or not match.branch_point:
- return match.identifier
- return match.branch_point + match.identifier
+ if '..' not in something:
+ match = self.find(something)
+ return (match.branch_point or 0) + match.identifier
+ a, b = something.split('..')
+ a = self.find(a)
+ b = self.find(b)
+ if a.branch_point == b.branch_point:
+ return abs(b.identifier - a.identifier)
+ return b.identifier
+
def branches_on(self, hash):
result = set()
found_identifier = 0
@@ -313,3 +336,89 @@
self.head = commit
self.detached = something not in self.commits.keys()
return True if commit else False
+
+ def filter_branch(self, range, identifier_template=None, environment_shell=None):
+ # We can't effectively mock the bash script in the command, but we can mock the python code that
+ # script calls, which is where the program logic is.
+ head, start = range.split('...')
+ head = self.find(head)
+ start = self.find(start)
+
+ commits_to_edit = []
+ for commit in reversed(self.commits[head.branch]):
+ if commit.branch == start.branch and commit.identifier <= start.identifier:
+ break
+ commits_to_edit.insert(0, commit)
+ if head.branch != self.default_branch:
+ for commit in reversed(self.commits[self.default_branch][:head.branch_point]):
+ if commit.identifier <= start.identifier:
+ break
+ commits_to_edit.insert(0, commit)
+
+ stdout = StringIO()
+ original_env = {key: os.environ.get('OLDPWD') for key in [
+ 'OLDPWD', 'GIT_COMMIT',
+ 'GIT_AUTHOR_NAME', 'GIT_AUTHOR_EMAIL',
+ 'GIT_COMMITTER_NAME', 'GIT_COMMITTER_EMAIL',
+ ]}
+
+ try:
+ count = 0
+ os.environ['OLDPWD'] = self.path
+ for commit in commits_to_edit:
+ count += 1
+ os.environ['GIT_COMMIT'] = commit.hash
+ os.environ['GIT_AUTHOR_NAME'] = commit.author.name
+ os.environ['GIT_AUTHOR_EMAIL'] = commit.author.email
+ os.environ['GIT_COMMITTER_NAME'] = commit.author.name
+ os.environ['GIT_COMMITTER_EMAIL'] = commit.author.email
+
+ stdout.write(
+ 'Rewrite {hash} ({count}/{total}) (--- seconds passed, remaining --- predicted)\n'.format(
+ hash=commit.hash,
+ count=count,
+ total=len(commits_to_edit),
+ ))
+
+ if identifier_template:
+ messagefile = StringIO()
+ messagefile.write(commit.message)
+ messagefile.seek(0)
+ with OutputCapture() as captured:
+ message_main(messagefile, identifier_template)
+ lines = captured.stdout.getvalue().splitlines()
+ if lines[-1].startswith('git-svn-id: https://svn'):
+ lines.pop(-1)
+ commit.message = '\n'.join(lines)
+
+ if not environment_shell:
+ continue
+ if re.search(r'echo "Overwriting', environment_shell):
+ stdout.write('Overwriting {}\n'.format(commit.hash))
+
+ match = re.search(r'(?P<json>\S+\.json)', environment_shell)
+ if match:
+ with OutputCapture() as captured:
+ committer_main(match.group('json'))
+ captured.stdout.seek(0)
+ for line in captured.stdout.readlines():
+ line = line.rstrip()
+ os.environ[line.split(' ')[0]] = ' '.join(line.split(' ')[1:])
+
+ commit.author = Contributor(name=os.environ['GIT_AUTHOR_NAME'], emails=[os.environ['GIT_AUTHOR_EMAIL']])
+
+ if re.search(r'echo "\s+', environment_shell):
+ for key in ['GIT_AUTHOR_NAME', 'GIT_AUTHOR_EMAIL', 'GIT_COMMITTER_NAME', 'GIT_COMMITTER_EMAIL']:
+ stdout.write(' {}={}\n'.format(key, os.environ[key]))
+
+ finally:
+ for key, value in original_env.items():
+ if value is not None:
+ os.environ[key] = value
+ else:
+ del os.environ[key]
+
+ return mocks.ProcessCompletion(
+ returncode=0,
+ stdout=stdout.getvalue(),
+ )
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program.py (271181 => 271182)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program.py 2021-01-06 00:33:17 UTC (rev 271181)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program.py 2021-01-06 00:35:52 UTC (rev 271182)
@@ -43,7 +43,7 @@
raise NotImplementedError("'{}' does not have a help message")
@classmethod
- def main(cls, args, repository):
+ def main(cls, args, repository, **kwargs):
sys.stderr.write('No command specified\n')
return -1
@@ -81,7 +81,7 @@
)
@classmethod
- def main(cls, args, repository):
+ def main(cls, args, repository, **kwargs):
try:
commit = repository.find(args.argument[0], include_log=args.include_log)
except (local.Scm.Exception, ValueError) as exception:
@@ -143,7 +143,7 @@
)
@classmethod
- def main(cls, args, repository):
+ def main(cls, args, repository, **kwargs):
if not repository.path:
sys.stderr.write("Cannot checkout on remote repository")
return 1
@@ -162,7 +162,7 @@
return 0
-def main(args=None, path=None, loggers=None, contributors=None):
+def main(args=None, path=None, loggers=None, contributors=None, identifier_template=None):
logging.basicConfig(level=logging.WARNING)
loggers = [logging.getLogger(), webkitcorepy_log, log] + (loggers or [])
@@ -183,7 +183,9 @@
subparsers = parser.add_subparsers(help='sub-command help')
- for program in [Find, Checkout]:
+ from webkitscmpy.canonicalize import Canonicalize
+
+ for program in [Find, Checkout, Canonicalize]:
subparser = subparsers.add_parser(program.name, help=program.help)
subparser.set_defaults(main=program.main)
program.parser(subparser, loggers=loggers)
@@ -195,4 +197,4 @@
else:
repository = local.Scm.from_path(path=parsed.repository, contributors=contributors)
- return parsed.main(args=parsed, repository=repository)
+ return parsed.main(args=parsed, repository=repository, identifier_template=identifier_template)
Added: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/canonicalize_unittest.py (0 => 271182)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/canonicalize_unittest.py (rev 0)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/canonicalize_unittest.py 2021-01-06 00:35:52 UTC (rev 271182)
@@ -0,0 +1,200 @@
+# Copyright (C) 2020 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 unittest
+
+from webkitcorepy import OutputCapture
+from webkitcorepy.mocks import Time as MockTime
+from webkitscmpy import program, mocks, local, Commit, Contributor
+
+
+class TestCanonicalize(unittest.TestCase):
+ path = '/mock/repository'
+
+ def test_invalid(self):
+ with OutputCapture(), mocks.local.Git(), mocks.local.Svn(self.path), MockTime:
+ self.assertEqual(1, program.main(
+ args=('canonicalize',),
+ path=self.path,
+ ))
+
+ def test_no_commits(self):
+ with OutputCapture() as captured, mocks.local.Git(self.path), mocks.local.Svn(), MockTime:
+ self.assertEqual(0, program.main(
+ args=('canonicalize',),
+ path=self.path,
+ ))
+
+ self.assertEqual(captured.stdout.getvalue(), 'No local commits to be edited\n')
+
+ def test_formated_identifier(self):
+ with OutputCapture() as captured, mocks.local.Git(self.path) as mock, mocks.local.Svn(), MockTime:
+ contirbutors = Contributor.Mapping()
+ contirbutors.create('\u017dan Dober\u0161ek', '[email protected]')
+
+ mock.commits[mock.default_branch].append(Commit(
+ hash='38ea50d28ae394c9c8b80e13c3fb21f1c262871f',
+ branch=mock.default_branch,
+ author=Contributor('\u017dan Dober\u0161ek', emails=['[email protected]']),
+ identifier=mock.commits[mock.default_branch][-1].identifier + 1,
+ timestamp=1601668000,
+ message='New commit\n',
+ ))
+
+ self.assertEqual(0, program.main(
+ args=('canonicalize', '-v',),
+ path=self.path,
+ contributors=contirbutors,
+ identifier_template='Canonical link: https://commits.webkit.org/{}',
+ ))
+
+ commit = local.Git(self.path).commit(branch=mock.default_branch)
+ self.assertEqual(commit.author, contirbutors['[email protected]'])
+ self.assertEqual(commit.message, 'New commit\nCanonical link: https://commits.webkit.org/5@main')
+
+ self.assertEqual(
+ captured.stdout.getvalue(),
+ 'Rewrite 38ea50d28ae394c9c8b80e13c3fb21f1c262871f (1/1) (--- seconds passed, remaining --- predicted)\n'
+ 'Overwriting 38ea50d28ae394c9c8b80e13c3fb21f1c262871f\n'
+ '1 commit successfully canonicalized!\n',
+ )
+
+ def test_existing_identifier(self):
+ with OutputCapture() as captured, mocks.local.Git(self.path) as mock, mocks.local.Svn(), MockTime:
+ contirbutors = Contributor.Mapping()
+ contirbutors.create('Jonathan Bedard', '[email protected]')
+
+ mock.commits[mock.default_branch].append(Commit(
+ hash='38ea50d28ae394c9c8b80e13c3fb21f1c262871f',
+ branch=mock.default_branch,
+ author=Contributor('Jonathan Bedard', emails=['[email protected]']),
+ identifier=mock.commits[mock.default_branch][-1].identifier + 1,
+ timestamp=1601668000,
+ message='New commit\nIdentifier: {}@{}'.format(
+ mock.commits[mock.default_branch][-1].identifier + 1,
+ mock.default_branch,
+ ),
+ ))
+
+ self.assertEqual(0, program.main(
+ args=('canonicalize', '-v',),
+ path=self.path,
+ contributors=contirbutors,
+ ))
+
+ commit = local.Git(self.path).commit(branch=mock.default_branch)
+ self.assertEqual(commit.author, contirbutors['[email protected]'])
+ self.assertEqual(commit.message, 'New commit\nIdentifier: 5@main')
+
+ self.assertEqual(
+ captured.stdout.getvalue(),
+ 'Rewrite 38ea50d28ae394c9c8b80e13c3fb21f1c262871f (1/1) (--- seconds passed, remaining --- predicted)\n'
+ 'Overwriting 38ea50d28ae394c9c8b80e13c3fb21f1c262871f\n'
+ '1 commit successfully canonicalized!\n',
+ )
+
+ def test_git_svn(self):
+ with OutputCapture() as captured, mocks.local.Git(self.path, git_svn=True) as mock, mocks.local.Svn(), MockTime:
+ contirbutors = Contributor.Mapping()
+ contirbutors.create('Jonathan Bedard', '[email protected]')
+
+ mock.commits[mock.default_branch].append(Commit(
+ hash='766609276fe201e7ce2c69994e113d979d2148ac',
+ branch=mock.default_branch,
+ author=Contributor('[email protected]', emails=['[email protected]']),
+ identifier=mock.commits[mock.default_branch][-1].identifier + 1,
+ timestamp=1601668000,
+ revision=9,
+ message='New commit\n',
+ ))
+
+ self.assertEqual(0, program.main(
+ args=('canonicalize', '-vv'),
+ path=self.path,
+ contributors=contirbutors,
+ ))
+
+ commit = local.Git(self.path).commit(branch=mock.default_branch)
+ self.assertEqual(commit.author, contirbutors['[email protected]'])
+ self.assertEqual(
+ commit.message,
+ 'New commit\n'
+ 'Identifier: 5@main\n'
+ 'svn-id: https://svn.example.org/repository/repository/trunk@9 268f45cc-cd09-0410-ab3c-d52691b4dbfc',
+ )
+
+ self.assertEqual(
+ captured.stdout.getvalue(),
+ 'Rewrite 766609276fe201e7ce2c69994e113d979d2148ac (1/1) (--- seconds passed, remaining --- predicted)\n'
+ 'Overwriting 766609276fe201e7ce2c69994e113d979d2148ac\n'
+ ' GIT_AUTHOR_NAME=Jonathan Bedard\n'
+ ' [email protected]\n'
+ ' GIT_COMMITTER_NAME=Jonathan Bedard\n'
+ ' [email protected]\n'
+ '1 commit successfully canonicalized!\n',
+ )
+
+ def test_branch_commits(self):
+ with OutputCapture() as captured, mocks.local.Git(self.path) as mock, mocks.local.Svn(), MockTime:
+ contirbutors = Contributor.Mapping()
+ contirbutors.create('Jonathan Bedard', '[email protected]')
+
+ local.Git(self.path).checkout('branch-a')
+ mock.commits['branch-a'].append(Commit(
+ hash='f93138e3bf1d5ecca25fc0844b7a2a78b8e00aae',
+ branch='branch-a',
+ author=Contributor('[email protected]', emails=['[email protected]']),
+ branch_point=mock.commits['branch-a'][-1].branch_point,
+ identifier=mock.commits['branch-a'][-1].identifier + 1,
+ timestamp=1601668000,
+ message='New commit 1\n',
+ ))
+ mock.commits['branch-a'].append(Commit(
+ hash='0148c0df0faf248aa133d6d5ad911d7cb1b56a5b',
+ branch='branch-a',
+ author=Contributor('[email protected]', emails=['[email protected]']),
+ branch_point=mock.commits['branch-a'][-1].branch_point,
+ identifier=mock.commits['branch-a'][-1].identifier + 1,
+ timestamp=1601669000,
+ message='New commit 2\n',
+ ))
+
+ self.assertEqual(0, program.main(
+ args=('canonicalize', ),
+ path=self.path,
+ contributors=contirbutors,
+ ))
+
+ commit_a = local.Git(self.path).commit(branch='branch-a~1')
+ self.assertEqual(commit_a.author, contirbutors['[email protected]'])
+ self.assertEqual(commit_a.message, 'New commit 1\nIdentifier: 2.3@branch-a')
+
+ commit_b = local.Git(self.path).commit(branch='branch-a')
+ self.assertEqual(commit_b.author, contirbutors['[email protected]'])
+ self.assertEqual(commit_b.message, 'New commit 2\nIdentifier: 2.4@branch-a')
+
+ self.assertEqual(
+ captured.stdout.getvalue(),
+ 'Rewrite f93138e3bf1d5ecca25fc0844b7a2a78b8e00aae (1/2) (--- seconds passed, remaining --- predicted)\n'
+ 'Rewrite 0148c0df0faf248aa133d6d5ad911d7cb1b56a5b (2/2) (--- seconds passed, remaining --- predicted)\n'
+ '2 commits successfully canonicalized!\n',
+ )