Michael Johnson 7da7c37170 Choose tracked branch for rebase when submitting
When choosing the branch to submit a changeset against, and
against which to rebase a changeset if it is being rebased, a new
gerrit.track configuration item and corresponding --track command
line option indicate to use the upstream branch being tracked as the
source of branch information, in preference to configuration. When
downloading a changeset, always set it up to track the matching
remote branch to make --track work for downloaded changesets.
If a branch name is provided explicitly on the command line, it
overrides the tracked branch.

Rationale:

Workflows with multiple active branches are common.  For example,
there may be one development branch (master) and multiple branches
representing prior releases to which bug fixes are still being
made.  Then a common workflow is to fix bugs in the earliest
affected branch still maintained and merge forward or cherry-pick
to master.  The commits being made to the earlier released branches
should not be rebased against master.

A typical usage pattern in this workflow is:

    git checkout -b my-feature origin/master
    ... implement feature ...
    git review -f

    git checkout -b my-bug-fix origin/maintenancebranch
    ... implement bug fix ...
    git review -f maintenancebranch

    git checkout -b my-bug-fix-merge origin/master
    git merge maintenancebranch / git cherry-pick -x ... / git review -x ...
    git review -f

The developer, who is usually implementing features and therefore
used to working against master, may accidentally forget to name
the release branch when running git review for the bug fix to the
release branch.  Mananging .gitreview files across branches and
repositories scales poorly with larger numbers of repositories
and branches and can be vulnerable to missed bad merges altering
configuration to point at wrong branches.

This change rebases changesets against the tracked remote and branch,
or if no branch is tracked, against the previously-specified branch,
instead of against <defaultremote>/master, only if gerrit.track has
been set to true.  With this change, the developer can safely omit
to specify the branch name when committing changes to non-default
branches such as "maintenancebranch" in the example.

When downloading a changeset, it will always be set up to track the
matching remote branch.  That way, whether or not the gerrit.track
configuration item is set when the changeset is downloaded, the right
branch will be chosen when it is submitted if gerrit.track is set
after the changeset is downloaded, or if the --track command line
option is specified.

Closes-Bug: #883176
Story: #883176
Story: #2000176
Change-Id: I25f22b9e3cda38598681d720a2f2ac534baec5a6
2015-03-18 09:17:34 -04:00

297 lines
11 KiB
Python

