Skip to content

Commit

Permalink
Support for pagure.io.
Browse files Browse the repository at this point in the history
  • Loading branch information
ralphbean committed Oct 27, 2015
1 parent eb08228 commit 9958d66
Show file tree
Hide file tree
Showing 3 changed files with 333 additions and 0 deletions.
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
228 changes: 228 additions & 0 deletions bugwarrior/services/pagure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import re
import six
import datetime

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'])),
}

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)

0 comments on commit 9958d66

Please sign in to comment.