public inbox for devel@edk2.groups.io
 help / color / mirror / Atom feed
From: "Nate DeSimone" <nathaniel.l.desimone@intel.com>
To: devel@edk2.groups.io
Cc: Ashley DeSimone <ashley.e.desimone@intel.com>,
	Puja Pandya <puja.pandya@intel.com>,
	Erik Bjorge <erik.c.bjorge@intel.com>,
	Bret Barkelew <Bret.Barkelew@microsoft.com>
Subject: [edk2-staging/EdkRepo] [PATCH V3 1/1] EdkRepo: Add squash command
Date: Thu, 12 Dec 2019 23:53:37 -0800	[thread overview]
Message-ID: <20191213075337.4416-2-nathaniel.l.desimone@intel.com> (raw)
In-Reply-To: <20191213075337.4416-1-nathaniel.l.desimone@intel.com>

Adds the squash command, which takes a range of commits
and compacts them into a single commit.

Cc: Ashley DeSimone <ashley.e.desimone@intel.com>
Cc: Puja Pandya <puja.pandya@intel.com>
Cc: Erik Bjorge <erik.c.bjorge@intel.com>
Cc: Bret Barkelew <Bret.Barkelew@microsoft.com>
Signed-off-by: Nate DeSimone <nathaniel.l.desimone@intel.com>
---
 edkrepo/commands/arguments/squash_args.py | 19 +++++
 edkrepo/commands/humble/squash_humble.py  | 19 +++++
 edkrepo/commands/squash_command.py        | 97 +++++++++++++++++++++++
 edkrepo/common/humble.py                  |  7 +-
 edkrepo/common/squash.py                  | 93 ++++++++++++++++++++++
 edkrepo/git_automation/__init__.py        |  8 ++
 edkrepo/git_automation/commit_msg.py      | 21 +++++
 edkrepo/git_automation/rebase_squash.py   | 23 ++++++
 setup.py                                  |  4 +-
 9 files changed, 289 insertions(+), 2 deletions(-)
 create mode 100644 edkrepo/commands/arguments/squash_args.py
 create mode 100644 edkrepo/commands/humble/squash_humble.py
 create mode 100644 edkrepo/commands/squash_command.py
 create mode 100644 edkrepo/common/squash.py
 create mode 100644 edkrepo/git_automation/__init__.py
 create mode 100644 edkrepo/git_automation/commit_msg.py
 create mode 100644 edkrepo/git_automation/rebase_squash.py

