Skip to content

Commit

Permalink
Merge branch 'develop' into feature/99_credential_storage
Browse files Browse the repository at this point in the history
  • Loading branch information
stopthatcow committed May 22, 2018
2 parents 2c15040 + 1a7dd46 commit f90bfe3
Show file tree
Hide file tree
Showing 9 changed files with 215 additions and 21 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ script:

notifications:
email:
on_success: never
on_success: never
23 changes: 23 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,29 @@ def git_repo_with_local_origin(git_repo):
return git_repo


@pytest.fixture()
def git_repo_with_out_of_date_local_origin(git_repo):
"""Create a local remote with 2 clients. Use 1 client to update develop so that the other client is out of date.
Return the out of date client."""
temp_dir = tempfile.mkdtemp()
git.Repo.init(temp_dir, bare=True)
git_repo.create_remote('origin', temp_dir)
git_repo.git.checkout('-b', 'develop')
git_repo.git.push('-u', 'origin', 'develop')
other_client = git.Repo.init(tempfile.mkdtemp())
other_client.create_remote('origin', temp_dir)
other_client.git.checkout('-b', 'develop')
other_client.git.pull('origin', 'develop')
readme = os.path.join(other_client.working_tree_dir, 'README.md')
with open(readme, 'w') as f:
f.write('foo')
other_client.index.add([readme])
other_client.index.commit('updated readme')
other_client.git.push('-u', 'origin', 'develop')

return git_repo


@contextlib.contextmanager
def working_directory(path):
"""Changes the working directory to the given path back to its previous value on exit"""
Expand Down
18 changes: 15 additions & 3 deletions tests/test_dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,18 @@ def test_rename_detached_head(git_repo):
assert result.exit_code != 0


def test_start(git_repo_with_local_origin, mocker):
git_repo = git_repo_with_local_origin
def test_branch_is_current(git_repo_with_out_of_date_local_origin):
assert not zazu.dev.commands.branch_is_current(git_repo_with_out_of_date_local_origin, 'develop')
git_repo_with_out_of_date_local_origin.git.pull('origin', 'develop')
assert zazu.dev.commands.branch_is_current(git_repo_with_out_of_date_local_origin, 'develop')
git_repo_with_out_of_date_local_origin.git.branch('--unset-upstream')
assert zazu.dev.commands.branch_is_current(git_repo_with_out_of_date_local_origin, 'develop')


def test_start(git_repo_with_out_of_date_local_origin, mocker):
git_repo = git_repo_with_out_of_date_local_origin
mocker.patch('zazu.util.prompt', return_value='description')
with zazu.util.cd(git_repo.working_tree_dir):
git_repo.git.checkout('HEAD', b='develop')
runner = click.testing.CliRunner()
result = runner.invoke(zazu.cli.cli, ['dev', 'start', 'bar-1', '--no-verify'])
assert not result.exception
Expand All @@ -166,6 +173,11 @@ def test_start(git_repo_with_local_origin, mocker):
result = runner.invoke(zazu.cli.cli, ['dev', 'start', 'foo-1_description2', '--no-verify', '--rename'])
assert not result.exception
assert result.exit_code == 0
# Test with no origin.
git_repo_with_out_of_date_local_origin.git.remote('remove', 'origin')
result = runner.invoke(zazu.cli.cli, ['dev', 'start', 'bar-2', '--no-verify'])
assert not result.exception
assert result.exit_code == 0


