wip
This commit is contained in:
parent
d4491cf7ab
commit
14a9af7e51
106
jiracli/cli.py
106
jiracli/cli.py
|
@ -1,15 +1,25 @@
|
|||
import configparser
|
||||
import functools
|
||||
import os
|
||||
from pathlib import Path
|
||||
from textwrap import indent, shorten, wrap
|
||||
from typing import Any, Callable, Dict
|
||||
|
||||
import click
|
||||
import editor
|
||||
|
||||
config = {
|
||||
'host': 'http://localhost:2990/jira/',
|
||||
'user': 'admin',
|
||||
'password': 'admin'
|
||||
}
|
||||
|
||||
CONFIG_PATH = Path(os.environ.get('XDG_CONFIG_HOME', '~/.config')).expanduser() / 'jiracli' / 'config'
|
||||
|
||||
|
||||
def get_config():
|
||||
config = configparser.ConfigParser()
|
||||
config.read(CONFIG_PATH)
|
||||
|
||||
if not config.has_section('server'):
|
||||
raise ValueError(f"Config in {CONFIG_PATH} does not contain a section [server]")
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def ipython():
|
||||
|
@ -23,7 +33,35 @@ def main(ctx):
|
|||
from jira import JIRA
|
||||
|
||||
ctx.ensure_object(dict)
|
||||
ctx.obj['client'] = JIRA(config['host'], basic_auth=(config['user'], config['password']))
|
||||
|
||||
config = get_config()
|
||||
ctx.obj['config'] = config
|
||||
|
||||
server_config = config['server']
|
||||
|
||||
skip_cert_verify = server_config.getboolean('skip_cert_verify', False)
|
||||
if skip_cert_verify:
|
||||
import urllib3
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
jira_kwargs = dict(
|
||||
options={
|
||||
'server': server_config['host'],
|
||||
'verify': not skip_cert_verify
|
||||
},
|
||||
timeout=server_config.getint('timeout', 2),
|
||||
max_retries=server_config.getint('max_retries', 2)
|
||||
)
|
||||
if 'password' in server_config:
|
||||
if 'user' not in server_config:
|
||||
raise ValueError('If password is set in [server], user needs to be set, too.')
|
||||
jira_kwargs['basic_auth'] = (server_config['user'], server_config['password'])
|
||||
elif 'token' in server_config:
|
||||
jira_kwargs['token_auth'] = server_config['token']
|
||||
else:
|
||||
raise ValueError("Need either user and password or token in [server] for login.")
|
||||
|
||||
ctx.obj['client'] = JIRA(**jira_kwargs)
|
||||
|
||||
|
||||
def pass_client(fn):
|
||||
|
@ -35,6 +73,15 @@ def pass_client(fn):
|
|||
return wrapped
|
||||
|
||||
|
||||
def pass_config(fn):
|
||||
@functools.wraps(fn)
|
||||
@click.pass_obj
|
||||
def wrapped(obj, *args, **kwargs):
|
||||
config = obj['config']
|
||||
return fn(config, *args, **kwargs)
|
||||
return wrapped
|
||||
|
||||
|
||||
@main.command()
|
||||
@pass_client
|
||||
def version(client):
|
||||
|
@ -42,15 +89,25 @@ def version(client):
|
|||
|
||||
|
||||
@main.command()
|
||||
@click.option('--project', help='Project to filter for. Default: take from config [filters]/default_project')
|
||||
@click.option('--status', multiple=True, help='Filter for issues with *any* of the given statuses')
|
||||
@click.option('--label', 'labels', multiple=True, help='Filter for issues with all the given labels')
|
||||
@click.option('--text', 'texts', multiple=True, help='Filter for issues containing all the given strings')
|
||||
@click.option('--order', help='Sort by this field')
|
||||
@click.option('--asc/--desc', 'asc', help='Sort ascending/descending. Default: descending')
|
||||
@click.option('--jql', default='', help='Provide your own JQL query to search for issues. '
|
||||
'Setting this ignores all other query options.')
|
||||
'Setting this ignores all other query options except --project.')
|
||||
@pass_client
|
||||
def issues(client, status, labels, texts, order, asc, jql):
|
||||
@pass_config
|
||||
def issues(config, client, project, status, labels, texts, order, asc, jql):
|
||||
filters_config = config.get('filters')
|
||||
|
||||
if not project and filters_config and 'default_project' in filters_config:
|
||||
project = filters_config['default_project']
|
||||
|
||||
# TODO add filtering by created since
|
||||
# TODO add filtering by updated
|
||||
# TODO support pagination I guess? i.e. a limit
|
||||
if not jql:
|
||||
filters = []
|
||||
|
||||
|
@ -71,6 +128,9 @@ def issues(client, status, labels, texts, order, asc, jql):
|
|||
if order:
|
||||
jql += f" ORDER BY {order} {'ASC' if asc else 'DESC'}"
|
||||
|
||||
if project:
|
||||
jql = f"project = {project} AND {jql}"
|
||||
|
||||
print(f"Searching with query: {jql}")
|
||||
issues = client.search_issues(jql)
|
||||
for issue in issues:
|
||||
|
@ -193,13 +253,14 @@ def link():
|
|||
@pass_client
|
||||
def link_list(client, issue):
|
||||
|
||||
issuelinks = issue.fields.issuelinks
|
||||
issuelinks = getattr(issue.fields, 'issuelinks', [])
|
||||
if issuelinks:
|
||||
print("\nIssue Links")
|
||||
# TODO group by type?
|
||||
for issuelink in issuelinks:
|
||||
summary = indent('\n'.join(wrap(issuelink.inwardIssue.fields.summary, width=72)), prefix=' ' * 4)
|
||||
print(f" {issuelink.type} - {issuelink.inwardIssue.key}\n{summary}")
|
||||
other_issue = getattr(issuelink, 'inwardIssue', issuelink.outwardIssue)
|
||||
summary = indent('\n'.join(wrap(other_issue.fields.summary, width=72)), prefix=' ' * 4)
|
||||
print(f" {issuelink.type} - {other_issue.key}\n{summary}")
|
||||
|
||||
remote_links = client.remote_links(issue.id)
|
||||
if remote_links:
|
||||
|
@ -209,7 +270,7 @@ def link_list(client, issue):
|
|||
print(f" {link.object.title} => {link.object.url}")
|
||||
|
||||
|
||||
def prompt(description: str, *, multiline_ok: bool = True, initial_content: str = '') -> str:
|
||||
def prompt(description: str, *, multiline_ok: bool = True, initial_content: str = '', empty_ok: bool = False) -> str:
|
||||
divider_line = '--- Everything after this line will be removed'
|
||||
|
||||
contents = f"{initial_content}\n{divider_line}\n\n{description}"
|
||||
|
@ -229,7 +290,7 @@ def prompt(description: str, *, multiline_ok: bool = True, initial_content: str
|
|||
# TODO build a retry into this?
|
||||
raise click.ClickException('Input needs to be a single line only')
|
||||
|
||||
if not contents_lines:
|
||||
if not empty_ok and not any(contents_lines):
|
||||
raise click.ClickException('Empty content. Cannot continue.')
|
||||
|
||||
return '\n'.join(contents_lines)
|
||||
|
@ -243,7 +304,7 @@ def prompt(description: str, *, multiline_ok: bool = True, initial_content: str
|
|||
def link_create(client, issue, url: str, title: str):
|
||||
|
||||
if url is None:
|
||||
url = prompt(f"Input the URL for the link to add to {issue.key}")
|
||||
url = prompt(f"Input the URL for the link to add to {issue.key}", multiline_ok=False)
|
||||
if title is None:
|
||||
title = prompt(f"Input a single line of title for {url}. This will be added to {issue.key}.",
|
||||
multiline_ok=False)
|
||||
|
@ -288,7 +349,8 @@ def label_edit(issue):
|
|||
original_labels = issue.fields.labels
|
||||
# TODO add a configurable list of "often used" labels into the description
|
||||
labels = set(prompt('Add or remove labels by adding them line by line or removing one',
|
||||
initial_content='\n'.join(original_labels)).splitlines())
|
||||
initial_content='\n'.join(original_labels),
|
||||
empty_ok=True).splitlines())
|
||||
|
||||
original_labels = set(original_labels)
|
||||
|
||||
|
@ -308,3 +370,17 @@ def label_edit(issue):
|
|||
|
||||
# inspired by jira.Issue.add_field_value
|
||||
jira.resources.Resource.update(issue, fields={"update": {"labels": changes}})
|
||||
|
||||
|
||||
@issue.command()
|
||||
@pass_issue
|
||||
def debug(issue):
|
||||
import IPython
|
||||
IPython.embed
|
||||
|
||||
|
||||
@issue.command()
|
||||
@pass_issue
|
||||
def show(issue):
|
||||
"""Show a lot of information about the issue"""
|
||||
raise NotImplementedError
|
||||
|
|
Loading…
Reference in New Issue