Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for pagure.io. #252

Merged
merged 2 commits into from
Oct 28, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions bugwarrior/docs/services/pagure.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
Pagure
======

You can import tasks from your private or public `pagure <https://pagure.io>`_
instance using the ``pagure`` service name.

Example Service
---------------

Here's an example of a Pagure target::

[my_issue_tracker]
service = pagure
pagure.tag = releng
pagure.base_url = https://pagure.io

The above example is the minimum required to import issues from
Pagure. You can also feel free to use any of the
configuration options described in :ref:`common_configuration_options`
or described in `Service Features`_ below.

Note that **either** ``pagure.tag`` or ``pagure.repo`` is required.

- ``pagure.tag`` offers a flexible way to import issues from many pagure repos.
It will include issues from *every* repo on the pagure instance that is
*tagged* with the specified tag. It is similar in usage to a github
"organization". In the example above, the entry will pull issues from all
"releng" pagure repos.
- ``pagure.repo`` offers a simple way to import issues from a single pagure repo.

Note -- no authentication tokens are needed to pull issues from pagure.

Service Features
----------------

Include and Exclude Certain Repositories
++++++++++++++++++++++++++++++++++++++++

If you happen to be working with a large number of projects, you
may want to pull issues from only a subset of your repositories. To
do that, you can use the ``pagure.include_repos`` option.

For example, if you would like to only pull-in issues from
your ``project_foo`` and ``project_fox`` repositories, you could add
this line to your service configuration::

pagure.tag = fedora-infra
pagure.include_repos = project_foo,project_fox

Alternatively, if you have a particularly noisy repository, you can
instead choose to import all issues excepting it using the
``pagure.exclude_repos`` configuration option.

In this example, ``noisy_repository`` is the repository you would
*not* like issues created for::

pagure.tag = fedora-infra
pagure.exclude_repos = noisy_repository

Import Labels as Tags
+++++++++++++++++++++

The Pagure issue tracker allows you to attach tags to issues; to
use those pagure tags as taskwarrior tags, you can use the
``pagure.import_tags`` option::

github.import_tags = True

Also, if you would like to control how these taskwarrior tags are created, you
can specify a template used for converting the Pagure tag into a Taskwarrior
tag.

For example, to prefix all incoming labels with the string 'pagure_' (perhaps
to differentiate them from any existing tags you might have), you could
add the following configuration option::

pagure.label_template = pagure_{{label}}

In addition to the context variable ``{{label}}``, you also have access
to all fields on the Taskwarrior task if needed.

.. note::

See :ref:`field_templates` for more details regarding how templates
are processed.

Provided UDA Fields
-------------------

+-----------------------+---------------------+---------------------+
| Field Name | Description | Type |
+=======================+=====================+=====================+
| ``paguredatecreated`` | Created | Date & Time |
+-----------------------+---------------------+---------------------+
| ``pagurenumber`` | Issue/PR # | Numeric |
+-----------------------+---------------------+---------------------+
| ``paguretitle`` | Title | Text (string) |
+-----------------------+---------------------+---------------------+
| ``paguretype`` | Type | Text (string) |
+-----------------------+---------------------+---------------------+
| ``pagureurl`` | URL | Text (string) |
+-----------------------+---------------------+---------------------+
| ``pagurerepo`` | username/reponame | Text (string) |
+-----------------------+---------------------+---------------------+
1 change: 1 addition & 0 deletions bugwarrior/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
'megaplan': 'bugwarrior.services.megaplan:MegaplanService',
'phabricator': 'bugwarrior.services.phab:PhabricatorService',
'versionone': 'bugwarrior.services.versionone:VersionOneService',
'pagure': 'bugwarrior.services.pagure:PagureService',
})


Expand Down
229 changes: 229 additions & 0 deletions bugwarrior/services/pagure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import re
import six
import datetime
import pytz

import requests

from jinja2 import Template
from twiggy import log

from bugwarrior.config import asbool, die
from bugwarrior.services import IssueService, Issue

class PagureIssue(Issue):
TITLE = 'paguretitle'
DATE_CREATED = 'paguredatecreated'
URL = 'pagureurl'
REPO = 'pagurerepo'
TYPE = 'paguretype'
ID = 'pagureid'

UDAS = {
TITLE: {
'type': 'string',
'label': 'Pagure Title',
},
DATE_CREATED: {
'type': 'date',
'label': 'Pagure Created',
},
REPO: {
'type': 'string',
'label': 'Pagure Repo Slug',
},
URL: {
'type': 'string',
'label': 'Pagure URL',
},
TYPE: {
'type': 'string',
'label': 'Pagure Type',
},
ID: {
'type': 'numeric',
'label': 'Pagure Issue/PR #',
},
}
UNIQUE_KEY = (URL, TYPE,)

