-
Notifications
You must be signed in to change notification settings - Fork 209
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
333 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | | ||
+-----------------------+---------------------+---------------------+ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |