Apply changes using custom rebase editor
Create a custom rebase-editor script that can be used to alter the contents of the rebase 'todo' file by being used as the editor. Use a wrapper object that sets and removes the environmental variables required for to enable use of the custom editor with git-rebase. Add an interactive mode to the import-upstream subcommand to allow the user to request that an editor be spawned after the rebase instructions file has been modified. Include a special debug interactive mode, where an editor will be launched by the script of the final todo instructions file just before it is pared by rebase. Should rely on the main editor launch in general to avoid not being able to process the stdout/stderr result from 'git rebase', because the debug mode means that we cannot capture I/O without hanging most editors. Change-Id: Idf5670abe7b9aca23d94cd52e9a7327bd575814d JIRA: CICD-733
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
from ghp.errors import HpgitError
|
||||
from ghp.log import LogDedentMixin
|
||||
from ghp.lib.utils import GitMixin
|
||||
from ghp.lib.rebaseeditor import RebaseEditor
|
||||
from ghp import subcommand, log
|
||||
from ghp.lib.searchers import UpstreamMergeBaseSearcher
|
||||
|
||||
@@ -206,7 +207,7 @@ class ImportUpstream(LogDedentMixin, GitMixin):
|
||||
self.git.checkout(base)
|
||||
self.git.merge(*self.extra_branches)
|
||||
|
||||
def start(self, strategy):
|
||||
def apply(self, strategy, interactive=False):
|
||||
"""Apply list of commits given onto latest import of upstream"""
|
||||
|
||||
self.log.debug(
|
||||
@@ -214,7 +215,41 @@ class ImportUpstream(LogDedentMixin, GitMixin):
|
||||
Should apply the following list of commits
|
||||
%s
|
||||
""", "\n ".join([c.id for c in strategy.filtered_iter()]))
|
||||
raise NotImplementedError("start() is not yet implemented")
|
||||
|
||||
base = self.import_branch + "-base"
|
||||
|
||||
first = next(strategy.filtered_iter())
|
||||
self._set_branch(self.import_branch, self.branch, force=True)
|
||||
|
||||
rebase = RebaseEditor(interactive, repo=self.repo)
|
||||
|
||||
self.log.info(
|
||||
"""\
|
||||
Rebase changes, dropping merges through editor:
|
||||
git rebase --onto %s \\
|
||||
%s %s
|
||||
""", base, first.parents[0].id, self.import_branch)
|
||||
status, out, err = rebase.run(strategy.filtered_iter(),
|
||||
first.parents[0].id, self.import_branch,
|
||||
onto=base)
|
||||
if status:
|
||||
if err and err.startswith("Nothing to do"):
|
||||
# cancelled by user
|
||||
self.log.notice("Cancelled by user")
|
||||
return
|
||||
|
||||
self.log.error("Rebase failed, will need user intervention to "
|
||||
"resolve.")
|
||||
if out:
|
||||
self.log.notice(out)
|
||||
if err:
|
||||
self.log.notice(err)
|
||||
|
||||
# once we support resuming/finishing add a message here to tell the
|
||||
# user to rerun this tool with the appropriate options to complete
|
||||
return
|
||||
|
||||
self.log.notice("Successfully applied all locally carried changes")
|
||||
|
||||
def resume(self, args):
|
||||
"""Resume previous partial import"""
|
||||
@@ -313,6 +348,8 @@ class LocateChangesWalk(LocateChangesStrategy):
|
||||
self.filters.append(ReverseCommitFilter())
|
||||
|
||||
|
||||
@subcommand.arg('-i', '--interactive', action='store_true', default=False,
|
||||
help='Let the user edit the list of commits before applying.')
|
||||
@subcommand.arg('-f', '--force', dest='force', required=False, action='store_true',
|
||||
default=False,
|
||||
help='Force overwrite of existing import branch if it exists.')
|
||||
@@ -382,7 +419,7 @@ def do_import_upstream(args):
|
||||
importupstream.create_import(force=args.force)
|
||||
logger.notice("Successfully created import branch")
|
||||
|
||||
importupstream.start(strategy)
|
||||
importupstream.apply(strategy, args.interactive)
|
||||
|
||||
|
||||
|
||||
|
||||
187
ghp/lib/rebaseeditor.py
Normal file
187
ghp/lib/rebaseeditor.py
Normal file
@@ -0,0 +1,187 @@
|
||||
#
|
||||
# Copyright (c) 2012, 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Confidential computer software. Valid license from HP required for
|
||||
# possession, use or copying. Consistent with FAR 12.211 and 12.212,
|
||||
# Commercial Computer Software, Computer Software Documentation, and
|
||||
# Technical Data for Commercial Items are licensed to the U.S. Government
|
||||
# under vendor's standard commercial license.
|
||||
#
|
||||
|
||||
from ghp.lib.utils import GitMixin
|
||||
from ghp.log import LogDedentMixin
|
||||
import ghp
|
||||
|
||||
from subprocess import call
|
||||
import os
|
||||
|
||||
REBASE_EDITOR_SCRIPT = os.path.abspath(
|
||||
os.path.join(os.path.dirname(ghp.__file__), "scripts", "rebase-editor.py"))
|
||||
|
||||
# insure name of file will match any naming filters used by editors to
|
||||
# enable syntax highlighting
|
||||
REBASE_EDITOR_TODO = "hpgit/git-rebase-todo"
|
||||
|
||||
TODO_EPILOGUE = """\
|
||||
|
||||
# Rebase %(shortrevisions)s onto %(shortonto)s
|
||||
#
|
||||
# All commands from normal rebase instructions files are supported
|
||||
#
|
||||
# If you remove a line, that commit will be dropped.
|
||||
# Removing all commits will abort the rebase.
|
||||
#
|
||||
"""
|
||||
|
||||
|
||||
class RebaseEditor(GitMixin, LogDedentMixin):
|
||||
|
||||
def __init__(self, interactive=False, *args, **kwargs):
|
||||
|
||||
self._interactive = interactive
|
||||
|
||||
super(RebaseEditor, self).__init__()
|
||||
|
||||
self._editor = REBASE_EDITOR_SCRIPT
|
||||
# interactive switch here determines if the script that is given
|
||||
# to git-rebase to run as it's editor, will in turn exec an editor
|
||||
# for the user to look through the instructions before rebase
|
||||
# applies them
|
||||
if interactive == 'debug':
|
||||
self.log.debug("Enabling interactive mode for rebase")
|
||||
self._editor = "%s --interactive" % self.editor
|
||||
|
||||
@property
|
||||
def editor(self):
|
||||
return self._editor
|
||||
|
||||
def _write_todo(self, commits, *args, **kwargs):
|
||||
todo_file = os.path.join(self.repo.path, REBASE_EDITOR_TODO)
|
||||
if os.path.exists(todo_file):
|
||||
os.remove(todo_file)
|
||||
|
||||
if not os.path.exists(os.path.dirname(todo_file)):
|
||||
os.mkdir(os.path.dirname(todo_file))
|
||||
|
||||
# see if onto is set in the args or kwargs
|
||||
onto = kwargs.get('onto', None)
|
||||
for idx, arg in enumerate(args):
|
||||
if arg.startswith("--onto"):
|
||||
# either onto is after the option in this arg, or it's the
|
||||
# next arg, or not providing it is an exception
|
||||
onto = arg[7:] or args[idx + 1]
|
||||
break
|
||||
|
||||
root = None
|
||||
with open(todo_file, "w") as todo:
|
||||
for commit in commits:
|
||||
if not root:
|
||||
root = commit.parents[0].id
|
||||
subject = commit.message.splitlines()[0]
|
||||
todo.write("pick %s %s\n" % (commit.id[:7], subject))
|
||||
|
||||
# if root isn't set at this point, then there were no commits
|
||||
if not root:
|
||||
todo.write("noop\n")
|
||||
|
||||
todo.write(TODO_EPILOGUE %
|
||||
{'shortrevisions': self._short_revisions(root,
|
||||
commit.id),
|
||||
'shortonto': self._short_onto(onto or root)})
|
||||
|
||||
return todo_file
|
||||
|
||||
def _short_revisions(self, root, commit):
|
||||
|
||||
if not root:
|
||||
return "<none>"
|
||||
|
||||
return "%s..%s" % (root[:7], commit[:7])
|
||||
|
||||
def _short_onto(self, onto):
|
||||
|
||||
if not onto:
|
||||
return "<none>"
|
||||
|
||||
return self.git.rev_parse(onto)[:7]
|
||||
|
||||
def _set_editor(self, editor):
|
||||
|
||||
if self.git_sequence_editor:
|
||||
self._saveeditor = self.git_sequence_editor
|
||||
if self._interactive == 'debug':
|
||||
os.environ['HPGIT_GIT_SEQUENCE_EDITOR'] = self._saveeditor
|
||||
os.environ['GIT_SEQUENCE_EDITOR'] = editor
|
||||
else:
|
||||
self._saveeditor = self.git_editor
|
||||
if self._interactive == 'debug':
|
||||
os.environ['HPGIT_GIT_EDITOR'] = self._saveeditor
|
||||
os.environ['GIT_EDITOR'] = editor
|
||||
|
||||
def _unset_editor(self):
|
||||
|
||||
for var in ['GIT_SEQUENCE_EDITOR', 'GIT_EDITOR']:
|
||||
# HPGIT_* variations should only be set if script was in a debug
|
||||
# mode.
|
||||
if os.environ.get('HPGIT_' + var, None):
|
||||
del os.environ['HPGIT_' + var]
|
||||
# Restore previous editor only if the environment var is set. This
|
||||
# isn't perfect since we should probably unset the env var if it
|
||||
# wasn't previously set, but this shouldn't cause any problems.
|
||||
if os.environ.get(var, None):
|
||||
os.environ[var] = self._saveeditor
|
||||
break
|
||||
|
||||
def run(self, commits, *args, **kwargs):
|
||||
"""
|
||||
Reads the list of commits given, and constructions the instuctions
|
||||
file to be used by rebase.
|
||||
Will spawn an editor if the constructor was told to be interactive.
|
||||
Additional arguments *args and **kwargs are to be passed to 'git
|
||||
rebase'.
|
||||
"""
|
||||
|
||||
todo_file = self._write_todo(commits, *args, **kwargs)
|
||||
if self._interactive:
|
||||
# spawn the editor
|
||||
user_editor = self.git_sequence_editor or self.git_editor
|
||||
status = call([user_editor, todo_file])
|
||||
if status:
|
||||
return (status, None, "Editor returned non-zero exit code")
|
||||
|
||||
editor = "%s %s" % (self.editor, todo_file)
|
||||
self._set_editor(editor)
|
||||
|
||||
try:
|
||||
if self._interactive == 'debug':
|
||||
# In general it's not recommended to run rebase in direct
|
||||
# interactive mode because it's not possible to capture the
|
||||
# stdout/stderr, but sometimes it's useful to allow it for
|
||||
# debugging to check the final result.
|
||||
#
|
||||
# It is not safe to redirect I/O channels as most editors will
|
||||
# be expecting that I/O is from/to proper terminal. YMMV
|
||||
cmd = ['git', 'rebase', '--interactive']
|
||||
cmd.extend(self.git.transform_kwargs(**kwargs))
|
||||
cmd.extend(args)
|
||||
return (call(cmd), None, None)
|
||||
else:
|
||||
return self.git.rebase(interactive=True, with_exceptions=False,
|
||||
with_extended_output=True, *args, **kwargs)
|
||||
finally:
|
||||
os.remove(todo_file)
|
||||
# make sure to remove the environment tweaks added so as not to
|
||||
# impact any subsequent use of git commands using editors
|
||||
self._unset_editor()
|
||||
|
||||
@property
|
||||
def git_sequence_editor(self):
|
||||
|
||||
return os.environ.get('GIT_SEQUENCE_EDITOR',
|
||||
self.git.config("sequence.editor",
|
||||
with_exceptions=False))
|
||||
|
||||
@property
|
||||
def git_editor(self):
|
||||
|
||||
return os.environ.get("GIT_EDITOR", self.git.var("GIT_EDITOR"))
|
||||
115
ghp/scripts/rebase-editor.py
Executable file
115
ghp/scripts/rebase-editor.py
Executable file
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (c) 2012, 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Confidential computer software. Valid license from HP required for
|
||||
# possession, use or copying. Consistent with FAR 12.211 and 12.212,
|
||||
# Commercial Computer Software, Computer Software Documentation, and
|
||||
# Technical Data for Commercial Items are licensed to the U.S. Government
|
||||
# under vendor's standard commercial license.
|
||||
#
|
||||
|
||||
"""
|
||||
Command line editor for modifying git rebase instructions file through use
|
||||
of the interactive mode. Will in turn launch an editor in turn if the user
|
||||
wished to use the interactive mode of git-rebase.
|
||||
|
||||
Script will replace all occurances of 'pick' or any other instruction entry
|
||||
with a list of instructions read from the given input file.
|
||||
|
||||
Avoid use of stdin for passing such information as many editors have problems
|
||||
if exec'ed and stdin is a pipe.
|
||||
"""
|
||||
|
||||
from argparse import ArgumentParser
|
||||
import fileinput
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def rebase_replace_insn(path, istream):
|
||||
"""
|
||||
Function replaces the current instructions listed in the rebase
|
||||
instructions (insn) file with those read from the given istream.
|
||||
"""
|
||||
echo_out = False
|
||||
for line in fileinput.input(path, inplace=True):
|
||||
stripped = line.strip()
|
||||
# first blank line indicates end of rebase instructions
|
||||
if not stripped:
|
||||
if not echo_out:
|
||||
while True:
|
||||
replacement = istream.readline().strip()
|
||||
if not replacement:
|
||||
break
|
||||
if not replacement.startswith("#"):
|
||||
print replacement
|
||||
print ""
|
||||
echo_out = True
|
||||
continue
|
||||
if echo_out:
|
||||
print stripped
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = ArgumentParser(
|
||||
description=__doc__.strip(),
|
||||
)
|
||||
parser.add_argument('-v', '--verbose', action='store_true', default=False,
|
||||
help='Enable verbose mode')
|
||||
parser.add_argument('-i', '--interactive', action='store_true',
|
||||
help='Enable interactive mode, where the user can edit '
|
||||
'the list of commits before being applied')
|
||||
parser.add_argument('ifile', metavar='<new-list>',
|
||||
help='File containing the new list of instructions to '
|
||||
'be placed into the rebase instructions file.')
|
||||
parser.add_argument('extra_args', metavar='<args>', nargs='?', default=[],
|
||||
help='Additional arguments to be passed to the '
|
||||
'subsequent editor')
|
||||
parser.add_argument('ofile', metavar='<todo-list>',
|
||||
help='Filename containing the list of instructions to'
|
||||
'be edited.')
|
||||
|
||||
args = parser.parse_args()
|
||||
VERBOSE = args.verbose
|
||||
|
||||
# don't attempt to use stdin to pass the information between the parent
|
||||
# process through 'git-rebase' and this script, as many editors will
|
||||
# have problems if stdin is a pipe.
|
||||
if VERBOSE:
|
||||
print "rebase-editor: Replacing contents of rebase instructions file"
|
||||
rebase_replace_insn(args.ofile, open(args.ifile, 'r'))
|
||||
|
||||
# if interactive mode, attempt to exec the editor defined by the user
|
||||
# for use with git
|
||||
if not args.interactive:
|
||||
if VERBOSE:
|
||||
print "rebase-editor: Interactive mode not enabled"
|
||||
sys.exit(0)
|
||||
|
||||
# calling code should only override one of the two editor variables,
|
||||
# starting with the one with the highest precedence
|
||||
env = os.environ
|
||||
for var in ['GIT_SEQUENCE_EDITOR', 'GIT_EDITOR']:
|
||||
editor = env.get('HPGIT_' + var, None)
|
||||
if editor:
|
||||
del env['HPGIT_' + var]
|
||||
env[var] = editor
|
||||
break
|
||||
|
||||
if editor:
|
||||
editor_args = [editor]
|
||||
editor_args.extend(args.extra_args)
|
||||
editor_args.append(args.ofile)
|
||||
sys.stdin.flush()
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
#os.dup2(sys.stdin.fileno(), 0)
|
||||
#os.dup2(sys.stdout.fileno(), 1)
|
||||
#os.dup2(sys.stderr.fileno(), 2)
|
||||
os.execvpe(editor, editor_args, env=env)
|
||||
|
||||
sys.stderr.write("rebase-editor: No git EDITOR variables defined in "
|
||||
"environment to call as requested by the "
|
||||
"--interactive option.\n")
|
||||
sys.exit(2)
|
||||
Reference in New Issue
Block a user