def _normalize_label_to_tag(self, label):
return re.sub(r'[^a-zA-Z0-9]', '_', label)

def to_taskwarrior(self):
if self.extra['type'] == 'pull_request':
priority = 'H'
else:
priority = self.origin['default_priority']

return {
'project': self.extra['project'],
'priority': priority,
'annotations': self.extra.get('annotations', []),
'tags': self.get_tags(),

self.URL: self.record['html_url'],
self.REPO: self.record['repo'],
self.TYPE: self.extra['type'],
self.TITLE: self.record['title'],
self.ID: self.record['id'],
self.DATE_CREATED: datetime.datetime.fromtimestamp(
int(self.record['date_created']), pytz.UTC),
}

def get_tags(self):
tags = []

if not self.origin['import_tags']:
return tags

context = self.record.copy()
tag_template = Template(self.origin['tag_template'])

for tagname in self.record.get('tags', []):
context.update({'label': self._normalize_label_to_tag(tagname) })
tags.append(tag_template.render(context))

return tags

def get_default_description(self):
return self.build_default_description(
title=self.record['title'],
url=self.get_processed_url(self.record['html_url']),
number=self.record['id'],
cls=self.extra['type'],
)


class PagureService(IssueService):
ISSUE_CLASS = PagureIssue
CONFIG_PREFIX = 'pagure'

def __init__(self, *args, **kw):
super(PagureService, self).__init__(*args, **kw)

self.auth = {}

self.tag = self.config_get_default('tag')
self.repo = self.config_get_default('repo')
self.base_url = self.config_get_default('base_url')

self.exclude_repos = []
if self.config_get_default('exclude_repos', None):
self.exclude_repos = [
item.strip() for item in
self.config_get('exclude_repos').strip().split(',')
]

self.include_repos = []
if self.config_get_default('include_repos', None):
self.include_repos = [
item.strip() for item in
self.config_get('include_repos').strip().split(',')
]

self.import_tags = self.config_get_default(
'import_tags', default=False, to_type=asbool
)
self.tag_template = self.config_get_default(
'tag_template', default='{{label}}', to_type=six.text_type
)

def get_service_metadata(self):
return {
'import_tags': self.import_tags,
'tag_template': self.tag_template,
}

def get_issues(self, repo, keys):
""" Grab all the issues """
key1, key2 = keys
key3 = key1[:-1] # Just the singular form of key1

url = self.base_url + "/api/0/" + repo + "/" + key1
response = requests.get(url)

if not bool(response):
raise IOError('Failed to talk to %r %r' % (url, response))

issues = []
for result in response.json()[key2]:
idx = six.text_type(result['id'])
result['html_url'] = "/".join([self.base_url, repo, key3, idx])
issues.append((repo, result))

return issues

def annotations(self, issue, issue_obj):
url = issue['html_url']
return self.build_annotations(
((
c['user']['name'],
c['comment'],
) for c in issue['comments']),
issue_obj.get_processed_url(url)
)

def get_owner(self, issue):
if issue[1]['assignee']:
return issue[1]['assignee']['name']

def filter_repos(self, repo):
if self.exclude_repos:
if repo in self.exclude_repos:
return False

if self.include_repos:
if repo in self.include_repos:
return True
else:
return False

return True

def issues(self):
if self.tag:
url = self.base_url + "/api/0/projects?tags=" + self.tag
response = requests.get(url)
if not bool(response):
raise IOError('Failed to talk to %r %r' % (url, response))

all_repos = [r['name'] for r in response.json()['projects']]
else:
all_repos = [self.repo]

repos = filter(self.filter_repos, all_repos)

issues = []
for repo in repos:
issues.extend(self.get_issues(repo, ('issues', 'issues')))
issues.extend(self.get_issues(repo, ('pull-requests', 'requests')))

log.name(self.target).debug(" Found {0} issues.", len(issues))
issues = filter(self.include, issues)
log.name(self.target).debug(" Pruned down to {0} issues.", len(issues))

for repo, issue in issues:
# Stuff this value into the upstream dict for:
# https://pagure.com/ralphbean/bugwarrior/issues/159
issue['repo'] = repo

issue_obj = self.get_issue_for_record(issue)
extra = {
'project': repo,
'type': 'pull_request' if 'branch' in issue else 'issue',
'annotations': self.annotations(issue, issue_obj)
}
issue_obj.update_extra(extra)
yield issue_obj

@classmethod
def validate_config(cls, config, target):
if not config.has_option(target, 'pagure.tag') and \
not config.has_option(target, 'pagure.repo'):
die("[%s] has no 'pagure.tag' or 'pagure.repo'" % target)

if not config.has_option(target, 'pagure.base_url'):
die("[%s] has no 'pagure.base_url'" % target)

super(PagureService, cls).validate_config(config, target)