Merge "Fix broken interactive mode to be usable"
This commit is contained in:
@@ -114,7 +114,9 @@ class ImportCommand(LogDedentMixin, GitUpstreamCommand):
|
||||
"""Perform additional parsing of args"""
|
||||
|
||||
if self.args.finish and not self.args.upstream_branch:
|
||||
self.args.upstream_branch = self.args.import_branch
|
||||
self.args.real_upstream_branch = self.args.import_branch
|
||||
else:
|
||||
self.args.real_upstream_branch = self.args.upstream_branch
|
||||
|
||||
def _finish(self, import_upstream):
|
||||
self.log.notice("Merging import to requested branch '%s'",
|
||||
@@ -126,7 +128,7 @@ class ImportCommand(LogDedentMixin, GitUpstreamCommand):
|
||||
target branch: '%s'
|
||||
upstream branch: '%s'
|
||||
import branch: '%s'""",
|
||||
self.args.branch, self.args.upstream_branch,
|
||||
self.args.branch, self.args.real_upstream_branch,
|
||||
import_upstream.import_branch)
|
||||
if self.args.branches:
|
||||
for branch in self.args.branches:
|
||||
@@ -140,14 +142,14 @@ class ImportCommand(LogDedentMixin, GitUpstreamCommand):
|
||||
|
||||
import_upstream = ImportUpstream(
|
||||
branch=self.args.branch,
|
||||
upstream=self.args.upstream_branch,
|
||||
upstream=self.args.real_upstream_branch,
|
||||
import_branch=self.args.import_branch,
|
||||
extra_branches=self.args.branches)
|
||||
|
||||
self.log.notice("Searching for previous import")
|
||||
strategy = ImportStrategiesFactory.create_strategy(
|
||||
self.args.strategy, branch=self.args.branch,
|
||||
upstream=self.args.upstream_branch,
|
||||
upstream=self.args.real_upstream_branch,
|
||||
search_refs=self.args.search_refs)
|
||||
|
||||
if not strategy.previous_upstream:
|
||||
@@ -179,7 +181,19 @@ class ImportCommand(LogDedentMixin, GitUpstreamCommand):
|
||||
import_upstream.create_import(force=self.args.force)
|
||||
self.log.notice("Successfully created import branch")
|
||||
|
||||
if not import_upstream.apply(strategy, self.args.interactive):
|
||||
# build suitable command line for interactive mode
|
||||
if self.args.merge:
|
||||
cmdline = self.args.script_cmdline + [
|
||||
self.name,
|
||||
'--finish',
|
||||
'--into=%s' % import_upstream.branch,
|
||||
'--import-branch=%s' % import_upstream.import_branch,
|
||||
import_upstream.upstream
|
||||
] + import_upstream.extra_branches
|
||||
else:
|
||||
cmdline = None
|
||||
|
||||
if not import_upstream.apply(strategy, self.args.interactive, cmdline):
|
||||
self.log.notice("Import cancelled")
|
||||
return False
|
||||
|
||||
|
||||
@@ -48,10 +48,6 @@ class ImportUpstream(LogDedentMixin, GitMixin):
|
||||
# any computation
|
||||
super(ImportUpstream, self).__init__(*args, **kwargs)
|
||||
|
||||
# test that we can use this git repo
|
||||
if self.is_detached():
|
||||
raise ImportUpstreamError("In 'detached HEAD' state")
|
||||
|
||||
if self.repo.bare:
|
||||
raise ImportUpstreamError("Cannot perform imports in bare repos")
|
||||
|
||||
@@ -141,6 +137,10 @@ class ImportUpstream(LogDedentMixin, GitMixin):
|
||||
automatically if checkout is true.
|
||||
"""
|
||||
|
||||
# test that we can use this git repo
|
||||
if self.is_detached():
|
||||
raise ImportUpstreamError("In 'detached HEAD' state")
|
||||
|
||||
if not commit:
|
||||
commit = self.upstream
|
||||
|
||||
@@ -264,7 +264,7 @@ class ImportUpstream(LogDedentMixin, GitMixin):
|
||||
# set root commit for next loop
|
||||
root = sequence[counter].hexsha
|
||||
|
||||
def apply(self, strategy, interactive=False):
|
||||
def apply(self, strategy, interactive=False, resume_cmdline=None):
|
||||
"""Apply list of commits given onto latest import of upstream"""
|
||||
|
||||
commit_list = list(strategy.filtered_iter())
|
||||
@@ -317,7 +317,8 @@ class ImportUpstream(LogDedentMixin, GitMixin):
|
||||
# reset head back to the tip of the changes to be rebased
|
||||
self._set_branch(self.import_branch, self.branch, force=True)
|
||||
|
||||
rebase = RebaseEditor(interactive, repo=self.repo)
|
||||
# build the command line
|
||||
rebase = RebaseEditor(resume_cmdline, interactive, repo=self.repo)
|
||||
if len(commit_list):
|
||||
first = commit_list[0]
|
||||
|
||||
@@ -366,6 +367,13 @@ class ImportUpstream(LogDedentMixin, GitMixin):
|
||||
performing suitable verification checks.
|
||||
"""
|
||||
self.log.info("No verification checks enabled")
|
||||
in_rebase = False
|
||||
if self.is_detached():
|
||||
# called via rebase exec
|
||||
target_sha = self.git.rev_parse("HEAD")
|
||||
in_rebase = True
|
||||
else:
|
||||
target_sha = self.import_branch
|
||||
self.git.checkout(self.branch)
|
||||
current_sha = self.git.rev_parse("HEAD")
|
||||
|
||||
@@ -385,7 +393,7 @@ class ImportUpstream(LogDedentMixin, GitMixin):
|
||||
Merging import branch to HEAD and ignoring changes:
|
||||
git merge -s ours --no-commit %s
|
||||
""", self.import_branch)
|
||||
self.git.merge('-s', 'ours', self.import_branch, no_commit=True)
|
||||
self.git.merge('-s', 'ours', target_sha, no_commit=True)
|
||||
self.log.info(
|
||||
"""
|
||||
Replacing tree contents with those from the import branch:
|
||||
@@ -404,7 +412,9 @@ class ImportUpstream(LogDedentMixin, GitMixin):
|
||||
self.git.rev_parse("%s^{tree}" % self.import_branch):
|
||||
raise ImportUpstreamError(
|
||||
"Resulting tree does not match import")
|
||||
except (GitCommandError, ImportUpstreamError):
|
||||
if in_rebase:
|
||||
self.git.checkout(target_sha)
|
||||
except (GitCommandError, ImportUpstreamError) as e:
|
||||
self.log.error(
|
||||
"""
|
||||
Failed to finish import by merging branch:
|
||||
@@ -412,6 +422,7 @@ class ImportUpstream(LogDedentMixin, GitMixin):
|
||||
into and replacing the contents of:
|
||||
'%s'
|
||||
""", self.import_branch, self.branch)
|
||||
self.log.error(str(e))
|
||||
self._set_branch(self.branch, current_sha, force=True)
|
||||
return False
|
||||
except Exception:
|
||||
|
||||
@@ -33,11 +33,11 @@ REBASE_EDITOR_TODO = "git-upstream/git-rebase-todo"
|
||||
|
||||
class RebaseEditor(GitMixin, LogDedentMixin):
|
||||
|
||||
def __init__(self, interactive=False, *args, **kwargs):
|
||||
def __init__(self, finish_args, interactive=False, *args, **kwargs):
|
||||
|
||||
self._interactive = interactive
|
||||
|
||||
super(RebaseEditor, self).__init__()
|
||||
super(RebaseEditor, self).__init__(*args, **kwargs)
|
||||
|
||||
self._editor = REBASE_EDITOR_SCRIPT
|
||||
# interactive switch here determines if the script that is given
|
||||
@@ -48,6 +48,8 @@ class RebaseEditor(GitMixin, LogDedentMixin):
|
||||
self.log.debug("Enabling interactive mode for rebase")
|
||||
self._editor = "%s --interactive" % self.editor
|
||||
|
||||
self.finish_args = finish_args
|
||||
|
||||
@property
|
||||
def editor(self):
|
||||
return self._editor
|
||||
@@ -97,6 +99,48 @@ class RebaseEditor(GitMixin, LogDedentMixin):
|
||||
|
||||
return todo_file
|
||||
|
||||
def _insert_exec_to_todo(self):
|
||||
if not self.finish_args:
|
||||
# no need to insert, as asked not to perform a finish/merge
|
||||
return
|
||||
|
||||
todo_file = os.path.join(self.repo.git_dir, REBASE_EDITOR_TODO)
|
||||
exec_line = "exec %s\n" % " ".join(self.finish_args)
|
||||
|
||||
insn_data = None
|
||||
with codecs.open(todo_file, "r", "utf-8") as todo:
|
||||
insn_data = todo.readlines()
|
||||
|
||||
# Cannot just append to file, as rebase appears to cut off
|
||||
# after the second blank line in a row is encountered.
|
||||
# Need to find the last instruction and insert afterwards,
|
||||
# or if we find noop replace.
|
||||
last = 0
|
||||
for idx, line in enumerate(insn_data):
|
||||
# comment line - ignore
|
||||
if line.startswith("#"):
|
||||
continue
|
||||
# found noop - just replace
|
||||
if line.rstrip() == "noop":
|
||||
insn_data[idx] = exec_line
|
||||
break
|
||||
# not an empty line
|
||||
if line.rstrip() != "":
|
||||
last = idx
|
||||
else:
|
||||
# didn't break so need to insert after last instruction
|
||||
insn_data.insert(last + 1, exec_line)
|
||||
|
||||
# replace contents to include exec
|
||||
try:
|
||||
todo = codecs.open(todo_file, "w", "utf-8")
|
||||
todo.writelines(insn_data)
|
||||
# ensure the filesystem has the correct contents
|
||||
todo.stream.flush()
|
||||
os.fsync(todo.stream.fileno())
|
||||
finally:
|
||||
todo.close()
|
||||
|
||||
def _shorten(self, commit):
|
||||
|
||||
if not commit:
|
||||
@@ -106,31 +150,24 @@ class RebaseEditor(GitMixin, LogDedentMixin):
|
||||
|
||||
def _set_editor(self, editor):
|
||||
|
||||
if self.git_sequence_editor:
|
||||
self._saveeditor = self.git_sequence_editor
|
||||
if self._interactive == 'debug':
|
||||
os.environ['GIT_UPSTREAM_GIT_SEQUENCE_EDITOR'] = \
|
||||
self._saveeditor
|
||||
os.environ['GIT_SEQUENCE_EDITOR'] = editor
|
||||
env = os.environ.copy()
|
||||
# if git is new enough, we can edit the sequence without overriding
|
||||
# the editor, which allows rebase to call the correct editor if
|
||||
# reaches a 'reword' command before it has exited for the first time
|
||||
# otherwise the custom editor of git-upstream will executed with
|
||||
# the path to a commit message as an argument and will need to be able
|
||||
# to call the preferred user editor instead
|
||||
if self.git.version_info >= (1, 7, 8):
|
||||
env['GIT_SEQUENCE_EDITOR'] = editor
|
||||
else:
|
||||
self._saveeditor = self.git_editor
|
||||
if self._interactive == 'debug':
|
||||
os.environ['GIT_UPSTREAM_GIT_EDITOR'] = self._saveeditor
|
||||
os.environ['GIT_EDITOR'] = editor
|
||||
env['GIT_UPSTREAM_GIT_EDITOR'] = self.git_editor
|
||||
env['GIT_EDITOR'] = editor
|
||||
return env
|
||||
|
||||
def _unset_editor(self):
|
||||
|
||||
for var in ['GIT_SEQUENCE_EDITOR', 'GIT_EDITOR']:
|
||||
# GIT_UPSTREAM_* variations should only be set if script was in a
|
||||
# debug mode.
|
||||
if os.environ.get('GIT_UPSTREAM_' + var, None):
|
||||
del os.environ['GIT_UPSTREAM_' + 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 cleanup(self):
|
||||
todo_file = os.path.join(self.repo.git_dir, REBASE_EDITOR_TODO)
|
||||
if os.path.exists(todo_file):
|
||||
os.remove(todo_file)
|
||||
|
||||
def run(self, commits, *args, **kwargs):
|
||||
"""
|
||||
@@ -144,37 +181,47 @@ class RebaseEditor(GitMixin, LogDedentMixin):
|
||||
todo_file = self._write_todo(commits, *args, **kwargs)
|
||||
if self._interactive:
|
||||
# spawn the editor
|
||||
# It is not safe to redirect I/O channels as most editors will
|
||||
# be expecting that I/O is from/to proper terminal. YMMV
|
||||
user_editor = self.git_sequence_editor or self.git_editor
|
||||
status = subprocess.call("%s %s" % (user_editor, todo_file),
|
||||
shell=True)
|
||||
if status:
|
||||
if status != 0:
|
||||
return status, None, "Editor returned non-zero exit code"
|
||||
|
||||
editor = "%s %s" % (self.editor, todo_file)
|
||||
self._set_editor(editor)
|
||||
environ = 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)
|
||||
cmd = ['git', 'rebase', '--interactive']
|
||||
cmd.extend(self.git.transform_kwargs(**kwargs))
|
||||
cmd.extend(args)
|
||||
mode = os.environ.get('TEST_GIT_UPSTREAM_REBASE_EDITOR', "")
|
||||
if mode.lower() == "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.
|
||||
try:
|
||||
return subprocess.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()
|
||||
finally:
|
||||
self.cleanup()
|
||||
elif mode == "1":
|
||||
# run in test mode to avoid replacing the existing process
|
||||
# to keep the majority of tests simple and only require special
|
||||
# launching code for those tests written to check the rebase
|
||||
# resume behaviour
|
||||
try:
|
||||
return 0, subprocess.check_output(
|
||||
cmd, stderr=subprocess.STDOUT, env=environ), None
|
||||
except subprocess.CalledProcessError as e:
|
||||
return e.returncode, e.output, None
|
||||
finally:
|
||||
self.cleanup()
|
||||
else:
|
||||
self._insert_exec_to_todo()
|
||||
|
||||
cmd.append(environ)
|
||||
os.execlpe('git', *cmd)
|
||||
|
||||
@property
|
||||
def git_sequence_editor(self):
|
||||
|
||||
@@ -22,6 +22,7 @@ Command-line tool for tracking upstream revisions
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import git
|
||||
@@ -63,6 +64,15 @@ def build_parsers():
|
||||
|
||||
subcommand_parsers = commands.get_subcommands(parser)
|
||||
|
||||
# calculate the correct path to allow the program be re-executed
|
||||
program = sys.argv[0]
|
||||
if os.path.split(program)[-1] != 'git-upstream':
|
||||
script_cmdline = ['python', program]
|
||||
else:
|
||||
script_cmdline = [program]
|
||||
|
||||
parser.set_defaults(script_cmdline=script_cmdline)
|
||||
|
||||
return subcommand_parsers, parser
|
||||
|
||||
|
||||
|
||||
@@ -80,29 +80,36 @@ def main():
|
||||
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 called with a commit message file as the argument, skip this next
|
||||
# section, and try to spawn the user preferred editor. Should only be
|
||||
# needed if reword command is encountered by git-rebase before an edit
|
||||
# or conflict has occurred and git is older than 1.7.8, as otherwise
|
||||
# sequence.editor will be used instead , which will ensure the normal
|
||||
# editor is called by rebase for the commit message, and this editor is
|
||||
# limited to only modifying the instruction sequence.
|
||||
if os.path.basename(args.ofile) != "COMMIT_EDITMSG":
|
||||
# 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: Interactive mode not enabled")
|
||||
sys.exit(0)
|
||||
print("rebase-editor: Replacing rebase instructions")
|
||||
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
|
||||
editor = None
|
||||
env = os.environ
|
||||
for var in ['GIT_SEQUENCE_EDITOR', 'GIT_EDITOR']:
|
||||
editor = env.get('GIT_UPSTREAM_' + var, None)
|
||||
editor = os.environ.get('GIT_UPSTREAM_' + var, None)
|
||||
if editor:
|
||||
del env['GIT_UPSTREAM_' + var]
|
||||
env[var] = editor
|
||||
del os.environ['GIT_UPSTREAM_' + var]
|
||||
os.environ[var] = editor
|
||||
break
|
||||
|
||||
if editor:
|
||||
@@ -112,11 +119,10 @@ def main():
|
||||
sys.stdin.flush()
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
os.execvpe(editor, editor_args, env=env)
|
||||
os.execvp(editor, editor_args)
|
||||
|
||||
sys.stderr.write("rebase-editor: No git EDITOR variables defined in "
|
||||
"environment to call as requested by the "
|
||||
"--interactive option.\n")
|
||||
"environment to call as required.\n")
|
||||
sys.exit(2)
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
#
|
||||
# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
---
|
||||
- desc: |
|
||||
Test that default behaviour and options work
|
||||
|
||||
Repository layout being checked (assumed already replayed)
|
||||
|
||||
C---D local/master
|
||||
/
|
||||
A---B---E---F upstream/master
|
||||
|
||||
tree:
|
||||
- [A, []]
|
||||
- [B, [A]]
|
||||
- [C, [B]]
|
||||
- [D, [C]]
|
||||
- [E, [B]]
|
||||
- [F, [E]]
|
||||
|
||||
branches:
|
||||
head: [master, D]
|
||||
upstream: [upstream/master, F]
|
||||
|
||||
parser_args: [-v, import, -i, upstream/master]
|
||||
|
||||
expect_rebased: [C, D]
|
||||
@@ -0,0 +1,65 @@
|
||||
#
|
||||
# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
---
|
||||
- desc: |
|
||||
Test the --no-merge option to the import command (interactive)
|
||||
|
||||
Given manual intervention to resolve conflicts and complete
|
||||
the import, check that if originally given --no-merge, that
|
||||
correctly reach the end of the rebase and skip performing the
|
||||
final merge.
|
||||
|
||||
Repository layout being checked (assumed already replayed)
|
||||
|
||||
C---D local/master
|
||||
/
|
||||
A---B---E---F custom/master
|
||||
|
||||
|
||||
Test that result is as follows
|
||||
|
||||
C---D local/master
|
||||
/
|
||||
/ C1---D1 import/F
|
||||
/ /
|
||||
A---B---E---F custom/master
|
||||
|
||||
|
||||
|
||||
tree:
|
||||
- [A, []]
|
||||
- [B, [A]]
|
||||
- [C, [B]]
|
||||
- [D, [C]]
|
||||
- [E, [B]]
|
||||
- [F, [E]]
|
||||
- [C1, [F]]
|
||||
- [D1, [C1]]
|
||||
|
||||
branches:
|
||||
head: [master, D]
|
||||
upstream: [custom/master, F]
|
||||
|
||||
parser_args:
|
||||
- -q
|
||||
- import
|
||||
- --no-merge
|
||||
- --import-branch=import/F
|
||||
- --into=master
|
||||
- custom/master
|
||||
|
||||
check_merge: False
|
||||
@@ -18,6 +18,7 @@
|
||||
import inspect
|
||||
import os
|
||||
|
||||
import mock
|
||||
from testscenarios import TestWithScenarios
|
||||
from testtools.content import text_content
|
||||
from testtools.matchers import Contains
|
||||
@@ -29,6 +30,8 @@ from git_upstream.tests.base import BaseTestCase
|
||||
from git_upstream.tests.base import get_scenarios
|
||||
|
||||
|
||||
@mock.patch.dict('os.environ',
|
||||
{'TEST_GIT_UPSTREAM_REBASE_EDITOR': '1'})
|
||||
class TestImportCommand(TestWithScenarios, BaseTestCase):
|
||||
|
||||
commands, parser = main.build_parsers()
|
||||
@@ -97,6 +100,10 @@ class TestImportCommand(TestWithScenarios, BaseTestCase):
|
||||
if extra_test_func:
|
||||
extra_test_func()
|
||||
|
||||
def _verify_basic(self):
|
||||
|
||||
self.assertThat(self.git.log(n=1), Contains("Merge branch 'import/"))
|
||||
|
||||
def _verify_basic_additional_missed(self):
|
||||
"""Additional verification that test produces a warning"""
|
||||
|
||||
|
||||
109
git_upstream/tests/commands/import/test_interactive.py
Normal file
109
git_upstream/tests/commands/import/test_interactive.py
Normal file
@@ -0,0 +1,109 @@
|
||||
# Copyright (c) 2016 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for the --interactive option to the 'import' command"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import mock
|
||||
from testscenarios import TestWithScenarios
|
||||
from testtools.content import text_content
|
||||
from testtools.matchers import Contains
|
||||
from testtools.matchers import Equals
|
||||
from testtools.matchers import Not
|
||||
|
||||
from git_upstream.lib.pygitcompat import Commit
|
||||
from git_upstream import main
|
||||
from git_upstream.tests.base import BaseTestCase
|
||||
from git_upstream.tests.base import get_scenarios
|
||||
|
||||
|
||||
@mock.patch.dict('os.environ', {'GIT_SEQUENCE_EDITOR': 'cat'})
|
||||
class TestImportInteractiveCommand(TestWithScenarios, BaseTestCase):
|
||||
|
||||
commands, parser = main.build_parsers()
|
||||
scenarios = get_scenarios(os.path.join(os.path.dirname(__file__),
|
||||
"interactive_scenarios"))
|
||||
|
||||
def setUp(self):
|
||||
# add description in case parent setup fails.
|
||||
self.addDetail('description', text_content(self.desc))
|
||||
|
||||
script_cmdline = self.parser.get_default('script_cmdline')
|
||||
script_cmdline[-1] = os.path.join(os.getcwd(), main.__file__)
|
||||
self.parser.set_defaults(script_cmdline=script_cmdline)
|
||||
|
||||
# builds the tree to be tested
|
||||
super(TestImportInteractiveCommand, self).setUp()
|
||||
|
||||
def test_interactive(self):
|
||||
upstream_branch = self.branches['upstream'][0]
|
||||
target_branch = self.branches['head'][0]
|
||||
|
||||
cmdline = self.parser.get_default('script_cmdline') + self.parser_args
|
||||
try:
|
||||
output = subprocess.check_output(cmdline, stderr=subprocess.STDOUT)
|
||||
except subprocess.CalledProcessError as cpe:
|
||||
self.addDetail('subprocess-output',
|
||||
text_content(cpe.output.decode('utf-8')))
|
||||
raise
|
||||
self.addDetail('subprocess-output',
|
||||
text_content(output.decode('utf-8')))
|
||||
|
||||
expected = getattr(self, 'expect_rebased', [])
|
||||
if expected:
|
||||
changes = list(Commit.iter_items(
|
||||
self.repo, '%s..%s^2' % (upstream_branch, target_branch)))
|
||||
self.assertThat(
|
||||
len(changes), Equals(len(expected)),
|
||||
"should only have seen %s changes, got: %s" %
|
||||
(len(expected),
|
||||
", ".join(["%s:%s" % (commit.hexsha,
|
||||
commit.message.splitlines()[0])
|
||||
for commit in changes])))
|
||||
|
||||
# expected should be listed in order from oldest to newest, so
|
||||
# reverse changes to match as it would be newest to oldest.
|
||||
changes.reverse()
|
||||
for commit, node in zip(changes, expected):
|
||||
subject = commit.message.splitlines()[0]
|
||||
node_subject = self.gittree.graph[node].message.splitlines()[0]
|
||||
self.assertThat(subject, Equals(node_subject),
|
||||
"subject '%s' of commit '%s' does not match "
|
||||
"subject '%s' of node '%s'" % (
|
||||
subject, commit.hexsha, node_subject,
|
||||
node))
|
||||
import_branch = [head for head in self.repo.heads
|
||||
if str(head).startswith("import") and
|
||||
not str(head).endswith("-base")]
|
||||
|
||||
self.assertThat(self.git.rev_parse(import_branch),
|
||||
Not(Equals(self.git.rev_parse(target_branch))),
|
||||
"Import branch and target should have identical "
|
||||
"contents, but not be the same")
|
||||
|
||||
# allow disabling of checking the merge commit contents
|
||||
# as some tests won't result in an import
|
||||
if getattr(self, 'check_merge', True):
|
||||
commit_message = self.git.log(target_branch, n=1)
|
||||
self.assertThat(commit_message,
|
||||
Contains("of '%s' into '%s'" % (upstream_branch,
|
||||
target_branch)))
|
||||
|
||||
# allow additional test specific verification methods below
|
||||
extra_test_func = getattr(self, '_verify_%s' % self.name, None)
|
||||
if extra_test_func:
|
||||
extra_test_func()
|
||||
Reference in New Issue
Block a user