Title: [271182] trunk/Tools
Revision
271182
Author
[email protected]
Date
2021-01-05 16:35:52 -0800 (Tue, 05 Jan 2021)

Log Message

[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):

Modified Paths

Added Paths

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',
+        )
_______________________________________________
webkit-changes mailing list
[email protected]
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to