Title: [295388] trunk/Tools/Scripts
Revision
295388
Author
zhifei_f...@apple.com
Date
2022-06-08 11:13:57 -0700 (Wed, 08 Jun 2022)

Log Message

Add git-webkit squash and refactory mock git

https://bugs.webkit.org/show_bug.cgi?id=237664
rdar://90040109

This change will make the mock git support commits without setting any identifier.
mock git now will assume branch in git-repo.json will have a base commit for each branch.

Reviewed by Jonathan Bedard.

* Tools/Scripts/hooks/prepare-commit-msg: Add new env val to support adding message content
* Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/git-repo.json: Add a new branch for testing squash.
* Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py: Make the mock more like real git, add rev-list which will resolve commits follow two dots and triple dots syntax.
(Git):
* Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/bitbucket.py:
(BitBucket):
(BitBucket.resolve_all_commits):
(BitBucket.commit):
* Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py:
(GitHub):
(GitHub.resolve_all_commits):
(GitHub.commit):
* Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/__init__.py:
* Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/squash.py: Added.
(Squash):
(Squash.parser):
(Squash.get_commits_hashes):
(Squash.undo_reset):
(Squash.squash_commit):
(Squash.main):
* Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/checkout_unittest.py:
* Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/git_unittest.py:
(TestGit.test_branches):
* Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/land_unittest.py:
(repository): Make sure all branch have a base commit.
* Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/pull_request_unittest.py:
* Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/squash_unittest.py: Added.
(TestSquash):
(TestSquash.setUp):
(TestSquash.test_github_with_previous_history):
(TestSquash.test_github_without_previous_history):
(TestSquash.test_github_two_step):
(TestSquash.test_modified):

Canonical link: https://commits.webkit.org/251394@main

Modified Paths

Added Paths

Diff

Modified: trunk/Tools/Scripts/hooks/prepare-commit-msg (295387 => 295388)