def test_start_make_ticket(git_repo_with_local_origin, mocker):
Expand Down
64 changes: 57 additions & 7 deletions tests/test_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import click.testing
import distutils.spawn
import pytest
import subprocess
import zazu.cli
import zazu.plugins.clang_format_styler
import zazu.plugins.astyle_styler
Expand Down Expand Up @@ -38,7 +39,7 @@ def repo_with_style_errors(repo_with_style):
def test_astyle(mocker):
mocker.patch('zazu.util.check_popen', return_value='bar')
styler = zazu.plugins.astyle_styler.AstyleStyler(options=['-U'])
ret = styler.style_string('foo')
ret = styler.style_string('foo', None)
zazu.util.check_popen.assert_called_once_with(args=['astyle', '-U'], stdin_str='foo')
assert ret == 'bar'
assert styler.default_extensions() == ['*.c',
Expand All @@ -52,22 +53,71 @@ def test_astyle(mocker):

def test_autopep8():
styler = zazu.plugins.autopep8_styler.Autopep8Styler()
ret = styler.style_string('def foo ():\n pass')
ret = styler.style_string('def foo ():\n pass', None)
assert ret == 'def foo():\n pass\n'
assert ['*.py'] == styler.default_extensions()


def test_docformatter():
styler = zazu.plugins.docformatter_styler.DocformatterStyler()
ret = styler.style_string('def foo ():\n"""doc"""\n pass')
ret = styler.style_string('def foo ():\n"""doc"""\n pass', None)
assert ret == 'def foo ():\n"""doc"""\n pass'
assert ['*.py'] == styler.default_extensions()


def test_eslint(mocker):
class MockPopen(object):
def __init__(self):
pass

def communicate(self, input=None):
pass

def returncode(self):
pass

mock_popen = MockPopen()
mocker.patch.object(MockPopen, 'communicate', return_value=('[{"output":"bar"}]', None))
mocker.patch('subprocess.Popen', return_value=mock_popen)
styler = zazu.plugins.eslint_styler.ESLintStyler(options=['--color'])
ret = styler.style_string('foo', 'baz')
subprocess.Popen.assert_called_once_with(
args=['eslint', '-f', 'json', '--fix-dry-run', '--stdin', '--stdin-filename', 'baz', '--color'],
stdin=subprocess.PIPE, stdout=subprocess.PIPE)
MockPopen.communicate.assert_called_once_with('foo')
assert ret == 'bar'
assert styler.default_extensions() == ['*.js']

ret = styler.style_string('foo', 'baz/qux/quux/quuz/corge')
assert ret == 'bar'

# eslint not found in file's parent directories
ret = styler.style_string('foo', '/')
assert ret == 'bar'

# local eslint not found at directory depth > 100
long_path = '/'
for i in range(105):
long_path = long_path + 'baz/'

with pytest.raises(click.ClickException):
styler.style_string('foo', long_path)

# local eslint fount
mocker.patch('os.path.isfile', return_value=True)
ret = styler.style_string('foo', 'baz/qux')
assert ret == 'bar'

# global eslint does not exist
mocker.patch('subprocess.Popen', side_effect=OSError())
with pytest.raises(click.ClickException):
styler.style_string('foo', 'baz/qux')


def test_goimports(mocker):
mocker.patch('zazu.util.check_popen', return_value='bar')
styler = zazu.plugins.goimports_styler.GoimportsStyler(options=['-U'])
ret = styler.style_string('foo')
ret = styler.style_string('foo', None)
zazu.util.check_popen.assert_called_once_with(args=['goimports', '-U'], stdin_str='foo')
assert ret == 'bar'
assert styler.default_extensions() == ['*.go']
Expand All @@ -76,7 +126,7 @@ def test_goimports(mocker):
def test_generic(mocker):
mocker.patch('zazu.util.check_popen', return_value='bar')
styler = zazu.plugins.generic_styler.GenericStyler(command='sed', options=['-U'])
ret = styler.style_string('foo')
ret = styler.style_string('foo', None)
zazu.util.check_popen.assert_called_once_with(args=['sed', '-U'], stdin_str='foo')
assert ret == 'bar'
assert styler.default_extensions() == []
Expand All @@ -85,7 +135,7 @@ def test_generic(mocker):
def test_esformatter(mocker):
mocker.patch('zazu.util.check_popen', return_value='bar')
styler = zazu.plugins.esformatter_styler.EsformatterStyler(options=['-U'])
ret = styler.style_string('foo')
ret = styler.style_string('foo', None)
zazu.util.check_popen.assert_called_once_with(args=['esformatter', '-U'], stdin_str='foo')
assert ret == 'bar'
assert styler.default_extensions() == ['*.js', '*.es', '*.es6']
Expand All @@ -95,7 +145,7 @@ def test_esformatter(mocker):
reason="requires clang-format")
def test_clang_format():
styler = zazu.plugins.clang_format_styler.ClangFormatStyler(options=['-style=google'])
ret = styler.style_string('void main ( ) { }')
ret = styler.style_string('void main ( ) { }', None)
assert ret == 'void main() {}'
assert styler.default_extensions()

Expand Down
29 changes: 22 additions & 7 deletions zazu/dev/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,13 @@ def find_branch_with_id(repo, id):
pass


def branch_is_current(repo, branch):
repo.remotes.origin.fetch()
if repo.heads[branch].tracking_branch() is None:
return True
return repo.git.rev_parse('{}@{{0}}'.format(branch)) == repo.git.rev_parse('{}@{{u}}'.format(branch))


@dev.command()
@click.argument('name', required=False)
@click.option('--no-verify', is_flag=True, help='Skip verification that ticket exists')
Expand All @@ -164,10 +171,13 @@ def find_branch_with_id(repo, id):
def start(ctx, name, no_verify, head, rename_flag, type):
"""Start a new feature, much like git-flow but with more sugar."""
repo = ctx.obj.repo

if rename_flag:
check_if_active_branch_can_be_renamed(repo)

# Fetch in the background.
develop_branch_name = ctx.obj.develop_branch_name()
if not (head or rename_flag):
develop_is_current_future = zazu.util.async(branch_is_current, repo, develop_branch_name)
if name is None:
try:
name = str(make_ticket(ctx.obj.issue_tracker()))
Expand All @@ -176,6 +186,13 @@ def start(ctx, name, no_verify, head, rename_flag, type):
raise click.ClickException(str(e))
click.echo('Created ticket "{}"'.format(name))
issue_descriptor = make_issue_descriptor(name)
# Sync with the background fetch process before touching the git repo.
if not (head or rename_flag):
try:
develop_is_current = develop_is_current_future.result()
except (git.exc.GitCommandError, AttributeError):
click.secho('WARNING: unable to fetch from origin!', fg='red')
develop_is_current = True
existing_branch = find_branch_with_id(repo, issue_descriptor.id)
if existing_branch and not (rename_flag and repo.active_branch.name == existing_branch):
raise click.ClickException('branch with same id exists: {}'.format(existing_branch))
Expand All @@ -186,14 +203,12 @@ def start(ctx, name, no_verify, head, rename_flag, type):
branch_name = issue_descriptor.get_branch_name()
if not (head or rename_flag):
offer_to_stash_changes(repo)
develop_branch_name = ctx.obj.develop_branch_name()
click.echo('Checking out {}...'.format(develop_branch_name))
repo.heads[develop_branch_name].checkout()
try:
click.echo('Pulling from origin...')
repo.remotes.origin.pull()
except git.exc.GitCommandError:
click.secho('WARNING: unable to pull from origin!', fg='red')
if not develop_is_current:
click.echo('Merging latest from origin...')
repo.git.merge()

try:
repo.git.checkout(branch_name)
click.echo('Branch {} already exists!'.format(branch_name))
Expand Down
75 changes: 75 additions & 0 deletions zazu/plugins/eslint_styler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
"""ESLint plugin for zazu."""
import zazu.styler
zazu.util.lazy_import(locals(), [
'click',
'json',
'os',
'subprocess'
])

__author__ = "Patrick Moore"
__copyright__ = "Copyright 2018"


class ESLintStyler(zazu.styler.Styler):
"""ESLint plugin for code styling."""

def style_string(self, string, filepath):
"""Fix a string to be within style guidelines.
Args:
string (str): the string to style
filepath (str): the filepath of the file being styled
Returns:
Styled string.
"""

eslint = 'eslint'
cwd = os.path.normpath(os.getcwd())
dirname = os.path.normpath(os.path.dirname(filepath))
loop_count = 0

local_eslint = os.path.join('node_modules', 'eslint', 'bin', 'eslint.js')

while True and loop_count < 100:
loop_count = loop_count + 1
maybe_eslint = os.path.join(dirname, local_eslint)

if os.path.isfile(maybe_eslint):
eslint = maybe_eslint
break
elif os.path.realpath(dirname) == cwd:
# Stop searching for eslint above current working directory
# An invalid filepath should break-out here
break
elif os.path.realpath(dirname) == os.path.normpath('/'):
# Stop searching when dirname and maybe_eslint are same
# This should be the filesystem root
break

dirname = os.path.normpath(os.path.join(dirname, '..'))

if loop_count >= 100:
raise click.ClickException('Unable to find eslint.js')

args = [eslint, '-f', 'json', '--fix-dry-run', '--stdin', '--stdin-filename', filepath] + self.options
try:
p = subprocess.Popen(args=args, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
results, _ = p.communicate(string)
except OSError:
raise click.ClickException('Unable to find {}'.format(args[0]))

return json.loads(results)[0].get('output', string)

@staticmethod
def default_extensions():
"""Return the list of file extensions that are compatible with this Styler."""
return ['*.js']

@staticmethod
def type():
"""Return the string type of this Styler."""
return 'eslint'
2 changes: 1 addition & 1 deletion zazu/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def style_file(styler, path, read_fn, write_fn):
write_fn: function used to write out the styled file, or None
"""
input_string = read_fn(path)
styled_string = styler.style_string(input_string)
styled_string = styler.style_string(input_string, path)
violation = styled_string != input_string
if violation and callable(write_fn):
write_fn(path, input_string, styled_string)
Expand Down
3 changes: 2 additions & 1 deletion zazu/styler.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,12 @@ def __init__(self, command=None, options=None, excludes=None, includes=None):
self.includes = [] if includes is None else includes
self.options += self.required_options()

def style_string(self, string):
def style_string(self, string, filepath):
"""Fix a string to be within style guidelines.
Args:
string (str): the string to style
filepath (str): the filepath of the file being styled
Returns:
Styled string.
Expand Down
20 changes: 19 additions & 1 deletion zazu/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,30 @@ def dispatch(work):
the results of the callables as they are finished.
"""
with concurrent.futures.ThreadPoolExecutor(max_workers=multiprocessing.cpu_count()) as executor:
with concurrent.futures.ThreadPoolExecutor(max_workers=multiprocessing.cpu_count()*5) as executor:
futures = {executor.submit(w): w for w in work}
for future in concurrent.futures.as_completed(futures):
yield future.result()


def async(call, *args, **kwargs):
"""Dispatch a call asynchronously and return the future.
Args:
fn: the function to call.
*args: args to forward to fn.
**kwargs: args to forward to fn
Returns:
the future for the return of the called function.
"""
executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
future = executor.submit(call, *args, **kwargs)
executor.shutdown(wait=False)
return future


FAIL_OK = [click.style('FAIL', fg='red', bold=True), click.style(' OK ', fg='green', bold=True)]


Expand Down

0 comments on commit f90bfe3

Please sign in to comment.