# -*- coding: utf-8 -*-
# Copyright (c) 2014 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.
import functools
import os
import textwrap
import fixtures
import mock
import testtools
from git_review import cmd
from git_review.tests import utils
# Use of io.StringIO in python =< 2.7 requires all strings handled to be
# unicode. See if StringIO.StringIO is available first
try:
import StringIO as io
except ImportError:
import io
class ConfigTestCase(testtools.TestCase):
"""Class testing config behavior."""
@mock.patch('git_review.cmd.LOCAL_MODE',
mock.PropertyMock(return_value=True))
@mock.patch('git_review.cmd.git_directories', return_value=['', 'fake'])
@mock.patch('git_review.cmd.run_command_exc')
def test_git_local_mode(self, run_mock, dir_mock):
cmd.git_config_get_value('abc', 'def')
run_mock.assert_called_once_with(
cmd.GitConfigException,
'git', 'config', '-f', 'fake/config', '--get', 'abc.def')
@mock.patch('git_review.cmd.LOCAL_MODE',
mock.PropertyMock(return_value=True))
@mock.patch('os.path.exists', return_value=False)
def test_gitreview_local_mode(self, exists_mock):
cmd.Config()
self.assertFalse(exists_mock.called)
class GitReviewConsole(testtools.TestCase, fixtures.TestWithFixtures):
"""Class for testing the console output of git-review."""
reviews = [
{
'number': '1010101',
'branch': 'master',
'subject': 'A simple short subject'
}, {
'number': '9877',
'branch': 'stable/codeword',
'subject': 'A longer and slightly more wordy subject'
}, {
'number': '12345',
'branch': 'master',
'subject': 'A ridiculously long subject that can exceed the '
'normal console width, just need to ensure the '
'max width is short enough'
}]
def setUp(self):
super(GitReviewConsole, self).setUp()
# ensure all tests get a separate git dir to work in to avoid
# local git config from interfering
self.tempdir = self.useFixture(fixtures.TempDir())
self._run_git = functools.partial(utils.run_git,
chdir=self.tempdir.path)
self.run_cmd_patcher = mock.patch('git_review.cmd.run_command_status')
run_cmd_partial = functools.partial(
cmd.run_command_status, GIT_WORK_TREE=self.tempdir.path,
GIT_DIR=os.path.join(self.tempdir.path, '.git'))
self.run_cmd_mock = self.run_cmd_patcher.start()
self.run_cmd_mock.side_effect = run_cmd_partial
self._run_git('init')
self._run_git('commit', '--allow-empty', '-m "initial commit"')
self._run_git('commit', '--allow-empty', '-m "2nd commit"')
def tearDown(self):
self.run_cmd_patcher.stop()
super(GitReviewConsole, self).tearDown()
@mock.patch('git_review.cmd.query_reviews')
@mock.patch('git_review.cmd.get_remote_url', mock.MagicMock)
@mock.patch('git_review.cmd._has_color', False)
def test_list_reviews_no_blanks(self, mock_query):
mock_query.return_value = self.reviews
with mock.patch('sys.stdout', new_callable=io.StringIO) as output:
cmd.list_reviews(None)
console_output = output.getvalue().split('\n')
wrapper = textwrap.TextWrapper(replace_whitespace=False,
drop_whitespace=False)
for text in console_output:
for line in wrapper.wrap(text):
self.assertEqual(line.isspace(), False,
"Extra blank lines appearing between reviews"
"in console output")
@mock.patch('git_review.cmd._use_color', None)
def test_color_output_disabled(self):
"""Test disabling of colour output color.ui defaults to enabled
"""
# git versions < 1.8.4 default to 'color.ui' being false
# so must be set to auto to correctly test
self._run_git("config", "color.ui", "auto")
self._run_git("config", "color.review", "never")
self.assertFalse(cmd.check_use_color_output(),
"Failed to detect color output disabled")
@mock.patch('git_review.cmd._use_color', None)
def test_color_output_forced(self):
"""Test force enable of colour output when color.ui
is defaulted to false
"""
self._run_git("config", "color.ui", "never")
self._run_git("config", "color.review", "always")
self.assertTrue(cmd.check_use_color_output(),
"Failed to detect color output forcefully "
"enabled")
@mock.patch('git_review.cmd._use_color', None)
def test_color_output_fallback(self):
"""Test fallback to using color.ui when color.review is not
set
"""
self._run_git("config", "color.ui", "always")
self.assertTrue(cmd.check_use_color_output(),
"Failed to use fallback to color.ui when "
"color.review not present")
class FakeResponse(object):
def __init__(self, code, text=""):
self.status_code = code
self.text = text
class FakeException(Exception):
def __init__(self, code, *args, **kwargs):
super(FakeException, self).__init__(*args, **kwargs)
self.code = code
FAKE_GIT_CREDENTIAL_FILL = """\
protocol=http
host=gerrit.example.com
username=user
password=pass
"""
class ResolveTrackingUnitTest(testtools.TestCase):
"""Class for testing resolve_tracking."""
def setUp(self):
testtools.TestCase.setUp(self)
patcher = mock.patch('git_review.cmd.run_command_exc')
self.addCleanup(patcher.stop)
self.run_command_exc = patcher.start()
def test_track_local_branch(self):
'Test that local tracked branch is not followed.'
self.run_command_exc.side_effect = [
'',
'refs/heads/other/branch',
]
self.assertEqual(cmd.resolve_tracking(u'remote', u'rbranch'),
(u'remote', u'rbranch'))
def test_track_untracked_branch(self):
'Test that local untracked branch is not followed.'
self.run_command_exc.side_effect = [
'',
'',
]
self.assertEqual(cmd.resolve_tracking(u'remote', u'rbranch'),
(u'remote', u'rbranch'))
def test_track_remote_branch(self):
'Test that remote tracked branch is followed.'
self.run_command_exc.side_effect = [
'',
'refs/remotes/other/branch',
]
self.assertEqual(cmd.resolve_tracking(u'remote', u'rbranch'),
(u'other', u'branch'))
def test_track_git_error(self):
'Test that local tracked branch is not followed.'
self.run_command_exc.side_effect = [cmd.CommandFailed(1, '', [], {})]
self.assertRaises(cmd.CommandFailed,
cmd.resolve_tracking, u'remote', u'rbranch')
class GitReviewUnitTest(testtools.TestCase):
"""Class for misc unit tests."""
@mock.patch('requests.get', return_value=FakeResponse(404))
def test_run_http_exc_raise_http_error(self, mock_get):
url = 'http://gerrit.example.com'
try:
cmd.run_http_exc(FakeException, url)
self.fails('Exception expected')
except FakeException as err:
self.assertEqual(cmd.http_code_2_return_code(404), err.code)
mock_get.assert_called_once_with(url)
@mock.patch('requests.get', side_effect=Exception())
def test_run_http_exc_raise_unknown_error(self, mock_get):
url = 'http://gerrit.example.com'
try:
cmd.run_http_exc(FakeException, url)
self.fails('Exception expected')
except FakeException as err:
self.assertEqual(255, err.code)
mock_get.assert_called_once_with(url)
@mock.patch('git_review.cmd.run_command_exc')
@mock.patch('requests.get', return_value=FakeResponse(200))
def test_run_http_exc_without_auth(self, mock_get, mock_run):
url = 'http://user@gerrit.example.com'
cmd.run_http_exc(FakeException, url)
self.assertFalse(mock_run.called)
mock_get.assert_called_once_with(url)
@mock.patch('git_review.cmd.run_command_exc',
return_value=FAKE_GIT_CREDENTIAL_FILL)
@mock.patch('requests.get',
side_effect=[FakeResponse(401), FakeResponse(200)])
def test_run_http_exc_with_auth(self, mock_get, mock_run):
url = 'http://user@gerrit.example.com'
cmd.run_http_exc(FakeException, url)
mock_run.assert_called_once_with(mock.ANY, 'git', 'credential', 'fill',
stdin='url=%s' % url)
calls = [mock.call(url), mock.call(url, auth=('user', 'pass'))]
mock_get.assert_has_calls(calls)
@mock.patch('git_review.cmd.run_command_exc',
return_value=FAKE_GIT_CREDENTIAL_FILL)
@mock.patch('requests.get', return_value=FakeResponse(401))
def test_run_http_exc_with_failing_auth(self, mock_get, mock_run):
url = 'http://user@gerrit.example.com'
try:
cmd.run_http_exc(FakeException, url)
self.fails('Exception expected')
except FakeException as err:
self.assertEqual(cmd.http_code_2_return_code(401), err.code)
mock_run.assert_called_once_with(mock.ANY, 'git', 'credential', 'fill',
stdin='url=%s' % url)
calls = [mock.call(url), mock.call(url, auth=('user', 'pass'))]
mock_get.assert_has_calls(calls)
@mock.patch('sys.argv', ['argv0', '--track', 'branch'])
@mock.patch('git_review.cmd.check_remote')
@mock.patch('git_review.cmd.resolve_tracking')
def test_command_line_no_track(self, resolve_tracking, check_remote):
check_remote.side_effect = Exception()
self.assertRaises(Exception, cmd._main)
self.assertFalse(resolve_tracking.called)
@mock.patch('sys.argv', ['argv0', '--track'])
@mock.patch('git_review.cmd.check_remote')
@mock.patch('git_review.cmd.resolve_tracking')
def test_track(self, resolve_tracking, check_remote):
check_remote.side_effect = Exception()
self.assertRaises(Exception, cmd._main)
self.assertTrue(resolve_tracking.called)