diff --git a/edkrepo/commands/arguments/squash_args.py b/edkrepo/commands/arguments/squash_args.py
new file mode 100644
index 0000000..db859ae
--- /dev/null
+++ b/edkrepo/commands/arguments/squash_args.py
@@ -0,0 +1,19 @@
+#!/usr/bin/env python3
+#
+## @file
+# squash_args.py
+#
+# Copyright (c) 2019, Intel Corporation. All rights reserved.<BR>
+# SPDX-License-Identifier: BSD-2-Clause-Patent
+#
+
+''' Contains the help and description strings for arguments in the
+squash command meta data.
+'''
+
+COMMAND_DESCRIPTION = 'Convert multiple commits in to a single commit'
+COMMIT_ISH_DESCRIPTION = 'A range of commits.'
+COMMIT_ISH_HELP = 'The range of commits to be squashed, specified using the same syntax as git rev-list.'
+NEW_BRANCH_DESCRIPTION = 'The name of the branch to be created'
+NEW_BRANCH_HELP = 'The single commit that is the result of the squash operation will be placed in to a new branch with this name.'
+ONELINE_HELP = 'Compact the commit messsages of the squashed commits down to one line'
diff --git a/edkrepo/commands/humble/squash_humble.py b/edkrepo/commands/humble/squash_humble.py
new file mode 100644
index 0000000..a7e3348
--- /dev/null
+++ b/edkrepo/commands/humble/squash_humble.py
@@ -0,0 +1,19 @@
+#!/usr/bin/env python3
+#
+## @file
+# squash_humble.py
+#
+# Copyright (c) 2019, Intel Corporation. All rights reserved.<BR>
+# SPDX-License-Identifier: BSD-2-Clause-Patent
+#
+
+'''
+Contains user visible strings printed by the squash command.
+'''
+
+#from colorama import Fore @todo
+#from colorama import Style
+
+BRANCH_EXISTS = 'A branch with the name {} already exists, a non-existant branch must be provided'
+MULTIPLE_COMMITS_REQUIRED = 'A range of commits is required, only a single commit was given'
+COMMIT_MESSAGE = 'Squash of the following commits:\n\n'
diff --git a/edkrepo/commands/squash_command.py b/edkrepo/commands/squash_command.py
new file mode 100644
index 0000000..5f74390
--- /dev/null
+++ b/edkrepo/commands/squash_command.py
@@ -0,0 +1,97 @@
+#!/usr/bin/env python3
+#
+## @file
+# squash_command.py
+#
+# Copyright (c) 2018 - 2019, Intel Corporation. All rights reserved.<BR>
+# SPDX-License-Identifier: BSD-2-Clause-Patent
+#
+
+import os
+
+from git import Repo
+
+from edkrepo.commands.edkrepo_command import EdkrepoCommand
+import edkrepo.commands.arguments.squash_args as arguments
+import edkrepo.commands.humble.squash_humble as humble
+from edkrepo.common.edkrepo_exception import EdkrepoInvalidParametersException, EdkrepoWorkspaceInvalidException
+from edkrepo.common.squash import get_git_repo_root, split_commit_range, get_start_and_end_commit
+from edkrepo.common.squash import commit_list_to_message, squash_commits
+
+class SquashCommand(EdkrepoCommand):
+    def __init__(self):
+        super().__init__()
+
+    def get_metadata(self):
+        metadata = {}
+        metadata['name'] = 'squash'
+        metadata['help-text'] = arguments.COMMAND_DESCRIPTION
+        args = []
+        metadata['arguments'] = args
+        args.append({'name' : 'commit-ish',
+                     'positional' : True,
+                     'position' : 0,
+                     'required': True,
+                     'description' : arguments.COMMIT_ISH_DESCRIPTION,
+                     'help-text' : arguments.COMMIT_ISH_HELP})
+        args.append({'name' : 'new-branch',
+                     'positional' : True,
+                     'position' : 1,
+                     'required': True,
+                     'description' : arguments.NEW_BRANCH_DESCRIPTION,
+                     'help-text' : arguments.NEW_BRANCH_HELP})
+        args.append({'name': 'oneline',
+                     'positional': False,
+                     'required': False,
+                     'help-text': arguments.ONELINE_HELP})
+        return metadata
+
+    def run_command(self, args, config):
+        commit_ish = vars(args)['commit-ish']
+        new_branch = vars(args)['new-branch']
+        one_line = vars(args)['oneline']
+        repo_path = get_git_repo_root()
+        #Initialize GitPython
+        repo = Repo(repo_path)
+        #Make sure the branch does not exist
+        if branch_name_exists(new_branch, repo):
+            raise EdkrepoInvalidParametersException(humble.BRANCH_EXISTS.format(new_branch))
+        single_commit = True
+        try:
+            repo.rev_parse(commit_ish)
+        except:
+            if len(split_commit_range(commit_ish)) <= 1:
+                raise EdkrepoInvalidParametersException(humble.MULTIPLE_COMMITS_REQUIRED)
+            single_commit = False
+        if single_commit:
+            raise EdkrepoInvalidParametersException(humble.MULTIPLE_COMMITS_REQUIRED)
+        (start_commit, end_commit) = get_start_and_end_commit(commit_ish, repo)
+        if one_line:
+            commit_message = humble.COMMIT_MESSAGE
+        else:
+            commit_message = ''
+        commit_message += get_squash_commit_message_list(repo, start_commit, end_commit, one_line)
+        if one_line:
+            commit_message += '\n'
+        original_branch = repo.heads[repo.active_branch.name]
+        try:
+            squash_commits(start_commit, end_commit, new_branch, commit_message, repo)
+        except:
+            if new_branch in repo.heads:
+                original_branch.checkout()
+                repo.git.branch('-D', new_branch)
+            raise
+        finally:
+            original_branch.checkout()
+
+def branch_name_exists(branch_name, repo):
+    if branch_name in [x.name for x in repo.heads]:
+        return True
+    else:
+        return False
+
+def get_commit_list(repo, start_commit, end_commit):
+    return repo.git.rev_list('{}..{}'.format(start_commit, end_commit)).split()
+
+def get_squash_commit_message_list(repo, start_commit, end_commit, one_line):
+    return commit_list_to_message(get_commit_list(repo, start_commit, end_commit), one_line, repo)
diff --git a/edkrepo/common/humble.py b/edkrepo/common/humble.py
index 11e853c..7f28d08 100644
--- a/edkrepo/common/humble.py
+++ b/edkrepo/common/humble.py
@@ -3,7 +3,7 @@
 ## @file
 # humble.py
 #
