Skip to content

Commit

Permalink
Add link command and linking to jira on new issues.
Browse files Browse the repository at this point in the history
  • Loading branch information
tkoscieln committed May 9, 2024
1 parent ff08000 commit fe0d5ff
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 5 deletions.
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ dependencies = [ # F39 / PyPI
"requests>=2.25.1", # 2.28.2 / 2.31.0
"ruamel.yaml>=0.16.6", # 0.17.32 / 0.17.32
"urllib3>=1.26.5, <3.0", # 1.26.16 / 2.0.4
"jira"
]

[project.optional-dependencies]
Expand Down Expand Up @@ -136,7 +137,7 @@ dependencies = [
"types-urllib3",
"types-jinja2",
"types-babel",
"types-docutils",
"types-docutils"
]
features = ["all"]

Expand Down
59 changes: 55 additions & 4 deletions tmt/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1261,6 +1261,15 @@ def _get_template_content(template: str, template_type: str) -> str:
force=force,
logger=logger)

if links.get('verifies') and dry is False:
test = Tree(
path=path,
logger=logger).tests(
names=[
directory_path.name],
apply_command_line=False)
tmt.utils.jira_link(nodes=[test], links=links)

@property
def manual_test_path(self) -> Path:
assert self.manual, 'Test is not manual yet path to manual instructions was requested'
Expand Down Expand Up @@ -1899,6 +1908,13 @@ def create(
# Override template with data provided on command line
plan_content = Plan.edit_template(plan_content)

# Append link with appropriate relation
links = Links(data=list(cast(list[_RawLink], Plan._opt('link', []))))
if links: # Output 'links' if and only if it is not empty
plan_content += dict_to_yaml({
'link': links.to_spec()
})

for name in names:
(directory, plan) = os.path.split(name)
directory_path = path / directory.lstrip('/')
Expand All @@ -1920,6 +1936,15 @@ def create(
force=force,
logger=logger)

if links.get('verifies') and dry is False:
plan_list = Tree(
path=path,
logger=logger).plans(
names=[
directory_path.name],
apply_remote_keys=False)
tmt.utils.jira_link(nodes=[plan_list], links=links)

def _iter_steps(self,
enabled_only: bool = True,
skip: Optional[list[str]] = None
Expand Down Expand Up @@ -2606,6 +2631,13 @@ def create(
except KeyError:
raise tmt.utils.GeneralError(f"Invalid template '{template}'.")

# Append link with appropriate relation
links = Links(data=list(cast(list[_RawLink], Story._opt('link', []))))
if links: # Output 'links' if and only if it is not empty
story_content += dict_to_yaml({
'link': links.to_spec()
})

for name in names:
# Prepare paths
(directory, story) = os.path.split(name)
Expand All @@ -2628,6 +2660,15 @@ def create(
force=force,
logger=logger)

if links.get('verifies') and dry is False:
story_list = Tree(
path=path,
logger=logger).stories(
names=[
directory_path.name],
apply_keys=False)
tmt.utils.jira_link(nodes=[story_list], links=links)

@staticmethod
def overview(tree: 'Tree') -> None:
""" Show overview of available stories """
Expand Down Expand Up @@ -2862,7 +2903,8 @@ def tests(
conditions: Optional[list[str]] = None,
unique: bool = True,
links: Optional[list['LinkNeedle']] = None,
excludes: Optional[list[str]] = None
excludes: Optional[list[str]] = None,
apply_command_line: Optional[bool] = True
) -> list[Test]:
""" Search available tests """
# Handle defaults, apply possible command line options
Expand All @@ -2887,6 +2929,8 @@ def name_filter(nodes: Iterable[fmf.Tree]) -> list[fmf.Tree]:
""" Filter nodes based on names provided on the command line """
if not cmd_line_names:
return list(nodes)
if not apply_command_line:
return list(nodes)
return [
node for node in nodes
if any(re.search(name, node.name) for name in cmd_line_names)]
Expand Down Expand Up @@ -2943,7 +2987,8 @@ def plans(
conditions: Optional[list[str]] = None,
run: Optional['Run'] = None,
links: Optional[list['LinkNeedle']] = None,
excludes: Optional[list[str]] = None
excludes: Optional[list[str]] = None,
apply_remote_keys: Optional[bool] = True
) -> list[Plan]:
""" Search available plans """
# Handle defaults, apply possible command line options
Expand Down Expand Up @@ -2976,6 +3021,9 @@ def plans(
else:
sources = None

if not apply_remote_keys:
remote_plan_keys = []

# Build the list, convert to objects, sort and filter
plans = [
Plan(
Expand All @@ -3000,7 +3048,6 @@ def plans(

if not Plan._opt('shallow'):
plans = [plan.import_plan() or plan for plan in plans]

return self._filters_conditions(
sorted(plans, key=lambda plan: plan.order),
filters, conditions, links, excludes)
Expand All @@ -3014,7 +3061,8 @@ def stories(
conditions: Optional[list[str]] = None,
whole: bool = False,
links: Optional[list['LinkNeedle']] = None,
excludes: Optional[list[str]] = None
excludes: Optional[list[str]] = None,
apply_keys: Optional[bool] = True
) -> list[Story]:
""" Search available stories """
# Handle defaults, apply possible command line options
Expand Down Expand Up @@ -3046,6 +3094,9 @@ def stories(
else:
sources = None

if not apply_keys:
keys = None

# Build the list, convert to objects, sort and filter
stories = [
Story(node=story, tree=self, logger=logger.descend()) for story
Expand Down
30 changes: 30 additions & 0 deletions tmt/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1160,6 +1160,9 @@ def plans_lint(
@option(
'--finish', metavar='YAML', multiple=True,
help='Finish phase content in yaml format.')
@option(
'--link', metavar='[RELATION:]TARGET', multiple=True,
help='Link to the relevant issues.')
@verbosity_options
@force_dry_options
def plans_create(
Expand Down Expand Up @@ -1345,6 +1348,9 @@ def stories_show(
'-t', '--template', metavar='TEMPLATE',
prompt=f'Template ({_story_templates})',
help=f'Story template ({_story_templates}).')
@option(
'--link', metavar='[RELATION:]TARGET', multiple=True,
help='Link to the relevant issues.')
@verbosity_options
@force_dry_options
def stories_create(
Expand Down Expand Up @@ -2081,3 +2087,27 @@ def completion_fish(context: Context, install: bool, **kwargs: Any) -> None:
Setup shell completions for fish.
"""
setup_completion('fish', install)


@main.command(name='link')
@pass_context
@click.argument('link', nargs=1, metavar='[RELATION:]TARGET')
@click.argument('names', nargs=-1, metavar='[NAME]...')
@option(
'--separate', is_flag=True,
help="Create linking separately for multiple passed objects.")
def link(context: Context,
names: list[str],
link: list[str],
separate: bool,
) -> None:
nodes: list[list['tmt.base.Core']] = []
for name in names:
if context.obj.tree.tests(names=[name]):
nodes.append(context.obj.tree.tests(names=[name]))
if context.obj.tree.plans(names=[name]):
nodes.append(context.obj.tree.plans(names=[name]))
if context.obj.tree.stories(names=[name]):
nodes.append(context.obj.tree.stories(names=[name]))
print(nodes)
tmt.utils.jira_link(nodes, tmt.base.Links(data=link), separate)
73 changes: 73 additions & 0 deletions tmt/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
import urllib3.exceptions
import urllib3.util.retry
from click import echo, style, wrap_text
from jira import JIRA
from ruamel.yaml import YAML, scalarstring
from ruamel.yaml.comments import CommentedMap
from ruamel.yaml.parser import ParserError
Expand Down Expand Up @@ -7270,3 +7271,75 @@ def render_rst(text: str, logger: Logger) -> str:
document.walkabout(visitor)

return visitor.rendered


def jira_link(
nodes: list[list['tmt.base.Core']],
links: 'tmt.base.Links',
separate: Optional[bool] = False) -> None:
""" Link the object to Jira issue and create the URL to tmt web service """

def create_url(linking_config: dict[str, str], tmt_object: 'tmt.base.Core',
append: bool = False, existing_url: Optional[str] = None) -> tuple[str, str]:
# Get the fmf id of the object
if isinstance(tmt_object, tmt.base.Test):
fmfid = tmt_object.fmf_id
tmt_type = "test"
elif isinstance(tmt_object, tmt.base.Plan):
fmfid = tmt_object.fmf_id
tmt_type = "plan"
elif isinstance(tmt_object, tmt.base.Story):
# TODO: not supported by the service yet
fmfid = tmt_object.fmf_id
tmt_type = "story"
if append and existing_url is not None:
url = existing_url
url += f'&{tmt_type}-url={fmfid.url}&{tmt_type}-name={fmfid.name}'
else:
url = f'{linking_config["service"]}?{tmt_type}-url={fmfid.url}&{tmt_type}-name={fmfid.name}'
if fmfid.path is not None:
url += f'&{tmt_type}-path={fmfid.path}'
if fmfid.ref is not None:
url += f'&{tmt_type}-ref={fmfid.ref}'
# Ask for HTML format (WIP, can be changed once FE web server is implemented)
if not append:
url = url + '&format=html'
return url, tmt_type

# Setup config tree
config_tree = tmt.utils.Config()
linking_config = config_tree.fmf_tree.find('/user/linking').data.get('linking')[0]
verifies = links.get('verifies')[0]
target = verifies.to_dict()['target']
# Parse the target url
jira_server_url = 'https://' + target.split('/')[0]
issue_id = target.split('/')[-1]
jira = JIRA(server=jira_server_url, token_auth=linking_config['token'])
service_url: str = ""
for index, node in enumerate(nodes):
# Single object in list of nodes = creating new object
# or linking multiple existing separately
if len(nodes) == 1 or (len(nodes) > 1 and separate):
service_url, obj_type = create_url(linking_config=linking_config, tmt_object=node[0])
link_object = {
"url": service_url,
"title": f"[tmt_web] Metadata of the {obj_type} covering this issue"
}
jira.add_simple_link(issue_id, link_object)
if len(nodes) > 1 and not separate:
if index == 0:
url_part, _ = create_url(linking_config=linking_config, tmt_object=node[0])
service_url = url_part
elif index == len(nodes) - 1:
url_part, _ = create_url(linking_config=linking_config,
tmt_object=node[0], append=True, existing_url=service_url)
service_url = url_part
link_object = {
"url": service_url,
"title": "[tmt_web] Metadata of the tests/plans/stories covering this issue"
}
jira.add_simple_link(issue_id, link_object)
else:
url_part, _ = create_url(linking_config=linking_config,
tmt_object=node[0], append=True, existing_url=service_url)
service_url = url_part

0 comments on commit fe0d5ff

Please sign in to comment.