--- trunk/Tools/Scripts/hooks/prepare-commit-msg	2022-06-08 18:03:58 UTC (rev 295387)
+++ trunk/Tools/Scripts/hooks/prepare-commit-msg	2022-06-08 18:13:57 UTC (rev 295388)
@@ -67,7 +67,7 @@
 '''.format(
         title=os.environ.get('COMMIT_MESSAGE_TITLE', '') or 'Need a short description (OOPS!).',
         bugs=os.environ.get('COMMIT_MESSAGE_BUG', '') or 'Need the bug URL (OOPS!).',
-        content='\n'.join(commit_message),
+        content='\n'.join(commit_message) + os.environ.get('COMMIT_MESSAGE_CONTENT', ''),
     )
 
     except subprocess.CalledProcessError:

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/setup.py (295387 => 295388)


--- trunk/Tools/Scripts/libraries/webkitscmpy/setup.py	2022-06-08 18:03:58 UTC (rev 295387)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/setup.py	2022-06-08 18:13:57 UTC (rev 295388)
@@ -29,7 +29,7 @@
 
 setup(
     name='webkitscmpy',
-    version='4.15.4',
+    version='5.0.0',
     description='Library designed to interact with git and svn repositories.',
     long_description=readme(),
     classifiers=[

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py (295387 => 295388)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py	2022-06-08 18:03:58 UTC (rev 295387)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py	2022-06-08 18:13:57 UTC (rev 295388)
@@ -46,7 +46,7 @@
         "Please install webkitcorepy with `pip install webkitcorepy --extra-index-url <package index URL>`"
     )
 
-version = Version(4, 15, 4)
+version = Version(5, 0, 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/git-repo.json (295387 => 295388)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/git-repo.json	2022-06-08 18:03:58 UTC (rev 295387)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/git-repo.json	2022-06-08 18:13:57 UTC (rev 295388)
@@ -118,6 +118,20 @@
     ], 
     "branch-a": [
         {
+            "hash": "fff83bb2d9171b4d9196e977eb0508fd57e7a08d", 
+            "author": {
+                "name": "Jonathan Bedard", 
+                "emails": [
+                    "jbed...@apple.com"
+                ]
+            }, 
+            "timestamp": 1601661000, 
+            "branch": "main", 
+            "message": "2nd commit\n", 
+            "identifier": "2@main", 
+            "revision": 2
+        }, 
+        {
             "hash": "a30ce8494bf1ac2807a69844f726be4a9843ca55", 
             "author": {
                 "name": "Jonathan Bedard", 
@@ -145,5 +159,67 @@
             "identifier": "2.2@branch-a", 
             "revision": 6
         }
+    ],
+    "eng/squash-branch": [
+        {
+            "hash": "d8bce26fa65c6fc8f39c17927abb77f69fab82fc", 
+            "author": {
+                "name": "Jonathan Bedard", 
+                "emails": [
+                    "jbed...@apple.com"
+                ]
+            }, 
+            "timestamp": 1601668000, 
+            "branch": "main", 
+            "message": "Patch Series\n", 
+            "identifier": "5@main", 
+            "revision": 9
+        },
+        {
+            "hash": "d631ca664d338ef8aa153a6e4088ab5f633fd43b", 
+            "author": {
+                "name": "Zhifei Fang", 
+                "emails": [
+                    "zhifei_f...@apple.com"
+                ]
+            }, 
+            "timestamp": 1601672000, 
+            "branch": "eng/squash-branch", 
+            "message": "Changed something 1",
+            "changeFiles": {
+                "a.cpp": "diff --git a/a.cpp b/a.cpp"
+            }
+        }, 
+        {
+            "hash": "651a4775399150c14d420ae29160a7111ff49306", 
+            "author": {
+                "name": "Zhifei Fang", 
+                "emails": [
+                    "zhifei_f...@apple.com"
+                ]
+            },
+            "timestamp": 1601674000, 
+            "branch": "eng/squash-branch", 
+            "message": "Changed something 2",
+            "changeFiles": {
+                "c.cpp": "diff --git a/c.cpp b/c.cpp"
+            }
+        }, 
+        {
+            "hash": "f8a78d2db925929b75b17396ec1d7296078d49b6", 
+            "author": {
+                "name": "Zhifei Fang", 
+                "emails": [
+                    "zhifei_f...@apple.com"
+                ]
+            },
+            "timestamp": 1601677000, 
+            "branch": "eng/squash-branch", 
+            "message": "Changed something 3",
+            "changeFiles": {
+                "a.cpp": "diff --git a/a.cpp b/a.cpp",
+                "b.cpp": "diff --git a/b.cpp b/b.cpp"
+            }
+        }
     ]
 }
\ No newline at end of file

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py (295387 => 295388)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py	2022-06-08 18:03:58 UTC (rev 295387)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py	2022-06-08 18:13:57 UTC (rev 295388)
@@ -67,16 +67,26 @@
         with open(datafile or os.path.join(os.path.dirname(os.path.dirname(__file__)), 'git-repo.json')) as file:
             self.commits = json.load(file)
         for key, commits in self.commits.items():
-            self.commits[key] = [Commit(**kwargs) for kwargs in commits]
+            commit_objs = []
+            for kwargs in commits:
+                changeFiles = None
+                if 'changeFiles' in kwargs:
+                    changeFiles = kwargs['changeFiles']
+                    del kwargs['changeFiles']
+                commit = Commit(**kwargs)
+                if changeFiles:
+                    setattr(commit, '__mock__changeFiles', changeFiles)
+                commit_objs.append(commit)
+            self.commits[key] = commit_objs
             if not git_svn:
                 for commit in self.commits[key]:
                     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.remotes = {'origin/{}'.format(branch): commits[:] for branch, commits in self.commits.items()}
         for name in (remotes or {}).keys():
             for branch, commits in self.commits.items():
-                self.remotes['{}/{}'.format(name, branch)] = commits[-1]
+                self.remotes['{}/{}'.format(name, branch)] = commits[:]
 
         self.tags = {}
 
@@ -303,7 +313,7 @@
                         ),
                 ) if self.find(args[2]) else mocks.ProcessCompletion(returncode=128),
             ), mocks.Subprocess.Route(
-                self.executable, 'log', '--format=fuller', '--no-decorate', '--date=unix', re.compile(r'.+\.\.\..+'),
+                self.executable, 'log', '--format=fuller', '--no-decorate', '--date=unix', re.compile(r'.+'),
                 cwd=self.path,
                 generator=lambda *args, **kwargs: mocks.ProcessCompletion(
                     returncode=0,
@@ -318,15 +328,16 @@
                             author=commit.author.name,
                             email=commit.author.email,
                             date=commit.timestamp,
-                            log='\n'.join([
-                                ('    ' + line) if line else '' for line in commit.message.splitlines()
-                            ] + ([
-                                '    git-svn-id: https://svn.{}/repository/{}/trunk@{} 268f45cc-cd09-0410-ab3c-d52691b4dbfc'.format(
+                            log='\n'.join(
+                                [
+                                    ('    ' + line) if line else '' for line in commit.message.splitlines()
+                                ] + (['    git-svn-id: https://svn.{}/repository/{}/trunk@{} 268f45cc-cd09-0410-ab3c-d52691b4dbfc'.format(
                                     self.remote.split('@')[-1].split(':')[0],
                                     os.path.basename(path),
-                                   commit.revision,
-                            )] if git_svn else []),
-                        )) for commit in list(self.commits_in_range(args[5].split('...')[-1], args[5].split('...')[0]))[:-1]
+                                    commit.revision,
+                                )] if git_svn else []),
+                            )
+                        ) for commit in list(self.rev_list(args[5]))
                     ])
                 )
             ), mocks.Subprocess.Route(
@@ -343,15 +354,16 @@
                             author=commit.author.name,
                             email=commit.author.email,
                             date=commit.timestamp if '--date=unix' in args else datetime.utcfromtimestamp(commit.timestamp + time.timezone).strftime('%a %b %d %H:%M:%S %Y +0000'),
-                            log='\n'.join([
-                                ('    ' + line) if line else '' for line in commit.message.splitlines()
-                            ] + ([
-                                '    git-svn-id: https://svn.{}/repository/{}/trunk@{} 268f45cc-cd09-0410-ab3c-d52691b4dbfc'.format(
+                            log='\n'.join(
+                                [
+                                    ('    ' + line) if line else '' for line in commit.message.splitlines()
+                                ] + (['    git-svn-id: https://svn.{}/repository/{}/trunk@{} 268f45cc-cd09-0410-ab3c-d52691b4dbfc'.format(
                                     self.remote.split('@')[-1].split(':')[0],
                                     os.path.basename(path),
-                                   commit.revision,
-                            )] if git_svn else []),
-                        )) for commit in self.commits_in_range(self.commits[self.default_branch][0].hash, args[2])
+                                    commit.revision,
+                                )] if git_svn else [])
+                            )
+                        ) for commit in self.rev_list(args[2])
                     ])
                 )
             ), mocks.Subprocess.Route(
@@ -362,6 +374,13 @@
                     stdout='{}\n'.format(self.count(args[4]))
                 ) if self.find(args[4]) else mocks.ProcessCompletion(returncode=128),
             ), mocks.Subprocess.Route(
+                self.executable, 'rev-list', re.compile(r'.+'),
+                cwd=self.path,
+                generator=lambda *args, **kwargs: mocks.ProcessCompletion(
+                    returncode=0,
+                    stdout='\n'.join(map(lambda commit: commit.hash, self.rev_list(args[2])))
+                ),
+            ), mocks.Subprocess.Route(
                 self.executable, 'show', '-s', '--format=%ct', re.compile(r'.+'),
                 cwd=self.path,
                 generator=lambda *args, **kwargs: mocks.ProcessCompletion(
@@ -548,10 +567,7 @@
                     stdout='\n'.join([
                         '--- a/ChangeLog\n+++ b/ChangeLog\n@@ -1,0 +1,0 @@\n{}'.format(
                             '\n'.join(['+ {}'.format(line) for line in commit.message.splitlines()])
-                        ) for commit in list(self.commits_in_range(
-                            args[2].split('..')[0],
-                            args[2].split('..')[-1] if '..' in args[2] else self.commits[self.default_branch][-1].hash,
-                        ))[:-1]
+                        ) for commit in list(self.rev_list(args[2] if '..' in args[2] else '{}..HEAD'.format(args[2])))
                     ])
                 )
             ), mocks.Subprocess.Route(
@@ -558,7 +574,11 @@
                 self.executable, 'reset', 'HEAD',
                 cwd=self.path,
                 generator=lambda *args, **kwargs: self.reset(int(args[2].split('~')[-1]) if '~' in args[2] else None),
-            ), mocks.Subprocess.Route(
+            ),  mocks.Subprocess.Route(
+                self.executable, 'reset', re.compile(r'.+'),
+                cwd=self.path,
+                generator=lambda *args, **kwargs: self.reset_commit(args[2]),
+            ),  mocks.Subprocess.Route(
                 self.executable,
                 cwd=self.path,
                 completion=mocks.ProcessCompletion(
@@ -617,12 +637,16 @@
             if len(split) == 2 and Commit.NUMBER_RE.match(split[1]):
                 found = self.find(split[0])
                 difference = int(split[1])
-                if difference < found.identifier:
-                    difference += self.commits[found.branch][0].identifier
-                    return self.commits[found.branch][found.identifier - difference]
-                difference -= found.identifier
-                if found.branch_point and difference < found.branch_point:
-                    return self.commits[self.default_branch][found.branch_point - difference - 1]
+                if split[0] in self.remotes:
+                    all_commits = self.resolve_all_commits(found.branch, remote=split[0].replace('/{}'.format(found.branch), ''))
+                else:
+                    all_commits = self.resolve_all_commits(found.branch)
+                head_index = None
+                for i in range(len(all_commits)):
+                    if found == all_commits[i]:
+                        head_index = i
+                if head_index is not None and head_index - difference >= 0:
+                    return all_commits[head_index - difference]
                 return None
 
         something = str(something).replace('remotes/', '')
@@ -639,7 +663,7 @@
         if something in self.tags.keys():
             return self.tags[something]
         if something in self.remotes.keys():
-            return self.remotes[something]
+            return self.remotes[something][-1]
 
         for branch, commits in self.commits.items():
             if branch == something:
@@ -652,17 +676,9 @@
         return None
 
     def count(self, something):
-        if '..' not in something:
-            match = self.find(something)
-            return (match.branch_point or 0) + match.identifier
+        rev_list = self.rev_list(something)
+        return len(rev_list)
 
-        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
@@ -670,8 +686,9 @@
 
             for commit in commits:
                 if commit.hash.startswith(hash):
-                    found_identifier = max(commit.identifier, found_identifier)
-                    result.add(branch)
+                    if commit.identifier is not None:
+                        found_identifier = max(commit.identifier, found_identifier)
+                    result.add(commit.branch)
 
         if self.default_branch in result:
             for branch, commits in self.commits.items():
@@ -691,15 +708,11 @@
                     self.detached = something not in self.commits.keys()
                     return True
                 return False
-            if self.head.branch == self.default_branch:
-                self.commits[something] = [self.head]
-            else:
-                self.commits[something] = [
-                    commit for commit in self.commits[self.head.branch]
-                    if not commit.branch_point or commit.identifier <= self.head.identifier
-                ]
-            self.commits[something][-1] = Commit.from_json(Commit.Encoder().default(self.head))
+            self.commits[something] = [Commit.from_json(Commit.Encoder().default(self.head))]
+            # Copy one more to create a bridge commit
+            self.commits[something].append(Commit.from_json(Commit.Encoder().default(self.head)))
             self.head = self.commits[something][-1]
+            setattr(self.head, 'bridge_commit', True)
             self.head.branch = something
             if not self.head.branch_point:
                 self.head.branch_point = self.head.identifier
@@ -808,52 +821,6 @@
             stdout=stdout.getvalue(),
         )
 
-    def commits_in_range(self, begin, end):
-        begin = begin.replace('remotes/', '') if begin else begin
-        end = end.replace('remotes/', '') if end else end
-
-        if begin and self.remotes.get(begin):
-            begin = self.remotes.get(begin).hash
-        if end and self.remotes.get(end):
-            end = self.remotes.get(end).hash
-
-        branches = [self.default_branch]
-        if end in self.commits.keys() and end != self.default_branch:
-            branches.insert(0, end)
-        else:
-            for branch, commits in self.commits.items():
-                if branch == self.default_branch:
-                    continue
-                for commit in commits:
-                    if commit.hash.startswith(end):
-                        branches.insert(0, branch)
-                        break
-                if len(branches) > 1:
-                    break
-
-        in_range = False
-        previous = None
-        for branch in branches:
-            for commit in reversed(self.commits[branch]):
-                if branch == begin:
-                    break
-                if commit.hash.startswith(end) or end == branch:
-                    in_range = True
-                if in_range and (not previous or commit.hash != previous.hash):
-                    yield commit
-                previous = commit
-                if begin and commit.hash.startswith(begin):
-                    in_range = False
-                    break
-
-            in_range = False
-            if not previous or branch == self.default_branch:
-                continue
-
-            for commit in reversed(self.commits[self.default_branch]):
-                if previous.branch_point == commit.identifier:
-                    end = commit.hash
-
     @decorators.hybridmethod
     def config(context, path=None):
         if isinstance(context, type):
@@ -917,6 +884,9 @@
             return mocks.ProcessCompletion(returncode=1, stdout='no changes added to commit (use "git add" and/or "git commit -a")\n')
 
         if not amend:
+            # Remove the temp bridge commit
+            if hasattr(self.head, 'bridge_commit'):
+                self.commits[self.head.branch].remove(self.head)
             self.head = Commit(
                 branch=self.branch, repository_id=self.head.repository_id,
                 timestamp=int(time.time()),
@@ -926,10 +896,11 @@
             self.commits[self.branch].append(self.head)
 
         self.head.author = Contributor(self.config()['user.name'], [self.config()['user.email']])
-        self.head.message = '{}{}\nReviewed by Jonathan Bedard\n\n * {}\n'.format(
+        self.head.message = '{}{}\nReviewed by Jonathan Bedard\n\n * {}\n{}'.format(
             env.get('COMMIT_MESSAGE_TITLE', '') or '[Testing] {} commits'.format('Amending' if amend else 'Creating'),
             ('\n' + env.get('COMMIT_MESSAGE_BUG', '')) if env.get('COMMIT_MESSAGE_BUG', '') else '',
             '\n * '.join(self.staged.keys()),
+            env.get('COMMIT_MESSAGE_CONTENT', '')
         )
         self.head.hash = hashlib.sha256(string_utils.encode(self.head.message)).hexdigest()[:40]
         self.staged = {}
@@ -1003,16 +974,19 @@
             return mocks.ProcessCompletion(returncode=128, stdout="fatal: pathspec '{}' did not match any files\n".format(file))
         for key, value in self.modified.items():
             self.staged[key] = value
-        self.modified = {}
+        del self.modified[file]
         return mocks.ProcessCompletion(returncode=0)
 
     def rebase(self, target, base, head):
         if target not in self.commits or base not in self.commits or head not in self.commits:
             return mocks.ProcessCompletion(returncode=1)
-        for commit in self.commits[head]:
-            commit.branch_point = self.commits[target][-1].branch_point or self.commits[target][-1].identifier
-            if self.commits[target][-1].branch_point:
-                commit.identifier += self.commits[target][-1].identifier
+
+        base = self.commits[target][-1]
+        self.commits[head][0] = base
+        for commit in self.commits[head][1:]:
+            commit.branch_point = base.branch_point or base.identifier
+            if base.branch_point:
+                commit.identifier += base.identifier
         return mocks.ProcessCompletion(returncode=0)
 
     def pull(self, autostash=False):
@@ -1052,17 +1026,43 @@
         )
 
     def push(self, remote, branch):
-        self.remotes['{}/{}'.format(remote, branch)] = self.commits[branch][-1]
+        self.remotes['{}/{}'.format(remote, branch)] = self.commits[branch][:]
         return mocks.ProcessCompletion(returncode=0)
 
     def dcommit(self, remote='origin', branch=None):
         branch = branch or self.default_branch
-        self.remotes['{}/{}'.format(remote, branch)] = self.commits[branch][-1]
+        self.remotes['{}/{}'.format(remote, branch)] = self.commits[branch][:]
         return mocks.ProcessCompletion(
             returncode=0,
             stdout='Committed r{}\n\tM\tFiles/Changed.txt\n'.format(self.commits[branch][-1].revision),
         )
 
+    def reset_commit(self, something):
+        commit = self.find(something)
+        pre_branch = self.branch
+        rev_list = self.rev_list('HEAD...{}'.format(something))
+        commits = self.commits[self.branch]
+        if commit is not None:
+            self.head = commit
+        for commit in rev_list:
+            if hasattr(commit, '__mock__changeFiles'):
+                files = getattr(commit, '__mock__changeFiles')
+                for file in files:
+                    self.modified[file] = files[file]
+            commits.remove(commit)
+        if pre_branch != self.branch:
+            # Add a fake commit to simulate a same commit in different branch
+            bridge_commit = Commit(
+                hash=self.head.hash, revision=self.head.revision,
+                identifier=self.head.identifier, branch=pre_branch, branch_point=self.head.branch_point,
+                timestamp=self.head.timestamp, author=self.head.author, message=self.head.message,
+                order=self.head.order, repository_id=self.head.repository_id
+            )
+            setattr(bridge_commit, 'bridge_commit', True)
+            commits.append(bridge_commit)
+            self.head = commits[-1]
+        return mocks.ProcessCompletion(returncode=0)
+
     def reset(self, index):
         if index is None:
             self.modified = {}
@@ -1072,6 +1072,100 @@
         self.head = self.commits[self.head.branch][-(index + 1)]
         return mocks.ProcessCompletion(returncode=0)
 
+    def resolve_all_commits(self, branch, remote=None):
+        if not remote:
+            all_commits = self.commits[branch][:]
+        else:
+            all_commits = self.remotes['{}/{}'.format(remote, branch)][:]
+        last_commit = all_commits[0]
+        while last_commit.branch != branch:
+            head_index = None
+            if not remote:
+                commits_part = self.commits[last_commit.branch]
+            else:
+                commits_part = self.remotes['{}/{}'.format(remote, last_commit.branch)]
+            for i in range(len(commits_part)):
+                if commits_part[i].hash == last_commit.hash:
+                    head_index = i
+                    break
+            all_commits = commits_part[:head_index] + all_commits
+            last_commit = all_commits[0]
+            if last_commit.branch == self.default_branch and last_commit.identifier == 1:
+                break
+        if remote:
+            for commit in all_commits:
+                setattr(commit, '__mock__remotes', set([remote]))
+        return all_commits
+
+    def rev_list(self, something):
+        """
+        A..B = A u B - A
+        A...B = A u B - A n B
+        """
+        two_dots = False
+        triple_dots = False
+        a_commit = None
+        a_remote = None
+        b_commit = None
+        b_remote = None
+        if '...' in something:
+            something = something.split('...')
+            triple_dots = True
+            a_commit = self.find(something[0])
+            b_commit = self.find(something[1])
+            if something[0] in self.remotes:
+                a_remote = something[0].replace('/{}'.format(a_commit.branch), '')
+            if something[1] in self.remotes:
+                b_remote = something[1].replace('/{}'.format(b_commit.branch), '')
+        elif '..' in something:
+            something = something.split('..')
+            two_dots = True
+            a_commit = self.find(something[0])
+            b_commit = self.find(something[1])
+            if something[0] in self.remotes:
+                a_remote = something[0].replace('/{}'.format(a_commit.branch), '')
+            if something[1] in self.remotes:
+                b_remote = something[1].replace('/{}'.format(b_commit.branch), '')
+        else:
+            a_commit = self.find(something)
+            if something in self.remotes:
+                a_remote = something.replace('/{}'.format(a_commit.branch), '')
+
+        a_commits = []
+        a_branch_commits = self.resolve_all_commits(a_commit.branch, remote=a_remote) if a_commit else []
+        for commit in a_branch_commits:
+            a_commits.append(commit)
+            if commit.hash == a_commit.hash:
+                break
+
+        b_commits = []
+        b_branch_commits = self.resolve_all_commits(b_commit.branch, remote=b_remote) if b_commit else []
+        for commit in b_branch_commits:
+            b_commits.append(commit)
+            if commit.hash == b_commit.hash:
+                break
+
+        if not two_dots and not triple_dots:
+            return list(reversed(a_commits))
+
+        res = []
+        # To make things easier, we only mock that two branch will share same init commit
+        assert a_commits[0].hash == b_commits[0].hash
+        for i in range(max(len(a_commits), len(b_commits))):
+            if i >= len(a_commits) and i < len(b_commits):
+                res.append(b_commits[i])
+            elif i >= len(b_commits) and i < len(a_commits):
+                if triple_dots:
+                    res.append(a_commits[i])
+            elif i < len(b_commits) and i < len(a_commits) and a_commits[i].hash != b_commits[i].hash:
+                if triple_dots:
+                    res.append(a_commits[i])
+                    res.append(b_commits[i])
+                if two_dots:
+                    res.append(b_commits[i])
+        res.reverse()
+        return res
+
     def _install_git_lfs(self):
         self.has_git_lfs = True
         return mocks.ProcessCompletion(

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/bitbucket.py (295387 => 295388)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/bitbucket.py	2022-06-08 18:03:58 UTC (rev 295387)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/bitbucket.py	2022-06-08 18:13:57 UTC (rev 295388)
@@ -47,7 +47,17 @@
         with open(datafile or os.path.join(os.path.dirname(os.path.dirname(__file__)), 'git-repo.json')) as file:
             self.commits = json.load(file)
         for key, commits in self.commits.items():
-            self.commits[key] = [Commit(**kwargs) for kwargs in commits]
+            commit_objs = []
+            for kwargs in commits:
+                changeFiles = None
+                if 'changeFiles' in kwargs:
+                    changeFiles = kwargs['changeFiles']
+                    del kwargs['changeFiles']
+                commit = Commit(**kwargs)
+                if changeFiles:
+                    setattr(commit, '__mock__changeFiles', changeFiles)
+                commit_objs.append(commit)
+            self.commits[key] = commit_objs
             if not git_svn:
                 for commit in self.commits[key]:
                     commit.revision = None
@@ -56,6 +66,22 @@
         self.tags = {}
         self.pull_requests = []
 
+    def resolve_all_commits(self, branch):
+        all_commits = self.commits[branch][:]
+        last_commit = all_commits[0]
+        while last_commit.branch != branch:
+            head_index = None
+            commits_part = self.commits[last_commit.branch]
+            for i in range(len(commits_part)):
+                if commits_part[i].hash == last_commit.hash:
+                    head_index = i
+                    break
+            all_commits = commits_part[:head_index] + all_commits
+            last_commit = all_commits[0]
+            if last_commit.branch == self.default_branch and last_commit.identifier == 1:
+                break
+        return all_commits
+
     def commit(self, ref):
         if ref in self.commits:
             return self.commits[ref][-1]
@@ -75,11 +101,14 @@
             return None
         delta = int(delta)
 
-        if delta < commit.identifier:
-            return self.commits[commit.branch][commit.identifier - delta - 1]
-        delta -= commit.identifier
-        if commit.branch_point and delta < commit.branch_point:
-            return self.commits[self.default_branch][commit.branch_point - delta - 1]
+        all_commits = self.resolve_all_commits(commit.branch)
+        commit_index = 0
+        for i in range(len(all_commits)):
+            if all_commits[i].hash == commit.hash:
+                commit_index = i
+                break
+        if commit_index - delta >= 0:
+            return all_commits[commit_index - delta]
         return None
 
     def _branches_default(self, url):

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py (295387 => 295388)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py	2022-06-08 18:03:58 UTC (rev 295387)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py	2022-06-08 18:13:57 UTC (rev 295388)
@@ -49,7 +49,17 @@
         with open(datafile or os.path.join(os.path.dirname(os.path.dirname(__file__)), 'git-repo.json')) as file:
             self.commits = jsonlib.load(file)
         for key, commits in self.commits.items():
-            self.commits[key] = [Commit(**kwargs) for kwargs in commits]
+            commit_objs = []
+            for kwargs in commits:
+                changeFiles = None
+                if 'changeFiles' in kwargs:
+                    changeFiles = kwargs['changeFiles']
+                    del kwargs['changeFiles']
+                commit = Commit(**kwargs)
+                if changeFiles:
+                    setattr(commit, '__mock__changeFiles', changeFiles)
+                commit_objs.append(commit)
+            self.commits[key] = commit_objs
             if not git_svn:
                 for commit in self.commits[key]:
                     commit.revision = None
@@ -59,6 +69,22 @@
         self.pull_requests = []
         self.releases = releases or dict()
 
+    def resolve_all_commits(self, branch):
+        all_commits = self.commits[branch][:]
+        last_commit = all_commits[0]
+        while last_commit.branch != branch:
+            head_index = None
+            commits_part = self.commits[last_commit.branch]
+            for i in range(len(commits_part)):
+                if commits_part[i].hash == last_commit.hash:
+                    head_index = i
+                    break
+            all_commits = commits_part[:head_index] + all_commits
+            last_commit = all_commits[0]
+            if last_commit.branch == self.default_branch and last_commit.identifier == 1:
+                break
+        return all_commits
+
     def commit(self, ref):
         if ref in self.commits:
             return self.commits[ref][-1]
@@ -78,11 +104,15 @@
             return None
         delta = int(delta)
 
-        if delta < commit.identifier:
-            return self.commits[commit.branch][commit.identifier - delta - 1]
-        delta -= commit.identifier
-        if commit.branch_point and delta < commit.branch_point:
-            return self.commits[self.default_branch][commit.branch_point - delta - 1]
+        all_commits = self.resolve_all_commits(commit.branch)
+        commit_index = 0
+        for i in range(len(all_commits)):
+            if all_commits[i].hash == commit.hash:
+                commit_index = i
+                break
+
+        if commit_index - delta >= 0:
+            return all_commits[commit_index - delta]
         return None
 
     def _api_response(self, url):

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/__init__.py (295387 => 295388)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/__init__.py	2022-06-08 18:03:58 UTC (rev 295387)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/__init__.py	2022-06-08 18:13:57 UTC (rev 295388)
@@ -31,6 +31,7 @@
 from .clean import Clean, DeletePRBranches
 from .command import Command
 from .commit import Commit
+from .squash import Squash
 from .checkout import Checkout
 from .credentials import Credentials
 from .find import Find, Info
@@ -79,7 +80,7 @@
         Blame, Branch, Canonicalize, Checkout,
         Clean, Find, Info, Land, Log, Pull,
         PullRequest, Revert, Setup, InstallGitLFS,
-        Credentials, Commit, DeletePRBranches,
+        Credentials, Commit, DeletePRBranches, Squash
     ]
     if subversion:
         programs.append(SetupGitSvn)

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/pull_request.py (295387 => 295388)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/pull_request.py	2022-06-08 18:03:58 UTC (rev 295387)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/pull_request.py	2022-06-08 18:13:57 UTC (rev 295388)
@@ -26,6 +26,7 @@
 
 from .command import Command
 from .branch import Branch
+from .squash import Squash
 
 from webkitbugspy import Tracker
 from webkitcorepy import arguments, run, Terminal
@@ -41,6 +42,7 @@
     @classmethod
     def parser(cls, parser, loggers=None):
         Branch.parser(parser, loggers=loggers)
+        Squash.parser(parser, loggers=loggers)
         parser.add_argument(
             '--add', '--no-add',
             dest='will_add', default=None,
@@ -54,6 +56,12 @@
             action=""
         )
         parser.add_argument(
+            '--squash',
+            dest='squash', default=False,
+            action='',
+            help='Combine all commits on the current development branch into a single commit before pushing',
+        )
+        parser.add_argument(
             '--defaults', '--no-defaults', action="" default=None,
             help='Do not prompt the user for defaults, always use (or do not use) them',
         )
@@ -431,8 +439,11 @@
         if not branch_point:
             return 1
 
-        result = cls.create_commit(args, repository, **kwargs)
-        if result:
-            return result
+        if args.squash:
+            result = Squash.squash_commit(args, repository, branch_point, **kwargs)
+        else:
+            result = cls.create_commit(args, repository, **kwargs)
+            if result:
+                return result
 
         return cls.create_pull_request(repository, args, branch_point)

Added: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/squash.py (0 => 295388)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/squash.py	                        (rev 0)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/squash.py	2022-06-08 18:13:57 UTC (rev 295388)
@@ -0,0 +1,145 @@
+# 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 logging
+import re
+import sys
+import os
+import re
+from unittest import result
+
+from .command import Command
+from .branch import Branch
+
+from webkitcorepy import arguments, run, Terminal
+from webkitscmpy import local, log, remote
+from ..commit import Commit
+
+
+class Squash(Command):
+    name = 'squash'
+    help = 'Combine all commits on the current development branch into a single commit'
+
+    @classmethod
+    def parser(cls, parser, loggers=None):
+        group = parser.add_mutually_exclusive_group(required=False)
+        group.add_argument(
+            '--interactive',
+            dest='interactive',
+            default=False,
+            action='',
+            help='Use git rebase to interactivly to select what commits you want to squash.'
+        )
+
+        group.add_argument(
+            '--new-msg', '--new-message', '--no-sub-commit-message',
+            default=False,
+            dest='no_sub_commit_message',
+            action='',
+            help='Do not include messages of commits you want squash.'
+        )
+
+        parser.add_argument(
+            '--base-commit',
+            dest='base_commit',
+            help='git hash, svn revision or identifer for the base commit that you want to squash to (merged commit will not include this commit)',
+            default=None,
+        )
+
+    @classmethod
+    def get_commits_hashes(cls, repository, base_commit):
+        result = run([repository.executable(), 'rev-list', 'HEAD...{}'.format(base_commit.hash)], capture_output=True, cwd=repository.root_path)
+        if result.returncode:
+            sys.stderr.write(result.stderr)
+            sys.stderr.write('Failed to get all commits from HEAD to {}'.format(base_commit.hash))
+            return None
+        return result.stdout.decode('utf-8').strip().splitlines()
+
+    @classmethod
+    def undo_reset(cls, repository):
+        return run([repository.executable(), 'reset', "'HEAD@{1}'"], cwd=repository.root_path)
+
+    @classmethod
+    def squash_commit(cls, args, repository, branch_point, **kwargs):
+        # Make sure we have the commit that user want to revert
+        try:
+            if args.base_commit:
+                base_commit = repository.find(args.base_commit, include_log=True)
+                if hasattr(base_commit, 'identifier') and base_commit <= branch_point:
+                    sys.stderr.write('It seems you are trying to sqaush beyond branch point.')
+                    return 1
+            else:
+                base_commit = branch_point
+            # Check if there are any outstanding changes:
+            if repository.modified():
+                sys.stderr.write('Please commit your changes or stash them before you squash to base commit: {}'.format(base_commit))
+                return 1
+        except (local.Scm.Exception, ValueError) as exception:
+            # ValueErrors and Scm exceptions usually contain enough information to be displayed
+            # to the user as an error
+            sys.stderr.write('Could not find "{}"'.format(args.base_commit) + '\n')
+            return 1
+
+        if args.interactive:
+            result = run([repository.executable(), 'rebase', '-i'] + [base_commit.hash], cwd=repository.root_path)
+        else:
+            previous_history = ''
+            commit_hash_list = cls.get_commits_hashes(repository, base_commit)
+            if not commit_hash_list:
+                return 1
+            if not args.no_sub_commit_message:
+                commits = map(lambda hash: repository.find(hash, include_log=True), commit_hash_list)
+                previous_history += '\n\n'.join(map(lambda commit: commit.message, commits))
+            result = run([repository.executable(), 'reset'] + [base_commit.hash], cwd=repository.root_path)
+            if result.returncode:
+                sys.stderr.write('Failed to merge the diff.')
+                cls.undo_reset()
+            modified_files = repository.modified()
+            if not modified_files:
+                sys.stderr.write('Failed to detect any diff to merge.')
+                return 1
+            for file in modified_files:
+                if run([repository.executable(), 'add', file], cwd=repository.root_path).returncode:
+                    sys.stderr.write("Failed to add '{}'\n".format(file))
+                    return 1
+            env = os.environ
+            env['COMMIT_MESSAGE_CONTENT'] = '\n\nThis commit include:\n\n' + previous_history
+
+            result = run([repository.executable(), 'commit', '--date=now'], cwd=repository.root_path, env=env)
+            log.info('    Squashed {} commits'.format(len(commit_hash_list)))
+
+        if result.returncode:
+            sys.stderr.write('Failed to generate squashed commit\n')
+            return 1
+        return 0
+
+    @classmethod
+    def main(cls, args, repository, **kwargs):
+        if not isinstance(repository, local.Git):
+            sys.stderr.write("Can only '{}' on a native Git repository\n".format(cls.name))
+            return 1
+
+        branch_point = Branch.branch_point(repository)
+        if not branch_point:
+            return 1
+
+        return cls.squash_commit(args, repository, branch_point, **kwargs)

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/checkout_unittest.py (295387 => 295388)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/checkout_unittest.py	2022-06-08 18:03:58 UTC (rev 295387)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/checkout_unittest.py	2022-06-08 18:13:57 UTC (rev 295388)
@@ -129,13 +129,16 @@
                 reviews=[dict(user=dict(login='rreviewer'), state='CHANGES_REQUESTED')],
                 draft=False,
             )]
-            repo.commits['eng/example'] = [Commit(
-                hash='a5fe8afe9bf7d07158fcd9e9732ff02a712db2fd',
-                identifier='3.1@eng/example',
-                timestamp=int(time.time()) - 60,
-                author=Contributor('Tim Committer', ['tcommit...@webkit.org']),
-                message='To Be Committed\n\nReviewed by NOBODY (OOPS!).\n',
-            )]
+            repo.commits['eng/example'] = [
+                repo.commits[repo.default_branch][2],
+                Commit(
+                    hash='a5fe8afe9bf7d07158fcd9e9732ff02a712db2fd',
+                    identifier='3.1@eng/example',
+                    timestamp=int(time.time()) - 60,
+                    author=Contributor('Tim Committer', ['tcommit...@webkit.org']),
+                    message='To Be Committed\n\nReviewed by NOBODY (OOPS!).\n',
+                )
+            ]
 
             self.assertEqual(0, program.main(
                 args=('checkout', 'PR-1'),
@@ -180,13 +183,16 @@
                     ),
                 ],
             )]
-            repo.commits['eng/example'] = [Commit(
-                hash='a5fe8afe9bf7d07158fcd9e9732ff02a712db2fd',
-                identifier='3.1@eng/example',
-                timestamp=int(time.time()) - 60,
-                author=Contributor('Tim Committer', ['tcommit...@webkit.org']),
-                message='To Be Committed\n\nReviewed by NOBODY (OOPS!).\n',
-            )]
+            repo.commits['eng/example'] = [
+                repo.commits[repo.default_branch][2],
+                Commit(
+                    hash='a5fe8afe9bf7d07158fcd9e9732ff02a712db2fd',
+                    identifier='3.1@eng/example',
+                    timestamp=int(time.time()) - 60,
+                    author=Contributor('Tim Committer', ['tcommit...@webkit.org']),
+                    message='To Be Committed\n\nReviewed by NOBODY (OOPS!).\n',
+                )
+            ]
 
             self.assertEqual(0, program.main(
                 args=('checkout', 'PR-1'),

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/git_unittest.py (295387 => 295388)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/git_unittest.py	2022-06-08 18:03:58 UTC (rev 295387)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/git_unittest.py	2022-06-08 18:13:57 UTC (rev 295388)
@@ -79,7 +79,7 @@
         with mocks.local.Git(self.path):
             self.assertEqual(
                 local.Git(self.path).branches,
-                ['branch-a', 'branch-b', 'main'],
+                ['branch-a', 'branch-b', 'eng/squash-branch', 'main'],
             )
 
     def test_tags(self):
@@ -345,7 +345,7 @@
         with mocks.local.Git(self.path, git_svn=True):
             self.assertEqual(
                 run([
-                    local.Git.executable(), 'log', '--format=fuller', '--no-decorate', '--date=unix', 'branch-b...main',
+                    local.Git.executable(), 'log', '--format=fuller', '--no-decorate', '--date=unix', 'main..branch-b',
                 ], cwd=self.path, capture_output=True, encoding='utf-8').stdout,
                 '''commit 790725a6d79e28db2ecdde29548d2262c0bd059d
 Author:     Jonathan Bedard <jbed...@apple.com>