-# Copyright (c) 2017- 2019, Intel Corporation. All rights reserved.<BR>
+# Copyright (c) 2017 - 2019, Intel Corporation. All rights reserved.<BR>
 # SPDX-License-Identifier: BSD-2-Clause-Patent
 #
 
@@ -23,11 +23,13 @@ RESET_HEAD = 'use "git reset HEAD <file>..."'
 CHECKOUT_HEAD = 'use "git checkout HEAD <file>..."'
 CHECKOUT = 'use "git checkout -- <file>..."'
 ADD = 'use "git add <file>..."'
+COMMIT_NOT_FOUND = 'The commit {} does not exist.'
 BRANCH_BEHIND = 'Your branch \'{local_branch}\' is {behind_count} commit(s) behind \'{target_remote}/{target_branch}\' and should be rebased.'
 COMMAND_NOT_SUPPORTED_MAC_OS = ' is not supported on macOS.'
 COMMAND_NOT_SUPPORT_LINUX = ' is not supported on Linux.'
 KEYBOARD_INTERRUPT = '\n\nKeyboard Interrupt'
 SUBMODULE_FAILURE = 'Error while performing submodule initialization and clone operations for {} repo.\n'
+NOT_GIT_REPO = 'The current directory does not appear to be a git repository'
 MULTIPLE_SOURCE_ATTRIBUTES_SPECIFIED = 'BRANCH or TAG name present with COMMIT ID in combination field for {} repo. Using COMMIT ID.\n'
 TAG_AND_BRANCH_SPECIFIED = 'BRANCH AND TAG name present in combination field for {} repo. Using TAG.\n'
 CHECKING_CONNECTION = 'Checking connection to remote url: {}\n'
@@ -134,6 +136,9 @@ INCLUDED_INSTEAD_OF_LINE = '	insteadOf = {}\n'
 INCLUDED_FILE_NAME = '.gitconfig-{}'
 ERROR_WRITING_INCLUDE = 'An error occured while writting the URL redirection configuration for {} repo.\n'
 
+#Error messages for squash.py
+SQUASH_COMMON_ANCESTOR_REQUIRED = '{} is not in the same branch history as {}, unable to operate on this commit range.'
+
 # Messages for common_repo_functions.verify_manifest_data()
 VERIFY_GLOBAL = 'Verifying the active projects in the global manifest repository\n'
 VERIFY_ARCHIVED = 'Verifying the archived projects in the global manifest repository\n'
