diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e722d61ad..5246a8909 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,4 @@ +1.2.0 ----- Lots of updates from various contributors: - Enable setuptools test command `d38fad025 `_ - Merge pull request #222 from koobs/patch-2 `7f9cdce9c `_ - Added only_if_assigned to gitlab `0f6fea7fc `_ - Merge pull request #224 from qwertos/feature-gitlab_only_assigned `156b5a908 `_ - Add a taskwarrior UDA for bugzilla status `2be150f6a `_ - Make BZ bug statuses configurable `ac30a2241 `_ - Ooops, add status field to tests `6411e4803 `_ - Merge pull request #226 from ryansb/feature/moarBugzillaStatus `90c81db1b `_ - [notifications] only_on_new_tasks option `b4a67ebfd `_ - Merge pull request #228 from devenv/only_on_new_tasks `89ef3d746 `_ - jira estimate UDA `2317a0516 `_ - Merge pull request #227 from devenv/jira_est `06adc5b16 `_ - Include an option to disable HTTPS for GitLab. `616a389d7 `_ - Support needinfo bugs where you are not CC/assignee/reporter `8ef53be9f `_ - gitlab: work around gitlab pagination bug `4caaa28ed `_ - gitlab: add uda for work-in-progress flag `fe940c268 `_ - githubutils: allow getting a key from the result `28e37218c `_ - github: add involved_issues option `67b93eb6e `_ - gitlab: bail on empty or False results `62008a22d `_ - Only import active Gitlab issues and merge requests `5890fe9ad `_ - Merge pull request #231 from ryansb/feature/needinfos `6722d2b96 `_ - Merge pull request #233 from mathstuf/gitlab-work-in-progress-flag `c4bbd955d `_ - Merge pull request #234 from mathstuf/github-involved-issues `6ff7cfc7d `_ - Merge pull request #235 from LordGaav/feature/close-gitlab-issues `0664bd02c `_ - Merge pull request #232 from mathstuf/handle-broken-gitlab-pagination `1677807bf `_ - Add Gitlab's assignee and author field to tasks `b7dd5c3e2 `_ - Add documentation on UDA fields `c88209063 `_ - Add config option `8c2c8c0c9 `_ - ewwwww, trailing whitespace `c48348fbb `_ - Make comment annotation configurable `1667619bf `_ - Clarify annotating by inverting conditional for `annotation_comments` `31c3ecdd3 `_ - Merge pull request #237 from ryansb/feature/noAnnotations `1887d7095 `_ - Merge pull request #236 from LordGaav/feature/gitlab-author-assignee-field `f84eca72f `_ - Document use_https for gitlab. `5d95424f6 `_ - Merge branch 'https-or-http' into develop `f3b63baf1 `_ Changelog ========= diff --git a/bugwarrior/db.py b/bugwarrior/db.py index 908fa4bc1..cf54935c6 100644 --- a/bugwarrior/db.py +++ b/bugwarrior/db.py @@ -315,7 +315,7 @@ def _bool_option(section, option, default): # Before running CRUD operations, call the pre_import hook(s). run_hooks(conf, 'pre_import') - notify = _bool_option('notifications', 'notifications', 'False') and not dry_run + notify = _bool_option('notifications', 'notifications', False) and not dry_run tw = TaskWarriorShellout( config_filename=get_taskrc_path(conf, main_section), @@ -444,17 +444,19 @@ def _bool_option(section, option, default): # Send notifications if notify: - send_notification( - dict( - description="New: %d, Changed: %d, Completed: %d" % ( - len(issue_updates['new']), - len(issue_updates['changed']), - len(issue_updates['closed']) - ) - ), - 'bw_finished', - conf, - ) + only_on_new_tasks = _bool_option('notifications', 'only_on_new_tasks', False) + if not only_on_new_tasks or len(issue_updates['new']) + len(issue_updates['changed']) + len(issue_updates['closed']) > 0: + send_notification( + dict( + description="New: %d, Changed: %d, Completed: %d" % ( + len(issue_updates['new']), + len(issue_updates['changed']), + len(issue_updates['closed']) + ) + ), + 'bw_finished', + conf, + ) def build_key_list(targets): diff --git a/bugwarrior/docs/common_configuration.rst b/bugwarrior/docs/common_configuration.rst index 99a3f9a80..3b90ef0d9 100644 --- a/bugwarrior/docs/common_configuration.rst +++ b/bugwarrior/docs/common_configuration.rst @@ -15,6 +15,10 @@ Optional options include: * ``shorten``: Set to ``True`` to shorten links. * ``inline_link``: When ``False``, links are appended as an annotation. Defaults to ``True``. +* ``annotation_links``: When ``True`` will include a link to the ticket as an + annotation. Defaults to ``False``. +* ``annotation_comments``: When ``True`` skips putting issue comments into + annotations. Defaults to ``True``. * ``legacy_matching``: Set to ``False`` to instruct Bugwarrior to match issues using only the issue's unique identifiers (rather than matching on description). @@ -158,6 +162,7 @@ by ``bugwarrior-pull``:: backend = growlnotify finished_querying_sticky = False task_crud_sticky = True + only_on_new_tasks = True Backend options: diff --git a/bugwarrior/docs/configuration.rst b/bugwarrior/docs/configuration.rst index 0cb53f49b..93017aa17 100644 --- a/bugwarrior/docs/configuration.rst +++ b/bugwarrior/docs/configuration.rst @@ -8,17 +8,17 @@ Example Configuration :: # Example bugwarriorrc - + # General stuff. [general] # Here you define a comma separated list of targets. Each of them must have a # section below determining their properties, how to query them, etc. The name # is just a symbol, and doesn't have any functional importance. targets = my_github, my_bitbucket, paj_bitbucket, moksha_trac, bz.redhat - + # If unspecified, the default taskwarrior config will be used. #taskrc = /path/to/.taskrc - + # Setting this to true will shorten links with http://da.gd/ #shorten = False @@ -28,24 +28,28 @@ Example Configuration # Setting this to True will include a link to the ticket as an annotation annotation_links = True + # Setting this to True will include issue comments and author name in task + # annotations + annotation_comments = True + # Defines whether or not issues should be matched based upon their description. # For historical reasons, and by default, we will attempt to match issues # based upon the presence of the '(bw)' marker in the task description. # If this is false, we will only select issues having the appropriate UDA # fields defined #legacy_matching=False - + # log.level specifices the verbosity. The default is DEBUG. # log.level can be one of DEBUG, INFO, WARNING, ERROR, CRITICAL, DISABLED #log.level = DEBUG - + # If log.file is specified, output will be redirected there. If it remains # unspecified, output is sent to sys.stderr #log.file = /var/log/bugwarrior.log - + # Configure the default description or annotation length. #annotation_length = 45 - + # Use hooks to run commands prior to importing from bugwarrior-pull. # bugwarrior-pull will run the commands in the order that they are specified # below. @@ -56,7 +60,7 @@ Example Configuration # exit early. [hooks] pre_import = /home/someuser/backup.sh, /home/someuser/sometask.sh - + # This section is for configuring notifications when bugwarrior-pull runs, # and when issues are created, updated, or deleted by bugwarrior-pull. # Three backend are currently suported: @@ -73,8 +77,9 @@ Example Configuration # backend = growlnotify # finished_querying_sticky = False # task_crud_sticky = True - - + # only_on_new_tasks = True + + # This is a github example. It says, "scrape every issue from every repository # on http://github.com/ralphbean. It doesn't matter if ralphbean owns the issue # or not." @@ -82,21 +87,21 @@ Example Configuration service = github default_priority = H add_tags = open_source - + # This specifies that we should pull issues from repositories belonging # to the 'ralphbean' github account. See the note below about # 'github.username' and 'github.login'. They are different, and you need # both. github.username = ralphbean - + # I want taskwarrior to include issues from all my repos, except these # two because they're spammy or something. github.exclude_repos = project_bar,project_baz - + # Working with a large number of projects, instead of excluding most of them I # can also simply include just a limited set. github.include_repos = project_foo,project_foz - + # Note that login and username can be different: I can login as me, but # scrape issues from an organization's repos. # @@ -106,30 +111,30 @@ Example Configuration # issues for. It could be you, or some other user entirely. github.login = ralphbean github.password = OMG_LULZ - - + + # Here's an example of a trac target. [moksha_trac] service = trac - + trac.base_uri = fedorahosted.org/moksha trac.username = ralph trac.password = OMG_LULZ - + only_if_assigned = ralph also_unassigned = True default_priority = H add_tags = work - + # Here's an example of a megaplan target. [my_megaplan] service = megaplan - + megaplan.hostname = example.megaplan.ru megaplan.login = alice megaplan.password = secret megaplan.project_name = example - + # Here's an example of a jira project. The ``jira-python`` module is # a bit particular, and jira deployments, like Bugzilla, tend to be # reasonably customized. So YMMV. The ``base_uri`` must not have a @@ -147,21 +152,21 @@ Example Configuration # the dashboard. jira.version = 5 add_tags = enterprisey work - + # Here's an example of a phabricator target [my_phabricator] service = phabricator # No need to specify credentials. They are gathered from ~/.arcrc - + # Here's an example of a teamlab target. [my_teamlab] service = teamlab - + teamlab.hostname = teamlab.example.com teamlab.login = alice teamlab.password = secret teamlab.project_name = example_teamlab - + # Here's an example of a redmine target. [my_redmine] service = redmine @@ -170,14 +175,14 @@ Example Configuration redmine.user_id = 7 redmine.project_name = redmine add_tags = chiliproject - + [activecollab] service = activecollab activecollab.url = https://ac.example.org/api.php activecollab.key = your-api-key activecollab.user_id = 15 add_tags = php - + [activecollab2] service = activecollab2 activecollab2.url = http://ac.example.org/api.php diff --git a/bugwarrior/docs/services/bugzilla.rst b/bugwarrior/docs/services/bugzilla.rst index 99e82d8e2..32f9ee053 100644 --- a/bugwarrior/docs/services/bugzilla.rst +++ b/bugwarrior/docs/services/bugzilla.rst @@ -39,6 +39,21 @@ There is an option to ignore bugs that you are only cc'd on:: But this will continue to include bugs that you reported, regardless of whether they are assigned to you. +If your bugzilla "actionable" bugs only include ON_QA, FAILS_QA, PASSES_QA, and POST:: + + bugzilla.open_statuses = ON_QA,FAILS_QA,PASSES_QA,POST + +This won't create tasks for bugs in other states. The default open statuses: +"NEW,ASSIGNED,NEEDINFO,ON_DEV,MODIFIED,POST,REOPENED,ON_QA,FAILS_QA,PASSES_QA" + +If you're on a more recent Bugzilla install, the NEEDINFO status no longer +exists, and has been replaced by the "needinfo?" flag. Set +"bugzilla.include_needinfos" to "True" to have taskwarrior also add bugs where +information is requested of you. The "bugzillaneedinfo" UDA will be filled in +with the date the needinfo was set. + +To see all your needinfo bugs, you can use "task bugzillaneedinfo.any: list". + If the filtering options are not sufficient to find the set of bugs you'd like, you can tell Bugwarrior exactly which bugs to sync by pasting a full query URL from your browser into the ``bugzilla.query_url`` option:: @@ -48,12 +63,16 @@ from your browser into the ``bugzilla.query_url`` option:: Provided UDA Fields ------------------- -+---------------------+---------------------+---------------------+ -| Field Name | Description | Type | -+=====================+=====================+=====================+ -| ``bugzillasummary`` | Summary | Text (string) | -+---------------------+---------------------+---------------------+ -| ``bugzillaurl`` | URL | Text (string) | -+---------------------+---------------------+---------------------+ -| ``bugzillabugid`` | Bug ID | Numeric (integer) | -+---------------------+---------------------+---------------------+ ++----------------------+---------------------+---------------------+ +| Field Name | Description | Type | ++======================+=====================+=====================+ +| ``bugzillasummary`` | Summary | Text (string) | ++----------------------+---------------------+---------------------+ +| ``bugzillaurl`` | URL | Text (string) | ++----------------------+---------------------+---------------------+ +| ``bugzillabugid`` | Bug ID | Numeric (integer) | ++----------------------+---------------------+---------------------+ +| ``bugzillastatus`` | Bugzilla Status | Text (string) | ++----------------------+---------------------+---------------------+ +| ``bugzillaneedinfo`` | Needinfo | Date | ++----------------------+---------------------+---------------------+ diff --git a/bugwarrior/docs/services/github.rst b/bugwarrior/docs/services/github.rst index 0c9df14dc..501b3d8d7 100644 --- a/bugwarrior/docs/services/github.rst +++ b/bugwarrior/docs/services/github.rst @@ -94,6 +94,17 @@ by adding the following configuration option:: github.filter_pull_requests = True +Get involved issues ++++++++++++++++++++ + +Instead of fetching issues and pull requests based on ``{{username}}``'s owned +repositories, you may instead get those that ``{{username}}`` is involved in. +This includes all issues and pull requests where the user is the author, the +assignee, mentioned in, or has commented on. To do so, add the following +configuration option:: + + github.involved_issues = True + Provided UDA Fields ------------------- diff --git a/bugwarrior/docs/services/gitlab.rst b/bugwarrior/docs/services/gitlab.rst index dcc04b264..653bd154e 100644 --- a/bugwarrior/docs/services/gitlab.rst +++ b/bugwarrior/docs/services/gitlab.rst @@ -82,6 +82,14 @@ by adding the following configuration option:: gitlab.filter_merge_requests = True +Use HTTP +++++++++ + +If your Gitlab instance is only available over HTTP, set:: + + gitlab.use_https = False + + Provided UDA Fields ------------------- @@ -110,3 +118,9 @@ Provided UDA Fields +-----------------------+-----------------------+---------------------+ | ``gitlabdownvotes`` | Number of downvotes | Numeric | +-----------------------+-----------------------+---------------------+ +| ``gitlabwip`` | Work-in-Progress flag | Numeric | ++-----------------------+-----------------------+---------------------+ +| ``gitlabauthor`` | Issue/MR author | Text (string) | ++-----------------------+-----------------------+---------------------+ +| ``gitlabassignee`` | Issue/MR assignee | Text (string) | ++-----------------------+-----------------------+---------------------+ diff --git a/bugwarrior/docs/services/jira.rst b/bugwarrior/docs/services/jira.rst index 6a7eeb448..0efb3d62e 100644 --- a/bugwarrior/docs/services/jira.rst +++ b/bugwarrior/docs/services/jira.rst @@ -109,3 +109,5 @@ Provided UDA Fields +---------------------+---------------------+---------------------+ | ``jiraurl`` | URL | Text (string) | +---------------------+---------------------+---------------------+ +| ``jiraestimate`` | Estimate | Decimal (numeric) | ++---------------------+---------------------+---------------------+ diff --git a/bugwarrior/services/__init__.py b/bugwarrior/services/__init__.py index 687c59bdb..bb01f61fe 100644 --- a/bugwarrior/services/__init__.py +++ b/bugwarrior/services/__init__.py @@ -75,6 +75,12 @@ def __init__(self, config, main_section, target): config.get(self.main_section, 'annotation_links') ) + self.annotation_comments = True + if config.has_option(self.main_section, 'annotation_comments'): + self.annotation_comments = asbool( + config.get(self.main_section, 'annotation_comments') + ) + self.shorten = False if config.has_option(self.main_section, 'shorten'): self.shorten = asbool(config.get(self.main_section, 'shorten')) @@ -165,18 +171,19 @@ def build_annotations(self, annotations, url): final = [] if self.annotation_links: final.append(url) - for author, message in annotations: - message = message.strip() - if not message or not author: - continue - message = message.replace('\n', '').replace('\r', '') - final.append( - '@%s - %s%s' % ( - author, - message[0:self.anno_len], - '...' if len(message) > self.anno_len else '' + if self.annotation_comments: + for author, message in annotations: + message = message.strip() + if not message or not author: + continue + message = message.replace('\n', '').replace('\r', '') + final.append( + '@%s - %s%s' % ( + author, + message[0:self.anno_len], + '...' if len(message) > self.anno_len else '' + ) ) - ) return final @classmethod diff --git a/bugwarrior/services/bz.py b/bugwarrior/services/bz.py index 33c475619..791b499df 100644 --- a/bugwarrior/services/bz.py +++ b/bugwarrior/services/bz.py @@ -1,6 +1,9 @@ import bugzilla from twiggy import log +import time +import pytz +import datetime import six from bugwarrior.config import die, asbool, get_service_password @@ -11,6 +14,8 @@ class BugzillaIssue(Issue): URL = 'bugzillaurl' SUMMARY = 'bugzillasummary' BUG_ID = 'bugzillabugid' + STATUS = 'bugzillastatus' + NEEDINFO = 'bugzillaneedinfo' UDAS = { URL: { @@ -21,10 +26,18 @@ class BugzillaIssue(Issue): 'type': 'string', 'label': 'Bugzilla Summary', }, + STATUS: { + 'type': 'string', + 'label': 'Bugzilla Status', + }, BUG_ID: { 'type': 'numeric', 'label': 'Bugzilla Bug ID', }, + NEEDINFO: { + 'type': 'date', + 'label': 'Bugzilla Needinfo', + }, } UNIQUE_KEY = (URL, ) @@ -37,7 +50,7 @@ class BugzillaIssue(Issue): } def to_taskwarrior(self): - return { + task = { 'project': self.record['component'], 'priority': self.get_priority(), 'annotations': self.extra.get('annotations', []), @@ -45,7 +58,12 @@ def to_taskwarrior(self): self.URL: self.extra['url'], self.SUMMARY: self.record['summary'], self.BUG_ID: self.record['id'], + self.STATUS: self.record['status'], } + if self.extra.get('needinfo_since', None) is not None: + task[self.NEEDINFO] = self.extra.get('needinfo_since') + + return task def get_default_description(self): return self.build_default_description( @@ -56,27 +74,31 @@ def get_default_description(self): ) +_open_statuses = [ + 'NEW', + 'ASSIGNED', + 'NEEDINFO', + 'ON_DEV', + 'MODIFIED', + 'POST', + 'REOPENED', + 'ON_QA', + 'FAILS_QA', + 'PASSES_QA', +] + + class BugzillaService(IssueService): ISSUE_CLASS = BugzillaIssue CONFIG_PREFIX = 'bugzilla' - OPEN_STATUSES = [ - 'NEW', - 'ASSIGNED', - 'NEEDINFO', - 'ON_DEV', - 'MODIFIED', - 'POST', - 'REOPENED', - 'ON_QA', - 'FAILS_QA', - 'PASSES_QA', - ] COLUMN_LIST = [ 'id', + 'status', 'summary', 'priority', 'component', + 'flags', 'longdescs', ] @@ -88,6 +110,11 @@ def __init__(self, *args, **kw): self.ignore_cc = self.config_get_default('ignore_cc', default=False, to_type=lambda x: x == "True") self.query_url = self.config_get_default('query_url', default=None) + self.include_needinfos = self.config_get_default( + 'include_needinfos', False, to_type=lambda x: x == "True") + self.open_statuses = self.config_get_default( + 'open_statuses', _open_statuses, to_type=lambda x: x.split(',')) + log.name(self.target).debug(" filtering on statuses: {0}", self.open_statuses) # So more modern bugzilla's require that we specify # query_format=advanced along with the xmlrpc request. @@ -176,7 +203,7 @@ def issues(self): else: query = dict( column_list=self.COLUMN_LIST, - bug_status=self.OPEN_STATUSES, + bug_status=self.open_statuses, email1=email, emailreporter1=1, emailassigned_to1=1, @@ -193,6 +220,19 @@ def issues(self): query['query_format'] = 'advanced' bugs = self.bz.query(query) + + if self.include_needinfos: + needinfos = self.bz.query(dict( + column_list=self.COLUMN_LIST, + quicksearch='flag:needinfo?%s' % email, + )) + exists = [b.id for b in bugs] + for bug in needinfos: + # don't double-add bugs that have already been found + if bug.id in exists: + continue + bugs.append(bug) + # Convert to dicts bugs = [ dict( @@ -211,6 +251,19 @@ def issues(self): 'url': base_url + six.text_type(issue['id']), 'annotations': self.annotations(tag, issue, issue_obj), } + + needinfos = filter(lambda f: ( f['name'] == 'needinfo' + and f['status'] == '?' + and f['requestee'] == self.username), + issue['flags']) + if needinfos: + last_mod = needinfos[0]['modification_date'] + # convert from RPC DateTime string to datetime.datetime object + mod_date = datetime.datetime.fromtimestamp( + time.mktime(last_mod.timetuple())) + + extra['needinfo_since'] = pytz.UTC.localize(mod_date) + issue_obj.update_extra(extra) yield issue_obj diff --git a/bugwarrior/services/github.py b/bugwarrior/services/github.py index c11b2825c..e70c9af91 100644 --- a/bugwarrior/services/github.py +++ b/bugwarrior/services/github.py @@ -177,6 +177,9 @@ def __init__(self, *args, **kw): self.filter_pull_requests = self.config_get_default( 'filter_pull_requests', default=False, to_type=asbool ) + self.involved_issues = self.config_get_default( + 'involved_issues', default=False, to_type=asbool + ) @classmethod def get_keyring_service(cls, config, section): @@ -197,6 +200,18 @@ def get_owned_repo_issues(self, tag): issues[issue['url']] = (tag, issue) return issues + def get_involved_issues(self, user): + """ Grab all 'interesting' issues """ + issues = {} + for issue in githubutils.get_involved_issues(user, auth=self.auth): + url = issue['html_url'] + tag = re.match('.*github\\.com/(.*)/(issues|pull)/[^/]*$', url) + if tag is None: + log.name(self.target).critical(" Unrecognized issue URL: {0}.", url) + continue + issues[url] = (tag.group(1), issue) + return issues + def get_directly_assigned_issues(self): project_matcher = re.compile( r'.*/repos/(?P[^/]+)/(?P[^/]+)/.*' @@ -264,10 +279,15 @@ def issues(self): repos = filter(self.filter_repos, all_repos) issues = {} - for repo in repos: + if self.involved_issues: issues.update( - self.get_owned_repo_issues(user + "/" + repo['name']) + self.get_involved_issues(user) ) + else: + for repo in repos: + issues.update( + self.get_owned_repo_issues(user + "/" + repo['name']) + ) issues.update(self.get_directly_assigned_issues()) log.name(self.target).debug(" Found {0} issues.", len(issues)) issues = filter(self.include, issues.values()) diff --git a/bugwarrior/services/githubutils.py b/bugwarrior/services/githubutils.py index 417685a05..4fd6f7139 100644 --- a/bugwarrior/services/githubutils.py +++ b/bugwarrior/services/githubutils.py @@ -34,6 +34,16 @@ def get_repos(username, auth): return _getter(url, auth) +def get_involved_issues(username, auth): + """ username should be a string + auth should be a tuple of username and password. + """ + + tmpl = "https://api.github.com/search/issues?q=involves%3A{username}&per_page=100" + url = tmpl.format(username=username) + return _getter(url, auth, subkey='items') + + def get_issues(username, repo, auth): """ username and repo should be strings auth should be a tuple of username and password. @@ -73,7 +83,7 @@ def get_pulls(username, repo, auth): return _getter(url, auth) -def _getter(url, auth): +def _getter(url, auth, subkey=None): """ Pagination utility. Obnoxious. """ kwargs = {} @@ -98,10 +108,15 @@ def _getter(url, auth): if callable(response.json): # Newer python-requests - results += response.json() + json_res = response.json() else: # Older python-requests - results += response.json + json_res = response.json + + if subkey is not None: + json_res = json_res[subkey] + + results += json_res link = _link_field_to_dict(response.headers.get('link', None)) diff --git a/bugwarrior/services/gitlab.py b/bugwarrior/services/gitlab.py index 7f76383ab..709af4f3d 100644 --- a/bugwarrior/services/gitlab.py +++ b/bugwarrior/services/gitlab.py @@ -22,6 +22,9 @@ class GitlabIssue(Issue): STATE = 'gitlabstate' UPVOTES = 'gitlabupvotes' DOWNVOTES = 'gitlabdownvotes' + WORK_IN_PROGRESS = 'gitlabwip' + AUTHOR = 'gitlabauthor' + ASSIGNEE = 'gitlabassignee' UDAS = { TITLE: { @@ -72,6 +75,18 @@ class GitlabIssue(Issue): 'type': 'numeric', 'label': 'Gitlab Downvotes', }, + WORK_IN_PROGRESS: { + 'type': 'numeric', + 'label': 'Gitlab MR Work-In-Progress Flag', + }, + AUTHOR: { + 'type': 'string', + 'label': 'Gitlab Author', + }, + ASSIGNEE: { + 'type': 'string', + 'label': 'Gitlab Assignee', + }, } UNIQUE_KEY = (REPO, TYPE, NUMBER,) @@ -87,6 +102,9 @@ def to_taskwarrior(self): state = self.record['state'] upvotes = self.record['upvotes'] downvotes = self.record['downvotes'] + work_in_progress = self.record.get('work_in_progress', 0) + author = self.record['author'] + assignee = self.record['assignee'] else: priority = self.origin['default_priority'] milestone = self.record['milestone'] @@ -95,6 +113,9 @@ def to_taskwarrior(self): state = self.record['state'] upvotes = 0 downvotes = 0 + work_in_progress = 0 + author = self.record['author'] + assignee = self.record['assignee'] if milestone: milestone = milestone['title'] @@ -102,6 +123,10 @@ def to_taskwarrior(self): created = self.parse_date(created) if updated: updated = self.parse_date(updated) + if author: + author = author['username'] + if assignee: + assignee = assignee['username'] return { 'project': self.extra['project'], @@ -121,6 +146,9 @@ def to_taskwarrior(self): self.STATE: state, self.UPVOTES: upvotes, self.DOWNVOTES: downvotes, + self.WORK_IN_PROGRESS: work_in_progress, + self.AUTHOR: author, + self.ASSIGNEE: assignee, } def get_tags(self): @@ -170,6 +198,11 @@ def __init__(self, *args, **kw): ) self.auth = (host, token) + if self.config_get_default('use_https', default=True, to_type=asbool): + self.scheme = 'https' + else: + self.scheme = 'http' + self.exclude_repos = [] if self.config_get_default('exclude_repos', None): self.exclude_repos = [ @@ -205,6 +238,11 @@ def get_service_metadata(self): 'label_template': self.label_template, } + + def get_owner(self, issue): + if issue[1]['assignee'] != None and issue[1]['assignee']['username']: + return issue[1]['assignee']['username'] + def filter_repos(self, repo): if self.exclude_repos: if repo['path_with_namespace'] in self.exclude_repos: @@ -219,7 +257,7 @@ def filter_repos(self, repo): return True def _get_notes(self, rid, issue_type, issueid): - tmpl = 'https://{host}/api/v3/projects/%d/%s/%d/notes' % (rid, issue_type, issueid) + tmpl = '{scheme}://{host}/api/v3/projects/%d/%s/%d/notes' % (rid, issue_type, issueid) return self._fetch_paged(tmpl) def annotations(self, repo, url, issue_type, issue, issue_obj): @@ -233,7 +271,7 @@ def annotations(self, repo, url, issue_type, issue, issue_obj): ) def _fetch(self, tmpl, **kwargs): - url = tmpl.format(host=self.auth[0]) + url = tmpl.format(scheme=self.scheme, host=self.auth[0]) headers = {'PRIVATE-TOKEN': self.auth[1]} response = requests.get(url, headers=headers, **kwargs) @@ -255,9 +293,23 @@ def _fetch_paged(self, tmpl): } full = [] + detect_broken_gitlab_pagination = [] while True: items = self._fetch(tmpl, params=params) + if not items: + break + + # XXX: Some gitlab versions have a bug where pagination doesn't + # work and instead return the entire result no matter what. Detect + # this by seeing if the results are the same as the last time + # around and bail if so. Unfortunately, while it is a GitLab bug, + # we have to deal with instances where it exists. + if items == detect_broken_gitlab_pagination: + break + detect_broken_gitlab_pagination = items + full += items + if len(items) < params['per_page']: break params['page'] += 1 @@ -265,21 +317,25 @@ def _fetch_paged(self, tmpl): return full def get_repo_issues(self, rid): - tmpl = 'https://{host}/api/v3/projects/%d/issues' % rid + tmpl = '{scheme}://{host}/api/v3/projects/%d/issues' % rid issues = {} for issue in self._fetch_paged(tmpl): + if issue['state'] != 'opened': + continue issues[issue['id']] = (rid, issue) return issues def get_repo_merge_requests(self, rid): - tmpl = 'https://{host}/api/v3/projects/%d/merge_requests' % rid + tmpl = '{scheme}://{host}/api/v3/projects/%d/merge_requests' % rid issues = {} for issue in self._fetch_paged(tmpl): + if issue['state'] != 'opened': + continue issues[issue['id']] = (rid, issue) return issues def issues(self): - tmpl = 'https://{host}/api/v3/projects' + tmpl = '{scheme}://{host}/api/v3/projects' all_repos = self._fetch_paged(tmpl) repos = filter(self.filter_repos, all_repos) diff --git a/bugwarrior/services/jira.py b/bugwarrior/services/jira.py index 35e1ef27f..a5a116b52 100644 --- a/bugwarrior/services/jira.py +++ b/bugwarrior/services/jira.py @@ -13,6 +13,7 @@ class JiraIssue(Issue): URL = 'jiraurl' FOREIGN_ID = 'jiraid' DESCRIPTION = 'jiradescription' + ESTIMATE = 'jiraestimate' UDAS = { SUMMARY: { @@ -30,6 +31,10 @@ class JiraIssue(Issue): FOREIGN_ID: { 'type': 'string', 'label': 'Jira Issue ID' + }, + ESTIMATE: { + 'type': 'numeric', + 'label': 'Estimate' } } UNIQUE_KEY = (URL, ) @@ -53,6 +58,7 @@ def to_taskwarrior(self): self.FOREIGN_ID: self.record['key'], self.DESCRIPTION: self.record.get('fields', {}).get('description'), self.SUMMARY: self.get_summary(), + self.ESTIMATE: self.get_estimate(), } def get_tags(self): @@ -91,6 +97,14 @@ def get_summary(self): return self.record['fields']['summary']['value'] return self.record['fields']['summary'] + def get_estimate(self): + if self.extra.get('jira_version') == 4: + return self.record['fields']['timeestimate']['value'] + try: + return self.record['fields']['timeestimate'] / 60 / 60 + except (TypeError, KeyError): + return None + def get_priority(self): value = self.record['fields'].get('priority') if isinstance(value, dict): diff --git a/setup.py b/setup.py index 51e5fb8cf..9ba19d361 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -version = '1.1.4' +version = '1.2.0' f = open('bugwarrior/README.rst') long_description = f.read().strip() @@ -52,6 +52,7 @@ "jira>=0.22", "megaplan>=1.4", ], + test_suite='nose.collector', entry_points=""" [console_scripts] bugwarrior-pull = bugwarrior:pull diff --git a/tests/test_bugzilla.py b/tests/test_bugzilla.py index 8b63315a0..c4981cb6f 100644 --- a/tests/test_bugzilla.py +++ b/tests/test_bugzilla.py @@ -20,6 +20,7 @@ def test_to_taskwarrior(self): arbitrary_record = { 'component': 'Something', 'priority': 'urgent', + 'status': 'NEW', 'summary': 'This is the issue summary', 'id': 1234567, } @@ -40,6 +41,7 @@ def test_to_taskwarrior(self): 'priority': issue.PRIORITY_MAP[arbitrary_record['priority']], 'annotations': arbitrary_extra['annotations'], + issue.STATUS: arbitrary_record['status'], issue.URL: arbitrary_extra['url'], issue.SUMMARY: arbitrary_record['summary'], issue.BUG_ID: arbitrary_record['id'] diff --git a/tests/test_gitlab.py b/tests/test_gitlab.py index 2fe974195..ccb90d645 100644 --- a/tests/test_gitlab.py +++ b/tests/test_gitlab.py @@ -54,7 +54,8 @@ class TestGitlabIssue(ServiceTest): }, "state": "opened", "updated_at": arbitrary_updated.isoformat(), - "created_at": arbitrary_created.isoformat() + "created_at": arbitrary_created.isoformat(), + "work_in_progress": True } arbitrary_extra = { 'issue_url': 'https://gitlab.example.com/arbitrary_username/project/issues/3', @@ -98,6 +99,7 @@ def test_to_taskwarrior(self): issue.MILESTONE: self.arbitrary_issue['milestone']['title'], issue.UPVOTES: 0, issue.DOWNVOTES: 0, + issue.WORK_IN_PROGRESS: 0, } actual_output = issue.to_taskwarrior() diff --git a/tests/test_jira.py b/tests/test_jira.py index af988d260..14bd040b2 100644 --- a/tests/test_jira.py +++ b/tests/test_jira.py @@ -21,10 +21,12 @@ def test_to_taskwarrior(self): arbitrary_id = '10' arbitrary_url = 'http://one' arbitrary_summary = 'lkjaldsfjaldf' + arbitrary_estimation = 3600 arbitrary_record = { 'fields': { 'priority': 'Blocker', 'summary': arbitrary_summary, + 'timeestimate': arbitrary_estimation, }, 'key': '%s-%s' % (arbitrary_project, arbitrary_id, ), } @@ -49,6 +51,7 @@ def test_to_taskwarrior(self): issue.FOREIGN_ID: arbitrary_record['key'], issue.SUMMARY: arbitrary_summary, issue.DESCRIPTION: None, + issue.ESTIMATE: arbitrary_estimation / 60 / 60, } def get_url(*args):