@@ -366,6 +366,15 @@
         Cherry pick
         git-svn-id: https://svn.webkit.org/repository/webkit/trunk@6 268f45cc-cd09-0410-ab3c-d52691b4dbfc
     git-svn-id: https://svn.example.org/repository/repository/trunk@5 268f45cc-cd09-0410-ab3c-d52691b4dbfc
+
+commit a30ce8494bf1ac2807a69844f726be4a9843ca55
+Author:     Jonathan Bedard <jbed...@apple.com>
+AuthorDate: {time_c}
+Commit:     Jonathan Bedard <jbed...@apple.com>
+CommitDate: {time_c}
+
+    3rd commit
+    git-svn-id: https://svn.example.org/repository/repository/trunk@3 268f45cc-cd09-0410-ab3c-d52691b4dbfc
 '''.format(
                 time_a=1601667000,
                 time_b=1601664000,
@@ -522,7 +531,7 @@
         with mocks.remote.GitHub():
             self.assertEqual(
                 remote.GitHub(self.remote).branches,
-                ['branch-a', 'branch-b', 'main'],
+                ['branch-a', 'branch-b', 'eng/squash-branch', 'main'],
             )
 
     def test_tags(self):
@@ -661,7 +670,6 @@
             ]), Commit.Encoder().default(list(git.commits(begin=dict(argument='9b8311f2'), end=dict(argument='621652ad')))))
 
     def test_commits_branch_ref(self):
-        self.maxDiff = None
         with mocks.remote.GitHub():
             git = remote.GitHub(self.remote)
             self.assertEqual(
@@ -683,7 +691,7 @@
         with mocks.remote.BitBucket():
             self.assertEqual(
                 remote.BitBucket(self.remote).branches,
-                ['branch-a', 'branch-b', 'main'],
+                ['branch-a', 'branch-b', 'eng/squash-branch', 'main'],
             )
 
     def test_tags(self):

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/land_unittest.py (295387 => 295388)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/land_unittest.py	2022-06-08 18:03:58 UTC (rev 295387)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/land_unittest.py	2022-06-08 18:13:57 UTC (rev 295388)
@@ -34,22 +34,25 @@
 def repository(path, has_oops=True, remote=None, git_svn=False, issue_url=None):
     branch = 'eng/example'
     result = mocks.local.Git(path, remote=remote, git_svn=git_svn)
-    result.commits[branch] = [Commit(
-        hash='a5fe8afe9bf7d07158fcd9e9732ff02a712db2fd',
-        identifier='3.1@{}'.format(branch),
-        revision=10,
-        timestamp=int(time.time()) - 60,
-        author=Contributor('Tim Committer', ['tcommit...@webkit.org']),
-        message='To Be Committed\n{}\nReviewed by {}.\n{}'.format(
-            '{}\n'.format(issue_url) if issue_url else '',
-            'NOBODY (OOPS!)' if has_oops else 'Ricky Reviewer',
-            '\ngit-svn-id: https://svn.{}/repository/{}/trunk@10 268f45cc-cd09-0410-ab3c-d52691b4dbfc\n'.format(
-                result.remote.split('@')[-1].split(':')[0],
-                os.path.basename(result.path),
-            ) if git_svn else '',
-        ),
-    )]
-    result.head = result.commits[branch][0]
+    result.commits[branch] = [
+        result.commits[result.default_branch][2],
+        Commit(
+            hash='a5fe8afe9bf7d07158fcd9e9732ff02a712db2fd',
+            identifier='3.1@{}'.format(branch),
+            revision=10,
+            timestamp=int(time.time()) - 60,
+            author=Contributor('Tim Committer', ['tcommit...@webkit.org']),
+            message='To Be Committed\n{}\nReviewed by {}.\n{}'.format(
+                '{}\n'.format(issue_url) if issue_url else '',
+                'NOBODY (OOPS!)' if has_oops else 'Ricky Reviewer',
+                '\ngit-svn-id: https://svn.{}/repository/{}/trunk@10 268f45cc-cd09-0410-ab3c-d52691b4dbfc\n'.format(
+                    result.remote.split('@')[-1].split(':')[0],
+                    os.path.basename(result.path),
+                ) if git_svn else '',
+            ),
+        )
+    ]
+    result.head = result.commits[branch][-1]
     return result
 
 

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/pull_request_unittest.py (295387 => 295388)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/pull_request_unittest.py	2022-06-08 18:03:58 UTC (rev 295387)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/pull_request_unittest.py	2022-06-08 18:13:57 UTC (rev 295388)
@@ -574,14 +574,17 @@
             remotes=dict(fork='https://{}/Contributor/WebKit'.format(remote.hosts[0])),
         ) as repo, mocks.local.Svn():
 
-            repo.commits['eng/pr-branch'] = [Commit(
-                hash='06de5d56554e693db72313f4ca1fb969c30b8ccb',
-                branch='eng/pr-branch',
-                author=dict(name='Tim Contributor', emails=['tcontribu...@example.com']),
-                identifier="5.1@eng/pr-branch",
-                timestamp=int(time.time()),
-                message='[Testing] Existing commit\nbugs.example.com/show_bug.cgi?id=1'
-            )]
+            repo.commits['eng/pr-branch'] = [
+                repo.commits[repo.default_branch][-1],
+                Commit(
+                    hash='06de5d56554e693db72313f4ca1fb969c30b8ccb',
+                    branch='eng/pr-branch',
+                    author=dict(name='Tim Contributor', emails=['tcontribu...@example.com']),
+                    identifier="5.1@eng/pr-branch",
+                    timestamp=int(time.time()),
+                    message='[Testing] Existing commit\nbugs.example.com/show_bug.cgi?id=1'
+                )
+            ]
             repo.head = repo.commits['eng/pr-branch'][-1]
             self.assertEqual(0, program.main(
                 args=('pull-request', '-v', '--no-history'),
@@ -698,14 +701,17 @@
         ) as repo, mocks.local.Svn():
 
             Tracker.instance().issue(1).close(why='Looks like we will not get to this')
-            repo.commits['eng/pr-branch'] = [Commit(
-                hash='06de5d56554e693db72313f4ca1fb969c30b8ccb',
-                branch='eng/pr-branch',
-                author=dict(name='Tim Contributor', emails=['tcontribu...@example.com']),
-                identifier="5.1@eng/pr-branch",
-                timestamp=int(time.time()),
-                message='[Testing] Existing commit\nbugs.example.com/show_bug.cgi?id=1'
-            )]
+            repo.commits['eng/pr-branch'] = [
+                repo.commits[repo.default_branch][-1],
+                Commit(
+                    hash='06de5d56554e693db72313f4ca1fb969c30b8ccb',
+                    branch='eng/pr-branch',
+                    author=dict(name='Tim Contributor', emails=['tcontribu...@example.com']),
+                    identifier="5.1@eng/pr-branch",
+                    timestamp=int(time.time()),
+                    message='[Testing] Existing commit\nbugs.example.com/show_bug.cgi?id=1'
+                )
+            ]
             repo.head = repo.commits['eng/pr-branch'][-1]
             self.assertEqual(0, program.main(
                 args=('pull-request', '-v', '--no-history'),
@@ -943,14 +949,17 @@
             self.path, remote='ssh://git@{}/{}/{}.git'.format(remote.hosts[0], remote.project.split('/')[1], remote.project.split('/')[3]),
         ) as repo, mocks.local.Svn(), Environment(RADAR_USERNAME='tcontributor'), bmocks.Radar(issues=bmocks.ISSUES), patch('webkitbugspy.Tracker._trackers', [radar.Tracker()]):
 
-            repo.commits['eng/pr-branch'] = [Commit(
-                hash='06de5d56554e693db72313f4ca1fb969c30b8ccb',
-                branch='eng/pr-branch',
-                author=dict(name='Tim Contributor', emails=['tcontribu...@example.com']),
-                identifier="5.1@eng/pr-branch",
-                timestamp=int(time.time()),
-                message='<rdar://problem/1> [Testing] Existing commit\n'
-            )]
+            repo.commits['eng/pr-branch'] = [
+                repo.commits[repo.default_branch][-1],
+                Commit(
+                    hash='06de5d56554e693db72313f4ca1fb969c30b8ccb',
+                    branch='eng/pr-branch',
+                    author=dict(name='Tim Contributor', emails=['tcontribu...@example.com']),
+                    identifier="5.1@eng/pr-branch",
+                    timestamp=int(time.time()),
+                    message='<rdar://problem/1> [Testing] Existing commit\n'
+                )
+            ]
             repo.head = repo.commits['eng/pr-branch'][-1]
             self.assertEqual(0, program.main(
                 args=('pull-request', '-v', '--no-history'),
@@ -992,14 +1001,17 @@
         ) as repo, mocks.local.Svn(), Environment(RADAR_USERNAME='tcontributor'), bmocks.Radar(issues=bmocks.ISSUES), patch('webkitbugspy.Tracker._trackers', [radar.Tracker()]):
 
             Tracker.instance().issue(1).close(why='Looks like we will not get to this')
-            repo.commits['eng/pr-branch'] = [Commit(
-                hash='06de5d56554e693db72313f4ca1fb969c30b8ccb',
-                branch='eng/pr-branch',
-                author=dict(name='Tim Contributor', emails=['tcontribu...@example.com']),
-                identifier="5.1@eng/pr-branch",
-                timestamp=int(time.time()),
-                message='<rdar://problem/1> [Testing] Existing commit\n'
-            )]
+            repo.commits['eng/pr-branch'] = [
+                repo.commits[repo.default_branch][-1],
+                Commit(
+                    hash='06de5d56554e693db72313f4ca1fb969c30b8ccb',
+                    branch='eng/pr-branch',
+                    author=dict(name='Tim Contributor', emails=['tcontribu...@example.com']),
+                    identifier="5.1@eng/pr-branch",
+                    timestamp=int(time.time()),
+                    message='<rdar://problem/1> [Testing] Existing commit\n'
+                )
+            ]
             repo.head = repo.commits['eng/pr-branch'][-1]
             self.assertEqual(0, program.main(
                 args=('pull-request', '-v', '--no-history'),

Added: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/squash_unittest.py (0 => 295388)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/squash_unittest.py	                        (rev 0)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/squash_unittest.py	2022-06-08 18:13:57 UTC (rev 295388)
@@ -0,0 +1,202 @@
+# 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 logging
+import os
+
+from mock import patch
+from webkitcorepy import OutputCapture, testing
+from webkitscmpy import local, program, mocks
+from webkitcorepy import run
+
+
+class TestSquash(testing.PathTestCase):
+
+    basepath = 'mock/repository'
+    BUGZILLA = 'https://bugs.example.com'
+
+    def setUp(self):
+        super(TestSquash, self).setUp()
+        os.mkdir(os.path.join(self.path, '.git'))
+        os.mkdir(os.path.join(self.path, '.svn'))
+
+    def test_github_with_previous_history(self):
+        with OutputCapture(level=logging.INFO) as captured, mocks.remote.GitHub() as remote, mocks.local.Git(
+            self.path, remote='https://{}'.format(remote.remote),
+            remotes=dict(fork='https://{}/Contributor/WebKit'.format(remote.hosts[0])),
+        ) as repo, mocks.local.Svn(), patch('webkitbugspy.Tracker._trackers', []):
+            repo.checkout('eng/squash-branch')
+            result = program.main(
+                args=('pull-request', '-v', '--squash', '--no-history'),
+                path=self.path,
+            )
+            self.assertEqual(0, result)
+            self.assertDictEqual(repo.modified, dict())
+            self.assertDictEqual(repo.staged, dict())
+            self.assertEqual(True, '[Testing] Creating commits' in repo.head.message)
+            self.assertEqual(True, '* a.cpp' in repo.head.message)
+            self.assertEqual(True, '* b.cpp' in repo.head.message)
+            self.assertEqual(True, '* c.cpp' in repo.head.message)
+            self.assertEqual(True, 'Changed something 1' in repo.head.message)
+            self.assertEqual(True, 'Changed something 2' in repo.head.message)
+            self.assertEqual(True, 'Changed something 3' in repo.head.message)
+            self.assertEqual(local.Git(self.path).remote().pull_requests.get(1).draft, False)
+
+        self.assertEqual(
+            captured.stdout.getvalue(),
+            "Created 'PR 1 | [Testing] Creating commits'!\n"
+            "https://github.example.com/WebKit/WebKit/pull/1\n",
+        )
+        self.assertEqual(captured.stderr.getvalue(), '')
+        log = captured.root.log.getvalue().splitlines()
+        self.assertEqual(
+            [line for line in log if 'Mock process' not in line], [
+                '    Found 1 commit...',
+                '    Found 2 commits...',
+                '    Found 3 commits...',
+                '    Squashed 3 commits',
+                "Rebasing 'eng/squash-branch' on 'main'...",
+                "Rebased 'eng/squash-branch' on 'main!'",
+                "    Found 1 commit...",
+                'Running pre-PR checks...',
+                'No pre-PR checks to run',
+                "Pushing 'eng/squash-branch' to 'fork'...",
+                "Syncing 'main' to remote 'fork'",
+                "Creating pull-request for 'eng/squash-branch'..."
+            ],
+        )
+
+    def test_github_without_previous_history(self):
+        with OutputCapture(level=logging.INFO) as captured, mocks.remote.GitHub() as remote, mocks.local.Git(
+            self.path, remote='https://{}'.format(remote.remote),
+            remotes=dict(fork='https://{}/Contributor/WebKit'.format(remote.hosts[0])),
+        ) as repo, mocks.local.Svn(), patch('webkitbugspy.Tracker._trackers', []):
+            repo.checkout('eng/squash-branch')
+            result = program.main(
+                args=('pull-request', '-v', '--squash', '--no-history', '--new-message'),
+                path=self.path,
+            )
+            self.assertEqual(0, result)
+            self.assertDictEqual(repo.modified, dict())
+            self.assertDictEqual(repo.staged, dict())
+            self.assertEqual(True, '[Testing] Creating commits' in repo.head.message)
+            self.assertEqual(True, '* a.cpp' in repo.head.message)
+            self.assertEqual(True, '* b.cpp' in repo.head.message)
+            self.assertEqual(True, '* c.cpp' in repo.head.message)
+            self.assertEqual(True, 'Changed something 1' not in repo.head.message)
+            self.assertEqual(True, 'Changed something 2' not in repo.head.message)
+            self.assertEqual(True, 'Changed something 3' not in repo.head.message)
+            self.assertEqual(local.Git(self.path).remote().pull_requests.get(1).draft, False)
+
+        self.assertEqual(
+            captured.stdout.getvalue(),
+            "Created 'PR 1 | [Testing] Creating commits'!\n"
+            "https://github.example.com/WebKit/WebKit/pull/1\n",
+        )
+        self.assertEqual(captured.stderr.getvalue(), '')
+        log = captured.root.log.getvalue().splitlines()
+        self.assertEqual(
+            [line for line in log if 'Mock process' not in line], [
+                '    Found 1 commit...',
+                '    Found 2 commits...',
+                '    Found 3 commits...',
+                '    Squashed 3 commits',
+                "Rebasing 'eng/squash-branch' on 'main'...",
+                "Rebased 'eng/squash-branch' on 'main!'",
+                "    Found 1 commit...",
+                'Running pre-PR checks...',
+                'No pre-PR checks to run',
+                "Pushing 'eng/squash-branch' to 'fork'...",
+                "Syncing 'main' to remote 'fork'",
+                "Creating pull-request for 'eng/squash-branch'..."
+            ],
+        )
+
+    def test_github_two_step(self):
+        with OutputCapture(level=logging.INFO) as captured, mocks.remote.GitHub() as remote, mocks.local.Git(
+            self.path, remote='https://{}'.format(remote.remote),
+            remotes=dict(fork='https://{}/Contributor/WebKit'.format(remote.hosts[0])),
+        ) as repo, mocks.local.Svn(), patch('webkitbugspy.Tracker._trackers', []):
+            repo.checkout('eng/squash-branch')
+            result = program.main(
+                args=('squash', '-v'),
+                path=self.path,
+            )
+            self.assertEqual(0, result)
+            self.assertDictEqual(repo.modified, dict())
+            self.assertDictEqual(repo.staged, dict())
+            self.assertEqual(True, '[Testing] Creating commits' in repo.head.message)
+            result = program.main(args=('pull-request', '-v', '--no-history'), path=self.path)
+            self.assertEqual(0, result)
+            self.assertEqual(local.Git(self.path).remote().pull_requests.get(1).draft, False)
+
+        self.assertEqual(
+            captured.stdout.getvalue(),
+            "Created 'PR 1 | [Testing] Creating commits'!\n"
+            "https://github.example.com/WebKit/WebKit/pull/1\n",
+        )
+        self.assertEqual(captured.stderr.getvalue(), '')
+        log = captured.root.log.getvalue().splitlines()
+        self.assertEqual(
+            [line for line in log if 'Mock process' not in line], [
+                '    Found 1 commit...',
+                '    Found 2 commits...',
+                '    Found 3 commits...',
+                '    Squashed 3 commits',
+                '    Found 1 commit...',
+                'Using committed changes...',
+                "Rebasing 'eng/squash-branch' on 'main'...",
+                "Rebased 'eng/squash-branch' on 'main!'",
+                "    Found 1 commit...",
+                'Running pre-PR checks...',
+                'No pre-PR checks to run',
+                "Pushing 'eng/squash-branch' to 'fork'...",
+                "Syncing 'main' to remote 'fork'",
+                "Creating pull-request for 'eng/squash-branch'..."
+            ],
+        )
+
+    def test_modified(self):
+        with OutputCapture(level=logging.INFO) as captured, mocks.remote.GitHub() as remote, \
+            mocks.local.Git(self.path, remote='https://{}'.format(remote.remote),) as repo, mocks.local.Svn(), \
+                patch('webkitbugspy.Tracker._trackers', []):
+
+            repo.modified = {
+                'a.py': """diff --git a/a.py b/a.py
+index 05e8751..0bf3c85 100644
+--- a/test
++++ b/test
+@@ -1,3 +1,4 @@
++1111
+ aaaa
+ cccc
+ bbbb
+"""
+            }
+            repo.checkout('eng/squash-branch')
+            result = program.main(
+                args=('squash', '-v'),
+                path=self.path,
+            )
+            self.assertEqual(1, result)
+
+        self.assertEqual(captured.stderr.getvalue(), 'Please commit your changes or stash them before you squash to base commit: d8bce26fa65c')
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to