diff --git a/edkrepo/common/squash.py b/edkrepo/common/squash.py
new file mode 100644
index 0000000..df37c5e
--- /dev/null
+++ b/edkrepo/common/squash.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python3
+#
+## @file
+# squash.py
+#
+# Copyright (c) 2018 - 2019, Intel Corporation. All rights reserved.<BR>
+# SPDX-License-Identifier: BSD-2-Clause-Patent
+#
+
+import os
+import sys
+from subprocess import check_call
+
+from edkrepo.common.edkrepo_exception import EdkrepoInvalidParametersException, EdkrepoWorkspaceInvalidException
+from edkrepo.common.humble import COMMIT_NOT_FOUND, NOT_GIT_REPO, SQUASH_COMMON_ANCESTOR_REQUIRED
+
+def get_git_repo_root():
+    path = os.path.realpath(os.getcwd())
+    while True:
+        if os.path.isdir(os.path.join(path, '.git')):
+            return path
+        if os.path.dirname(path) == path:
+            break
+        path = os.path.dirname(path)
+    raise EdkrepoWorkspaceInvalidException(NOT_GIT_REPO)
+
+def split_commit_range(commit_range):
+    if len(commit_range.split('...')) == 2:
+        return commit_range.split('...')
+    else:
+        return commit_range.split('..')
+
+def get_start_and_end_commit(commit_ish, repo):
+    #Determine the start and end commit for the upcoming squash
+    (commit1, commit2) = split_commit_range(commit_ish)
+    try:
+        repo.rev_parse(commit1)
+    except:
+        raise EdkrepoInvalidParametersException(COMMIT_NOT_FOUND.format(commit1))
+    try:
+        repo.rev_parse(commit2)
+    except:
+        raise EdkrepoInvalidParametersException(COMMIT_NOT_FOUND.format(commit2))
+    if len(repo.git.rev_list(commit_ish).split()) <= 0:
+        raise EdkrepoInvalidParametersException(SQUASH_COMMON_ANCESTOR_REQUIRED.format(commit1, commit2))
+    if str(repo.commit(commit1)) not in repo.git.rev_list(commit2).split():
+        temp_commit = commit1
+        commit1 = commit2
+        commit2 = temp_commit
+        if str(repo.commit(commit1)) not in repo.git.rev_list(commit2).split():
+            raise EdkrepoInvalidParametersException(SQUASH_COMMON_ANCESTOR_REQUIRED.format(commit1, commit2))
+    start_commit = get_oldest_ancestor(commit1, commit2, repo)
+    end_commit = str(repo.commit(commit2))
+    return (start_commit, end_commit)
+
+def get_oldest_ancestor(commit1, commit2, repo):
+    a = repo.git.rev_list('--first-parent', commit1).split()
+    b = repo.git.rev_list('--first-parent', commit2).split()
+    for commit in a:
+        if commit in b:
+            return commit
+    return None
+
+def commit_list_to_message(commit_list, one_line, repo):
+    message_list = []
+    for commit in commit_list:
+        if one_line:
+            message_list.append('{}: {}'.format(commit, repo.commit(commit).message.split('\n')[0]))
+        else:
+            for line in repo.commit(commit).message.split('\n'):
+                message_list.append(line)
+    return '\n'.join(message_list)
+
+def squash_commits(start_commit, end_commit, branch_name, commit_message, repo, reset_author=True):
+    #Create a branch at the latest commit
+    local_branch = repo.create_head(branch_name, end_commit)
+    repo.heads[local_branch.name].checkout()
+    #Set up environment variables for automating the interactive rebase
+    git_automation_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'git_automation')
+    if sys.platform == "win32":
+        os.environ['GIT_SEQUENCE_EDITOR'] = 'py "{}"'.format(os.path.join(git_automation_dir, "rebase_squash.py"))
+        os.environ['GIT_EDITOR'] = 'py "{}"'.format(os.path.join(git_automation_dir, "commit_msg.py"))
+    else:
+        os.environ['GIT_SEQUENCE_EDITOR'] = os.path.join(git_automation_dir, "rebase_squash.py")
+        os.environ['GIT_EDITOR'] = os.path.join(git_automation_dir, "commit_msg.py")
+    os.environ['COMMIT_MESSAGE'] = commit_message
+    #Do an interactive rebase back to the oldest commit, squashing all commits between then and now
+    check_call(['git', 'rebase', '-i', '{}'.format(start_commit)])
+    if reset_author:
+        repo.git.commit('--amend', '--no-edit', '--reset-author', '--signoff')
+    del os.environ['GIT_SEQUENCE_EDITOR']
+    del os.environ['GIT_EDITOR']
+    del os.environ['COMMIT_MESSAGE']
diff --git a/edkrepo/git_automation/__init__.py b/edkrepo/git_automation/__init__.py
new file mode 100644
index 0000000..c810c1f
--- /dev/null
+++ b/edkrepo/git_automation/__init__.py
@@ -0,0 +1,8 @@
+#!/usr/bin/env python3
+#
+## @file
+# __init__.py
+#
+# Copyright (c) 2018 - 2019, Intel Corporation. All rights reserved.<BR>
+# SPDX-License-Identifier: BSD-2-Clause-Patent
+#
\ No newline at end of file
diff --git a/edkrepo/git_automation/commit_msg.py b/edkrepo/git_automation/commit_msg.py
new file mode 100644
index 0000000..228a8ab
--- /dev/null
+++ b/edkrepo/git_automation/commit_msg.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python3
+#
+## @file
+# commit_msg.py
+#
+# Copyright (c) 2018 - 2019, Intel Corporation. All rights reserved.<BR>
+# SPDX-License-Identifier: BSD-2-Clause-Patent
+#
+
+import sys
+import os
+def main():
+    if 'COMMIT_MESSAGE_NO_EDIT' in os.environ:
+        return
+    with open(sys.argv[1], 'w', encoding="utf-8", errors="backslashreplace") as f:
+        if 'COMMIT_MESSAGE' in os.environ:
+            f.write(os.environ['COMMIT_MESSAGE'])
+        else:
+            f.write("Cherry Pick\n")
+if __name__ == "__main__":
+    main()
diff --git a/edkrepo/git_automation/rebase_squash.py b/edkrepo/git_automation/rebase_squash.py
new file mode 100644
index 0000000..26abbbc
--- /dev/null
+++ b/edkrepo/git_automation/rebase_squash.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python3
+#
+## @file
+# rebase_squash.py
+#
+# Copyright (c) 2018 - 2019, Intel Corporation. All rights reserved.<BR>
+# SPDX-License-Identifier: BSD-2-Clause-Patent
+#
+
+import sys
+import re
+def main():
+    with open(sys.argv[1], 'r', errors="surrogateescape") as f:
+        lines = f.readlines()
+    r = re.compile("^pick(.*)$")
+    with open(sys.argv[1], 'w', encoding="utf-8", errors="backslashreplace") as f:
+        f.write(lines[0])
+        for i in range(1,len(lines)):
+            m = r.match(lines[i])
+            if m: f.write("s {}\n".format(m.group(1)))
+            else: f.write(lines[i])
+if __name__ == "__main__":
+    main()
diff --git a/setup.py b/setup.py
index 1b9edad..e14aed1 100755
--- a/setup.py
+++ b/setup.py
@@ -12,7 +12,9 @@ from setuptools import setup
 setup(name='edkrepo',
       version='2.0.0',
       description='The edkrepo tools',
-      packages=['edkrepo', 'edkrepo.commands', 'edkrepo.commands.arguments', 'edkrepo.commands.humble', 'edkrepo.common', 'edkrepo.config', 'edkrepo_manifest_parser', 'project_utils'],
+      packages=['edkrepo', 'edkrepo.commands', 'edkrepo.commands.arguments', 'edkrepo.commands.humble',
+                'edkrepo.git_automation', 'edkrepo.common', 'edkrepo.config', 'edkrepo_manifest_parser',
+                'project_utils'],
       package_data={
          },
       include_package_data=True,
-- 
2.24.0.windows.2


  reply	other threads:[~2019-12-13  7:53 UTC|newest]

Thread overview: 4+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2019-12-13  7:53 [edk2-staging/EdkRepo] [PATCH V3 0/1] EdkRepo: Add squash command Nate DeSimone
2019-12-13  7:53 ` Nate DeSimone [this message]
2019-12-13 22:09   ` [edk2-staging/EdkRepo] [PATCH V3 1/1] " Desimone, Ashley E
2019-12-13 22:09 ` [edk2-staging/EdkRepo] [PATCH V3 0/1] " Desimone, Ashley E

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-list from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20191213075337.4416-2-nathaniel.l.desimone@intel.com \
    --to=devel@edk2.groups.io \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox