Skip to content

Commit

Permalink
Merge f91c4e6 into 9c461c9
Browse files Browse the repository at this point in the history
  • Loading branch information
stopthatcow committed Apr 7, 2019
2 parents 9c461c9 + f91c4e6 commit 136301e
Show file tree
Hide file tree
Showing 13 changed files with 204 additions and 85 deletions.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
'GitPython>=2.1.8', # BSD
'dict-recursive-update>=1.0.1', # MIT
'ruamel.yaml<=0.15', # MIT
'keyring>=11.0', # MIT
'keyring<=8.0', # MIT
'keyrings.alt>=2.3', # MIT
'autopep8>=1.3.4', # MIT
'docformatter>=1.0', # Expat
Expand Down
21 changes: 21 additions & 0 deletions tests/test_dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,3 +325,24 @@ def test_builds():
result = runner.invoke(zazu.cli.cli, ['dev', 'builds'])
assert result.exception
assert result.exit_code != 0


def test_complete_git_branch(git_repo):
with zazu.util.cd(git_repo.working_tree_dir):
assert zazu.dev.commands.complete_git_branch(None, [], 'mas') == ['master']


def test_complete_issue_and_complete_feature(mocker):
mocked_config = mocker.Mock()
mocked_tracker = mocker.Mock()
mocked_issue = mocker.Mock()
mocked_issue.__str__ = mocker.Mock(return_value='ZZ-1')
mocked_issue.name = 'name'
mocked_tracker.issues = mocker.Mock(return_value=[mocked_issue])
mocked_config.issue_tracker = mocker.Mock(return_value=mocked_tracker)
mocker.patch('zazu.config.Config', return_value=mocked_config)
assert zazu.dev.commands.complete_issue(None, [], 'Z') == [(mocked_issue, 'name')]
assert zazu.dev.commands.complete_issue(None, [], 'Na') == [(mocked_issue, 'name')]
assert zazu.dev.commands.complete_issue(None, [], '') == [(mocked_issue, 'name')]
assert zazu.dev.commands.complete_issue(None, [], 'foo') == []
assert zazu.dev.commands.complete_feature(None, [], 'Z') == [('feature/ZZ-1', 'name')]
20 changes: 20 additions & 0 deletions tests/test_github_issue_tracker.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import conftest
import copy
import github
import pytest
import zazu.git_helper
Expand Down Expand Up @@ -64,6 +65,19 @@ def test_github_issue_tracker_create_issue(mocker, mocked_github_issue_tracker):
zazu.plugins.github_issue_tracker.GitHubIssueTracker._github_repo.create_issue.call_count == 1


def test_github_issue_tracker_list_issues(mocker, mocked_github_issue_tracker):
mocked_github_issue_tracker._github.get_issues = mocker.Mock(return_value=[mock_issue])
mocked_github_issue_tracker.get_issues = mocker.Mock()
zazu.plugins.github_issue_tracker.GitHubIssueTracker._github_repo.get_issues.call_count == 1


def test_github_issue_tracker_list_issues_error(mocker, mocked_github_issue_tracker):
mocked_github_issue_tracker._github.get_issues = mocker.Mock(side_effect=github.GithubException(404, {}))
with pytest.raises(zazu.issue_tracker.IssueTrackerError) as e:
mocked_github_issue_tracker.issues()
assert '404' in str(e.value)


def test_from_config_no_project(git_repo):
with zazu.util.cd(git_repo.working_tree_dir):
with pytest.raises(zazu.issue_tracker.IssueTrackerError) as e:
Expand Down Expand Up @@ -135,3 +149,9 @@ def test_github_issue_adaptor():
assert uut.browse_url == 'https://github.com/stopthatcow/zazu/issues/1'
assert uut.id == '1'
assert str(uut) == uut.id
mock_issue2 = copy.copy(mock_issue)
mock_issue2.number = 2
uut2 = zazu.plugins.github_issue_tracker.GitHubIssueAdaptor(mock_issue2)
assert uut < uut2
assert uut < 2
assert uut < '2'
15 changes: 15 additions & 0 deletions tests/test_jira_issue_tracker.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import conftest
import copy
import jira
import jira.client
import pytest
Expand Down Expand Up @@ -121,6 +122,14 @@ def test_jira_assign_issue(mocker, mocked_jira_issue_tracker):
mocked_jira_issue_tracker._jira_handle.assign_issue.assert_called_once_with(mock_issue, 'me')


def test_jira_list_issues(mocker, mocked_jira_issue_tracker):
mocked_jira_issue_tracker._jira_handle.current_user = mocker.Mock(return_value='me')
mocked_jira_issue_tracker._jira_handle.search_issues = mocker.Mock(return_value=[])
mocked_jira_issue_tracker.issues()
mocked_jira_issue_tracker._jira_handle.search_issues.assert_called_once_with(
'assignee=me AND resolution="Unresolved"', fields='key, summary, description')


def test_jira_issue_tracker_no_components(mocker):
uut = zazu.plugins.jira_issue_tracker.JiraIssueTracker.from_config({'url': 'https://jira',
'project': 'ZZ'})
Expand Down Expand Up @@ -167,5 +176,11 @@ def test_jira_issue_adaptor(tracker_mock):
assert uut.type == 'type'
assert uut.browse_url == 'https://jira/browse/ZZ-1'
assert uut.id == 'ZZ-1'
assert uut.parse_key() == ('ZZ', 1)
assert str(uut) == uut.id
assert repr(uut) == uut.id
mock_issue2 = copy.copy(mock_issue)
mock_issue2.key = 'ZZ-2'
uut2 = zazu.plugins.jira_issue_tracker.JiraIssueAdaptor(mock_issue2, tracker_mock)
assert uut < uut2
assert uut < 'ZZ-2'
7 changes: 2 additions & 5 deletions zazu/cli.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
# -*- coding: utf-8 -*-
"""Entry point for zazu."""
import click
import os
import zazu.config
import zazu.dev.commands
import zazu.git_helper
import zazu.repo.commands
import zazu.style
import zazu.upgrade
Expand All @@ -15,10 +13,9 @@

@click.group()
@click.version_option(version=zazu.__version__)
@click.pass_context
def cli(ctx):
def cli():
"""Entry point for zazu cli."""
ctx.obj = zazu.config.Config(zazu.git_helper.get_repo_root(os.getcwd()))
pass


def init():
Expand Down
20 changes: 18 additions & 2 deletions zazu/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
"""Config classes and methods for zazu."""
import zazu.code_reviewer
import zazu.git_helper
import zazu.issue_tracker
import zazu.scm_host
import zazu.util
Expand Down Expand Up @@ -199,8 +200,10 @@ def write(self):
class Config(object):
"""Hold all zazu configuration info."""

def __init__(self, repo_root):
def __init__(self, repo_root=None):
"""Constructor, doesn't parse configuration or require repo to be valid."""
if repo_root is None:
repo_root = zazu.git_helper.get_repo_root(os.getcwd())
self.repo_root = repo_root
if self.repo_root is not None:
try:
Expand Down Expand Up @@ -339,6 +342,9 @@ def check_repo(self):
raise click.UsageError('The current working directory is not in a git repo')


pass_config = click.make_pass_decorator(Config, ensure=True)


def maybe_write_default_user_config(path):
"""Write a default user config file if it doesn't exist."""
DEFAULT_USER_CONFIG = """# User configuration file for zazu.
Expand All @@ -354,13 +360,23 @@ def maybe_write_default_user_config(path):
f.write(DEFAULT_USER_CONFIG)


def complete_param(ctx, args, incomplete):
"""Completion function that returns parameter names."""
if '--add' in args:
return [] # Don't offer completions when adding new params.
config_file = ConfigFile(user_config_filepath())
config_dict = config_file.dict
flattened = zazu.util.flatten_dict(config_dict)
return sorted([param for param in flattened.keys() if incomplete in param])


@click.command()
@click.pass_context
@click.option('-l', '--list', is_flag=True, help='list config')
@click.option('--show-origin', is_flag=True, help='show origin of each config variable, (implies --list)')
@click.option('--add', is_flag=True, help='add a new variable')
@click.option('--unset', is_flag=True, help='remove a variable')
@click.argument('param_name', required=False, type=str)
@click.argument('param_name', required=False, type=str, autocompletion=complete_param)
@click.argument('param_value', required=False, type=str)
def config(ctx, list, add, unset, show_origin, param_name, param_value):
"""Manage zazu user configuration."""
Expand Down
95 changes: 54 additions & 41 deletions zazu/dev/commands.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
# -*- coding: utf-8 -*-
"""Dev subcommand for zazu."""

import zazu.github_helper
import zazu.config
import zazu.util
zazu.util.lazy_import(locals(), [
'click',
'concurrent.futures',
'git',
'os',
'webbrowser',
'textwrap',
'urllib'

])

__author__ = 'Nicholas Wiles'
__copyright__ = 'Copyright 2016'

Expand Down Expand Up @@ -97,10 +99,10 @@ def make_issue_descriptor(name, require_type=False):


@click.group()
@click.pass_context
def dev(ctx):
@zazu.config.pass_config
def dev(config):
"""Create or update work items."""
ctx.obj.check_repo()
config.check_repo()


def check_if_branch_is_protected(branch_name):
Expand Down Expand Up @@ -135,12 +137,29 @@ def rename_branch(repo, old_branch, new_branch):
pass


def complete_git_branch(ctx, args, incomplete):
"""Completion function that returns current branch list."""
repo = git.Repo(os.getcwd())
return sorted([b.name for b in repo.branches])


def complete_issue(ctx, args, incomplete):
"""Completion function that returns ids for open issues."""
issues = zazu.config.Config().issue_tracker().issues()
return sorted([(i, i.name) for i in issues if str(i).startswith(incomplete) or incomplete.lower() in i.name.lower()])


def complete_feature(ctx, args, incomplete):
"""Completion function that returns feature/<id> for open issues."""
return sorted([('feature/{}'.format(id), description) for id, description in complete_issue(ctx, args, incomplete)])


@dev.command()
@click.argument('name')
@click.pass_context
def rename(ctx, name):
@click.argument('name', autocompletion=complete_feature)
@zazu.config.pass_config
def rename(config, name):
"""Rename the current branch, locally and remotely."""
repo = ctx.obj.repo
repo = config.repo
check_if_active_branch_can_be_renamed(repo)
rename_branch(repo, repo.active_branch.name, name)

Expand All @@ -163,26 +182,26 @@ def branch_is_current(repo, branch):


@dev.command()
@click.argument('name', required=False)
@click.argument('name', required=False, autocompletion=complete_issue)
@click.option('--no-verify', is_flag=True, help='Skip verification that ticket exists')
@click.option('--head', is_flag=True, help='Branch off of the current head rather than develop')
@click.option('rename_flag', '--rename', is_flag=True, help='Rename the current branch rather than making a new one')
@click.option('-t', '--type', type=click.Choice(['feature/', 'release/', 'hotfix/', 'support/']), help='the ticket type to make',
default='feature/')
@click.pass_context
def start(ctx, name, no_verify, head, rename_flag, type):
@zazu.config.pass_config
def start(config, name, no_verify, head, rename_flag, type):
"""Start a new feature, much like git-flow but with more sugar."""
repo = ctx.obj.repo
repo = config.repo
if rename_flag:
check_if_active_branch_can_be_renamed(repo)

# Fetch in the background.
develop_branch_name = ctx.obj.develop_branch_name()
develop_branch_name = config.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()))
name = str(make_ticket(config.issue_tracker()))
no_verify = True # Making the ticket implicitly verifies it.
except zazu.issue_tracker.IssueTrackerError as e:
raise click.ClickException(str(e))
Expand All @@ -198,7 +217,7 @@ def start(ctx, name, no_verify, head, rename_flag, type):
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))
issue = None if no_verify else verify_ticket_exists(ctx.obj.issue_tracker(), issue_descriptor.id)
issue = None if no_verify else verify_ticket_exists(config.issue_tracker(), issue_descriptor.id)
if not issue_descriptor.description:
issue_descriptor.description = zazu.util.prompt('Enter a short description for the branch')
issue_descriptor.type = type
Expand All @@ -222,7 +241,7 @@ def start(ctx, name, no_verify, head, rename_flag, type):
click.echo('Creating new branch named "{}"...'.format(branch_name))
repo.git.checkout('HEAD', b=branch_name)
if issue is not None:
ctx.obj.issue_tracker().assign_issue(issue, ctx.obj.issue_tracker().user())
config.issue_tracker().assign_issue(issue, config.issue_tracker().user())


def wrap_text(text, width=90, indent=''):
Expand All @@ -244,14 +263,15 @@ def wrap_text(text, width=90, indent=''):


@dev.command()
@click.pass_context
def status(ctx):
"""Get status of this branch."""
issue_id = make_issue_descriptor(ctx.obj.repo.active_branch.name).id
@click.argument('name', required=False, autocompletion=complete_issue)
@zazu.config.pass_config
def status(config, name):
"""Get status of a issue."""
issue_id = make_issue_descriptor(config.repo.active_branch.name).id if name is None else name
# Dispatch REST calls asynchronously
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
issue_future = executor.submit(ctx.obj.issue_tracker().issue, issue_id)
pulls_future = executor.submit(ctx.obj.code_reviewer().review, status='all', head=ctx.obj.repo.active_branch.name)
issue_future = executor.submit(config.issue_tracker().issue, issue_id)
pulls_future = executor.submit(config.code_reviewer().review, status='all', head=config.repo.active_branch.name)

click.echo(click.style('Ticket info:', bg='white', fg='black'))
try:
Expand All @@ -276,22 +296,22 @@ def status(ctx):


@dev.command()
@click.pass_context
@click.option('--base', help='The base branch to target')
@zazu.config.pass_config
@click.option('--base', help='The base branch to target', autocompletion=complete_git_branch)
@click.option('--head', help='The head branch (defaults to current branch and origin organization)')
def review(ctx, base, head):
def review(config, base, head):
"""Create or display pull request."""
code_reviewer = ctx.obj.code_reviewer()
head = ctx.obj.repo.active_branch.name if head is None else head
code_reviewer = config.code_reviewer()
head = config.repo.active_branch.name if head is None else head
existing_reviews = code_reviewer.review(status='open', head=head, base=base)
if existing_reviews:
pr = zazu.util.pick(existing_reviews, 'Multiple reviews found, pick one')
else:
descriptor = make_issue_descriptor(head)
issue_id = descriptor.id
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
issue_future = executor.submit(ctx.obj.issue_tracker().issue, issue_id)
base = ctx.obj.develop_branch_name() if base is None else base
issue_future = executor.submit(config.issue_tracker().issue, issue_id)
base = config.develop_branch_name() if base is None else base
click.echo('No existing review found, creating one...')
title = zazu.util.prompt('Title', default=descriptor.readable_description())
body = zazu.util.prompt('Summary')
Expand All @@ -305,19 +325,12 @@ def review(ctx, base, head):


@dev.command()
@click.pass_context
@click.argument('ticket', default='')
def ticket(ctx, ticket):
@zazu.config.pass_config
@click.argument('ticket', required=False, autocompletion=complete_issue)
def ticket(config, ticket):
"""Open the ticket for the current feature or the one supplied in the ticket argument."""
issue_id = make_issue_descriptor(ctx.obj.repo.active_branch.name).id if not ticket else ticket
issue = verify_ticket_exists(ctx.obj.issue_tracker(), issue_id)
issue_id = make_issue_descriptor(config.repo.active_branch.name).id if not ticket else ticket
issue = verify_ticket_exists(config.issue_tracker(), issue_id)
url = issue.browse_url
click.echo('Opening "{}"'.format(url))
webbrowser.open_new(url)


@dev.command()
@click.pass_context
def builds(ctx):
"""Display build statuses."""
raise NotImplementedError
2 changes: 1 addition & 1 deletion zazu/githooks/commit-msg
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/bin/sh
#adds the branch name to the end of each commit message
# Adds the branch name to the end of each commit message.
echo "\n($(git rev-parse --abbrev-ref HEAD))" >> "$1"
Loading

0 comments on commit 136301e

Please sign in to comment.