pax_global_header00006660000000000000000000000064137647124660014532gustar00rootroot0000000000000052 comment=809f3a56bd74a420ff430de02f620c6bc52786e6 bugwarrior-1.8.0/000077500000000000000000000000001376471246600137235ustar00rootroot00000000000000bugwarrior-1.8.0/.codecov.yml000066400000000000000000000000171376471246600161440ustar00rootroot00000000000000comment: false bugwarrior-1.8.0/.gitignore000066400000000000000000000002141376471246600157100ustar00rootroot00000000000000*.pyc *.swp *.pyo .build build *.egg-info dist test.html *.pdf bin/* etc/* lib/* include/* share/* man/* .Python *.egg* *.idea .tox/* *.bak bugwarrior-1.8.0/.travis.yml000066400000000000000000000020021376471246600160260ustar00rootroot00000000000000language: python dist: bionic arch: - AMD64 - ppc64le python: - "3.7" - "3.6" before_install: - sudo apt-get update -qq - sudo apt-get install uuid-dev - wget http://taskwarrior.org/download/task-2.4.4.tar.gz - gunzip task-2.4.4.tar.gz - tar xf task-2.4.4.tar - cd task-2.4.4 - cmake . - make - sudo make install - sudo apt-get install -qq pandoc - task --version - pip install --upgrade pip - pip install taskw - pip install responses - pip install PySimpleSOAP - pip install phabricator - | if [[ "$TRAVIS_CPU_ARCH" == "ppc64le" ]]; then sudo chown -Rv $USER:$GROUP ~/.cache/pip/wheels fi - cd $TRAVIS_BUILD_DIR env: - JIRAVERSION=2.0.0 - JIRAVERSION=1.0.10 matrix: exclude: - python: "3.7" env: JIRAVERSION=1.0.10 install: - pip install .[jira,megaplan,activecollab,bts,bugzilla,trac,gmail] - pip install codecov - pip install coverage script: nosetests -w tests --with-coverage --cover-branches --cover-package=bugwarrior -v after_success: codecov bugwarrior-1.8.0/CHANGELOG.rst000066400000000000000000004347521376471246600157630ustar00rootroot00000000000000 1.7.0 ----- Pull Requests - (@jspricke) #610, Ignore closed sprints for due date https://github.com/ralphbean/bugwarrior/pull/610 - (@ssbarnea) #618, Fix tests on py27 due to configparser https://github.com/ralphbean/bugwarrior/pull/618 - (@pawamoy) #621, Complete tasks with end value, add state and closed UDAs to GitHub https://github.com/ralphbean/bugwarrior/pull/621 - (@jspricke) #630, Add Debian BTS to the Readme https://github.com/ralphbean/bugwarrior/pull/630 - (@mattcen) #629, Add support for Trello due dates https://github.com/ralphbean/bugwarrior/pull/629 - (@Fongshway) #633, [JIRA] Add status and issue type UDAs https://github.com/ralphbean/bugwarrior/pull/633 - (@ssbarnea) #497, Unpin futures to avoid dependency conflicts https://github.com/ralphbean/bugwarrior/pull/497 - (@gustavoheinz) #646, Fix config exceptions https://github.com/ralphbean/bugwarrior/pull/646 - (@mathiashls) #648, optionally set bug to "started" when assigned https://github.com/ralphbean/bugwarrior/pull/648 - (@ralphbean) #647, Update python3 versions. https://github.com/ralphbean/bugwarrior/pull/647 - (@danihodovic) #642, Add the ability to exclude Github pull-requests https://github.com/ralphbean/bugwarrior/pull/642 - (@gustavoheinz) #649, Update gitlab project requests https://github.com/ralphbean/bugwarrior/pull/649 - (@mathiashls) #650, Reduce redundant code in services/gitlab.py https://github.com/ralphbean/bugwarrior/pull/650 - (@ralphbean) #651, Use explicit due date from jira, if set. https://github.com/ralphbean/bugwarrior/pull/651 - (@stashlukj) #645, jira: split priority.name for lookup https://github.com/ralphbean/bugwarrior/pull/645 - (@cure) #657, Do not abort with ValueError when default_permission is blank https://github.com/ralphbean/bugwarrior/pull/657 - (@ralphbean) #653, GitHub: Only query for user repos if an include_repos list is not specified https://github.com/ralphbean/bugwarrior/pull/653 - (@ralphbean) #654, De-duplicate issues before first write to taskwarrior. https://github.com/ralphbean/bugwarrior/pull/654 Commits - 18b6e5232 Unpin futures to avoid dependency conflicts https://github.com/ralphbean/bugwarrior/commit/18b6e5232 - b512100d5 Fix UDA fields table in gmail https://github.com/ralphbean/bugwarrior/commit/b512100d5 - b949cbb01 Ignore closed sprints for due date https://github.com/ralphbean/bugwarrior/commit/b949cbb01 - 5f9fc143d Support Phabricator installations without Maniphest installed https://github.com/ralphbean/bugwarrior/commit/5f9fc143d - eea91cabf Remove unused `projects` dictionary https://github.com/ralphbean/bugwarrior/commit/eea91cabf - dc614824a Make include_repos work the way the documentation suggests (#605) https://github.com/ralphbean/bugwarrior/commit/dc614824a - 3e28b11f7 Fix tests on py27 due to configparser https://github.com/ralphbean/bugwarrior/commit/3e28b11f7 - 41ee8405a Add state and closedon UDAs to GitHub service https://github.com/ralphbean/bugwarrior/commit/41ee8405a - b8a2e9a47 Mark new and updated tasks as completed if end date https://github.com/ralphbean/bugwarrior/commit/b8a2e9a47 - e9ab77786 Update tests for new GitHub closedon and state UDAs https://github.com/ralphbean/bugwarrior/commit/e9ab77786 - 4c5cedf94 Make tests coherent https://github.com/ralphbean/bugwarrior/commit/4c5cedf94 - efce86574 Add UDAs in documentation https://github.com/ralphbean/bugwarrior/commit/efce86574 - 9297ba717 Add test to expose sprint parsing bug https://github.com/ralphbean/bugwarrior/commit/9297ba717 - 1471b7f45 Fix for jira sprint parsing bug https://github.com/ralphbean/bugwarrior/commit/1471b7f45 - 4a3805373 Handle paths absolutes or with envvars for logging https://github.com/ralphbean/bugwarrior/commit/4a3805373 - 30e0fb2f9 Log exceptions during command execution with their backtrace https://github.com/ralphbean/bugwarrior/commit/30e0fb2f9 - abaa17936 Add support for Trello due dates https://github.com/ralphbean/bugwarrior/commit/abaa17936 - 5a2c6810d fix tests https://github.com/ralphbean/bugwarrior/commit/5a2c6810d - 641842ef5 Add Debian BTS to the Readme https://github.com/ralphbean/bugwarrior/commit/641842ef5 - 726afa652 [JIRA] Add status and issue type UDAs https://github.com/ralphbean/bugwarrior/commit/726afa652 - 779bf8dd8 Add the ability to exclude Github pull-requests https://github.com/ralphbean/bugwarrior/commit/779bf8dd8 - 28879506b jira: split priority.name for lookup https://github.com/ralphbean/bugwarrior/commit/28879506b - bf468543d Check if taskrc file exists before trying to load config https://github.com/ralphbean/bugwarrior/commit/bf468543d - 05c5e2eb2 Raise proper exception when trying to handle data_path on config https://github.com/ralphbean/bugwarrior/commit/05c5e2eb2 - 52bdd8ae2 optionally set bug to "started" when assigned https://github.com/ralphbean/bugwarrior/commit/52bdd8ae2 - ccc371e15 code simplification based on PR feedback https://github.com/ralphbean/bugwarrior/commit/ccc371e15 - 0c139f34c gitlab: make the gitlabnumber UDA a string (fixes #552) https://github.com/ralphbean/bugwarrior/commit/0c139f34c - 7a0b2959c Fix tests and docs for #557. https://github.com/ralphbean/bugwarrior/commit/7a0b2959c - ce4beba12 Update python3 versions. https://github.com/ralphbean/bugwarrior/commit/ce4beba12 - 94a724b8a Update gitlab service to correct project request https://github.com/ralphbean/bugwarrior/commit/94a724b8a - 4dccbed38 Fix when request single project for include_repos https://github.com/ralphbean/bugwarrior/commit/4dccbed38 - 6fc90d8cf Gitlab request projects in simple mode https://github.com/ralphbean/bugwarrior/commit/6fc90d8cf - ec2024150 Fix default_priority option https://github.com/ralphbean/bugwarrior/commit/ec2024150 - 58c3b4edb Revert "Make pull requests a top priority." https://github.com/ralphbean/bugwarrior/commit/58c3b4edb - ba93fabdd Don't hardcode priority for trello cards https://github.com/ralphbean/bugwarrior/commit/ba93fabdd - 5af3b30a1 Set default priority in trello test. https://github.com/ralphbean/bugwarrior/commit/5af3b30a1 - 8fdb37826 Gitlab config membership filter https://github.com/ralphbean/bugwarrior/commit/8fdb37826 - d3d4e59e6 Gitlab config owner filter https://github.com/ralphbean/bugwarrior/commit/d3d4e59e6 - c01b977bf Fix gitlab test https://github.com/ralphbean/bugwarrior/commit/c01b977bf - ec1128c41 Support ignoring some fields in Phabricator https://github.com/ralphbean/bugwarrior/commit/ec1128c41 - 64f0c2880 Merge branch 'ignore_stuff' into develop https://github.com/ralphbean/bugwarrior/commit/64f0c2880 - ffe11e743 Add a log message to help debug keyring password errors https://github.com/ralphbean/bugwarrior/commit/ffe11e743 - 0465f09ec services/gitlab.py: Refactor redundant code to _get_issue_objs() https://github.com/ralphbean/bugwarrior/commit/0465f09ec - 92d60802f JIRA: Use explicit due date, if set. https://github.com/ralphbean/bugwarrior/commit/92d60802f - 6397b2bb8 Refactor logic to return None if no valid sprint found. https://github.com/ralphbean/bugwarrior/commit/6397b2bb8 - 9f340b128 Redmine description is optional. https://github.com/ralphbean/bugwarrior/commit/9f340b128 - f69bbed8a Make newline stripping in annotations configurable. https://github.com/ralphbean/bugwarrior/commit/f69bbed8a - 41664dc0f GitHub: Only query for user repos if an include_repos list is not specified. https://github.com/ralphbean/bugwarrior/commit/41664dc0f - e322d4e42 An initial flake8 config. https://github.com/ralphbean/bugwarrior/commit/e322d4e42 - 7aad5253d PEP8: youtrack service. https://github.com/ralphbean/bugwarrior/commit/7aad5253d - ca3d416c0 PEP8: jira cleanup. https://github.com/ralphbean/bugwarrior/commit/ca3d416c0 - 8a56c4b14 PEP8: taiga cleanup. https://github.com/ralphbean/bugwarrior/commit/8a56c4b14 - e874eece7 Adding initial teamworks projects support https://github.com/ralphbean/bugwarrior/commit/e874eece7 - 8118d8dcf Correcting name, adding dates, adding priority https://github.com/ralphbean/bugwarrior/commit/8118d8dcf - 026be2439 Removing start, adding tests https://github.com/ralphbean/bugwarrior/commit/026be2439 - 2a88cb0fa Fixing project id, adding docs https://github.com/ralphbean/bugwarrior/commit/2a88cb0fa - 244208db8 Doc edits, fixing return value https://github.com/ralphbean/bugwarrior/commit/244208db8 - 7fba706a4 Do not abort with ValueError when default_permission is blank https://github.com/ralphbean/bugwarrior/commit/7fba706a4 - ff3352785 Move this up a bit. https://github.com/ralphbean/bugwarrior/commit/ff3352785 - 28e6b178c Apply mock during instantiation. https://github.com/ralphbean/bugwarrior/commit/28e6b178c - 12f214656 De-duplicate issues before first write to taskwarrior. https://github.com/ralphbean/bugwarrior/commit/12f214656 1.6.0 ----- Pull Requests - #536, Merge pull request #536 from mikem23/bugzilla-flags https://github.com/ralphbean/bugwarrior/pull/536 - #582, Merge pull request #582 from westurner/patch-1 https://github.com/ralphbean/bugwarrior/pull/582 - #585, Merge pull request #585 from chikei/gitlabv4 https://github.com/ralphbean/bugwarrior/pull/585 Commits - b41f814af Pagure API now requires a parameter. https://github.com/ralphbean/bugwarrior/commit/b41f814af - 41280d6eb Add path attribute to BugwarriorData class so services can store data files. https://github.com/ralphbean/bugwarrior/commit/41280d6eb - 8b987734b GMail service for bugwarrior https://github.com/ralphbean/bugwarrior/commit/8b987734b - f914c1358 Add documentation for gmail service. https://github.com/ralphbean/bugwarrior/commit/f914c1358 - ddc60f6ee Wrap gmail authentication in a multiprocessing Lock. https://github.com/ralphbean/bugwarrior/commit/ddc60f6ee - 51eaa4049 Gmail oauth flow shouldn't attempt to parse the command line. https://github.com/ralphbean/bugwarrior/commit/51eaa4049 - 9c5cf1025 github: prefix project name with org name https://github.com/ralphbean/bugwarrior/commit/9c5cf1025 - f91afba4b github: fix involves: query https://github.com/ralphbean/bugwarrior/commit/f91afba4b - 39762c669 Update docs for common service configuration options https://github.com/ralphbean/bugwarrior/commit/39762c669 - 510e454ad Validate all common service configuration options https://github.com/ralphbean/bugwarrior/commit/510e454ad - 3642b02bf services/__init__: Refactor handling of defaults (#493) https://github.com/ralphbean/bugwarrior/commit/3642b02bf - 7b935b8d9 Use aslist everywhere when parsing list config options. https://github.com/ralphbean/bugwarrior/commit/7b935b8d9 - b2f7463a9 Update test for db handling of multiple static fields. https://github.com/ralphbean/bugwarrior/commit/b2f7463a9 - 89d6deddc Also include tasks for taiga (#499) https://github.com/ralphbean/bugwarrior/commit/89d6deddc - 1d5b2af43 Globally exclude __pycache__ and py[co] from sdist https://github.com/ralphbean/bugwarrior/commit/1d5b2af43 - 992d35804 Update tests to support responses-0.6. https://github.com/ralphbean/bugwarrior/commit/992d35804 - c1f7fc57b add fallback way for annotation on gerrit service https://github.com/ralphbean/bugwarrior/commit/c1f7fc57b - 65a957a30 adds namespace to gitlab uda https://github.com/ralphbean/bugwarrior/commit/65a957a30 - c5ed53be6 gitlab: filter repositories by regex https://github.com/ralphbean/bugwarrior/commit/c5ed53be6 - 05eb98ab7 doc: rework section Common Service Configuration Options https://github.com/ralphbean/bugwarrior/commit/05eb98ab7 - be408d291 Add python3.6 to the test matrix. https://github.com/ralphbean/bugwarrior/commit/be408d291 - fca71c87f Modify drop static field logic to prevent KeyError https://github.com/ralphbean/bugwarrior/commit/fca71c87f - 370cad3ce Prepend Bitbucket project name to Taskwarrior project https://github.com/ralphbean/bugwarrior/commit/370cad3ce - f84187139 Prepend Gitlab namespace path to Taskwarrior project https://github.com/ralphbean/bugwarrior/commit/f84187139 - 6c8788360 TypeError: not enough arguments for format string https://github.com/ralphbean/bugwarrior/commit/6c8788360 - 38e175a5f Remove dbus-python from keyring dependencies. https://github.com/ralphbean/bugwarrior/commit/38e175a5f - 614f021f5 Work around bugzilla flags issue https://github.com/ralphbean/bugwarrior/commit/614f021f5 - c87044c1f Use comma instead of space separated list for [service].add_tags in example configuration https://github.com/ralphbean/bugwarrior/commit/c87044c1f - 8ac1927c5 Update installation docs. https://github.com/ralphbean/bugwarrior/commit/8ac1927c5 - 55ab7ea31 Use get_password to evaluate API key https://github.com/ralphbean/bugwarrior/commit/55ab7ea31 - afa28123a Preserve existing environment variables when calling task https://github.com/ralphbean/bugwarrior/commit/afa28123a - dc30b38db config.py/get_data_path: Copy our environment before changing https://github.com/ralphbean/bugwarrior/commit/dc30b38db - 1fbdc7c62 config.py: Don't try to remove empty TASKDATA env variables https://github.com/ralphbean/bugwarrior/commit/1fbdc7c62 - fe5da905b test_config.py: Test TASKDATA is not set, rather than setting it empty https://github.com/ralphbean/bugwarrior/commit/fe5da905b - 7fda40572 Phabricator: mention Maniphest, how to create .arcrc https://github.com/ralphbean/bugwarrior/commit/7fda40572 - bfbcc3a36 Store GMail labels in a new gmaillabels UDA (#555) https://github.com/ralphbean/bugwarrior/commit/bfbcc3a36 - 0724da884 Gitlab Weight UDA https://github.com/ralphbean/bugwarrior/commit/0724da884 - a132cbf9f Convert Jira time to UTC and strip microseconds (Closes: #450) https://github.com/ralphbean/bugwarrior/commit/a132cbf9f - 8403cb817 Add support for annotation_links in Debian BTS https://github.com/ralphbean/bugwarrior/commit/8403cb817 - 6d04fc2cf Add phabricator.host option to enable multiple instances https://github.com/ralphbean/bugwarrior/commit/6d04fc2cf - f49fbf63d Phabricator: Fix maniphest.query parameter projectPHIDs https://github.com/ralphbean/bugwarrior/commit/f49fbf63d - 903f782a1 Phabricator: Fix "local variable 'issue' referenced before assignment" for diffs https://github.com/ralphbean/bugwarrior/commit/903f782a1 - a06d3712a Phabricator: Catch KeyErrors when fetching issues and diffs https://github.com/ralphbean/bugwarrior/commit/a06d3712a - eddc690bb Phabricator: Fix comparing with projects/repositories https://github.com/ralphbean/bugwarrior/commit/eddc690bb - 58da68f60 Phabricator: Use priorities of issues https://github.com/ralphbean/bugwarrior/commit/58da68f60 - 776a917e8 JiraIssue: set issue due to sprint endDate (#549) https://github.com/ralphbean/bugwarrior/commit/776a917e8 - 9dad3c180 Remove unnecessary conditional (#572) https://github.com/ralphbean/bugwarrior/commit/9dad3c180 - 2ff724d10 [phabricator] Connect to phabricator without Differential (#576) https://github.com/ralphbean/bugwarrior/commit/2ff724d10 - ee5c46f88 DOC: services/gmail.rst: Add newlines after .. code: https://github.com/ralphbean/bugwarrior/commit/ee5c46f88 - 4b02f4a62 gitlab: migrate to APIv4 https://github.com/ralphbean/bugwarrior/commit/4b02f4a62 - 37269a2e9 gitlab WIP status is True or False. Use asbool to convert from string to numeric https://github.com/ralphbean/bugwarrior/commit/37269a2e9 - e831d80c3 added testcase for wip flip https://github.com/ralphbean/bugwarrior/commit/e831d80c3 - 3a6de6f9c Moved data definition to setUp method https://github.com/ralphbean/bugwarrior/commit/3a6de6f9c - 2664b5669 redmine: add fetch_ssl configuration option https://github.com/ralphbean/bugwarrior/commit/2664b5669 - e8c15289c gitlab: uses created at date as entry date https://github.com/ralphbean/bugwarrior/commit/e8c15289c - 104325271 github: uses created at date as entry date https://github.com/ralphbean/bugwarrior/commit/104325271 - d48f735ce gitlab: fetch annotations only if necessary https://github.com/ralphbean/bugwarrior/commit/d48f735ce - 1bc853026 gitlab: uses due date as taskwarrior due date https://github.com/ralphbean/bugwarrior/commit/1bc853026 - 7df339e67 build list for multiple taiga tags (#594) https://github.com/ralphbean/bugwarrior/commit/7df339e67 - c9abd1a2d gitlab: fixes tests https://github.com/ralphbean/bugwarrior/commit/c9abd1a2d - cc1cb7a33 github: adds namespace uda (#587) https://github.com/ralphbean/bugwarrior/commit/cc1cb7a33 - da9221ea6 set allow_no_value=True so that it doesn't break on Python 3.7 https://github.com/ralphbean/bugwarrior/commit/da9221ea6 1.5.1 ----- Resolve merge inconsistency with master branch. 1.5.0 ----- Pull Requests - #292, Merge pull request #292 from ryneeverett/debug-flag https://github.com/ralphbean/bugwarrior/pull/292 - #293, Merge pull request #293 from ryneeverett/purge-unittest2 https://github.com/ralphbean/bugwarrior/pull/293 - #298, Merge pull request #298 from ryneeverett/redmine-auth https://github.com/ralphbean/bugwarrior/pull/298 - #299, Merge pull request #299 from ryneeverett/purge-urllib2 https://github.com/ralphbean/bugwarrior/pull/299 - #301, Merge pull request #301 from ralphbean/feature/taiga https://github.com/ralphbean/bugwarrior/pull/301 - #300, Merge pull request #300 from ryneeverett/service-client-base https://github.com/ralphbean/bugwarrior/pull/300 - #302, Merge pull request #302 from ryneeverett/cleanup-include https://github.com/ralphbean/bugwarrior/pull/302 - #303, Merge pull request #303 from ryansb/performance/fetch-comments https://github.com/ralphbean/bugwarrior/pull/303 - #306, Merge pull request #306 from ralphbean/feature/bugfixes https://github.com/ralphbean/bugwarrior/pull/306 - #305, Merge pull request #305 from ralphbean/feature/gerrit https://github.com/ralphbean/bugwarrior/pull/305 - #307, Merge pull request #307 from ralphbean/feature/include-bugfixes https://github.com/ralphbean/bugwarrior/pull/307 - #294, Merge pull request #294 from ryneeverett/responses-tests https://github.com/ralphbean/bugwarrior/pull/294 - #308, Merge pull request #308 from ryneeverett/simplify_json_response https://github.com/ralphbean/bugwarrior/pull/308 - #309, Merge pull request #309 from ryneeverett/refactor-gihubutils-serviceclient https://github.com/ralphbean/bugwarrior/pull/309 - #312, Merge pull request #312 from ryneeverett/docs-contributing-pull-request https://github.com/ralphbean/bugwarrior/pull/312 - #313, Merge pull request #313 from gdetrez/bitbucket-fixes https://github.com/ralphbean/bugwarrior/pull/313 - #318, Merge pull request #318 from gdetrez/typo https://github.com/ralphbean/bugwarrior/pull/318 - #317, Merge pull request #317 from ryneeverett/more-contributing-docs https://github.com/ralphbean/bugwarrior/pull/317 - #316, Merge pull request #316 from ryneeverett/bitbucket-refactor https://github.com/ralphbean/bugwarrior/pull/316 - #321, Merge pull request #321 from ryneeverett/readthedocs.io https://github.com/ralphbean/bugwarrior/pull/321 - #320, Merge pull request #320 from gdetrez/issue314 https://github.com/ralphbean/bugwarrior/pull/320 - #322, Merge pull request #322 from relrod/patch-1 https://github.com/ralphbean/bugwarrior/pull/322 - #324, Merge pull request #324 from ryneeverett/fix-github-private-repos https://github.com/ralphbean/bugwarrior/pull/324 - #325, Merge pull request #325 from joshainglis/improve-jira-integration https://github.com/ralphbean/bugwarrior/pull/325 - #328, Merge pull request #328 from jwilk/spelling https://github.com/ralphbean/bugwarrior/pull/328 - #329, Merge pull request #329 from jwilk/missing-import https://github.com/ralphbean/bugwarrior/pull/329 - #332, Merge pull request #332 from ralphbean/feature/http-sessions https://github.com/ralphbean/bugwarrior/pull/332 - #331, Merge pull request #331 from ralphbean/feature/kill-twiggy https://github.com/ralphbean/bugwarrior/pull/331 - #337, Merge pull request #337 from ralphbean/feature/fix-die https://github.com/ralphbean/bugwarrior/pull/337 - #338, Merge pull request #338 from irl/feature/trac/store-component https://github.com/ralphbean/bugwarrior/pull/338 - #340, Merge pull request #340 from irl/feature/trac/store-component https://github.com/ralphbean/bugwarrior/pull/340 - #341, Merge pull request #341 from irl/feature/trac/store-component https://github.com/ralphbean/bugwarrior/pull/341 - #346, Merge pull request #346 from ralphbean/feature/user-agent https://github.com/ralphbean/bugwarrior/pull/346 - #344, Merge pull request #344 from ralphbean/feature/docs-authors https://github.com/ralphbean/bugwarrior/pull/344 - #1, Merge pull request #1 from ryneeverett/debianbts-mock https://github.com/ralphbean/bugwarrior/pull/1 - #2, Merge pull request #2 from ryneeverett/ioerror-no-config-ryne https://github.com/ralphbean/bugwarrior/pull/2 - #348, Merge pull request #348 from irl/feature/debianbts https://github.com/ralphbean/bugwarrior/pull/348 - #359, Merge pull request #359 from jwilk/spelling https://github.com/ralphbean/bugwarrior/pull/359 - #3, Merge pull request #3 from ryneeverett/ioerror-no-config-ryne https://github.com/ralphbean/bugwarrior/pull/3 - #373, Merge pull request #373 from gdetrez/unicode-fix https://github.com/ralphbean/bugwarrior/pull/373 - #372, Merge pull request #372 from gdetrez/github-milestone https://github.com/ralphbean/bugwarrior/pull/372 - #371, Merge pull request #371 from gdetrez/gitlab-repos https://github.com/ralphbean/bugwarrior/pull/371 - #357, Merge pull request #357 from irl/fix/ioerror-no-config https://github.com/ralphbean/bugwarrior/pull/357 - #362, Merge pull request #362 from mathstuf/gitlab-todos https://github.com/ralphbean/bugwarrior/pull/362 - #370, Merge pull request #370 from gdetrez/bugwarriorrc https://github.com/ralphbean/bugwarrior/pull/370 - #319, Merge pull request #319 from gdetrez/trello https://github.com/ralphbean/bugwarrior/pull/319 - #378, Merge pull request #378 from irl/task/reject-config-udd https://github.com/ralphbean/bugwarrior/pull/378 - #381, Merge pull request #381 from ralphbean/feature/jira-sprints https://github.com/ralphbean/bugwarrior/pull/381 - #389, Merge pull request #389 from beav/377 https://github.com/ralphbean/bugwarrior/pull/389 - #382, Merge pull request #382 from ryneeverett/improve-travis-coverage https://github.com/ralphbean/bugwarrior/pull/382 - #383, Merge pull request #383 from gdetrez/doc-pass https://github.com/ralphbean/bugwarrior/pull/383 - #386, Merge pull request #386 from gdetrez/issue/376 https://github.com/ralphbean/bugwarrior/pull/386 - #364, Merge pull request #364 from ryneeverett/taskdata-dir-ryne https://github.com/ralphbean/bugwarrior/pull/364 - #397, Merge pull request #397 from mathstuf/gitlab-all-todos https://github.com/ralphbean/bugwarrior/pull/397 - #399, Merge pull request #399 from mathstuf/filter-involved-issues https://github.com/ralphbean/bugwarrior/pull/399 - #408, Merge pull request #408 from bowlofeggs/docs-systemd_timer https://github.com/ralphbean/bugwarrior/pull/408 - #384, Merge pull request #384 from ryneeverett/test-db-module https://github.com/ralphbean/bugwarrior/pull/384 - #401, Merge pull request #401 from stbenjam/githubuser-uda https://github.com/ralphbean/bugwarrior/pull/401 - #403, Merge pull request #403 from beav/only_if_author https://github.com/ralphbean/bugwarrior/pull/403 - #416, Merge pull request #416 from ralphbean/feature/unhappy-future https://github.com/ralphbean/bugwarrior/pull/416 - #421, Merge pull request #421 from gdetrez/github-token-oracle https://github.com/ralphbean/bugwarrior/pull/421 - #424, Merge pull request #424 from ryneeverett/keyring-optional https://github.com/ralphbean/bugwarrior/pull/424 - #428, Merge pull request #428 from lubomir/jira-kerberos https://github.com/ralphbean/bugwarrior/pull/428 - #430, Merge pull request #430 from ralphbean/feature/secure-by-default https://github.com/ralphbean/bugwarrior/pull/430 - #431, Merge pull request #431 from lubomir/pickle-error https://github.com/ralphbean/bugwarrior/pull/431 - #434, Merge pull request #434 from lyarwood/develop https://github.com/ralphbean/bugwarrior/pull/434 - #435, Merge pull request #435 from ralphbean/feature/jira-urls https://github.com/ralphbean/bugwarrior/pull/435 - #395, Merge pull request #395 from kostajh/redmine-improvements https://github.com/ralphbean/bugwarrior/pull/395 - #439, Merge pull request #439 from wookietreiber/develop https://github.com/ralphbean/bugwarrior/pull/439 - #440, Merge pull request #440 from ralphbean/pesky-jira https://github.com/ralphbean/bugwarrior/pull/440 - #452, Merge pull request #452 from djmitche/issue423 https://github.com/ralphbean/bugwarrior/pull/452 - #422, Merge pull request #422 from gdetrez/360-enterprize-github-support https://github.com/ralphbean/bugwarrior/pull/422 - #437, Merge pull request #437 from ryneeverett/getint-None https://github.com/ralphbean/bugwarrior/pull/437 - #456, Merge pull request #456 from ralphbean/feature/remove-pynotify https://github.com/ralphbean/bugwarrior/pull/456 - #464, Merge pull request #464 from ssbarnea/develop https://github.com/ralphbean/bugwarrior/pull/464 - #468, Merge pull request #468 from nblock/dev/bugzilla-uda https://github.com/ralphbean/bugwarrior/pull/468 - #476, Merge pull request #476 from Fongshway/issue/450 https://github.com/ralphbean/bugwarrior/pull/476 Commits - 69bd63659 Replace development config with --debug flag. https://github.com/ralphbean/bugwarrior/commit/69bd63659 - e0931eafc Purge unittest2 now that we only test in 2.7+. https://github.com/ralphbean/bugwarrior/commit/e0931eafc - e55371e39 #295: Proof of concept. https://github.com/ralphbean/bugwarrior/commit/e55371e39 - 18baef532 Fix get_keyring_service, add docs, and style. https://github.com/ralphbean/bugwarrior/commit/18baef532 - 5969ed2fb Replace remaining urrlib2 usage with requests. https://github.com/ralphbean/bugwarrior/commit/5969ed2fb - 4150569ae Switch redmine from urllib2 to requests. https://github.com/ralphbean/bugwarrior/commit/4150569ae - fd66b5d8b A service for pulling issues from a taiga instance. https://github.com/ralphbean/bugwarrior/commit/fd66b5d8b - 76409b411 Clean up IssueService.include https://github.com/ralphbean/bugwarrior/commit/76409b411 - 28a6e2bab Give config_get_password a default argument for login. https://github.com/ralphbean/bugwarrior/commit/28a6e2bab - 0febb90b4 Simplify taiga headers. https://github.com/ralphbean/bugwarrior/commit/0febb90b4 - 567bf72b2 This is actually numeric. https://github.com/ralphbean/bugwarrior/commit/567bf72b2 - 3e4cbf716 Add a test_to_taskwarrior test case for taiga. https://github.com/ralphbean/bugwarrior/commit/3e4cbf716 - 4a42012bd Add ServiceClient base class. https://github.com/ralphbean/bugwarrior/commit/4a42012bd - 8b663eb33 Fetch github comments only when they will be used https://github.com/ralphbean/bugwarrior/commit/8b663eb33 - ece326830 Fix a bug from #303. https://github.com/ralphbean/bugwarrior/commit/ece326830 - 81e8d74d0 Fix a bug from #302, when values are absent from the config. https://github.com/ralphbean/bugwarrior/commit/81e8d74d0 - 3d3fcf2bb A gerrit service.. and only a gerrit service. https://github.com/ralphbean/bugwarrior/commit/3d3fcf2bb - ea7f08f98 Explain this slice. https://github.com/ralphbean/bugwarrior/commit/ea7f08f98 - 41f88170c Add an explanatory comment. https://github.com/ralphbean/bugwarrior/commit/41f88170c - 82b75f52d This is not a bool. https://github.com/ralphbean/bugwarrior/commit/82b75f52d - f99909433 Complain when users specify old config values. https://github.com/ralphbean/bugwarrior/commit/f99909433 - 6b3a8109f Add responses and test Bitbucket issues. https://github.com/ralphbean/bugwarrior/commit/6b3a8109f - 6c487d742 Test github issues. https://github.com/ralphbean/bugwarrior/commit/6c487d742 - 399597c3a Test gitlab issues. https://github.com/ralphbean/bugwarrior/commit/399597c3a - ff6b06056 Test jira issues. https://github.com/ralphbean/bugwarrior/commit/ff6b06056 - 355f3365c Test activecollab issues. https://github.com/ralphbean/bugwarrior/commit/355f3365c - 4084ab965 Test activecollab2 issues and fix bugs. https://github.com/ralphbean/bugwarrior/commit/4084ab965 - b222c991f Test bugzilla issues. https://github.com/ralphbean/bugwarrior/commit/b222c991f - d375a2aba Test gerrit issues. https://github.com/ralphbean/bugwarrior/commit/d375a2aba - 228febfb2 Run assertEqual against record dict. https://github.com/ralphbean/bugwarrior/commit/228febfb2 - 0ee936712 Test megaplan issues. https://github.com/ralphbean/bugwarrior/commit/0ee936712 - 52c3ca2e5 Test redmine issues. https://github.com/ralphbean/bugwarrior/commit/52c3ca2e5 - 85ebb829c Fix taiga ServiceClient.json_response TypeError. https://github.com/ralphbean/bugwarrior/commit/85ebb829c - 6dfb913f9 Test taiga issues. https://github.com/ralphbean/bugwarrior/commit/6dfb913f9 - 6953f96be Test teamlab issues. https://github.com/ralphbean/bugwarrior/commit/6953f96be - 9a87148ab Test trac issues. https://github.com/ralphbean/bugwarrior/commit/9a87148ab - 2b75ce4a7 Add AbstractServiceTest base class. https://github.com/ralphbean/bugwarrior/commit/2b75ce4a7 - 5cad26267 Add ServiceTest.add_response method. https://github.com/ralphbean/bugwarrior/commit/5cad26267 - 38a79c8ac Simplify SerivceClient.json_response interface. https://github.com/ralphbean/bugwarrior/commit/38a79c8ac - bd7d866ab Refactor githubutils into a ServiceClient. https://github.com/ralphbean/bugwarrior/commit/bd7d866ab - 4ca49a7df Add a docstring to ServiceClient. https://github.com/ralphbean/bugwarrior/commit/4ca49a7df - acda8d3e4 Move GithubClient to github.py. https://github.com/ralphbean/bugwarrior/commit/acda8d3e4 - ecad1b175 Add pull request documentation. https://github.com/ralphbean/bugwarrior/commit/ecad1b175 - c6704f4e9 Bitbucket: Paginate through object collection returned by API call https://github.com/ralphbean/bugwarrior/commit/c6704f4e9 - b32bb03ad Bitbucket: Fix `only_if_assigned` with API 2.0 https://github.com/ralphbean/bugwarrior/commit/b32bb03ad - d796bc83c Bitbucket: add tests for the pagination and assignee https://github.com/ralphbean/bugwarrior/commit/d796bc83c - 7253b61b7 Return a list instead of an iterator in `fetch_issues` https://github.com/ralphbean/bugwarrior/commit/7253b61b7 - e8a1fad91 Generate bitbucket's requests kwargs in __init__. https://github.com/ralphbean/bugwarrior/commit/e8a1fad91 - 2dc28f9de Eliminate one of the data fetching methods. https://github.com/ralphbean/bugwarrior/commit/2dc28f9de - 231333190 Encourage work in progress PR's. https://github.com/ralphbean/bugwarrior/commit/231333190 - af6b7f05a Point to CONTRIBUTING.md to constributing docs. https://github.com/ralphbean/bugwarrior/commit/af6b7f05a - 542a7fc2a Fix typo in option name 'inline_links' in docs https://github.com/ralphbean/bugwarrior/commit/542a7fc2a - 032ee5128 Enable syntax coloring for configuration examples https://github.com/ralphbean/bugwarrior/commit/032ee5128 - cc88528e8 Revert "Point to CONTRIBUTING.md to constributing docs." https://github.com/ralphbean/bugwarrior/commit/cc88528e8 - 1810f05f2 Github: add an option to skip user issues https://github.com/ralphbean/bugwarrior/commit/1810f05f2 - b6dbd9671 Update docs link to readthedocs.io. https://github.com/ralphbean/bugwarrior/commit/b6dbd9671 - cdef5e7b8 Use https for da.gd :) https://github.com/ralphbean/bugwarrior/commit/cdef5e7b8 - b0cee49d5 Include github private repos (take 2). Fix #282. https://github.com/ralphbean/bugwarrior/commit/b0cee49d5 - 10cc232c7 Improve JIRA integration https://github.com/ralphbean/bugwarrior/commit/10cc232c7 - 68ec0798a Fix typos. https://github.com/ralphbean/bugwarrior/commit/68ec0798a - 2b936e01a Add missing import. https://github.com/ralphbean/bugwarrior/commit/2b936e01a - 2e1419dc2 Kill twiggy. https://github.com/ralphbean/bugwarrior/commit/2e1419dc2 - 24f1de44c Use requests "sessions". https://github.com/ralphbean/bugwarrior/commit/24f1de44c - 48bcea1db Fix messed up logging in die(). https://github.com/ralphbean/bugwarrior/commit/48bcea1db - f393d2f4c Store Trac component in UDA for Trac service https://github.com/ralphbean/bugwarrior/commit/f393d2f4c - 511b1f534 Update Sphinx docs to reflect new traccomponent UDA https://github.com/ralphbean/bugwarrior/commit/511b1f534 - fae7e710e Update Trac tests to include component UDA https://github.com/ralphbean/bugwarrior/commit/fae7e710e - 2d3f7f6ef Add full docs authors list to the man page. https://github.com/ralphbean/bugwarrior/commit/2d3f7f6ef - 7439ad815 Modify headers instead of overwriting. https://github.com/ralphbean/bugwarrior/commit/7439ad815 - 5cdb0d137 Make Jira installation example in docs ZSH compatible (#349) https://github.com/ralphbean/bugwarrior/commit/5cdb0d137 - d116513c4 Skip activecollab tests if pandoc is not installed (#351) https://github.com/ralphbean/bugwarrior/commit/d116513c4 - 037673212 Add help text to bugwarrior-pull's --debug flag. (#352) https://github.com/ralphbean/bugwarrior/commit/037673212 - 5be94b753 Make bitbucketid numeric. (#353) https://github.com/ralphbean/bugwarrior/commit/5be94b753 - f4f1fc193 Initial Debian BTS support https://github.com/ralphbean/bugwarrior/commit/f4f1fc193 - 162f7fe8d Use requests not urllib{,2} https://github.com/ralphbean/bugwarrior/commit/162f7fe8d - 80e757382 Use asbool in place of local bool parsing https://github.com/ralphbean/bugwarrior/commit/80e757382 - dff2e7772 Add debianbts as a dependency for the bts service https://github.com/ralphbean/bugwarrior/commit/dff2e7772 - df71ee28e Add tests for BTS service https://github.com/ralphbean/bugwarrior/commit/df71ee28e - 539d70918 travis: pip install PySimpleSOAP before installing everything else. https://github.com/ralphbean/bugwarrior/commit/539d70918 - c0427c9fb Updates to the docs for BTS service https://github.com/ralphbean/bugwarrior/commit/c0427c9fb - 3a82edb9d Adds Iain R. Learmonth to the contributors list in the README https://github.com/ralphbean/bugwarrior/commit/3a82edb9d - 34f998b21 Removes debianbts library as an attribute from the BTS service class https://github.com/ralphbean/bugwarrior/commit/34f998b21 - e81ac423a Fixes documentation for BTS module, email -> bts.email https://github.com/ralphbean/bugwarrior/commit/e81ac423a - 00fc7f508 Adds useful validation of the configuration for the BTS service https://github.com/ralphbean/bugwarrior/commit/00fc7f508 - 87dbd3aa6 Allow to ignore tasks marked as pending in the BTS https://github.com/ralphbean/bugwarrior/commit/87dbd3aa6 - 1f15f947f PEP8 fixes for new BTS service https://github.com/ralphbean/bugwarrior/commit/1f15f947f - 5cc1e64f6 Use mock for monkeypatching. https://github.com/ralphbean/bugwarrior/commit/5cc1e64f6 - a3c67890b Catch IOError when loading config, useful message https://github.com/ralphbean/bugwarrior/commit/a3c67890b - 8d2783bee Don't ignore exit return code when thrown in pull https://github.com/ralphbean/bugwarrior/commit/8d2783bee - 088a0d5fd DRY up ioerror exception handling. https://github.com/ralphbean/bugwarrior/commit/088a0d5fd - 0eee37341 Eliminate unnecessary exception handling. https://github.com/ralphbean/bugwarrior/commit/0eee37341 - 2f76cf9da Set up logging in _try_load_config. https://github.com/ralphbean/bugwarrior/commit/2f76cf9da - 112d63045 Fix typos https://github.com/ralphbean/bugwarrior/commit/112d63045 - 5c42553db Fix bugwarrior-uda TypeError. https://github.com/ralphbean/bugwarrior/commit/5c42553db - 8e8dec87f Log exception type and reason but not traceback. https://github.com/ralphbean/bugwarrior/commit/8e8dec87f - 6405e1ad3 gitlab: expose duedates https://github.com/ralphbean/bugwarrior/commit/6405e1ad3 - f065c3a2c gitlab: update up and downvotes for issues https://github.com/ralphbean/bugwarrior/commit/f065c3a2c - a68d11a69 bitbucket: update docs https://github.com/ralphbean/bugwarrior/commit/a68d11a69 - 9486d7daa gitlab: return {} for API failures https://github.com/ralphbean/bugwarrior/commit/9486d7daa - bcdb48c22 gitlab: implement support for todo items https://github.com/ralphbean/bugwarrior/commit/bcdb48c22 - 98118d837 gitlab: catch HTTP errors more accurately https://github.com/ralphbean/bugwarrior/commit/98118d837 - 912a579f2 Only filter todos if "include_all_todos" is False. https://github.com/ralphbean/bugwarrior/commit/912a579f2 - 2d283e3f2 Minimize duplication in to_taskwarrior. https://github.com/ralphbean/bugwarrior/commit/2d283e3f2 - 28335a203 Starting on a trello backend https://github.com/ralphbean/bugwarrior/commit/28335a203 - 5cc12e275 Add TrelloIssue and TrelloService class https://github.com/ralphbean/bugwarrior/commit/5cc12e275 - 6695b04d0 Add option to import trello labels as tags https://github.com/ralphbean/bugwarrior/commit/6695b04d0 - 4307026ee Update documentation https://github.com/ralphbean/bugwarrior/commit/4307026ee - a5eddd696 Clean the code a bit https://github.com/ralphbean/bugwarrior/commit/a5eddd696 - 6b34822b1 Trello: Refactor service to add card filtering https://github.com/ralphbean/bugwarrior/commit/6b34822b1 - d386e13ef Trello: Don't change label case by default https://github.com/ralphbean/bugwarrior/commit/d386e13ef - bcde60297 Trello: Add URL screenshot to the doc https://github.com/ralphbean/bugwarrior/commit/bcde60297 - e07c025b5 Fix some unicode problems https://github.com/ralphbean/bugwarrior/commit/e07c025b5 - c202739f0 Trello: Use json_response https://github.com/ralphbean/bugwarrior/commit/c202739f0 - 766c3cc3a Trello: Replace config_get_list by a function aslist https://github.com/ralphbean/bugwarrior/commit/766c3cc3a - 01ca30418 Use the new 'aslist' function in other service https://github.com/ralphbean/bugwarrior/commit/01ca30418 - 7ab59b3e5 Trello: Allow multiple boards in a block https://github.com/ralphbean/bugwarrior/commit/7ab59b3e5 - 6a9bb00da Trello: Add test for method issues() https://github.com/ralphbean/bugwarrior/commit/6a9bb00da - 2cf320d93 Trello: Update and extend documentation https://github.com/ralphbean/bugwarrior/commit/2cf320d93 - a35343879 Trello: Change option name to only_if_assigned https://github.com/ralphbean/bugwarrior/commit/a35343879 - ac2996dfa Remove documetation for trello.only_if_assigned https://github.com/ralphbean/bugwarrior/commit/ac2996dfa - 90802d0c8 Trello: Implement common option "also_unassigned" https://github.com/ralphbean/bugwarrior/commit/90802d0c8 - a5adcbc71 Use ServiceClient in trello service https://github.com/ralphbean/bugwarrior/commit/a5adcbc71 - cd7125fb0 Add support for environment variable $BUGWARRIORRC https://github.com/ralphbean/bugwarrior/commit/cd7125fb0 - f1eb43bf2 Add comment fetching to trello service https://github.com/ralphbean/bugwarrior/commit/f1eb43bf2 - 3e47a474c Saving and restoring environment in tests https://github.com/ralphbean/bugwarrior/commit/3e47a474c - 28def751a Fix gitlab include/exclude repos https://github.com/ralphbean/bugwarrior/commit/28def751a - 321f79f8f Use milestone title instead of id for githubmilestone https://github.com/ralphbean/bugwarrior/commit/321f79f8f - 6872ab8c3 Fix some problems with unicode https://github.com/ralphbean/bugwarrior/commit/6872ab8c3 - 6816ebb3c Fix typos https://github.com/ralphbean/bugwarrior/commit/6816ebb3c - 4e12ad597 Change RC file priorities https://github.com/ralphbean/bugwarrior/commit/4e12ad597 - c4da18737 Add documentation about config file https://github.com/ralphbean/bugwarrior/commit/c4da18737 - 4a7da3d6a Add trello to the README https://github.com/ralphbean/bugwarrior/commit/4a7da3d6a - 1c8fa7c6a bts: Reject configuration if UDD options specified but UDD not used https://github.com/ralphbean/bugwarrior/commit/1c8fa7c6a - 9199e9aae Get data location from taskwarrior. https://github.com/ralphbean/bugwarrior/commit/9199e9aae - 122a694ce docs: refer to taskrc as a file, not a path https://github.com/ralphbean/bugwarrior/commit/122a694ce - f0fcbcb4a Anchor data location search pattern. https://github.com/ralphbean/bugwarrior/commit/f0fcbcb4a - 34ccf04f4 If data location cannot be found, raise exception. https://github.com/ralphbean/bugwarrior/commit/34ccf04f4 - 6c1f9c259 Set up a taskrc and data directory for tests. https://github.com/ralphbean/bugwarrior/commit/6c1f9c259 - a1815929b os.mkdir does not return the path. https://github.com/ralphbean/bugwarrior/commit/a1815929b - 5ba5c88a0 Upgrade taskwarrior 2.3.0 -> 2.4.4. https://github.com/ralphbean/bugwarrior/commit/5ba5c88a0 - 088f68b97 Import JIRA sprint names as labels. https://github.com/ralphbean/bugwarrior/commit/088f68b97 - 74fbce77a Guard against non-iterable NoneType here. https://github.com/ralphbean/bugwarrior/commit/74fbce77a - de3f13a71 Make the option name more similar to another nearby, similar option. https://github.com/ralphbean/bugwarrior/commit/de3f13a71 - a21a62189 Make sure to initialize this. https://github.com/ralphbean/bugwarrior/commit/a21a62189 - fba32cfa2 Add PATH to environment. https://github.com/ralphbean/bugwarrior/commit/fba32cfa2 - bcec847c8 Reuse config setup/teardown. https://github.com/ralphbean/bugwarrior/commit/bcec847c8 - 50ffaa0f2 Fresh config for every test unit. https://github.com/ralphbean/bugwarrior/commit/50ffaa0f2 - 201750e7e Set data file mode to 0600 and test BugwarriorData. https://github.com/ralphbean/bugwarrior/commit/201750e7e - 67e8f3642 Switch from statement coverage to branch coverage. https://github.com/ralphbean/bugwarrior/commit/67e8f3642 - f2982ee24 Improve @oracle documentation https://github.com/ralphbean/bugwarrior/commit/f2982ee24 - 76f7f616b Switch from coveralls -> codecov. https://github.com/ralphbean/bugwarrior/commit/76f7f616b - 109499264 Fix unicode issues in notifications https://github.com/ralphbean/bugwarrior/commit/109499264 - a266278b9 Supress warning when using gobject notifications https://github.com/ralphbean/bugwarrior/commit/a266278b9 - b2ca93d8e Handle multiple kinds of sprint fields. https://github.com/ralphbean/bugwarrior/commit/b2ca93d8e - 9997f188d Only query for open github PRs (#377) https://github.com/ralphbean/bugwarrior/commit/9997f188d - d5b3e07fa Test config.get_data_path. https://github.com/ralphbean/bugwarrior/commit/d5b3e07fa - f8c8c0342 Remove unused function. https://github.com/ralphbean/bugwarrior/commit/f8c8c0342 - 6a3b34b2f More tests for db.merge_left. https://github.com/ralphbean/bugwarrior/commit/6a3b34b2f - f7a1cbb4d Don't re-implement for/else control flow. https://github.com/ralphbean/bugwarrior/commit/f7a1cbb4d - 6a1ff739e Test db.synchronize. https://github.com/ralphbean/bugwarrior/commit/6a1ff739e - d74946ded Test db.get_defined_udas_as_strings. https://github.com/ralphbean/bugwarrior/commit/d74946ded - 431c958af redmine: import first 100 issues instead 25 https://github.com/ralphbean/bugwarrior/commit/431c958af - 643b318dc Fix redmine test. https://github.com/ralphbean/bugwarrior/commit/643b318dc - fb41c63cf gitlab: match types when including all todo items https://github.com/ralphbean/bugwarrior/commit/fb41c63cf - b11bc154f gitlab: ignore the id https://github.com/ralphbean/bugwarrior/commit/b11bc154f - 757de15e6 github: refactor filtering based on the repo name https://github.com/ralphbean/bugwarrior/commit/757de15e6 - 08378614b github: filter out involved issues by repo https://github.com/ralphbean/bugwarrior/commit/08378614b - 14ee268a8 github: simpler repo name parsing https://github.com/ralphbean/bugwarrior/commit/14ee268a8 - df374f120 github: also filter assigned issues https://github.com/ralphbean/bugwarrior/commit/df374f120 - ecd8309c0 Add a github UDA for user field https://github.com/ralphbean/bugwarrior/commit/ecd8309c0 - f78b9e5fb Filter gitlab requests by author https://github.com/ralphbean/bugwarrior/commit/f78b9e5fb - f766dda0f Document how to use systemd timers to run bugwarrior-pull. https://github.com/ralphbean/bugwarrior/commit/f766dda0f - cff7427b6 Add documentation for static_fields config (#407) https://github.com/ralphbean/bugwarrior/commit/cff7427b6 - 267f0129e add tox support to test both py27 and py34 https://github.com/ralphbean/bugwarrior/commit/267f0129e - 2f7ca55f9 use XDG_CACHE_HOME if available https://github.com/ralphbean/bugwarrior/commit/2f7ca55f9 - 88808e49b futurize bugwarrior: add support for python3 https://github.com/ralphbean/bugwarrior/commit/88808e49b - 8e52e136e fixes a compatibility problem in Issue.__str__ and friends https://github.com/ralphbean/bugwarrior/commit/8e52e136e - f84d83afb depends on pyac>=0.1.5 for python3 support https://github.com/ralphbean/bugwarrior/commit/f84d83afb - 9bb049b2f depends on python-debianbts>=2.6.1 for improved ssl support on py34 https://github.com/ralphbean/bugwarrior/commit/9bb049b2f - 3abe0720a Remove unrequired list coercion. https://github.com/ralphbean/bugwarrior/commit/3abe0720a - 36cd992af Remove functools32 and lru_cache usage. https://github.com/ralphbean/bugwarrior/commit/36cd992af - 9d909affb In python3, octals must begin with "0o". https://github.com/ralphbean/bugwarrior/commit/9d909affb - 3f0f36923 In python3, query param order is unpredictable. https://github.com/ralphbean/bugwarrior/commit/3f0f36923 - 16f34794d Python3 compatibility for recent code additions. https://github.com/ralphbean/bugwarrior/commit/16f34794d - 67f0458a6 Skip megaplan tests in python3. https://github.com/ralphbean/bugwarrior/commit/67f0458a6 - fc48a15a4 Add python3 to travis matrix. https://github.com/ralphbean/bugwarrior/commit/fc48a15a4 - 9d3d689f8 Add python3 to classifiers. https://github.com/ralphbean/bugwarrior/commit/9d3d689f8 - 8132b9658 Add requirement section in the documentation https://github.com/ralphbean/bugwarrior/commit/8132b9658 - f52e7788c Set highlight to console in contributing docs https://github.com/ralphbean/bugwarrior/commit/f52e7788c - 371fb0b4c Add an FAQ https://github.com/ralphbean/bugwarrior/commit/371fb0b4c - 877bbdd7a Taskwarrior expects tags to not have spaces, otherwise you cannot query with them. https://github.com/ralphbean/bugwarrior/commit/877bbdd7a - 4e4fea19c Merge branch 'feature/jira-sprints' into develop https://github.com/ralphbean/bugwarrior/commit/4e4fea19c - 2e3beeb16 Merge branch 'develop' of github.com:ralphbean/bugwarrior into develop https://github.com/ralphbean/bugwarrior/commit/2e3beeb16 - 9d8eb35b4 Typofix. https://github.com/ralphbean/bugwarrior/commit/9d8eb35b4 - 83c13b0e8 Trailing comma. https://github.com/ralphbean/bugwarrior/commit/83c13b0e8 - 16c4cae28 Avoid installing a version of future. https://github.com/ralphbean/bugwarrior/commit/16c4cae28 - 48d91a76b Update our required version of `six`. https://github.com/ralphbean/bugwarrior/commit/48d91a76b - 9e49220f0 With future, we are expected to use py3 import aliases. https://github.com/ralphbean/bugwarrior/commit/9e49220f0 - 10d391889 Update the db test with a change from another PR. https://github.com/ralphbean/bugwarrior/commit/10d391889 - 66357380b Use a dependable sorting order when comparing. https://github.com/ralphbean/bugwarrior/commit/66357380b - a0b923bf9 Use a different dbm cache file for different python versions. https://github.com/ralphbean/bugwarrior/commit/a0b923bf9 - ff9b56319 Test py35 with tox as well. https://github.com/ralphbean/bugwarrior/commit/ff9b56319 - 9a806e23f Exposition. https://github.com/ralphbean/bugwarrior/commit/9a806e23f - 43ea5ba0d Check for errors from the taiga API and surface them. https://github.com/ralphbean/bugwarrior/commit/43ea5ba0d - 122e1a3ac Fix @oracle:eval with Github and python 3 https://github.com/ralphbean/bugwarrior/commit/122e1a3ac - 01ae4f580 Put the BugwarriorData instance in the config object https://github.com/ralphbean/bugwarrior/commit/01ae4f580 - efec9fcf0 Support passing issue_limit in the config https://github.com/ralphbean/bugwarrior/commit/efec9fcf0 - fa6fe1240 Map due_date https://github.com/ralphbean/bugwarrior/commit/fa6fe1240 - 2fbc89c13 Add description field, convert ID to numeric https://github.com/ralphbean/bugwarrior/commit/2fbc89c13 - cd4eae1c8 Add more core fields https://github.com/ralphbean/bugwarrior/commit/cd4eae1c8 - f033927b7 Remove unneeded user_id, add Assigned To field https://github.com/ralphbean/bugwarrior/commit/f033927b7 - 52e38dd99 Make keyring dependency optional. Resolve #343. https://github.com/ralphbean/bugwarrior/commit/52e38dd99 - ca03f8db4 Better date/time handling for created, updated, due https://github.com/ralphbean/bugwarrior/commit/ca03f8db4 - 47837358f Adjust variable name for consistency https://github.com/ralphbean/bugwarrior/commit/47837358f - 7968a93c1 Add some TODOs https://github.com/ralphbean/bugwarrior/commit/7968a93c1 - ab0929644 More date handling fixes, use task calc for estimated hours https://github.com/ralphbean/bugwarrior/commit/ab0929644 - 959f998a4 Specify a more realistic limit in the docs https://github.com/ralphbean/bugwarrior/commit/959f998a4 - b899e8349 Also use task calc on spent_hours https://github.com/ralphbean/bugwarrior/commit/b899e8349 - 1581ed709 Make project name alphanumeric and lowercase https://github.com/ralphbean/bugwarrior/commit/1581ed709 - 574542563 Add support for YouTrack issue tracker. https://github.com/ralphbean/bugwarrior/commit/574542563 - 3325a0460 Standardize docs for `.verify_ssl` service option in supported issue trackers. https://github.com/ralphbean/bugwarrior/commit/3325a0460 - fc3693ee3 Work on tests https://github.com/ralphbean/bugwarrior/commit/fc3693ee3 - 516961b05 Improved formatting of project names https://github.com/ralphbean/bugwarrior/commit/516961b05 - 71e2f1fda Add kerberos authentication to JiraService https://github.com/ralphbean/bugwarrior/commit/71e2f1fda - 69e63154e Update redmine tests. https://github.com/ralphbean/bugwarrior/commit/69e63154e - c9e07229f redmine: Respect only_if_assigned configuration. self.issue_limit = issue_limit https://github.com/ralphbean/bugwarrior/commit/c9e07229f - 7ba172536 redmine: Create redmineduedate UDA. https://github.com/ralphbean/bugwarrior/commit/7ba172536 - 580db8712 We should verify by default here. https://github.com/ralphbean/bugwarrior/commit/580db8712 - 7c1fae2d9 Remove unpickleable attributes from exception https://github.com/ralphbean/bugwarrior/commit/7c1fae2d9 - 517e7021b Make sure e.request is not None before removing hooks (#433) https://github.com/ralphbean/bugwarrior/commit/517e7021b - 7e0cf8e28 Include the issue url for jira tasks even if there are no annotations. https://github.com/ralphbean/bugwarrior/commit/7e0cf8e28 - fb10cf294 gerrit: Track the branch and topic used by each change https://github.com/ralphbean/bugwarrior/commit/fb10cf294 - 6acb4695a Pesky None. https://github.com/ralphbean/bugwarrior/commit/6acb4695a - 834d56844 Allow unlimited description and annotation lengths https://github.com/ralphbean/bugwarrior/commit/834d56844 - 1498ea1cf fixes typo in documentation https://github.com/ralphbean/bugwarrior/commit/1498ea1cf - 87e679a81 Test against different python-jira versions. https://github.com/ralphbean/bugwarrior/commit/87e679a81 - 135c840e1 Fix JIRA test failure on the latest python-jira. https://github.com/ralphbean/bugwarrior/commit/135c840e1 - f71ebba55 Pesky gerrit. https://github.com/ralphbean/bugwarrior/commit/f71ebba55 - c822ec1be Test annotation and description builders. https://github.com/ralphbean/bugwarrior/commit/c822ec1be - 9e190c646 Implement `github.query` for the github service https://github.com/ralphbean/bugwarrior/commit/9e190c646 - 087bb7f3e py3 compatibility https://github.com/ralphbean/bugwarrior/commit/087bb7f3e - 0f2cae217 Implement involved_issues as default query. https://github.com/ralphbean/bugwarrior/commit/0f2cae217 - 94492b9c8 Add github.include_user_repos config option. https://github.com/ralphbean/bugwarrior/commit/94492b9c8 - 37465a53f update README to suggest how to disable pre-defined queries https://github.com/ralphbean/bugwarrior/commit/37465a53f - 75884833b Support enterprize github https://github.com/ralphbean/bugwarrior/commit/75884833b - d634d2ef4 Interpret trac.no_xmlrpc as a bool. https://github.com/ralphbean/bugwarrior/commit/d634d2ef4 - b1470e71d Remove pynotify notifications. https://github.com/ralphbean/bugwarrior/commit/b1470e71d - 3ad97f445 Simplify, as per review. https://github.com/ralphbean/bugwarrior/commit/3ad97f445 - a5653d308 Warn about the misleading 404 error code from github. https://github.com/ralphbean/bugwarrior/commit/a5653d308 - 95abdc644 Modify tests to reproduce #350. https://github.com/ralphbean/bugwarrior/commit/95abdc644 - 8aeda4593 Cosmetic formatting. https://github.com/ralphbean/bugwarrior/commit/8aeda4593 - 0517a4a88 Decode all byte strings from utf8 before any db actions. https://github.com/ralphbean/bugwarrior/commit/0517a4a88 - f07b53983 Break out ServiceConfig from IssueService. https://github.com/ralphbean/bugwarrior/commit/f07b53983 - 9a7bba3c9 Rename config_get_password -> get_password. https://github.com/ralphbean/bugwarrior/commit/9a7bba3c9 - fb8d3ae27 Roll service_config.get_default into .get method. https://github.com/ralphbean/bugwarrior/commit/fb8d3ae27 - 44c01a507 Test ServiceConfig. https://github.com/ralphbean/bugwarrior/commit/44c01a507 - 57e755fc3 ServiceConfig.has -> ServiceConfig.__contains__ https://github.com/ralphbean/bugwarrior/commit/57e755fc3 - 351fee08a Fix bug introduced in #458. https://github.com/ralphbean/bugwarrior/commit/351fee08a - c06280ad6 Make trac and bugzilla packages optional. Fix #460 https://github.com/ralphbean/bugwarrior/commit/c06280ad6 - 0c937969c Added authentication method detection to gerrit https://github.com/ralphbean/bugwarrior/commit/0c937969c - 21f46eabf add support for api keys https://github.com/ralphbean/bugwarrior/commit/21f46eabf - 318db0161 The required version for python-bugzilla is 2.1.0 https://github.com/ralphbean/bugwarrior/commit/318db0161 - faf9d4c57 Fix gerrit tests. https://github.com/ralphbean/bugwarrior/commit/faf9d4c57 - 2170254d4 Force list to get keys of differential reviewers https://github.com/ralphbean/bugwarrior/commit/2170254d4 - 83bf9539e Use service prefix for field templates https://github.com/ralphbean/bugwarrior/commit/83bf9539e - 571092542 Add product and component as UDA for bugzilla https://github.com/ralphbean/bugwarrior/commit/571092542 - 7b1a9502d Issue #450 fix for JIRA entry datetime comparison https://github.com/ralphbean/bugwarrior/commit/7b1a9502d - 4fc43422e Remove unused import https://github.com/ralphbean/bugwarrior/commit/4fc43422e 1.4.0 ----- Pull Requests - (@gdetrez) #253, Update instructions to get a github token https://github.com/ralphbean/bugwarrior/pull/253 - (@muxync) #260, Ignore microseconds for gitlab https://github.com/ralphbean/bugwarrior/pull/260 - (@muxync) #258, Add gitlab.host to example Gitlab target https://github.com/ralphbean/bugwarrior/pull/258 - (@sayanchowdhury) #255, Fix documentation for pagure https://github.com/ralphbean/bugwarrior/pull/255 - (@muxync) #261, add verify_ssl option to gitlab service https://github.com/ralphbean/bugwarrior/pull/261 - (@gdetrez) #266, Add missing test dependencies https://github.com/ralphbean/bugwarrior/pull/266 - (@gdetrez) #265, Fix some subtle option parsing problems: https://github.com/ralphbean/bugwarrior/pull/265 - (@gdetrez) #264, Fix the broken tests https://github.com/ralphbean/bugwarrior/pull/264 - (@bexelbie) #269, Add information about Fedora Package https://github.com/ralphbean/bugwarrior/pull/269 - (@ryneeverett) #273, Minimal CI and documentation fixes. https://github.com/ralphbean/bugwarrior/pull/273 - (@ryneeverett) #274, Use TASKRC environmental variable when assigned. https://github.com/ralphbean/bugwarrior/pull/274 - (@ryneeverett) #275, bitbucket.login is a required setting https://github.com/ralphbean/bugwarrior/pull/275 - (@ryneeverett) #277, Add --interactive flag to bugwarrior-pull. https://github.com/ralphbean/bugwarrior/pull/277 - (@ryneeverett) #281, Bitbucket closed status https://github.com/ralphbean/bugwarrior/pull/281 - (@ryneeverett) #276, bitbucket: More v2 API. Progress on #129. https://github.com/ralphbean/bugwarrior/pull/276 - (@gdetrez) #285, Password oracle improvements https://github.com/ralphbean/bugwarrior/pull/285 - (@johl) #286, Avoid time out with Phabricator installations with huge userbase https://github.com/ralphbean/bugwarrior/pull/286 - (@gdetrez) #287, Move some test dependencies to install dependencies https://github.com/ralphbean/bugwarrior/pull/287 - (@ryneeverett) #290, Add Coveralls coverage testing to CI. https://github.com/ralphbean/bugwarrior/pull/290 - (@ryneeverett) #289, Fix nosetests. https://github.com/ralphbean/bugwarrior/pull/289 - (@ryneeverett) #288, Fix bitbucket undocumented API change. https://github.com/ralphbean/bugwarrior/pull/288 - (@ryneeverett) #280, Bitbucket OAuth. Resolve #201. https://github.com/ralphbean/bugwarrior/pull/280 - (@ryneeverett) #291, Fix #254 "Edit on Github" documentation links. https://github.com/ralphbean/bugwarrior/pull/291 Commits - c79d7e1c8 Update instructions to get a github token https://github.com/ralphbean/bugwarrior/commit/c79d7e1c8 - 43aa33755 Fix documentation for pagure https://github.com/ralphbean/bugwarrior/commit/43aa33755 - 812300ac5 add gitlab.host to example Gitlab target https://github.com/ralphbean/bugwarrior/commit/812300ac5 - 20ef13da0 ignore microseconds for gitlab to prevent issue updates on every bugwarrior-pull https://github.com/ralphbean/bugwarrior/commit/20ef13da0 - a67e7eebc add verify_ssl option to gitlab service https://github.com/ralphbean/bugwarrior/commit/a67e7eebc - f3b9eba04 Fix the broken tests https://github.com/ralphbean/bugwarrior/commit/f3b9eba04 - a86e6e392 Add missing test dependencies https://github.com/ralphbean/bugwarrior/commit/a86e6e392 - 7d90c1925 Fix some subtle option parsing problems: https://github.com/ralphbean/bugwarrior/commit/7d90c1925 - aa91974e8 Handle pagure repos with disabled trackers. https://github.com/ralphbean/bugwarrior/commit/aa91974e8 - a6462057b Merge branch 'develop' of github.com:ralphbean/bugwarrior into develop https://github.com/ralphbean/bugwarrior/commit/a6462057b - 8bf4b4cf7 Add information about Fedora Package https://github.com/ralphbean/bugwarrior/commit/8bf4b4cf7 - 96435cd57 Fix test command documentation. https://github.com/ralphbean/bugwarrior/commit/96435cd57 - 00c660924 Drop python 2.6 support to fix travis build. https://github.com/ralphbean/bugwarrior/commit/00c660924 - f1cfad268 Use TASKRC environmental variable when assigned. https://github.com/ralphbean/bugwarrior/commit/f1cfad268 - 69362e312 Fix docs typo. https://github.com/ralphbean/bugwarrior/commit/69362e312 - d8a82d2b8 Document description_length option. https://github.com/ralphbean/bugwarrior/commit/d8a82d2b8 - f48f489fc bitbucket.login is a required setting https://github.com/ralphbean/bugwarrior/commit/f48f489fc - b8032db9d Add --interactive flag to bugwarrior-pull. https://github.com/ralphbean/bugwarrior/commit/b8032db9d - a0e4b74cb Bitbucket: add 'closed' status. https://github.com/ralphbean/bugwarrior/commit/a0e4b74cb - 1b5b71496 bitbucket: More v2 API. Progress on #129. https://github.com/ralphbean/bugwarrior/commit/1b5b71496 - 0b91ff2d9 Fix using @oracle with gitlab https://github.com/ralphbean/bugwarrior/commit/0b91ff2d9 - 544dae6fa Improve feedback using @oracle:eval https://github.com/ralphbean/bugwarrior/commit/544dae6fa - cfd85ab08 Extract method config_get_password https://github.com/ralphbean/bugwarrior/commit/cfd85ab08 - 4903451aa Push exception raising https://github.com/ralphbean/bugwarrior/commit/4903451aa - bf6b3ad7f Fix hang when a service die https://github.com/ralphbean/bugwarrior/commit/bf6b3ad7f - ca19b58c8 Suppress stack trace for SystemExit and RuntimeError https://github.com/ralphbean/bugwarrior/commit/ca19b58c8 - bd4e33434 If self.shown_user_phids or self.shown_project_phids is set, restrict API calls to user_phids or project_phids to avoid time out with Phabricator installations with huge userbase. https://github.com/ralphbean/bugwarrior/commit/bd4e33434 - a48f01cf3 Fix bitbucket test https://github.com/ralphbean/bugwarrior/commit/a48f01cf3 - a65a26af6 Refactor the get_keyring_service method in gitlab https://github.com/ralphbean/bugwarrior/commit/a65a26af6 - 8516f968c Call self.get_keyring_service in IssueService.config_get_password https://github.com/ralphbean/bugwarrior/commit/8516f968c - 8cf8950de Move some test dependencies to install dependencies https://github.com/ralphbean/bugwarrior/commit/8cf8950de - 022713932 Add jira, megaplan and activecollab deps as extras https://github.com/ralphbean/bugwarrior/commit/022713932 - fad124f6f Upgrade the pip version on travis https://github.com/ralphbean/bugwarrior/commit/fad124f6f - 0e6a153ad Update install documentation https://github.com/ralphbean/bugwarrior/commit/0e6a153ad - 4c04c0c1a Remove unused dependency pycurl https://github.com/ralphbean/bugwarrior/commit/4c04c0c1a - ef1935c03 Clarified documentation for Phabricator. https://github.com/ralphbean/bugwarrior/commit/ef1935c03 - d02d507fd Fix bitbucket undocumented API change. https://github.com/ralphbean/bugwarrior/commit/d02d507fd - a03db792c Add extras back to tests_require. https://github.com/ralphbean/bugwarrior/commit/a03db792c - 623a02a25 Add Coveralls coverage testing to CI. https://github.com/ralphbean/bugwarrior/commit/623a02a25 - a6fa41d68 Bitbucket OAuth. Resolve #201. https://github.com/ralphbean/bugwarrior/commit/a6fa41d68 - e2dfc826f Data store improvements. https://github.com/ralphbean/bugwarrior/commit/e2dfc826f - 65895fbd2 Fix #254 "Edit on Github" documentation links. https://github.com/ralphbean/bugwarrior/commit/65895fbd2 1.3.0 ----- Pull Requests - (@ralphbean) #241, Turn legacy_matching off by default. https://github.com/ralphbean/bugwarrior/pull/241 - (@ralphbean) #242, Comment out this section header. https://github.com/ralphbean/bugwarrior/pull/242 - (@mathstuf) #246, Better json info in errors https://github.com/ralphbean/bugwarrior/pull/246 - (@mathstuf) #247, Reformat changelog https://github.com/ralphbean/bugwarrior/pull/247 - (@mathstuf) #248, Fix gitlab tests https://github.com/ralphbean/bugwarrior/pull/248 - (@mathstuf) #249, Rhbz handle open needinfo https://github.com/ralphbean/bugwarrior/pull/249 - (@mathstuf) #251, Gitlab disabled features https://github.com/ralphbean/bugwarrior/pull/251 - (@ralphbean) #252, Support for pagure.io. https://github.com/ralphbean/bugwarrior/pull/252 - (@puiterwijk) #245, Use setuptools entry points instead of DeferredImport https://github.com/ralphbean/bugwarrior/pull/245 Commits - 220228d55 Turn legacy_matching off by default. https://github.com/ralphbean/bugwarrior/commit/220228d55 - edd2938c5 Comment out this section header. https://github.com/ralphbean/bugwarrior/commit/edd2938c5 - 2f3645bad githubutils: use the json_res for the exception info https://github.com/ralphbean/bugwarrior/commit/2f3645bad - a34d66bd8 changelog: fix formatting https://github.com/ralphbean/bugwarrior/commit/a34d66bd8 - 3b0663b75 gitlab: expect author and assignee https://github.com/ralphbean/bugwarrior/commit/3b0663b75 - 3106350c3 bz: handle open-ended needinfo requests https://github.com/ralphbean/bugwarrior/commit/3106350c3 - 095ac8bc1 gitlab: use the proper json result https://github.com/ralphbean/bugwarrior/commit/095ac8bc1 - 11ddf04bc gitlab: handle projects with disable MRs or issues https://github.com/ralphbean/bugwarrior/commit/11ddf04bc - 4d5f61b1d gitlab: handle reopened issues and MRs https://github.com/ralphbean/bugwarrior/commit/4d5f61b1d - 9958d6662 Support for pagure.io. https://github.com/ralphbean/bugwarrior/commit/9958d6662 - 49abe33f5 Make that a timezone-aware object. https://github.com/ralphbean/bugwarrior/commit/49abe33f5 - 0750259ae Use setuptools entry points instead of DeferredImport https://github.com/ralphbean/bugwarrior/commit/0750259ae - 285f9b1ba Add pagure to the README. https://github.com/ralphbean/bugwarrior/commit/285f9b1ba - 93f0d6e8b Remove old changelog header. https://github.com/ralphbean/bugwarrior/commit/93f0d6e8b 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 `_ 1.1.4 ----- - Alter default JIRA query to handle situations in which instances do not use the column names we are expecting. `34d99341e `_ - Merge pull request #213 from coddingtonbear/generalize_jira_query `9ef8f17e3 `_ - It's a gerund! `5189ef81d `_ - gitlab: handle pagination `3067b32bc `_ - gitlab: fix documentation typo `a2f1e87c9 `_ - gitlab: add a state entry `7790450a3 `_ - gitlab: fill in milestone and update/create time `a37eff259 `_ - Merge pull request #214 from mathstuf/gitlab-pagination `befe0ed46 `_ - Phabricator service is not called phabricator, but phab `df96e346b `_ - Phabricator service: Adding option to filter on users and projects `584b28fc3 `_ - Unified filtering handling `29714c432 `_ - Fixing a slightly-out-of-date gitlab test. `7174361ab `_ - Adding the documentation for phabricator filtering options. `15a6a43a0 `_ - Fix link to remove the browser warning of invalid certificate `77f84855b `_ - Merge pull request #218 from jonan/develop `07ef02dbd `_ - Merge pull request #216 from ivan-cukic/develop `1f1f4f00e `_ - Add tests to MANIFEST.in `a4d643234 `_ - Merge pull request #221 from koobs/patch-1 `42d320a05 `_ 1.1.3 ----- - Bugfix for legacy_matching. `b973e925b `_ 1.1.2 ----- - Make merging in annotations to the task db optional. `52468ac5c `_ - Merge pull request #207 from ralphbean/feature/optional-annotations `9b65f6cf4 `_ - Fixup notification error with bad encoding `2348b8ac5 `_ - Merge pull request #208 from metal3d/develop `e7928d343 `_ 1.1.1 ----- - Fixes a couple minor typos in service classpaths listed in DeferredImportingDict. `7844a0beb `_ - Merge pull request #206 from coddingtonbear/fix_service_classpath `d50486ee6 `_ 1.1.0 ----- - Rudimentary support for VersionOne. `c774952e9 `_ - Adding working VersionOne implementation. Fixes #149. `1ee7a01e7 `_ - Collect the OID, too, just in case it might be needed for future API operations. `c0e7c88d3 `_ - Add story number and priority fields. `a98fb97bf `_ - Follow the same pattern as the redmine importer for what to name the project name configuration option. `f5f9ef067 `_ - Adding documentation for new VersionOnes service. `894bfec02 `_ - Assemble keyring URL in get_keyring_service method; allow blank passwords to be entered. `709bd7036 `_ - There's no reason for this to be a set rather than just a normal tuple. `a43c28386 `_ - Merge pull request #150 from coddingtonbear/add_version_one `8297f18d7 `_ - Further limit which tasks are returned to only actionable items. `6e8333e0a `_ - Merge pull request #152 from coddingtonbear/versionone_tweaks `4da7f2208 `_ - Adding VersionOne link to readme. `4a0ad1779 `_ - Merge pull request #153 from coddingtonbear/versionone_in_readme `b4f757f2c `_ - Handle debugging in odd case where uuid doesn't return a task. `b987c9859 `_ - Messy... `0f11061e4 `_ - Extract priorities from redmine responses appropriately. `6dccc13c7 `_ - Use priority Name instead of id. `89b0195fc `_ - Add a test for new redmine behavior and fix another bug. `4a3960256 `_ - Merge pull request #155 from ralphbean/feature/redmine-priorities `2a8c1d889 `_ - Add a github repo UDA. `d136b9894 `_ - Allow trac scheme to be configurable. `e932b20d6 `_ - Mention the new githubrepo UDA in the docs. `51ac27931 `_ - Add bugzilla bug id as a UDA. `a3dc9aebc `_ - Document the ignore_cc option. `d74788b50 `_ - Merge pull request #164 from ralphbean/feature/bz-filter `d0e608394 `_ - Numeric, for sure. `ea50d7107 `_ - Merge pull request #163 from ralphbean/feature/bz-id-uda `c56ae0bbd `_ - Merge pull request #162 from ralphbean/feature/trac-scheme `0e65c59c6 `_ - Merge pull request #161 from ralphbean/feature/github-updates `7dc3a69e4 `_ - Normalize github labels to fit tag syntax `bc04158c1 `_ - add test `177d69be6 `_ - trac: use CSV downloads if TracXmlRpc is not available `5bc5a768f `_ - Clarify that filtering doesn't work for Bugzilla `f3f800118 `_ - Merge pull request #168 from djmitche/bz-docs-fix `c3627304f `_ - Merge pull request #166 from djmitche/normalize-github-tags `84a084550 `_ - Merge pull request #167 from djmitche/trac-csv `8a696a8a2 `_ - Only use github issues `821a864dc `_ - add test `b5b76d5db `_ - remove non-functional optparse usage `51f06c89f `_ - VersionOne: Adds support for timebox data and due dates. `2b0609bed `_ - Add a --dry-run option `ae66d6ae8 `_ - Merge pull request #170 from djmitche/issue148 `fe1e1557e `_ - Allow users to specify a Bugzilla query URL `014f5b60a `_ - Merge pull request #172 from djmitche/issue160 `c050b6553 `_ - Merge pull request #169 from coddingtonbear/add_versionone_timebox_and_due_date `c328c2503 `_ - Better handling for due dates for VersionOne tasks. `cafd926f2 `_ - Merge pull request #173 from coddingtonbear/add_timezone_support_to_versionone `fb0c8f832 `_ - Adding minimal documentation regarding what external packages are required for each service. `0cb81a124 `_ - Merge branch 'normalize-github-tags' of git://github.com/djmitche/bugwarrior into develop `634601f7d `_ - Fix labels-as-tags test. `85d9a6822 `_ - Merge pull request #175 from coddingtonbear/add_external_requirements `28c27d006 `_ - Use os.makedirs for directory creation. `15e537c28 `_ - Add an option to disable SSL verification for Jira `37354467e `_ - Add doc about jira.verify_ssl `ec8b773a6 `_ - Merge pull request #179 from mavant/feature/ssl-verify-flag `df19eda63 `_ - Merge pull request #178 from mavant/develop `dbef39509 `_ - Adding handling for NoneDeref instances returned by VersionOne. `0d0d9bc4d `_ - Merge pull request #180 from coddingtonbear/handle_v1_nonederef `e3a959988 `_ - Fix 'not empty' filter for string-type UDAs, #181 `e7f2328fc `_ - Merge pull request #182 from bmbove/empty-filter-fix `765b90759 `_ - Show a message to the user in the event that we were unable to perform the operation. `4b0184b6f `_ - Merge pull request #183 from coddingtonbear/show_errors_when_unable_to_add `e407d6e85 `_ - Adding a new 'bugwarrior-uda' command that will print a list of UDAs to the console directly. `054e5045c `_ - Adding a note about how to export UDAs. `64ad46544 `_ - Also add markers so users will find it easier to know which UDAs were generated by Bugwarrior. `c5f97314c `_ - Merge pull request #184 from coddingtonbear/add_uda_export_command `462794241 `_ - Hack to let task-2.4.x search for url UDAs. `ae3db7d94 `_ - Merge pull request #185 from ralphbean/feature/url-hack `a59743514 `_ - Typofix. `a9d273637 `_ - Merge branch 'feature/url-hack' into develop `c01f68359 `_ - fixed docs for Jira, requirements `7664e1264 `_ - config: add support for XDG paths `feda0993d `_ - docs: update references to .bugwarriorrc `07148bce5 `_ - Mention nosetests in the contributing docs. `c1d54e908 `_ - README: use https where possible `2f8d2b26c `_ - docs: fix a typo `4e94081f0 `_ - gitlab: add initial gitlab support `23c1d2491 `_ - gitlab: add docs `4d2dedf5b `_ - gitlab: add tests `8215127cf `_ - config: add --flavor option `6af8b6f0f `_ - Merge pull request #192 from mathstuf/configuration-option `063d03d27 `_ - Merge pull request #190 from mathstuf/xdg-support `ce5b8ffda `_ - Merge pull request #191 from mathstuf/gitlab-support `ed9af7ff5 `_ - config: give a meaningful error message for empty targets `7d910ff29 `_ - gitlab: remove 'username' configuration `060e9da15 `_ - removed requirements, fixed typo `62520981d `_ - gitlab: verify SSL certs `52473d6e5 `_ - Merge pull request #194 from mathstuf/gitlab-username `b5275da70 `_ - Merge pull request #195 from mathstuf/gitlab-verify-ssl `0e5fd2ff8 `_ - Merge pull request #187 from fradeve/FDV_fix_jira_docs `35ad25fe3 `_ - Merge pull request #193 from mathstuf/empty-targets `d170615d3 `_ - targets: ignore notifications section as well `49d95f9eb `_ - db: fix missing argument `4c7e84e1b `_ - Merge pull request #196 from mathstuf/ignore-notifications `2ce32161c `_ - Merge pull request #197 from mathstuf/fix-missing-argument `0e9d0c6a5 `_ - github: add support for OAuth2 authentication `7f96476ca `_ - bitbucket: allow filtering repos `74b9ded52 `_ - bitbucket: fix url logic `4a327ab3f `_ - bitbucket: support fetching pull requests `970e20bf7 `_ - bitbucket: prefer https `8725635b0 `_ - Merge pull request #199 from mathstuf/github-oauth `3e02be4e3 `_ - Merge pull request #200 from mathstuf/bitbucket-filter-repo `408421ec2 `_ - Defer importing services until they are needed. `63d1a8365 `_ - Add some tests for importability. `c07481093 `_ - Merge pull request #203 from ralphbean/feature/dynamic-services `09105b029 `_ - (trac) Fix misquote of "@" character. `bc1d0421b `_ - (trac) support both xmlrpc and the other way. `0365275fd `_ - It's a shame that twiggy doesn't handle encodings gracefully. Bad choice of a logging lib, @ralphbean. `e3442f517 `_ - Add uuid for debuggery. `671be26a1 `_ 1.0.2 ----- - Fix dep typo. `bd53a4c73 `_ 1.0.1 ----- - Elaborate on github.username and github.login. `06dfee567 `_ - This definitely requires taskw. Fixes 146. `7cf09804b `_ - Setup logging before we check the config. `bce65c0c8 `_ - Reorganize the way docs are shipped.. `027f05c63 `_ 1.0.0 ----- - Clock how long each target takes. `4a580b722 `_ - Pull requests should honour include and exclude filters too `129fd40c3 `_ - Off by one `b67cdccf2 `_ - style(github): cleanup `fb3dbb422 `_ - Merge pull request #91 from do3cc/repo_filter_for_prs `ab1a44354 `_ - Significant bugwarrior refactor. `182c0ddcd `_ - Testing and cleanup of bugwarrior refactor. `cde5c2e4d `_ - Adding tests. `09685d671 `_ - Re-adding URL shortening via Bit.ly. `179a4c4f5 `_ - Fixing two PEP-8 failures. `2a2f4f858 `_ - Updating a slightly out-of-date line in the readme. `5d6af8f18 `_ - Don't declare tasks different if the user has modified the priority locally. `0596653b7 `_ - Careful for the default locale here... \ó/ `bbf5e29b2 `_ - Strip links when doing legacy comparisons. `e29f5c612 `_ - Pass along details of the MultipleMatches exception. `b64169bd9 `_ - Proceed along happily if taskwarrior shellout fails at something. `595b77850 `_ - Misc fixes to the bugzilla service. `48eb4c4ed `_ - Misc fixes to the trac service. `fd18dd656 `_ - Bugfix. `44ed534a6 `_ - Removing EXPERIMENTAL.rst. `d52327f0c `_ - Adding a couple clarification docstrings. `6df94a864 `_ - Let's actually explain how this works. `0dfd5cdb0 `_ - Adding myself to contributors list. `af6585053 `_ - Converting from str to six.text_type. `b442d9691 `_ - Fixing error handling such that processing is aborted if there is a single failure. `c96ef590e `_ - Improve logging during task-db manipulation. `eb53716b0 `_ - Improve bitbucket error message. `8059b11a4 `_ - Typofix. `57462968b `_ - Check specifically for pending and waiting tasks. `324de2944 `_ - Only remove existing uuids if they are found. `2b09d2f35 `_ - Log a little more here. `371622be1 `_ - Update UDAS documentation to properly describe the data structure in use. `23882caf3 `_ - Change service-defined UDAs message to not imply necessity. `cf78e6884 `_ - Confining myself to 80 chars. `c5408d938 `_ - Restrict description matches during check for managed tasks to tasks that are not completed; move managed task gathering into a separate function. `a1c17a6a2 `_ - Read config file in as unicode to allow one to specify tags containing non-ascii characters. `2b2b6823c `_ - Adding option allowing one to specify tags that will be automaically added to all incoming issues of this type. `7e78f7506 `_ - Updating and fixing documentation. `79b322036 `_ - Adding option allowing one to import github labels as tags. `1f2cbf8f6 `_ - Merge pull request #93 from coddingtonbear/refactor_bugwarrior `8d0dd7ac1 `_ - Merge pull request #94 from coddingtonbear/add_tags_option `64e6b26fe `_ - Merge pull request #95 from coddingtonbear/add_github_labels `b83864c22 `_ - Avoid false positive in tasks_differ. `3b5be9a72 `_ - Include just the description here. `e03fe0b23 `_ - Support multiple UNIQUE_KEYs per service. `fdfecbf86 `_ - Use the TYPE as a second unique key for github issues. `cccbe7da3 `_ - Stop duplicating github pull requests. `3abdc9d2a `_ - Break out and fix "merge_annotations" `466cfa2df `_ - Initial refactoring of ActiveCollab3 integration `dc18c30b9 `_ - Rename ActiveCollab3 to ActiveCollab `143f68513 `_ - Resolve merge `ee02377df `_ - More search and replace `0bb531388 `_ - Clean up due dates, permalinks, misc `aabb28e3c `_ - Store the parent task id for subtasks `8590e4a82 `_ - Merge pull request #96 from kostajh/refactor_bugwarrior_ac3 `833f7c5c4 `_ - Start up a new hacking doc. `9a2b8da28 `_ - Ignore eggs. `0784be364 `_ - Add a phabricator service. `74072bda2 `_ - Initial work on adding a pre_import hook `4a1304a43 `_ - Merge pull request #99 from kostajh/hooks `17f4f5ff1 `_ - Use FOREIGN_ID for task matching instead of PERMALINK `3ec1e206e `_ - Initial work on Travis CI `a5e6f4224 `_ - Remove IRC for now `4fa9a503d `_ - Install some modules `a1736bf04 `_ - Fix jira-python reference `85710f6ea `_ - Merge pull request #101 from kostajh/develop `102fb6073 `_ - Merge pull request #102 from kostajh/travis `dd785d39f `_ - Only use this identifier. `8812b94bb `_ - Add irc notifications to travis config. `c0073bf62 `_ - Fix failing test for activecollab `41cc4580a `_ - Merge branch 'develop' of https://github.com/ralphbean/bugwarrior into activecollab-test `878a5af3c `_ - Merge pull request #103 from kostajh/activecollab-test `ee2b4e2f3 `_ - Fix identification of matching tasks by UDA. `f01159934 `_ - PEP-8/style fixes. `307069f5c `_ - Merge pull request #104 from coddingtonbear/fix_local_uuid_matching_keys `968b02747 `_ - Merge pull request #105 from coddingtonbear/fix_pep8_errors `9eb3f6d10 `_ - Gather a couple of additional fields from github while we're up there. `13db46fae `_ - Merge pull request #106 from coddingtonbear/github_description `496f881e9 `_ - Handle JIRA priority slightly more gracefully. `277a8850a `_ - Merge pull request #108 from coddingtonbear/handle_jira_priority_more_gracefully `3008ce157 `_ - Adding JIRA's 'description' field to stored task data. `715a7dfc0 `_ - Fixing ability to pull-in annotations; updating readme. `1be6dc037 `_ - Merge pull request #109 from coddingtonbear/jira_enhancements `0aa464a50 `_ - Use the pyac library for calling ActiveCollab. Tests need work. `3eda81dc2 `_ - Convert body text to markdown `db3f6dff7 `_ - Pull comments from tasks in as annotations. (work in progress) `875bc4ab1 `_ - Implement get_annotations(). Try to fix tests. `cd95e1da4 `_ - Install required python modules `4c2aafea9 `_ - Fix test case for pypandoc conversion. Pass annotations to TW for test. `129037c88 `_ - PEP8 `79488f4a8 `_ - Kill off dep information if present. `44421dc93 `_ - Move from bitly over to da.gd. It is free software. `383b55cac `_ - Install pandoc `be94dbb89 `_ - Update jira python module `5fd48177c `_ - Install latest stable of taskwarrior `689ed3d01 `_ - Install libuuid `a3f650ef3 `_ - Wrong packagename, try uuid-dev `697d1a1b0 `_ - cd back to build dir. `daaf5d3bd `_ - Add in the Travis CI status images `be19334e6 `_ - Hmm, let's fix that table. `d46affcff `_ - Try to sanitize strings before logging here. Twiggy freaks out in some cases. `883b3abbf `_ - Github's API sometimes returns a troublesome dict here. `21a08f09b `_ - A little more debugging. `945099b9f `_ - Handle some conversion cases to minimize erroneous "diffs" `89a82ebc0 `_ - Sometimes, also, this is None. `15f678ea0 `_ - Fixing various test failures that are all my fault. `f844a1f3a `_ - Also gather issues directly-assigned to a user, regardless of whether the originating repository is owned by the user. `c62dbc0e2 `_ - Add a development mode flag. `8187b5776 `_ - Use a PID lockfile to prevent multiple bugwarrior processes from running simultaneously on the same repository. Fixes #112. `c4de7f030 `_ - Updating an inaccurate docstring. `fe54aa088 `_ - Merge pull request #116 from coddingtonbear/issue_112 `a9519a8b8 `_ - Merge pull request #115 from coddingtonbear/add_development_mode_flag `7a4dd8d0e `_ - Merge pull request #114 from coddingtonbear/gather_directly_assigned_issues `286e92a46 `_ - Merge pull request #113 from coddingtonbear/fix_tests_apr `4d698561a `_ - Merge pull request #111 from kostajh/activecollab-enhancements `26d8380e8 `_ - Older versions of lockfile don't support timeout in the context manager.. unfortunately. :( `9cbf0e5e4 `_ - Make activecollab optional (mostly due to the pandoc dep). `f3166d378 `_ - Add new UDA handling; use task object journaling instead of checking for changes manually. `71e0bea70 `_ - Removing now-unncessary function for finding task changes. `f6d64b66b `_ - Always add timezone information to parsed datetimes; allow one to specify a default timezone. `ba2899335 `_ - Do not attempt to use task methods for new tasks. `3817537df `_ - Make sure that an array exists always. `4f03bb43c `_ - Adding arbitrary timezone information to test datetimes. `595f4544e `_ - Adding timezone information to github test. `5e158c9f7 `_ - Convert incoming annotations to strings. `a3acc1da4 `_ - Merge pull request #119 from coddingtonbear/always_timezones_always `a4a745c38 `_ - Merge remote-tracking branch 'upstream/develop' into bugwarrior_marshalling `367801ea5 `_ - Report which fields have changed when updating a task. `8d19b6edc `_ - Github milestones are integers. `525add3bd `_ - And so it begins. `841698744 `_ - Create sphinx (read-the-docs compatible) docs for Bugwarrior. `e981cc2cb `_ - Merge pull request #120 from coddingtonbear/hor_em_akhet `8ce1c0227 `_ - Link to rtfd. `61f9070a2 `_ - Link common configuration options explicitly. `0e13c4bed `_ - Merge pull request #121 from coddingtonbear/make_common_options_explicit `7047c354b `_ - Merge branch 'develop' into bugwarrior_marshalling `2c811b88c `_ - Generalize field templating logic to allow overriding the generated value of any field. `baf15abd9 `_ - Updating documentation to link to field templates rather than description templates. `ffad15b9b `_ - ActiveCollab Service: Make dates timezone aware, and default to US/Eastern. If users request a change we can add this as a config option `de34d36e9 `_ - Merge pull request #124 from kostajh/develop `572faf9fa `_ - Merge pull request #122 from coddingtonbear/generalize_template_handling `259c75ed4 `_ - Add new UDA handling; use task object journaling instead of checking for changes manually. `5ff726337 `_ - Removing now-unncessary function for finding task changes. `cf2502559 `_ - Do not attempt to use task methods for new tasks. `f7765ef7c `_ - Make sure that an array exists always. `fbbaa2661 `_ - Convert incoming annotations to strings. `82c36e994 `_ - Report which fields have changed when updating a task. `f8d3b2599 `_ - Github milestones are integers. `eb2247af7 `_ - Nope. That's numeric... `021e59dac `_ - Merge pull request #118 from coddingtonbear/bugwarrior_marshalling `92fdb5de1 `_ - Allow one to specify tags using templates, too. `62f3f0581 `_ - Fixes a broken activecollab test. `cc7ed66ac `_ - Merge pull request #127 from coddingtonbear/fix_activecollab_test `d3c4e7d98 `_ - Merge pull request #126 from coddingtonbear/tag_templates `12e37342a `_ - Add a failing test for db.merge_left. `c50fce5b8 `_ - Static fields. `14dbcff0e `_ - WIP `502f2789a `_ - Update docs and test `e3a4af4c0 `_ - Project ID is a string `b964e4679 `_ - Use six `b5db5d0bb `_ - Set issue Label as a UDA rather than a task. Remove unnecessary use of six.text_type(). Set created on as a date, not a string. And fix the tests! `96182a4d9 `_ - Merge pull request #128 from kostajh/activecollab-refactor `1e5489468 `_ - Make pull requests a top priority. `79b7d3194 `_ - Suppress stderr. `416f52e24 `_ - Make tasktools.org an example for JIRA. Fixes #107. `9ca33e0a8 `_ - fix issue with missing longdesc `458e9b460 `_ - Merge pull request #133 from mvcisback/longdesc `c235822be `_ - optionally ignore cc'd bugs `95fca9595 `_ - Merge pull request #134 from mvcisback/no_cc `c7fdf2b39 `_ - New inline_links option. `de0071048 `_ - Sleep so we can take it easy on gpg-agent. `a531f3ae5 `_ - Include a message indicating how many pull requests were found. `1373df691 `_ - Conditionally filter pull requests, too, if github.filter_pull_requests is true. `469d14dfa `_ - Adding documentation of the 'github.filter_pull_requests' option. `6b5a03b38 `_ - Cleaning up log messages to be slightly more consistent. `cf5489ad2 `_ - Removing unnecessary whitespace. `4ac7b7fbb `_ - Properly link to the 'Common Configuration Options' reference. `d4e320688 `_ - Merge pull request #137 from coddingtonbear/github_filterable_pull_requests `7c72431f1 `_ - Make trac.py url quote the username/password `f52bf411e `_ - Merge pull request #138 from puiterwijk/feature/complex-passwords `44201a97a `_ - Allow explicit configuration setting for disabling/enabling Issue URL annotations. `f8358a61d `_ - Fixing JIRA issue gathering. `d2a4dd346 `_ - Shortening one of the lines to satisfy Pep8Bot. `7a5a02c75 `_ - Merge pull request #139 from coddingtonbear/inline_annotation_links_fix `1410fff72 `_ - Adding functionality allowing one to update extra post-object-creation. `e91636fd4 `_ - Only create JiraIssue instance once. `2a9f1b8fa `_ - Only create ActivecollabIssue instance once. `3ef7e3f5b `_ - Only create BitbucketIssue instance once. `e63745500 `_ - Only create BugzillaIssue instance once. `8c572587c `_ - Only create GithubIssue instance once. `2ca1ec0ed `_ - Only create TracIssue instance once. `61ed88f76 `_ - Merge pull request #140 from coddingtonbear/inline_annotation_links_fix_single_create `e6d78175a `_ - More prominently document these options. `bfdb3975b `_ - Fix incorrect logic. `93fc03fef `_ - Fix a typo in the github docs `87c10db6a `_ - Merge pull request #142 from lmacken/develop `2bbc92fd8 `_ - Add a bugwarrior-vault command. `7f1c31798 `_ - Merge pull request #143 from ralphbean/feature/vault `c97e512c6 `_ 0.7.0 ----- - Add some hacking instructions for @teranex. `340a5e2ea `_ - Add support for include_repos `265683b78 `_ - Merge pull request #88 from pypingou/develop `c7703c4f6 `_ - Add @oracle:eval: option to get the password from an external command `47d3cf189 `_ - Merge pull request #89 from puiterwijk/add-oracle-eval `d47f90d78 `_ - Use new taskw lingo. `bf1ea4ff1 `_ - Handle a bunch of contingencies for python-bugzilla>=0.9.0 `ee4df9935 `_ - Conditionalize jira inclusion. `423040cea `_ - Merge pull request #90 from ralphbean/feature/new-taskw `ce574868d `_ - Knock out jira-python by default for now. `b4f8112a2 `_ 0.6.3 ----- - Another tweak for #85. `b732b4f47 `_ 0.6.2 ----- - Issue #82: Implement mechanism for asking the user or a keyring for passwords (see: bugwarrior.config:get_service_password()). `ad0c1729d `_ - Issue #82 related: Cleanup some debug statements. `7f98990cd `_ - Issue #82 related: Some pep8 cleanup. `d915515a1 `_ - Issue #82 related: Add example description for password lookup strategies. `2cb57e752 `_ - Merge pull request #83 from jenisys/feature/ask_password `d2a7f6695 `_ - Bitbucket with authorization and on requests `1b74cc0a9 `_ - Bitbucket - password asking logic `c388c6b89 `_ - Reformat by pep8 `5b2556247 `_ - Merge pull request #84 from paulrzcz/develop `f25be82a0 `_ - Make bitbucket authn optional. `84a0c51b6 `_ - Try to support older bugzilla instances. `474e61eb8 `_ - Update only_if_assigned github logic for #85. `86a0dd6c2 `_ 0.6.1 ----- - Make the jira service version 4 compatible `9d8347655 `_ - Fixes for backward compatibility `e144f5b02 `_ - Make the multiprocessing option really optional. `3eb477c0f `_ - Merge pull request #68 from nikolavp/jira4-fixes `9c000d8b7 `_ - Support filtering by repo for github. Fixes #72. `5a116e1d2 `_ - Use permalink for subtasks if provided `22e639197 `_ - Merge pull request #73 from kostajh/develop `72a851472 `_ - Make the annotation and description length configurable. `5dc896661 `_ - Set default description and annotation lengths `9f3c5c7dd `_ - Merge pull request #76 from lmacken/feature/longer `8feb6903f `_ - Fix ticket inclusion logic for #79. `263da5657 `_ 0.6.0 ----- - First run at multiprocessing. Awesome. `59f89be81 `_ - Config and logging for multiprocessing. `cf7fbe9a5 `_ - Misc cleanup. `1a11898d8 `_ - Handle worker failure more explicitly. `2a80de244 `_ - Merge pull request #49 from ralphbean/feature/multiprocessing `3d0c8f456 `_ - Can now define prefix to be added to project name of pulled Jira tasks `34400d761 `_ - First pass at adding ActiveCollab3 service `251a92472 `_ - Add notes to README `5633ca1ad `_ - Merge pull request #50 from ubuntudroid/develop `6e08dd36f `_ - Get the bugzilla service working again after recent API changes (#53) `506de20dc `_ - Merge pull request #54 from lmacken/develop `eedb0f8a9 `_ - Remove some debug statements `00b6f788b `_ - Merge pull request #56 from kostajh/activecollab3 `2e6fabc8d `_ - Try using dogpile.cache to stop bitly api crises. `afbab3607 `_ - More verbose debugging. `125cbaeac `_ - Merge branch 'develop' into feature/cache-for-bitly `f965e1b45 `_ - Some pep8. `314c16229 `_ - Finished pep8 pass. `8326b85c9 `_ - First pass at adding notifications. `8b258b39e `_ - Strip illegal(?) characters from message `740e4314d `_ - Merge pull request #58 from ralphbean/feature/cache-for-bitly `003438184 `_ - Merge pull request #57 from ralphbean/feature/pep8 `3700fbec5 `_ - Handle empty comments from bz. `f8ef9736c `_ - Backwards compatibility for bugzilla annotations `1c05d2bff `_ - Refactor, use growlnotify `7a0d0975f `_ - Allow for configuring stickiness of notifications `16182e82b `_ - Update readme `8d714dd92 `_ - Cleanup some unused imports `cc6b0f376 `_ - Change binary to "backend" `03128dda8 `_ - Merge pull request #59 from kostajh/notifications `953adfe6c `_ - pynotify notifications for Linux. `4513b0711 `_ - Typofix. `3a37cde64 `_ - Some bugfixes to #59. `2eaa69313 `_ - Added a third "gobject" notification backend. `a7a51ae9c `_ - Merge pull request #60 from ralphbean/feature/pynotify `f0f9b600f `_ - More notification bugfixes. `47edf0e55 `_ - Mention how to use notifications under cron. Fixes #61. `febc2128c `_ - Use project slug instead of full name, makes typing project name in TW simpler `83e45a8cd `_ - PEP8. `fb9b7dfc6 `_ - Fixes for AC3 `a720374fb `_ - Merge branch 'develop' into active-collab3-fixes `b677b5970 `_ - Merge pull request #65 from kostajh/active-collab3-fixes `306b62344 `_ - Initial work on task merge approach `897f869b0 `_ - Load correct config before merge `c4f8341b0 `_ - Set project name to project slug `aac7afb9b `_ - Cleanup `af3599801 `_ - Merge branch 'develop' into task-merge `069f10c4b `_ - Ignore annotations for task updates. Call task_done in users primary TW database when completing a task, as task merge wont get them. `56bbdd388 `_ - Delete completed tasks from Bugwarrior DB. This allows for assigning/reassigning tasks. `aa111cec9 `_ - Do not need to load only pending tasks since we are marking BW database tasks as completed at the end of each sync `903a67228 `_ - Remove pprint `6010caa6b `_ - Remove slashes from project slug `ba6dce557 `_ - Merge branch 'develop' into task-merge `fbb7941ee `_ - Merge pull request #66 from kostajh/develop `d011e555f `_ - Merge branch 'task-merge' of git://github.com/kostajh/bugwarrior into task-merge `3a2cd196d `_ - Crucial. `5d831bec1 `_ - Be still more careful with the way we load options. `3aa3bce81 `_ - PEP8 pass. `91e92ad77 `_ - Github supports ticket assignment these days. Fixes #29. `3950146a0 `_ - Add notes on using Bugwarrior in experimental mode `b9122a1ca `_ - Fix link to taskw `21e115ac1 `_ - Merge pull request #69 from kostajh/develop `72bd6faf2 `_ - Update AUTHORS section of the README re @kostajh. `a300aad8d `_ - Be more careful with this header dict. `6287a4235 `_ - Loosen version constraint on python-requests. `0f8b3690c `_ - Merge pull request #71 from ralphbean/feature/modern-requests `67fadc63d `_ 0.5.8 ----- - Typofix in docs. `f725ad5f9 `_ - Merge remote-tracking branch 'upstream/develop' into develop `9a21b33a1 `_ - First pass at adding priority and due date support for AC service `9e40f56f4 `_ - Fix due dates and priority `3fb653258 `_ - Add debug statements `30496b785 `_ - More debug statements `38b2c832f `_ - Check if priority is set before assigning to issue `7d566463f `_ - Add due info only if it is present in the issue `382e8ec29 `_ - Merge pull request #44 from kostajh/priority-and-due-activecollab `22ce01be1 `_ - User defined JQL queries for Jira services `24b996753 `_ - Support older python-requests. Relates to #46. `929991954 `_ - Pin requests<=0.14.2. Fixes #46. `1a71117ce `_ - Add default for jira.query `64e18b7fb `_ - More elegant setting of jira query variable `f4f164e7c `_ - Merge pull request #47 from ubuntudroid/develop `df71d745c `_ 0.5.7 ----- - Added list of contributors to the bottom of the README. `726a91986 `_ - minor: correction to annotation format `6d5fedad0 `_ - Merge pull request #37 from tychoish/jira-patch-1 `7e7361aa5 `_ - Added notes about format for the .isues() method. #39 `737d2ea82 `_ - First pass at activecollab2 integration `94d5cff9c `_ - Reformat task description, add code for stripping html `b152c3eba `_ - Only add permalink as annotation. Comments are not useful. `78ce70cb9 `_ - Cleanup formatting. `d117667ab `_ - Handle cases where user tasks data isn't returned `18494dc1e `_ - Log task count for debugging `ae0396b8c `_ - Debug formatting. `5e2a34716 `_ - Add notes to README `06b8d0cad `_ - Add more info on configuring service `0ac46e26b `_ - Add notes on API usage, minor code cleanup `a041bc16f `_ - Add note about api compatibility `925a638b1 `_ - Merge pull request #41 from kostajh/active-collab `1175c4dc3 `_ 0.5.6 ----- - support for jira to complete #32 `4d2e6bf53 `_ - minor: tweak query `d61083c38 `_ - jira: correcting link and tightening display `a390609e3 `_ - jira: adding docs and example config information to readme `5d3e70a3b `_ - Merge pull request #36 from tychoish/jira-service `e51bed803 `_ - README tweaks. `364883c17 `_ 0.5.5 ----- - Support for TeamLab `c81999adc `_ - Support for RedMine `39aeb8b09 `_ - Merge pull request #34 from umonkey/develop `fe1915c85 `_ - Death to pygithub3. `18fc9c59e `_ - Protect against bitbucket repos that have no issues. For #35 `c7b739956 `_ - Bugzilla comments as annotations. Fixes #27. `5126b6a99 `_ 0.5.4 ----- - Support for megaplan.ru `392b8c9a6 `_ - Support for Unicode in issue descriptions `125564ece `_ - Updated README.rst to include megaplan support `01fe88a7c `_ - Re-import deleted tasks `15e568a84 `_ - Fixed bad links to megaplan issue pages `4117bd8ca `_ - Merge pull request #33 from umonkey/develop `c35f5a2c9 `_ bugwarrior-1.8.0/LICENSE.txt000066400000000000000000001045131376471246600155520ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . bugwarrior-1.8.0/MANIFEST.in000066400000000000000000000003231376471246600154570ustar00rootroot00000000000000include bugwarrior/README.rst include README.rst include LICENSE.txt recursive-include docs * recursive-include bugwarrior/docs *.rst recursive-include tests * global-exclude __pycache__ global-exclude *.py[co] bugwarrior-1.8.0/README.rst000077700000000000000000000000001376471246600212572bugwarrior/README.rstustar00rootroot00000000000000bugwarrior-1.8.0/bugwarrior/000077500000000000000000000000001376471246600161065ustar00rootroot00000000000000bugwarrior-1.8.0/bugwarrior/README.rst000066400000000000000000000053411376471246600176000ustar00rootroot00000000000000bugwarrior - Pull tickets from github, bitbucket, bugzilla, jira, trac, and others into taskwarrior =================================================================================================== .. split here ``bugwarrior`` is a command line utility for updating your local `taskwarrior `_ database from your forge issue trackers. It currently supports the following remote resources: - `activecollab `_ (2.x and 4.x) - `bitbucket `_ - `bugzilla `_ - `Debian BTS `_ - `gerrit `_ - `github `_ (api v3) - `gitlab `_ (api v3) - `gmail `_ - `jira `_ - `megaplan `_ - `pagure `_ - `phabricator `_ - `Pivotal Tracker `_ - `redmine `_ - `taiga `_ - `teamlab `_ - `trac `_ - `trello `_ - `versionone `_ - `youtrack `_ Documentation ------------- For information on how to install and use bugwarrior, read `the docs `_ on RTFD. Build Status ------------ .. |master| image:: https://secure.travis-ci.org/ralphbean/bugwarrior.png?branch=master :alt: Build Status - master branch :target: https://travis-ci.org/#!/ralphbean/bugwarrior .. |develop| image:: https://secure.travis-ci.org/ralphbean/bugwarrior.png?branch=develop :alt: Build Status - develop branch :target: https://travis-ci.org/#!/ralphbean/bugwarrior +----------+-----------+ | Branch | Status | +==========+===========+ | master | |master| | +----------+-----------+ | develop | |develop| | +----------+-----------+ Contributors ------------ - Ralph Bean (primary author) - Ben Boeckel (contributed support for Gitlab) - Justin Forest (contributed support for RedMine, TeamLab, and MegaPlan, as well as some unicode help) - Tycho Garen (contributed support for Jira) - Kosta Harlan (contributed support for activeCollab, notifications, and experimental taskw support) - Luke Macken (contributed some code cleaning) - James Rowe (contributed to the docs) - Adam Coddington (anti-entropy crusader) - Iain R. Learmonth (contributed support for the Debian BTS and maintains the Debian package) - BinaryBabel (contributed support for YouTrack) - Matthew Cengia (contributed extra support for Trello) - Andrew Demas (contributed support for PivotalTracker) bugwarrior-1.8.0/bugwarrior/__init__.py000066400000000000000000000001451376471246600202170ustar00rootroot00000000000000# from bugwarrior.command import pull, vault, uda __all__ = [ 'pull', 'vault', 'uda', ] bugwarrior-1.8.0/bugwarrior/command.py000066400000000000000000000115331376471246600201010ustar00rootroot00000000000000from __future__ import print_function import os import sys from lockfile import LockTimeout from lockfile.pidlockfile import PIDLockFile import getpass import click from bugwarrior.config import ( get_data_path, get_keyring, load_config, ServiceConfig) from bugwarrior.services import aggregate_issues, get_service from bugwarrior.db import ( get_defined_udas_as_strings, synchronize, ) import logging log = logging.getLogger(__name__) # We overwrite 'list' further down. lst = list def _get_section_name(flavor): if flavor: return 'flavor.' + flavor return 'general' def _try_load_config(main_section, interactive=False): try: return load_config(main_section, interactive) except IOError: # Our standard logging configuration depends on the bugwarrior # configuration file which just failed to load. logging.basicConfig() exc_info = sys.exc_info() log.critical("Could not load configuration. " "Maybe you have not created a configuration file.", exc_info=(exc_info[0], exc_info[1], None)) sys.exit(1) @click.command() @click.option('--dry-run', is_flag=True) @click.option('--flavor', default=None, help='The flavor to use') @click.option('--interactive', is_flag=True) @click.option('--debug', is_flag=True, help='Do not use multiprocessing (which breaks pdb).') def pull(dry_run, flavor, interactive, debug): """ Pull down tasks from forges and add them to your taskwarrior tasks. Relies on configuration in bugwarriorrc """ try: main_section = _get_section_name(flavor) config = _try_load_config(main_section, interactive) lockfile_path = os.path.join(get_data_path(config, main_section), 'bugwarrior.lockfile') lockfile = PIDLockFile(lockfile_path) lockfile.acquire(timeout=10) try: # Get all the issues. This can take a while. issue_generator = aggregate_issues(config, main_section, debug) # Stuff them in the taskwarrior db as necessary synchronize(issue_generator, config, main_section, dry_run) finally: lockfile.release() except LockTimeout: log.critical( 'Your taskrc repository is currently locked. ' 'Remove the file at %s if you are sure no other ' 'bugwarrior processes are currently running.' % ( lockfile_path ) ) except RuntimeError as e: log.exception("Aborted (%s)" % e) @click.group() def vault(): """ Password/keyring management for bugwarrior. If you use the keyring password oracle in your bugwarrior config, this tool can be used to manage your keyring. """ pass def targets(): config = load_config('general') for section in config.sections(): if section in ['general', 'notifications'] or \ section.startswith('flavor.'): continue service_name = config.get(section, 'service') service_class = get_service(service_name) for option in config.options(section): value = config.get(section, option) if not value: continue if '@oracle:use_keyring' in value: service_config = ServiceConfig( service_class.CONFIG_PREFIX, config, section) yield service_class.get_keyring_service(service_config) @vault.command() def list(): pws = lst(targets()) print("%i @oracle:use_keyring passwords in bugwarriorrc" % len(pws)) for section in pws: print("-", section) @vault.command() @click.argument('target') @click.argument('username') def clear(target, username): target_list = lst(targets()) if target not in target_list: raise ValueError("%s must be one of %r" % (target, target_list)) keyring = get_keyring() if keyring.get_password(target, username): keyring.delete_password(target, username) print("Password cleared for %s, %s" % (target, username)) else: print("No password found for %s, %s" % (target, username)) @vault.command() @click.argument('target') @click.argument('username') def set(target, username): target_list = lst(targets()) if target not in target_list: raise ValueError("%s must be one of %r" % (target, target_list)) keyring = get_keyring() keyring.set_password(target, username, getpass.getpass()) print("Password set for %s, %s" % (target, username)) @click.command() @click.option('--flavor', default=None, help='The flavor to use') def uda(flavor): main_section = _get_section_name(flavor) conf = _try_load_config(main_section) print("# Bugwarrior UDAs") for uda in get_defined_udas_as_strings(conf, main_section): print(uda) print("# END Bugwarrior UDAs") bugwarrior-1.8.0/bugwarrior/config.py000066400000000000000000000252111376471246600177260ustar00rootroot00000000000000from future import standard_library standard_library.install_aliases() import codecs from six.moves import configparser import os import subprocess import sys import re import six import logging log = logging.getLogger(__name__) from bugwarrior.data import BugwarriorData # The name of the environment variable that can be used to ovewrite the path # to the bugwarriorrc file BUGWARRIORRC = "BUGWARRIORRC" def asbool(some_value): """ Cast config values to boolean. """ return six.text_type(some_value).lower() in [ 'y', 'yes', 't', 'true', '1', 'on' ] def aslist(value): """ Cast config values to lists of strings """ return [item.strip() for item in re.split(",(?![^{]*})",value.strip())] def asint(value): """ Cast config values to int, or None for empty strings.""" if value == '': return None return int(value) def get_keyring(): """ Try to import and return optional keyring dependency. """ try: import keyring except ImportError: raise ImportError( "Extra dependencies must be installed to use the keyring feature. " "Install them with `pip install bugwarrior[keyring]`.") return keyring def get_service_password(service, username, oracle=None, interactive=False): """ Retrieve the sensitive password for a service by: * retrieving password from a secure store (@oracle:use_keyring, default) * asking the password from the user (@oracle:ask_password, interactive) * executing a command and use the output as password (@oracle:eval:) Note that the keyring may or may not be locked which requires that the user provides a password (interactive mode). :param service: Service name, may be key into secure store (as string). :param username: Username for the service (as string). :param oracle: Hint which password oracle strategy to use. :return: Retrieved password (as string) .. seealso:: https://bitbucket.org/kang/python-keyring-lib """ import getpass password = None if not oracle or oracle == "@oracle:use_keyring": keyring = get_keyring() password = keyring.get_password(service, username) if interactive and password is None: # -- LEARNING MODE: Password is not stored in keyring yet. oracle = "@oracle:ask_password" password = get_service_password(service, username, oracle, interactive=True) if password: keyring.set_password(service, username, password) elif not interactive and password is None: log.error( 'Unable to retrieve password from keyring. ' 'Re-run in interactive mode to set a password' ) elif interactive and oracle == "@oracle:ask_password": prompt = "%s password: " % service password = getpass.getpass(prompt) elif oracle.startswith('@oracle:eval:'): command = oracle[13:] return oracle_eval(command) if password is None: die("MISSING PASSWORD: oracle='%s', interactive=%s for service=%s" % (oracle, interactive, service)) return password def oracle_eval(command): """ Retrieve password from the given command """ p = subprocess.Popen( command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) p.wait() if p.returncode == 0: return p.stdout.readline().strip().decode('utf-8') else: die( "Error retrieving password: `{command}` returned '{error}'".format( command=command, error=p.stderr.read().strip())) def load_example_rc(): fname = os.path.join( os.path.dirname(__file__), 'docs/configuration.rst' ) with open(fname, 'r') as f: readme = f.read() example = readme.split('.. example')[1][4:] return example error_template = """ ************************************************* * There was a problem with your bugwarriorrc * * {error_msg} * Here's an example template to help: * ************************************************* {example}""" def die(error_msg): log.critical(error_template.format( error_msg=error_msg, example=load_example_rc(), )) sys.exit(1) def validate_config(config, main_section): if not config.has_section(main_section): die("No [%s] section found." % main_section) logging.basicConfig( level=getattr(logging, config.get(main_section, 'log.level')), filename=config.get(main_section, 'log.file'), ) # In general, its nice to log "everything", but some of the loggers from # our dependencies are very very spammy. Here, we silence most of their # noise: spammers = [ 'bugzilla.base', 'bugzilla.bug', 'requests.packages.urllib3.connectionpool', ] for spammer in spammers: logging.getLogger(spammer).setLevel(logging.WARN) if not config.has_option(main_section, 'targets'): die("No targets= item in [%s] found." % main_section) targets = aslist(config.get(main_section, 'targets')) targets = [t for t in targets if len(t)] if not targets: die("Empty targets= item in [%s]." % main_section) for target in targets: if target not in config.sections(): die("No [%s] section found." % target) # Validate each target one by one. for target in targets: service = config.get(target, 'service') if not service: die("No 'service' in [%s]" % target) if not get_service(service): die("'%s' in [%s] is not a valid service." % (service, target)) # Call the service-specific validator service = get_service(service) service_config = ServiceConfig(service.CONFIG_PREFIX, config, target) service.validate_config(service_config, target) def get_config_path(): """ Determine the path to the config file. This will return, in this order of precedence: - the value of $BUGWARRIORRC if set - $XDG_CONFIG_HOME/bugwarrior/bugwarriorc if exists - ~/.bugwarriorrc if exists - /bugwarrior/bugwarriorc if exists, for dir in $XDG_CONFIG_DIRS - $XDG_CONFIG_HOME/bugwarrior/bugwarriorc otherwise """ if os.environ.get(BUGWARRIORRC): return os.environ[BUGWARRIORRC] xdg_config_home = ( os.environ.get('XDG_CONFIG_HOME') or os.path.expanduser('~/.config')) xdg_config_dirs = ( (os.environ.get('XDG_CONFIG_DIRS') or '/etc/xdg').split(':')) paths = [ os.path.join(xdg_config_home, 'bugwarrior', 'bugwarriorrc'), os.path.expanduser("~/.bugwarriorrc")] paths += [ os.path.join(d, 'bugwarrior', 'bugwarriorrc') for d in xdg_config_dirs] for path in paths: if os.path.exists(path): return path return paths[0] def fix_logging_path(config, main_section): """ Expand environment variables and user home (~) in the log.file and return as relative path. """ log_file = config.get(main_section, 'log.file') if log_file: log_file = os.path.expanduser(os.path.expandvars(log_file)) if os.path.isabs(log_file): log_file = os.path.relpath(log_file) return log_file def load_config(main_section, interactive=False): config = BugwarriorConfigParser({'log.level': "INFO", 'log.file': None}, allow_no_value=True) path = get_config_path() config.readfp(codecs.open(path, "r", "utf-8",)) config.interactive = interactive config.data = BugwarriorData(get_data_path(config, main_section)) config.set(main_section, 'log.file', fix_logging_path(config, main_section)) validate_config(config, main_section) return config def get_taskrc_path(conf, main_section): path = os.getenv('TASKRC', '~/.taskrc') if conf.has_option(main_section, 'taskrc'): path = conf.get(main_section, 'taskrc') return os.path.normpath( os.path.expanduser(path) ) def get_data_path(config, main_section): taskrc = get_taskrc_path(config, main_section) # We cannot use the taskw module here because it doesn't really support # the `_` subcommands properly (`rc:` can't be used for them). line_prefix = 'data.location=' # Take a copy of the environment and add our taskrc to it. env = dict(os.environ) env['TASKRC'] = taskrc if not os.path.isfile(taskrc): raise IOError('Unable to find taskrc file.') tw_show = subprocess.Popen( ('task', '_show'), stdout=subprocess.PIPE, env=env) data_location = subprocess.check_output( ('grep', '-e', '^' + line_prefix), stdin=tw_show.stdout) tw_show.wait() data_path = data_location[len(line_prefix):].rstrip().decode('utf-8') if not data_path: raise IOError('Unable to determine the data location.') return os.path.normpath(os.path.expanduser(data_path)) # ConfigParser is not a new-style class, so inherit from object to fix super(). class BugwarriorConfigParser(configparser.ConfigParser, object): def getint(self, section, option): """ Accepts both integers and empty values. """ try: return super(BugwarriorConfigParser, self).getint(section, option) except ValueError: if self.get(section, option) == u'': return None else: raise ValueError( "{section}.{option} must be an integer or empty.".format( section=section, option=option)) class ServiceConfig(object): """ A service-aware wrapper for ConfigParser objects. """ def __init__(self, config_prefix, config_parser, service_target): self.config_prefix = config_prefix self.config_parser = config_parser self.service_target = service_target def __getattr__(self, name): """ Proxy undefined attributes/methods to ConfigParser object. """ return getattr(self.config_parser, name) def __contains__(self, key): """ Does service section specify this option? """ return self.config_parser.has_option( self.service_target, self._get_key(key)) def get(self, key, default=None, to_type=None): try: value = self.config_parser.get(self.service_target, self._get_key(key)) if to_type: return to_type(value) return value except configparser.NoSectionError: return default except configparser.NoOptionError: return default def _get_key(self, key): return '%s.%s' % (self.config_prefix, key) # This needs to be imported here and not above to avoid a circular-import. from bugwarrior.services import get_service bugwarrior-1.8.0/bugwarrior/data.py000066400000000000000000000020601376471246600173670ustar00rootroot00000000000000import os import json from lockfile.pidlockfile import PIDLockFile class BugwarriorData(object): def __init__(self, data_path): self.datafile = os.path.join(data_path, 'bugwarrior.data') self.lockfile = os.path.join(data_path, 'bugwarrior-data.lockfile') self.path = data_path def get_data(self): with open(self.datafile, 'r') as jsondata: return json.load(jsondata) def get(self, key): try: return self.get_data()[key] except IOError: # File does not exist. return None def set(self, key, value): with PIDLockFile(self.lockfile): try: data = self.get_data() except IOError: # File does not exist. with open(self.datafile, 'w') as jsondata: json.dump({key: value}, jsondata) else: with open(self.datafile, 'w') as jsondata: data[key] = value json.dump(data, jsondata) os.chmod(self.datafile, 0o600) bugwarrior-1.8.0/bugwarrior/db.py000066400000000000000000000461731376471246600170600ustar00rootroot00000000000000from __future__ import unicode_literals from future import standard_library standard_library.install_aliases() from builtins import zip from builtins import object from six.moves.configparser import NoOptionError, NoSectionError import json import os import re import subprocess import sys import requests import dogpile.cache import six from taskw import TaskWarriorShellout from taskw.exceptions import TaskwarriorError from bugwarrior.config import asbool, get_taskrc_path, aslist from bugwarrior.notifications import send_notification import logging log = logging.getLogger(__name__) MARKUP = "(bw)" # In Python 2.3 through 2.7, the stdlib dbm module include a berkeley db # interface, which was used by default by dogpile.cache. In Python3, the # berkeley db module was removed which means that cache files created by # bugwarrior on python 2 will not be compatible with cache files as read by # bugwarrior on python 3. Attempting to read them generates a traceback. # To work around this, we use different filenames for py2 and py3 here. # https://github.com/ralphbean/bugwarrior/pull/416 PYVER = '%i.%i' % sys.version_info[:2] DOGPILE_CACHE_PATH = os.path.expanduser(''.join([ os.getenv('XDG_CACHE_HOME', '~/.cache'), '/dagd-py', PYVER, '.dbm'])) if not os.path.isdir(os.path.dirname(DOGPILE_CACHE_PATH)): os.makedirs(os.path.dirname(DOGPILE_CACHE_PATH)) CACHE_REGION = dogpile.cache.make_region().configure( "dogpile.cache.dbm", arguments=dict(filename=DOGPILE_CACHE_PATH), ) class URLShortener(object): _instance = None def __new__(cls, *args, **kwargs): if not cls._instance: cls._instance = super(URLShortener, cls).__new__( cls, *args, **kwargs ) return cls._instance @CACHE_REGION.cache_on_arguments() def shorten(self, url): if not url: return '' base = 'https://da.gd/s' return requests.get(base, params=dict(url=url)).text.strip() class NotFound(Exception): pass class MultipleMatches(Exception): pass def get_normalized_annotation(annotation): return re.sub( r'[\W_]', '', six.text_type(annotation) ) def get_annotation_hamming_distance(left, right): left = get_normalized_annotation(left) right = get_normalized_annotation(right) if len(left) > len(right): left = left[0:len(right)] elif len(right) > len(left): right = right[0:len(left)] return hamdist(left, right) def hamdist(str1, str2): """Count the # of differences between equal length strings str1 and str2""" diffs = 0 for ch1, ch2 in zip(str1, str2): if ch1 != ch2: diffs += 1 return diffs def get_managed_task_uuids(tw, key_list): expected_task_ids = set([]) for keys in key_list.values(): tasks = tw.filter_tasks({ 'and': [('%s.any' % key, None) for key in keys], 'or': [ ('status', 'pending'), ('status', 'waiting'), ], }) expected_task_ids = expected_task_ids | set([ task['uuid'] for task in tasks ]) return expected_task_ids def make_unique_identifier(keys, issue): """ For a given issue, make an identifier from its unique keys. This is not the same as the taskwarrior uuid, which is assigned only once the task is created. :params: * `keys`: A list of lists of keys to use for uniquely identifying an issue. * `issue`: An instance of a subclass of `bugwarrior.services.Issue`. :returns: * A single string UUID. """ for service, key_list in six.iteritems(keys): if all([key in issue for key in key_list]): subset = dict([(key, issue[key]) for key in key_list]) return json.dumps(subset, sort_keys=True) raise RuntimeError("Could not determine unique identifier for %s" % issue) def find_taskwarrior_uuid(tw, keys, issue): """ For a given issue issue, find its local taskwarrior UUID. Assembles a list of task IDs existing in taskwarrior matching the supplied issue (`issue`) on the combination of any set of supplied unique identifiers (`keys`). :params: * `tw`: An instance of `taskw.TaskWarriorShellout` * `keys`: A list of lists of keys to use for uniquely identifying an issue. To clarify the "list of lists" behavior, assume that there are two services, one having a single primary key field -- 'serviceAid' -- and another having a pair of fields composing its primary key -- 'serviceBproject' and 'serviceBnumber' --, the incoming data for this field would be:: [ ['serviceAid'], ['serviceBproject', 'serviceBnumber'], ] * `issue`: An instance of a subclass of `bugwarrior.services.Issue`. :returns: * A single string UUID. :raises: * `bugwarrior.db.MultipleMatches`: if multiple matches were found. * `bugwarrior.db.NotFound`: if an issue was not found. """ if not issue['description']: raise ValueError('Issue %s has no description.' % issue) possibilities = set([]) for service, key_list in six.iteritems(keys): if any([key in issue for key in key_list]): results = tw.filter_tasks({ 'and': [("%s.is" % key, issue[key]) for key in key_list], 'or': [ ('status', 'pending'), ('status', 'waiting'), ('status', 'completed') ], }) possibilities = possibilities | set([ task['uuid'] for task in results ]) if len(possibilities) == 1: return possibilities.pop() if len(possibilities) > 1: raise MultipleMatches( "Issue %s matched multiple IDs: %s" % ( issue['description'], possibilities ) ) raise NotFound( "No issue was found matching %s" % issue ) def replace_left(field, local_task, remote_issue, keep_items=[]): """ Replace array field from the remote_issue to the local_task * Local 'left' entries are suppressed, unless those listed in keep_items. * Remote 'left' are appended to task, if not present in local. :param `field`: Task field to merge. :param `local_task`: `taskw.task.Task` object into which to replace remote changes. :param `remote_issue`: `dict` instance from which to add into local task. :param `keep_items`: list of items to keep into local_task even if not present in remote_issue """ # Ensure that empty default are present local_field = local_task.get(field, []).copy() remote_field = remote_issue.get(field, []) # We need to make sure an array exists for this field because # we will be appending to it in a moment. if field not in local_task: local_task[field] = [] #Delete all items in local_task, unless they are in keep_items or in remote_issue #This ensure that the task is not being updated if there is no changes for item in local_field: if keep_items.count(item) == 0 and remote_field.count(item) == 0: log.debug('found %s to remove' % (item)) local_task[field].remove(item) elif remote_field.count(item) > 0: remote_field.remove(item) if len(remote_field) > 0: local_task[field] += remote_field def merge_left(field, local_task, remote_issue, hamming=False): """ Merge array field from the remote_issue into local_task * Local 'left' entries are preserved without modification * Remote 'left' are appended to task if not present in local. :param `field`: Task field to merge. :param `local_task`: `taskw.task.Task` object into which to merge remote changes. :param `remote_issue`: `dict` instance from which to merge into local task. :param `hamming`: (default `False`) If `True`, compare entries by truncating to maximum length, and comparing hamming distances. Useful generally only for annotations. """ # Ensure that empty defaults are present local_field = local_task.get(field, []) remote_field = remote_issue.get(field, []) # We need to make sure an array exists for this field because # we will be appending to it in a moment. if field not in local_task: local_task[field] = [] # If a remote does not appear in local, add it to the local task new_count = 0 for remote in remote_field: for local in local_field: if ( # For annotations, they don't have to match *exactly*. ( hamming and get_annotation_hamming_distance(remote, local) == 0 ) # But for everything else, they should. or ( remote == local ) ): break else: log.debug("%s not found in %r" % (remote, local_field)) local_task[field].append(remote) new_count += 1 if new_count > 0: log.debug('Added %s new values to %s (total: %s)' % ( new_count, field, len(local_task[field]),)) def run_hooks(conf, name): if conf.has_option('hooks', name): pre_import = aslist(conf.get('hooks', name)) if pre_import is not None: for hook in pre_import: exit_code = subprocess.call(hook, shell=True) if exit_code != 0: msg = 'Non-zero exit code %d on hook %s' % ( exit_code, hook ) log.error(msg) raise RuntimeError(msg) def synchronize(issue_generator, conf, main_section, dry_run=False): def _bool_option(section, option, default): try: return asbool(conf.get(section, option)) except (NoSectionError, NoOptionError): return default targets = aslist(conf.get(main_section, 'targets')) services = set([conf.get(target, 'service') for target in targets]) key_list = build_key_list(services) uda_list = build_uda_config_overrides(services) if uda_list: log.info( 'Service-defined UDAs exist: you can optionally use the ' '`bugwarrior-uda` command to export a list of UDAs you can ' 'add to your taskrc file.' ) static_fields = ['priority'] if conf.has_option(main_section, 'static_fields'): static_fields = aslist(conf.get(main_section, 'static_fields')) # 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 tw = TaskWarriorShellout( config_filename=get_taskrc_path(conf, main_section), config_overrides=uda_list, marshal=True, ) merge_annotations = _bool_option(main_section, 'merge_annotations', True) merge_tags = _bool_option(main_section, 'merge_tags', True) replace_tags = _bool_option(main_section, 'replace_tags', False) static_tags = [] if conf.has_option(main_section, 'static_tags'): static_tags = aslist(conf.get(main_section, 'static_tags')) issue_updates = { 'new': [], 'existing': [], 'changed': [], 'closed': get_managed_task_uuids(tw, key_list), } seen = [] for issue in issue_generator: try: issue_dict = dict(issue) # We received this issue from The Internet, but we're not sure what # kind of encoding the service providers may have handed us. Let's try # and decode all byte strings from UTF8 off the bat. If we encounter # other encodings in the wild in the future, we can revise the handling # here. https://github.com/ralphbean/bugwarrior/issues/350 for key in issue_dict.keys(): if isinstance(issue_dict[key], six.binary_type): try: issue_dict[key] = issue_dict[key].decode('utf-8') except UnicodeDecodeError: log.warn("Failed to interpret %r as utf-8" % key) # Blank priority should mean *no* priority if issue_dict['priority'] == u'': issue_dict['priority'] = None # De-duplicate issues coming in unique_identifier = make_unique_identifier(key_list, issue) if unique_identifier in seen: log.debug("Skipping. Seen %s of %r" % (unique_identifier, issue)) continue seen.append(unique_identifier) existing_taskwarrior_uuid = find_taskwarrior_uuid(tw, key_list, issue) _, task = tw.get_task(uuid=existing_taskwarrior_uuid) if task['status'] == 'completed': # Reopen task task['status'] = 'pending' task['end'] = None # Drop static fields from the upstream issue. We don't want to # overwrite local changes to fields we declare static. for field in static_fields: if field in issue_dict: del issue_dict[field] # Merge annotations & tags from online into our task object if merge_annotations: merge_left('annotations', task, issue_dict, hamming=True) if merge_tags: if replace_tags: replace_left('tags', task, issue_dict, static_tags) else: merge_left('tags', task, issue_dict) issue_dict.pop('annotations', None) issue_dict.pop('tags', None) task.update(issue_dict) if task.get_changes(keep=True): issue_updates['changed'].append(task) else: issue_updates['existing'].append(task) if existing_taskwarrior_uuid in issue_updates['closed']: issue_updates['closed'].remove(existing_taskwarrior_uuid) except MultipleMatches as e: log.exception("Multiple matches: %s", six.text_type(e)) except NotFound: issue_updates['new'].append(issue_dict) notreally = ' (not really)' if dry_run else '' # Add new issues log.info("Adding %i tasks", len(issue_updates['new'])) for issue in issue_updates['new']: log.info(u"Adding task %s%s", issue['description'], notreally) if dry_run: continue if notify: send_notification(issue, 'Created', conf) try: new_task = tw.task_add(**issue) if 'end' in issue and issue['end']: tw.task_done(uuid=new_task['uuid']) except TaskwarriorError as e: log.exception("Unable to add task: %s" % e.stderr) log.info("Updating %i tasks", len(issue_updates['changed'])) for issue in issue_updates['changed']: changes = '; '.join([ '{field}: {f} -> {t}'.format( field=field, f=repr(ch[0]), t=repr(ch[1]) ) for field, ch in six.iteritems(issue.get_changes(keep=True)) ]) log.info( "Updating task %s, %s; %s%s", six.text_type(issue['uuid']), issue['description'], changes, notreally ) if dry_run: continue try: _, updated_task = tw.task_update(issue) if 'end' in issue and issue['end']: tw.task_done(uuid=updated_task['uuid']) except TaskwarriorError as e: log.exception("Unable to modify task: %s" % e.stderr) log.info("Closing %i tasks", len(issue_updates['closed'])) for issue in issue_updates['closed']: _, task_info = tw.get_task(uuid=issue) log.info( "Completing task %s %s%s", issue, task_info.get('description', ''), notreally ) if dry_run: continue if notify: send_notification(task_info, 'Completed', conf) try: tw.task_done(uuid=issue) except TaskwarriorError as e: log.exception("Unable to close task: %s" % e.stderr) # Send notifications if notify: 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): from bugwarrior.services import get_service keys = {} for target in targets: keys[target] = get_service(target).ISSUE_CLASS.UNIQUE_KEY return keys def get_defined_udas_as_strings(conf, main_section): targets = aslist(conf.get(main_section, 'targets')) services = set([conf.get(target, 'service') for target in targets]) uda_list = build_uda_config_overrides(services) for uda in convert_override_args_to_taskrc_settings(uda_list): yield uda def build_uda_config_overrides(targets): """ Returns a list of UDAs defined by given targets For all targets in `targets`, build a dictionary of configuration overrides representing the UDAs defined by the passed-in services (`targets`). Given a hypothetical situation in which you have two services, the first of which defining a UDA named 'serviceAid' ("Service A ID", string) and a second service defining two UDAs named 'serviceBproject' ("Service B Project", string) and 'serviceBnumber' ("Service B Number", numeric), this would return the following structure:: { 'uda': { 'serviceAid': { 'label': 'Service A ID', 'type': 'string', }, 'serviceBproject': { 'label': 'Service B Project', 'type': 'string', }, 'serviceBnumber': { 'label': 'Service B Number', 'type': 'numeric', } } } """ from bugwarrior.services import get_service targets_udas = {} for target in targets: targets_udas.update(get_service(target).ISSUE_CLASS.UDAS) return { 'uda': targets_udas } def convert_override_args_to_taskrc_settings(config, prefix=''): args = [] for k, v in six.iteritems(config): if isinstance(v, dict): args.extend( convert_override_args_to_taskrc_settings( v, prefix='.'.join([ prefix, k, ]) if prefix else k ) ) else: v = six.text_type(v) left = (prefix + '.' if prefix else '') + k args.append('='.join([left, v])) return args bugwarrior-1.8.0/bugwarrior/docs/000077500000000000000000000000001376471246600170365ustar00rootroot00000000000000bugwarrior-1.8.0/bugwarrior/docs/Makefile000066400000000000000000000151711376471246600205030ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Bugwarrior.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Bugwarrior.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Bugwarrior" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Bugwarrior" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." bugwarrior-1.8.0/bugwarrior/docs/common_configuration.rst000066400000000000000000000216721376471246600240170ustar00rootroot00000000000000How to Configure ================ First, add a file named ``.config/bugwarrior/bugwarriorrc`` to your home folder. This file must include at least a ``[general]`` section including the following option: * ``targets``: A comma-separated list of *other* section names to use as task sources. Optional options include: * ``taskrc``: Specify which TaskRC configuration file to use. By default, will use the system default (usually ``~/.taskrc``). * ``shorten``: Set to ``True`` to shorten links. * ``inline_links``: 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 ``False`` skips putting issue comments into annotations. Defaults to ``True``. * ``annotation_newlines``: When ``False`` strips newlines from comments in annotations. Defaults to ``False``. * ``log.level``: Set to one of ``DEBUG``, ``INFO``, ``WARNING``, ``ERROR``, ``CRITICAL``, or ``DISABLED`` to control the logging verbosity. By default, this is set to ``INFO``. * ``log.file``: Set to the path at which you would like logging messages written. By default, logging messages will be written to stderr. * ``annotation_length``: Import maximally this number of characters of incoming annotations. Default: 45. * ``description_length``: Use maximally this number of characters in the description. Default: 35. * ``merge_annotations``: If ``False``, bugwarrior won't bother with adding annotations to your tasks at all. Default: ``True``. * ``merge_tags``: If ``False``, bugwarrior won't bother with adding tags to your tasks at all. Default: ``True``. * ``replace_tags``: If ``True``, bugwarrior will delete all tags prior to fetching new ones, except those listed in ``static_tags``. Only work if merge_tags is ``True``. Default: ``False``. * ``static_tags``: A comma separated list of tags that shouldn't be *removed* by bugwarrior. Use for tags that you want to keep when replace_tags is set to ``True``. * ``static_fields``: A comma separated list of attributes that shouldn't be *updated* by bugwarrior. Use for values that you want to tune manually. Default: ``priority``. In addition to the ``[general]`` section, sections may be named ``[flavor.myflavor]`` and may be selected using the ``--flavor`` option to ``bugwarrior-pull``. This section will then be used rather than the ``[general]`` section. A more-detailed example configuration can be found at :ref:`example_configuration`. .. _common_configuration_options: Common Service Configuration Options ------------------------------------ All services support common configuration options in addition to their service-specific features. These configuration options are meant to be prefixed with the service name, e.g. ``github.add_tags``, or ``gitlab.default_priority``. The following options are supported: * ``SERVICE.only_if_assigned``: If set to a username, only import issues assigned to the specified user. * ``SERVICE.also_unassigned``: If set to ``True`` and ``only_if_assigned`` is set, then also create tasks for issues that are not assigned to anybody. Defaults to ``False``. * ``SERVICE.default_priority``: Assign this priority ('L', 'M', or 'H') to newly-imported issues. Defaults to ``M``. * ``SERVICE.add_tags``: A comma-separated list of tags to add to an issue. In most cases, plain strings will suffice, but you can also specify templates. See the section `Field Templates`_ for more information. .. _field_templates: Field Templates --------------- By default, Bugwarrior will import issues with a fairly verbose description template looking something like this:: (BW)Issue#10 - Fix perpetual motion machine .. http://media.giphy.com/media/LldEzRPqyo2Yg/giphy.gif but depending upon your workflow, the information presented may not be useful to you. To help users build descriptions that suit their needs, all services allow one to specify a ``SERVICE.description_template`` configuration option, in which one can enter a one-line Jinja template. The context available includes all Taskwarrior fields and all UDAs (see section named 'Provided UDA Fields' for each service) defined for the relevant service. .. note:: Jinja templates can be very complex. For more details about Jinja templates, please consult `Jinja's Template Documentation `_. For example, to pull-in Github issues assigned to `@ralphbean `_, setting the issue description such that it is composed of only the Github issue number and title, you could create a service entry like this:: [ralphs_github_account] service = github github.username = ralphbean github.description_template = {{githubnumber}}: {{githubtitle}} You can also use this tool for altering the generated value of any other Taskwarrior record field by using the same kind of template. Uppercasing the project name for imported issues:: SERVICE.project_template = {{project|upper}} You can also use this feature to override the generated value of any field. This example causes imported issues to be assigned to the 'Office' project regardless of what project was assigned by the service itself:: SERVICE.project_template = Office Password Management ------------------- You need not store your password in plain text in your `bugwarriorrc` file; you can enter the following values to control where to gather your password from: ``password = @oracle:use_keyring`` Retrieve a password from the system keyring. The ``bugwarrior-vault`` command line tool can be used to manage your passwords as stored in your keyring (say to reset them or clear them). Extra dependencies must be installed with `pip install bugwarrior[keyring]` to enable this feature. ``password = @oracle:ask_password`` Ask for a password at runtime. ``password = @oracle:eval:`` Use the output of as the password. For instance, to integrate bugwarrior with the password manager `pass `_ you can use ``@oracle:eval:pass my/password``. Hooks ----- 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. To use hooks, add a ``[hooks]`` section to your configuration, mapping the hook you'd like to use with a comma-separated list of scripts to execute. :: [hooks] pre_import = /home/someuser/backup.sh, /home/someuser/sometask.sh Hook options: * ``pre_import``: The pre_import hook is invoked after all issues have been pulled from remote sources, but before they are synced to the TW db. If your pre_import script has a non-zero exit code, the ``bugwarrior-pull`` command will exit early. Notifications ------------- Add a ``[notifications]`` section to your configuration to receive notifications when a bugwarrior pull runs, and when issues are created, updated, or deleted by ``bugwarrior-pull``:: [notifications] notifications = True backend = growlnotify finished_querying_sticky = False task_crud_sticky = True only_on_new_tasks = True Backend options: +------------------+------------------+-------------------------+ | Backend Name | Operating System | Required Python Modules | +==================+==================+=========================+ | ``growlnotify`` | MacOS X | ``gntp`` | +------------------+------------------+-------------------------+ | ``gobject`` | Linux | ``gobject`` | +------------------+------------------+-------------------------+ .. note:: The ``finished_querying_sticky`` and ``task_crud_sticky`` options have no effect if you are using a notification backend other than ``growlnotify``. Configuration files ------------------- bugwarrior will look at the following paths and read its configuration from the first existing file in this order: * :file:`~/.config/bugwarrior/bugwarriorrc` * :file:`~/.bugwarriorrc` * :file:`/etc/xdg/bugwarrior/bugwarriorrc` The default paths can be altered using the environment variables :envvar:`BUGWARRIORRC`, :envvar:`XDG_CONFIG_HOME` and :envvar:`XDG_CONFIG_DIRS`. Environment Variables --------------------- .. envvar:: BUGWARRIORRC This overrides the default RC file. .. envvar:: XDG_CONFIG_HOME By default, :program:`bugwarrior` looks for a configuration file named ``$XDG_CONFIG_HOME/bugwarrior/bugwarriorrc``. If ``$XDG_CONFIG_HOME`` is either not set or empty, a default equal to ``$HOME/.config`` is used. .. envvar:: XDG_CONFIG_DIRS If it can't find a user-specific configuration file (either ``$XDG_CONFIG_HOME/bugwarrior/bugwarriorrc`` or ``$HOME/.bugwarriorrc``), :program:`bugwarrior` looks through the directories in ``$XDG_CONFIG_DIRS`` for a configuration file named ``bugwarrior/bugwarriorrc``. The directories in ``$XDG_CONFIG_DIRS`` should be separated with a colon ':'. If ``$XDG_CONFIG_DIRS`` is either not set or empty, a value equal to ``/etc/xdg`` is used. bugwarrior-1.8.0/bugwarrior/docs/conf.py000066400000000000000000000213571376471246600203450ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Bugwarrior documentation build configuration file, created by # sphinx-quickstart on Wed Apr 16 15:09:22 2014. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Bugwarrior' copyright = u'2014-2016, Ralph Bean and contributors' docs_authors = [ 'Adam Coddington', 'Ben Boeckel', 'Boris Churzin', 'Brian (bex) Exelbierd', 'Dustin J. Mitchell', 'Francesco de Virgilio', 'Grégoire Détrez', 'Iain R. Learmonth', 'Ivan ÄŒukić', 'Jakub Wilk', 'Jens Ohlig', 'Mark Mulligan', 'Matthew Avant', 'Nick Douma', 'Ralph Bean', 'Ryan S. Brown', 'Ryne Everett', 'Sayan Chowdhury', ] # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '0.8.0' # The full version, including alpha/beta/rc tags. release = '0.8.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The default language to highlight source code in. highlight_language = 'ini' # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'Bugwarriordoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'Bugwarrior.tex', u'Bugwarrior Documentation', u'Ralph Bean', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'bugwarrior', u'Bugwarrior Documentation', docs_authors, 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'Bugwarrior', u'Bugwarrior Documentation', u'Ralph Bean', 'Bugwarrior', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {'http://docs.python.org/': None} bugwarrior-1.8.0/bugwarrior/docs/configuration.rst000066400000000000000000000167371376471246600224550ustar00rootroot00000000000000.. _example_configuration: Example Configuration ====================== .. example :: # 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, pivotaltracker # 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 # Setting this to True will include a link to the ticket in the description inline_links = False # 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 # Setting this to False will strip newlines from comment annotations annotation_newlines = False # log.level specifies 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. # # pre_import: The pre_import hook is invoked after all issues have been pulled # from remote sources, but before they are synced to the TW db. If your # pre_import script has a non-zero exit code, the `bugwarrior-pull` command will # 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 backends are currently supported: # # - growlnotify (v2) Mac OS X "gntp" must be installed # - gobject Linux python gobject must be installed # # To configure, adjust the settings below. Note that neither of the # # "sticky" options have any effect on Linux. They only work for # growlnotify. #[notifications] # notifications = True # 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." [my_github] service = github github.default_priority = H github.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. # # - 'github.login' is the username you ask bugwarrior to # login as. Set it to your account. # - 'github.username' is the github entity you want to pull # 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 trac.only_if_assigned = ralph trac.also_unassigned = True trac.default_priority = H trac.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 # have a trailing slash. In this case we fetch comments and # cases from jira assigned to 'ralph' where the status is not closed or # resolved. [jira_project] service = jira jira.base_uri = https://jira.example.org jira.username = ralph jira.password = OMG_LULZ jira.query = assignee = ralph and status != closed and status != resolved # Set this to your jira major version. We currently support only jira version # 4 and 5(the default). You can find your particular version in the footer at # the dashboard. jira.version = 5 jira.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 redmine.url = http://redmine.example.org/ redmine.key = c0c4c014cafebabe redmine.user_id = 7 redmine.project_name = redmine redmine.add_tags = chiliproject [activecollab] service = activecollab activecollab.url = https://ac.example.org/api.php activecollab.key = your-api-key activecollab.user_id = 15 activecollab.add_tags = php [activecollab2] service = activecollab2 activecollab2.url = http://ac.example.org/api.php activecollab2.key = your-api-key activecollab2.user_id = 15 activecollab2.projects = 1:first_project, 5:another_project [my_gmail] service = gmail gmail.query = label:action OR label:readme gmail.login_name = you@example.com [pivotaltracker] service = pivotaltracker pivotaltracker.token = your-api-key pivotaltracker.version = v5 pivotaltracker.user_id = your-user-id pivotaltracker.account_ids = first_account_id,second_account_id pivotaltracker.only_if_assigned = True pivotaltracker.also_unassigned = False pivotaltracker.only_if_author = False pivotaltracker.import_labels_as_tags = True pivotaltracker.label_template = pivotal_{{label}} pivotaltracker.import_blockers = True pivotaltracker.blocker_template = "Description: {{description}} State: {{resolved}}\n" pivotaltracker.annotation_comments = True pivotaltracker.annotation_template = "status: {{completed}} - MYDESC {{description}}" piivotaltracker.exclude_projects = first_project_id,second_project_id pivotaltracker.exclude_stories = first_story_id,second_story_id pivotaltracker.exclude_tags = "wont fix", "should fix" pivotaltracker.query = mywork:1234 -has:label bugwarrior-1.8.0/bugwarrior/docs/contributing.rst000066400000000000000000000040251376471246600223000ustar00rootroot00000000000000How to Contribute ================= .. highlight:: console Setting up your development environment --------------------------------------- First, make sure you have the necessary :ref:`requirements`. You should also install the `virtualenv `_ tool for python. (I use a wrapper for it called `virtualenvwrapper `_ which is awesome but not required.) Virtualenv will help isolate your dependencies from the rest of your system. :: $ sudo yum install python-virtualenv git $ mkdir -p ~/virtualenvs/ $ virtualenv ~/virtualenvs/bugwarrior You should now have a virtualenv in a ``~/virtualenvs/`` directory. To use it, you need to "activate" it like this:: $ source ~/virtualenv/bugwarrior/bin/activate (bugwarrior)$ which python At any time, you can deactivate it by typing ``deactivate`` at the command prompt. Next step -- get the code! :: (bugwarrior)$ git clone git@github.com:ralphbean/bugwarrior.git (bugwarrior)$ cd bugwarrior (bugwarrior)$ python setup.py develop (bugwarrior)$ which bugwarrior-pull This will actually run it.. be careful and back up your task directory! :: (bugwarrior)$ bugwarrior-pull Making a pull request --------------------- Create a new branch for each pull request based off the ``develop`` branch:: (bugwarrior)$ git checkout -b my-shiny-new-feature develop Please add tests when appropriate and run the test suite before opening a PR:: (bugwarrior)$ python setup.py nosetests We look forward to your contribution! Works in progress ----------------- The best way to get help and feedback before you pour too much time and effort into your branch is to open a "work in progress" pull request. We will not leave it open indefinitely if it doesn't seem to be progressing, but there's nothing to lose in soliciting some pointers and concerns. Please begin the title of your work in progress pr with "[WIP]" and explain what remains to be done or what you're having trouble with. bugwarrior-1.8.0/bugwarrior/docs/faq.rst000066400000000000000000000010331376471246600203340ustar00rootroot00000000000000FAQ === Can bugwarrior support ? ---------------------------------------------------- Sure! But our general rule here is that we won't write a backend for a service unless we use it personally, otherwise it's hard to be sure that it really works. We also try to rely on people to become maintainers of the different backend plugins they use so that they don't suffer bit rot over time. In summary, we need someone who 1) uses and 2) can develop the plugin. Could it be you? :) bugwarrior-1.8.0/bugwarrior/docs/generate_service_template.py000066400000000000000000000051421376471246600246170ustar00rootroot00000000000000from __future__ import print_function import inspect import os import sys from jinja2 import Template from bugwarrior.services import Issue from functools import reduce def make_table(grid): """ Make a RST-compatible table From http://stackoverflow.com/a/12539081 """ cell_width = 2 + max( reduce( lambda x, y: x+y, [[len(item) for item in row] for row in grid], [] ) ) num_cols = len(grid[0]) rst = table_div(num_cols, cell_width, 0) header_flag = 1 for row in grid: rst = rst + '| ' + '| '.join( [normalize_cell(x, cell_width-1) for x in row] ) + '|\n' rst = rst + table_div(num_cols, cell_width, header_flag) header_flag = 0 return rst def table_div(num_cols, col_width, header_flag): if header_flag == 1: return num_cols*('+' + (col_width)*'=') + '+\n' else: return num_cols*('+' + (col_width)*'-') + '+\n' def normalize_cell(string, length): return string + ((length - len(string)) * ' ') def import_by_path(name): m = __import__(name) for n in name.split(".")[1:]: m = getattr(m, n) return m def row_comparator(left_row, right_row): left = left_row[0] right = right_row[0] if left > right: return 1 elif right > left or left == 'Field Name': return -1 return 0 TYPE_NAME_MAP = { 'date': 'Date & Time', 'numeric': 'Numeric', 'string': 'Text (string)', 'duration': 'Duration' } if __name__ == '__main__': service = sys.argv[1] module = import_by_path( 'bugwarrior.services.{service}'.format(service=service) ) rows = [] for name, obj in inspect.getmembers(module): if inspect.isclass(obj) and issubclass(obj, Issue): for field_name, details in obj.UDAS.items(): rows.append( [ '``%s``' % field_name, ' '.join(details['label'].split(' ')[1:]), TYPE_NAME_MAP.get( details['type'], '``%s``' % details['type'], ), ] ) rows = sorted(rows, cmp=row_comparator) rows.insert(0, ['Field Name', 'Description', 'Type']) filename = os.path.join(os.path.dirname(__file__), 'service_template.html') with open(filename) as template: rendered = Template(template.read()).render({ 'service_name_humane': service.title(), 'service_name': service, 'uda_table': make_table(rows) }) print(rendered) bugwarrior-1.8.0/bugwarrior/docs/getting.rst000066400000000000000000000046301376471246600212340ustar00rootroot00000000000000Getting bugwarrior ================== .. _requirements: Requirements ------------ To use bugwarrior, you need python 3 and taskwarrior. Upon installation, the setup script will automatically download and install missing python dependencies. Note that some of those dependencies have a C extension module (e.g. the ``cryptography`` package). If those packages are not yet present on your system, the setup script will try to build them locally, for which you will need a C compiler (e.g. ``gcc``) and the necessary header files (python and, for the cryptography package, openssl). A convenient way to install those is to use your usual package manager (``dnf``, ``yum``, ``apt``, etc). Header files are installed from development packages (e.g. ``python-devel`` and ``openssl-devel`` on Fedora or ``python-dev`` ``libssl-dev`` on Debian). Installing from the Python Package Index ---------------------------------------- .. highlight:: console Installing from https://pypi.python.org/pypi/bugwarrior is easy with :command:`pip`:: $ pip install bugwarrior By default, ``bugwarrior`` will be installed with support for the following services: Bitbucket, Github, Gitlab, Pagure, Phabricator, Redmine, Teamlab, and Versionone. There is optional support for Jira, Megaplan.ru, Active Collab, Debian BTS, Trac, Bugzilla, and but those require extra dependencies that are installed by specifying ``bugwarrior[service]`` in the commands above. For example, if you want to use bugwarrior with Jira:: $ pip install "bugwarrior[jira]" The following extra dependency sets are available: - keyring (See also `linux installation instructions `_.) - jira - megaplan - activecollab - bts - trac - bugzilla - gmail Installing from Source ---------------------- You can find the source on github at http://github.com/ralphbean/bugwarrior. Either fork/clone if you plan to do development on bugwarrior, or you can simply download the latest tarball:: $ wget https://github.com/ralphbean/bugwarrior/tarball/master -O bugwarrior-latest.tar.gz $ tar -xzvf bugwarrior-latest.tar.gz $ cd ralphbean-bugwarrior-* $ python setup.py install Installing from Distribution Packages ------------------------------------- bugwarrior has been packaged for Fedora. You can install it with the standard :command:`dnf` (:command:`yum`) package management tools as follows:: $ sudo dnf install bugwarrior bugwarrior-1.8.0/bugwarrior/docs/index.rst000066400000000000000000000022651376471246600207040ustar00rootroot00000000000000.. Bugwarrior documentation master file, created by sphinx-quickstart on Wed Apr 16 15:09:22 2014. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Bugwarrior ========== ``bugwarrior`` is a command line utility for updating your local `taskwarrior `_ database from your forge issue trackers. Build Status ------------ .. |master| image:: https://secure.travis-ci.org/ralphbean/bugwarrior.png?branch=master :alt: Build Status - master branch :target: http://travis-ci.org/#!/ralphbean/bugwarrior .. |develop| image:: https://secure.travis-ci.org/ralphbean/bugwarrior.png?branch=develop :alt: Build Status - develop branch :target: http://travis-ci.org/#!/ralphbean/bugwarrior +----------+-----------+ | Branch | Status | +==========+===========+ | master | |master| | +----------+-----------+ | develop | |develop| | +----------+-----------+ Contents -------- .. toctree:: :maxdepth: 2 getting using common_configuration services configuration contributing faq Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` bugwarrior-1.8.0/bugwarrior/docs/make.bat000066400000000000000000000150641376471246600204510ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Bugwarrior.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Bugwarrior.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end bugwarrior-1.8.0/bugwarrior/docs/service_template.html000066400000000000000000000012541376471246600232610ustar00rootroot00000000000000{{ service_name_humane }} ========================= You can import tasks from your {{ service_name_humane }} instance using the ``{{ service_name }}`` service name. Instructions ------------ Example Service --------------- Here's an example of an {{ service_name_humane }} target:: [my_issue_tracker] service = {{ service_name }} The above example is the minimum required to import issues from {{ service_name_humane }}. You can also feel free to use any of the configuration options described in :ref:`common_configuration_options` or described in `Service Features`_ below. Service Features ---------------- Provided UDA Fields ------------------- {{ uda_table }} bugwarrior-1.8.0/bugwarrior/docs/services.rst000066400000000000000000000002051376471246600214100ustar00rootroot00000000000000Supported Services ================== Bugwarrior currently supports the following services: .. toctree:: :glob: services/* bugwarrior-1.8.0/bugwarrior/docs/services/000077500000000000000000000000001376471246600206615ustar00rootroot00000000000000bugwarrior-1.8.0/bugwarrior/docs/services/activecollab.rst000066400000000000000000000061641376471246600240520ustar00rootroot00000000000000.. _activecollab4: ActiveCollab 4 ============== You can import tasks from your activeCollab 4.x instance using the ``activecollab`` service name. Additional Requirements ----------------------- Install the following packages using ``pip``: * ``pypandoc`` * ``pyac`` Instructions ------------ Obtain your user ID and API url by logging in, clicking on your avatar on the lower left-hand of the page. When on that page, look at the URL. The number that appears after "/user/" is your user ID. On the same page, go to Options and API Subscriptions. Generate a read-only API key and add that to your bugwarriorrc file. Bugwarrior will gather tasks and subtasks returned from the `my-tasks` API call. Additional API calls will be made to gather comments associated with each task. .. note:: Use of the ActiveCollab service requires that the following additional python modules be installed. - `pypandoc `_ - `pyac `_ Example Service --------------- Here's an example of an activecollab target. This is only valid for activeCollab 4.x and greater, see :ref:`activecollab2` for activeCollab2.x. :: [my_bug_tracker] service = activecollab activecollab.url = https://ac.example.org/api.php activecollab.key = your-api-key activecollab.user_id = 15 The above example is the minimum required to import issues from ActiveCollab 4. You can also feel free to use any of the configuration options described in :ref:`common_configuration_options`. Provided UDA Fields ------------------- +---------------------+-----------------+----------------+ | Field Name | Description | Type | +=====================+=================+================+ | ``acbody`` | Body | Text (string) | +---------------------+-----------------+----------------+ | ``accreatedbyname`` | Created By Name | Text (string) | +---------------------+-----------------+----------------+ | ``accreatedon`` | Created On | Date & Time | +---------------------+-----------------+----------------+ | ``acid`` | ID | Text (string) | +---------------------+-----------------+----------------+ | ``acname`` | Name | Text (string) | +---------------------+-----------------+----------------+ | ``acpermalink`` | Permalink | Text (string) | +---------------------+-----------------+----------------+ | ``acprojectid`` | Project ID | Text (string) | +---------------------+-----------------+----------------+ | ``actaskid`` | Task ID | Text (string) | +---------------------+-----------------+----------------+ | ``actype`` | Task Type | Text (string) | +---------------------+-----------------+----------------+ | ``acestimatedtime`` | Estimated Time | Text (numeric) | +---------------------+-----------------+----------------+ | ``actrackedtime`` | Tracked Time | Text (numeric) | +---------------------+-----------------+----------------+ | ``acmilestone`` | Milestone | Text (string) | +---------------------+-----------------+----------------+ bugwarrior-1.8.0/bugwarrior/docs/services/activecollab2.rst000066400000000000000000000057011376471246600241300ustar00rootroot00000000000000.. _activecollab2: ActiveCollab 2 ============== You can import tasks from your ActiveCollab2 instance using the ``activecollab2`` service name. Instructions ------------ You can obtain your user ID and API url by logging into ActiveCollab and clicking on "Profile" and then "API Settings". When on that page, look at the URL. The integer that appears after "/user/" is your user ID. Projects should be entered in a comma-separated list, with the project id as the key and the name you'd like to use for the project in Taskwarrior entered as the value. For example, if the project ID is 8 and the project's name in ActiveCollab is "Amazing Website" then you might enter 8:amazing_website Note that due to limitations in the ActiveCollab API, there is no simple way to get a list of all tasks you are responsible for in AC. Instead you need to look at each ticket that you are subscribed to and check to see if your user ID is responsible for the ticket/task. What this means is that if you have 5 projects you want to query and each project has 20 tickets, you'll make 100 API requests each time you run ``bugwarrior-pull``. Example Service --------------- Here's an example of an activecollab2 target. Note that this will only work with ActiveCollab 2.x - see above for 3.x and greater. :: [my_bug_tracker] services = activecollab2 activecollab2.url = http://ac.example.org/api.php activecollab2.key = your-api-key activecollab2.user_id = 15 activecollab2.projects = 1:first_project, 5:another_project The above example is the minimum required to import issues from ActiveCollab 2. You can also feel free to use any of the configuration options described in :ref:`common_configuration_options`. Provided UDA Fields ------------------- +--------------------+--------------------+--------------------+ | Field Name | Description | Type | +====================+====================+====================+ | ``ac2body`` | Body | Text (string) | +--------------------+--------------------+--------------------+ | ``ac2createdbyid`` | Created By | Text (string) | +--------------------+--------------------+--------------------+ | ``ac2createdon`` | Created On | Date & Time | +--------------------+--------------------+--------------------+ | ``ac2name`` | Name | Text (string) | +--------------------+--------------------+--------------------+ | ``ac2permalink`` | Permalink | Text (string) | +--------------------+--------------------+--------------------+ | ``ac2projectid`` | Project ID | Text (string) | +--------------------+--------------------+--------------------+ | ``ac2ticketid`` | Ticket ID | Text (string) | +--------------------+--------------------+--------------------+ | ``ac2type`` | Task Type | Text (string) | +--------------------+--------------------+--------------------+ bugwarrior-1.8.0/bugwarrior/docs/services/bitbucket.rst000066400000000000000000000065631376471246600234010ustar00rootroot00000000000000Bitbucket ========= You can import tasks from your Bitbucket instance using the ``bitbucket`` service name. Example Service --------------- Here's an example of a Bitbucket target:: [my_issue_tracker] service = bitbucket bitbucket.username = ralphbean bitbucket.login = ralphbean bitbucket.password = mypassword The above example is the minimum required to import issues from Bitbucket. You can also feel free to use any of the configuration options described in :ref:`common_configuration_options`. Note that both ``bitbucket.username`` and ``bitbucket.login`` are required and can be set to different values. ``bitbucket.login`` is used to specify what account bugwarrior should use to login to bitbucket. ``bitbucket.username`` indicates which repositories should be scraped. For instance, I always have ``bitbucket.login`` set to ralphbean (my account). But I have some targets with ``bitbucket.username`` pointed at organizations or other users to watch issues there. As an alternative to password authentication, there is OAuth. To get a key and secret, go to the "OAuth" section of your profile settings and click "Add consumer". Set the "Callback URL" to ``https://localhost/`` and set the appropriate permissions. Then assign your consumer's credentials to ``bitbucket.key`` and ``bitbucket.secret``. Note that you will have to provide a password (only) the first time you pull, so you may want to set ``bitbucket.password = @oracle:ask_password`` and run ``bugwarrior-pull --interactive`` on your next pull. 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 ``bitbucket.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:: bitbucket.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 ``bitbucket.exclude_repos`` configuration option. In this example, ``noisy_repository`` is the repository you would *not* like issues created for:: bitbucket.exclude_repos = noisy_repository Please note that the API returns all lowercase names regardless of the case of the repository in the web interface. Filter Merge Requests +++++++++++++++++++++ Although you can filter issues using :ref:`common_configuration_options`, pull requests are not filtered by default. You can filter pull requests by adding the following configuration option:: bitbucket.filter_merge_requests = True Provided UDA Fields ------------------- +--------------------+--------------------+--------------------+ | Field Name | Description | Type | +====================+====================+====================+ | ``bitbucketid`` | Issue ID | Text (string) | +--------------------+--------------------+--------------------+ | ``bitbuckettitle`` | Title | Text (string) | +--------------------+--------------------+--------------------+ | ``bitbucketurl`` | URL | Text (string) | +--------------------+--------------------+--------------------+ bugwarrior-1.8.0/bugwarrior/docs/services/bts.rst000066400000000000000000000114061376471246600222050ustar00rootroot00000000000000Debian Bug Tracking System (BTS) ================================ You can import tasks from the Debian Bug Tracking System (BTS) using the ``bts`` service name. Debian's bugs are public and no authentication information is required by bugwarrior for this service. Additional Requirements ----------------------- You will need to install the following additional packages via ``pip``: * ``PySimpleSOAP`` * ``python-debianbts`` .. note:: If you have installed the Debian package for bugwarrior, this dependency will already be satisfied. Example Service --------------- Here's an example of a Debian BTS target:: [debian_bts] service = bts bts.email = username@debian.org The above example is the minimum required to import issues from the Debian BTS. You can also feel free to use any of the configuration options described in :ref:`common_configuration_options` or described in `Service Features`_ below. Service Features ---------------- Include all bugs for packages +++++++++++++++++++++++++++++ If you would like more bugs than just those you are the owner of, you can specify the ``bts.packages`` option. For example if you wanted to include bugs on the ``hello`` package, you can add this line to your service configuration:: bts.packages = hello More packages can be specified seperated by commas. Ultimate Debian Database (UDD) Bugs Search ++++++++++++++++++++++++++++++++++++++++++ If you maintain a large number of packages and you wish to include bugs from all packages where you are listed as a Maintainer or an Uploader in the Debian archive, you can enable the use of the `UDD Bugs Search `_. This will peform a search and include the bugs from the result. To enable this feature, you can add this line to your service configuration:: bts.udd = True Excluding bugs marked pending +++++++++++++++++++++++++++++ Debian bugs are not considered closed until the fixed package is present in the Debian archive. Bugs do cease to be outstanding tasks however as soon as you have completed the work, and so it can be useful to exclude bugs that you have marked with the pending tag in the BTS. This is the default behaviour, but if you feel you would like to include bugs that are marked as pending in the BTS, you can disable this by adding this line to your service configuration:: bts.ignore_pending = False Excluding sponsored and NMU'd packages ++++++++++++++++++++++++++++++++++++++ If you maintain an even larger number of packages, you may wish to exclude some packages. You can exclude packages that you have sponsored or have uploaded as a non-maintainer upload or team upload by adding the following line to your service configuration:: bts.udd_ignore_sponsor = True .. note:: This will only affect the bugs returned by the UDD bugs search service and will not exclude bugs that are discovered due to ownership or due to packages explicitly specified in the service configuration. Excluding packages explicitly +++++++++++++++++++++++++++++ If you would like to exclude a particularly noisy package, that is perhaps team maintained, or a package that you have orphaned and no longer have interest in but are still listed as Maintainer or Uploader in stable suites, you can explicitly ignore bugs based on their binary or source package names. To do this add one of the following lines to your service configuration:: bts.ignore_pkg = hello,anarchism bts.ignore_src = linux .. note:: The ``src:`` prefix that is commonly seen in the Debian BTS interface is not required when specifying source packages to exclude. Provided UDA Fields ------------------- +---------------------+---------------------+---------------------+ | Field Name | Description | Type | +=====================+=====================+=====================+ | ``btsnumber`` | Bug Number | Text (string) | +---------------------+---------------------+---------------------+ | ``btsurl`` | bugs.d.o URL | Text (string) | +---------------------+---------------------+---------------------+ | ``btssubject`` | Subject | Text (string) | +---------------------+---------------------+---------------------+ | ``btssource`` | Source Package | Text (string) | +---------------------+---------------------+---------------------+ | ``btspackage`` | Binary Package | Text (string) | +---------------------+---------------------+---------------------+ | ``btsforwarded`` | Forwarded URL | Text (string) | +---------------------+---------------------+---------------------+ | ``btsstatus`` | Status | Text (string) | +---------------------+---------------------+---------------------+ bugwarrior-1.8.0/bugwarrior/docs/services/bugzilla.rst000066400000000000000000000076201376471246600232310ustar00rootroot00000000000000Bugzilla ========================= You can import tasks from your Bz instance using the ``bugzilla`` service name. Additional Dependencies ----------------------- Install packages needed for Bugzilla support with:: pip install bugwarrior[bugzilla] Example Service --------------- Here's an example of a bugzilla target. This will scrape every ticket that: 1. Is not closed and 2. rbean@redhat.com is either the owner, reporter or is cc'd on the issue. Bugzilla instances can be quite different from one another so use this with caution and please report bugs so we can make bugwarrior support more robust! :: [my_issue_tracker] service = bugzilla bugzilla.base_uri = bugzilla.redhat.com bugzilla.username = rbean@redhat.com bugzilla.password = OMG_LULZ Alternately, if you are using a version of python-bugzilla newer than 2.1.0, you can specify an API key instead of a password. Note that the username is still required in this case, in order to identify bugs belonging to you. :: bugzilla.api_key = 4f4d475f4c554c5a4f4d475f4c554c5a The above example is the minimum required to import issues from Bugzilla. You can also feel free to use any of the configuration options described in :ref:`common_configuration_options`. There is also an option to ignore bugs that you are only cc'd on:: bugzilla.ignore_cc = True 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:: bugzilla.query_url = https://bugzilla.mozilla.org/query.cgi?bug_status=ASSIGNED&email1=myname%40mozilla.com&emailassigned_to1=1&emailtype1=exact Note that versions of Python-Bugzilla newer than 2.3.0 support the Bugzilla REST interface, but prefer the XMLRPC interface if both are configured. To force use of the REST interface, ensure you are using a newer version of the library and add:: bugzilla.force_rest = True Provided UDA Fields ------------------- +------------------------+-------------------------------+---------------------+ | 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 | +------------------------+-------------------------------+---------------------+ | ``bugzillaassignedon`` | date BZ was set to 'assigned' | Date | +------------------------+-------------------------------+---------------------+ bugwarrior-1.8.0/bugwarrior/docs/services/gerrit.rst000066400000000000000000000040331376471246600227070ustar00rootroot00000000000000Gerrit ====== You can import code reviews from a Gerrit instance using the ``gerrit`` service name. Example Service --------------- Here's an example of a gerrit project:: [my_issue_tracker] service = gerrit gerrit.base_uri = https://yourhomebase.xyz/gerrit/ gerrit.username = your_username gerrit.password = your_http_digest_password The above example is the minimum required to import issues from Gerrit. **Note** that the password is typically not your normal login password. Go to the "HTTP Password" section in your account settings to generate/retrieve this password. You can also pass an optional ``gerrit.ssl_ca_path`` option which will use an alternative certificate authority to verify the connection. You can also feel free to use any of the configuration options described in :ref:`common_configuration_options`. Specify the Query to Use for Gathering Patchsets ++++++++++++++++++++++++++++++++++++++++++++++++ By default, the Gerrit plugin will query patchsets based on this simple API query is:open+is:reviewer You may override this query string through your `bugwarriorrc` file. For example: gerrit.query = is:open+((reviewer:self+-owner:self+-is:ignored)+OR+assignee:self) Provided UDA Fields ------------------- +---------------------+---------------------+---------------------+ | Field Name | Description | Type | +=====================+=====================+=====================+ | ``gerritid`` | Issue ID | Text (string) | +---------------------+---------------------+---------------------+ | ``gerritsummary`` | Summary | Text (string) | +---------------------+---------------------+---------------------+ | ``gerriturl`` | URL | Text (string) | +---------------------+---------------------+---------------------+ The Gerrit service provides a limited set of UDAs. If you have need for some other values not present here, please file a request (there's lots of metadata in there that we could expose). bugwarrior-1.8.0/bugwarrior/docs/services/github.rst000066400000000000000000000165151376471246600227050ustar00rootroot00000000000000Github ====== You can import tasks from your Github instance using the ``github`` service name. Example Service --------------- Here's an example of a Github target:: [my_issue_tracker] service = github github.login = ralphbean github.password = OMG_LULZ github.username = ralphbean The above example is the minimum required to import issues from Github. You can also feel free to use any of the configuration options described in :ref:`common_configuration_options` or described in `Service Features`_ below. ``github.login`` is used to specify what account bugwarrior should use to login to github, combined with ``github.password``. If two-factor authentication is used, ``github.token`` must be given rather than ``github.password``. To get a token, go to the "Personal access tokens" section of your profile settings. Only the ``public_repo`` scope is required, but access to private repos can be gained with ``repo`` as well. Service Features ---------------- Repo Owner ++++++++++ ``github.username`` indicates which repositories should be scraped. For instance, I always have ``github.login`` set to ralphbean (my account). But I have some targets with ``github.username`` pointed at organizations or other users to watch issues there. This parameter is required unless ``github.query`` is provided. Include and Exclude Certain Repositories ++++++++++++++++++++++++++++++++++++++++ By default, issues from all repos belonging to ``github.username`` are included. To turn this off, set:: github.include_user_repos = False 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 ``github.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:: github.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 ``github.exclude_repos`` configuration option. In this example, ``noisy_repository`` is the repository you would *not* like issues created for:: github.exclude_repos = noisy_repository Import Labels as Tags +++++++++++++++++++++ The Github issue tracker allows you to attach labels to issues; to use those labels as tags, you can use the ``github.import_labels_as_tags`` option:: github.import_labels_as_tags = True Also, if you would like to control how these labels are created, you can specify a template used for converting the Github label into a Taskwarrior tag. For example, to prefix all incoming labels with the string 'github_' (perhaps to differentiate them from any existing tags you might have), you could add the following configuration option:: github.label_template = github_{{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. Filter Pull Requests ++++++++++++++++++++ Although you can filter issues using :ref:`common_configuration_options`, pull requests are not filtered by default. You can filter pull requests by adding the following configuration option:: github.filter_pull_requests = True Exclude Pull Requests +++++++++++++++++++++ If you want bugwarrior to not track pull requests you can disable it altogether and ensure bugwarrior only tracks issues. github.exclude_pull_requests = True Get involved issues +++++++++++++++++++ By default, bugwarrior pulls all issues across owned and member repositories assigned to the authenticated user. To disable this behavior, use:: github.include_user_issues = False 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 Queries +++++++ If you want to write your own github query, as described at https://help.github.com/articles/searching-issues/:: github.query = assignee:octocat is:open Note that this search covers both issues and pull requests, which github treats as a special kind of issue. To disable the pre-defined queries described above and synchronize only the issues matched by the query, set:: github.include_user_issues = False github.include_user_repos = False GitHub Enterprise Instance ++++++++++++++++++++++++++ If you're using GitHub Enterprise, the on-premises version of GitHub, you can point bugwarrior to it with the ``github.host`` configuration option. E.g.:: github.host = github.acme.biz Synchronizing Issue Content +++++++++++++++++++++++++++ This service synchronizes most GitHub fields to UDAs, as described below. Comments are synchronized as annotations. To limit the amount of content synchronized into TaskWarrior (which can help to avoid issues with synchronization), use * ``annotation_comments=False`` (a global configuration) to disable synchronizing comments to annotations; and * ``github.body_length=0``` to disable synchronizing the Github Body UDA (or set it to a small value to limit size). Provided UDA Fields ------------------- +---------------------+---------------------+---------------------+ | Field Name | Description | Type | +=====================+=====================+=====================+ | ``githubbody`` | Body | Text (string) | +---------------------+---------------------+---------------------+ | ``githubcreatedon`` | Created | Date & Time | +---------------------+---------------------+---------------------+ | ``githubclosedon`` | Closed | Date & Time | +---------------------+---------------------+---------------------+ | ``githubmilestone`` | Milestone | Text (string) | +---------------------+---------------------+---------------------+ | ``githubnumber`` | Issue/PR # | Numeric | +---------------------+---------------------+---------------------+ | ``githubtitle`` | Title | Text (string) | +---------------------+---------------------+---------------------+ | ``githubtype`` | Type | Text (string) | +---------------------+---------------------+---------------------+ | ``githubupdatedat`` | Updated | Date & Time | +---------------------+---------------------+---------------------+ | ``githuburl`` | URL | Text (string) | +---------------------+---------------------+---------------------+ | ``githubrepo`` | username/reponame | Text (string) | +---------------------+---------------------+---------------------+ | ``githubuser`` | Author of issue/PR | Text (string) | +---------------------+---------------------+---------------------+ | ``githubnamespace`` | project namespace | Text (string) | +---------------------+---------------------+---------------------+ | ``githubstate`` | Issue/PR state | Text (string) | +---------------------+---------------------+---------------------+ bugwarrior-1.8.0/bugwarrior/docs/services/gitlab.rst000066400000000000000000000175241376471246600226660ustar00rootroot00000000000000Gitlab ====== You can import tasks from your Gitlab instance using the ``gitlab`` service name. Example Service --------------- Here's an example of a Gitlab target:: [my_issue_tracker] service = gitlab gitlab.login = ralphbean gitlab.token = OMG_LULZ gitlab.host = gitlab.com The above example is the minimum required to import issues from Gitlab. You can also feel free to use any of the configuration options described in :ref:`common_configuration_options` or described in `Service Features`_ below. The ``gitlab.token`` is your private API token. 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 ``gitlab.include_repos`` option. For example, if you would like to only pull-in issues from your own ``project_foo`` and team ``bar``'s ``project_fox`` repositories, you could add this line to your service configuration (replacing ``me`` by your own login):: gitlab.include_repos = me/project_foo, bar/project_fox Alternatively, if you have a particularly noisy repository, you can instead choose to import all issues excepting it using the ``gitlab.exclude_repos`` configuration option. In this example, ``noisy/repository`` is the repository you would *not* like issues created for:: gitlab.exclude_repos = noisy/repository .. hint:: If you omit the repository's namespace, bugwarrior will automatically add your login as namespace. E.g. the following are equivalent:: gitlab.login = foo gitlab.include_repos = bar and:: gitlab.login = foo gitlab.include_repos = foo/bar Alternatively, you can use project IDs instead of names by prefixing the project id with `id:`:: gitlab.include_repos = id:1234,id:3141 Filtering Repositories with Regular Expressions ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If you don't want to list every single repository you want to include or exclude, you can additionally use the options ``gitlab.include_regex`` and ``gitlab.exclude_regex`` and specify a regular expression (suitable for Python's ``re`` module). No default namespace is applied here, the regular expressions are matched to the full repository name with its namespace. The regular expressions can be used in addition to the lists explained above. So if a repository is not included in ``gitlab.include_repos``, it can still be included by ``gitlab.include_regex``, and vice versa; and likewise for ``gitlab.exclude_repos`` and ``gitlab.exclude_regex``. .. note:: If a repository matches both the inclusion and the exclusion options, the exclusion takes precedence. For example, you want to include only the repositories ``foo/node`` and ``bar/node`` as well as all repositories in the namespace ``foo`` starting with ``ep_``, but not ``foo/ep_example``:: gitlab.include_repos = foo/node, bar/node gitlab.include_regex = foo/ep_.* gitlab.exclude_repos = foo/ep_example Filtering Membership ^^^^^^^^^^^^^^^^^^^^ If you want to filter repositories that you have a membership. gitlab.membership = True Filtering Owned ^^^^^^^^^^^^^^^^^^^^ If you want to filter repositories that you own. gitlab.owned = True Import Labels as Tags +++++++++++++++++++++ The gitlab issue tracker allows you to attach labels to issues; to use those labels as tags, you can use the ``gitlab.import_labels_as_tags`` option:: gitlab.import_labels_as_tags = True Also, if you would like to control how these labels are created, you can specify a template used for converting the gitlab label into a Taskwarrior tag. For example, to prefix all incoming labels with the string 'gitlab_' (perhaps to differentiate them from any existing tags you might have), you could add the following configuration option:: gitlab.label_template = gitlab_{{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. Include Merge Requests ++++++++++++++++++++++ Although you can filter issues using :ref:`common_configuration_options`, merge requests are not filtered by default. You can filter merge requests by adding the following configuration option:: gitlab.filter_merge_requests = True Include Todo Items ++++++++++++++++++ By default todo items are not included. You may include them by adding the following configuration option:: gitlab.include_todos = True If todo items are included, by default, todo items for all projects are included. To only fetch todo items for projects which are being fetched, you may set:: gitlab.include_all_todos = False Include Only One Author +++++++++++++++++++++++ If you would like to only pull issues and MRs that you've authored, you may set:: gitlab.only_if_author = myusername Use HTTP ++++++++ If your Gitlab instance is only available over HTTP, set:: gitlab.use_https = False Do Not Verify SSL Certificate +++++++++++++++++++++++++++++ If you want to ignore verifying the SSL certificate, set:: gitlab.verify_ssl = False Provided UDA Fields ------------------- +-----------------------+-----------------------+---------------------+ | Field Name | Description | Type | +=======================+=======================+=====================+ | ``gitlabdescription`` | Description | Text (string) | +-----------------------+-----------------------+---------------------+ | ``gitlabcreatedon`` | Created | Date & Time | +-----------------------+-----------------------+---------------------+ | ``gitlabmilestone`` | Milestone | Text (string) | +-----------------------+-----------------------+---------------------+ | ``gitlabnumber`` | Issue/MR # | Text (string) | +-----------------------+-----------------------+---------------------+ | ``gitlabtitle`` | Title | Text (string) | +-----------------------+-----------------------+---------------------+ | ``gitlabtype`` | Type | Text (string) | +-----------------------+-----------------------+---------------------+ | ``gitlabupdatedat`` | Updated | Date & Time | +-----------------------+-----------------------+---------------------+ | ``gitlabduedate`` | Due Date | Date | +-----------------------+-----------------------+---------------------+ | ``gitlaburl`` | URL | Text (string) | +-----------------------+-----------------------+---------------------+ | ``gitlabrepo`` | username/reponame | Text (string) | +-----------------------+-----------------------+---------------------+ | ``gitlabupvotes`` | Number of upvotes | Numeric | +-----------------------+-----------------------+---------------------+ | ``gitlabdownvotes`` | Number of downvotes | Numeric | +-----------------------+-----------------------+---------------------+ | ``gitlabwip`` | Work-in-Progress flag | Numeric | +-----------------------+-----------------------+---------------------+ | ``gitlabweight`` | Weight | Numeric | +-----------------------+-----------------------+---------------------+ | ``gitlabauthor`` | Issue/MR author | Text (string) | +-----------------------+-----------------------+---------------------+ | ``gitlabassignee`` | Issue/MR assignee | Text (string) | +-----------------------+-----------------------+---------------------+ | ``gitlabnamespace`` | project namespace | Text (string) | +-----------------------+-----------------------+---------------------+ bugwarrior-1.8.0/bugwarrior/docs/services/gmail.rst000066400000000000000000000047011376471246600225060ustar00rootroot00000000000000Gmail ===== You can create tasks from e-mails in your Gmail account using the ``gmail`` service name. Additional Dependencies ----------------------- Install packages needed for Gmail support with: .. code:: bash pip install bugwarrior[gmail] Client Secret ------------- In order to use this service, you need to create a product and download a client secret file. Do this by following the instructions on: ``https://developers.google.com/gmail/api/quickstart/python``. You should save the resulting secret in your home directory as ``.gmail_client_secret.json``. You can override this location by setting the ``client_secret_path`` option. Example Service --------------- Here's an example of a gmail target: :: [my_gmail] service = gmail gmail.query = label:action OR label:readme gmail.login_name = you@example.com The specified query can be any gmail search term. By default it will select starred threads. One task is created per selected thread, not per e-mail. You do not need to specify the ``login_name``, but it can be useful to avoid accidentally fetching data from the wrong account. (This also allows multiple targets with the same login to share the same authentication token.) Authentication -------------- When you first run ``bugwarrior-pull``, a browser will be opened and you'll be asked to authorise the application to access your e-mail. Once authorised a token will be stored in your bugwarrior data directory. Provided UDA Fields ------------------- +---------------------+-----------------------------------+---------------+ | ``gmailthreadid`` | Thread Id | Text (string) | +---------------------+-----------------------------------+---------------+ | ``gmailsubject`` | Subject | Text (string) | +---------------------+-----------------------------------+---------------+ | ``gmailurl`` | URL | Text (string) | +---------------------+-----------------------------------+---------------+ | ``gmaillastsender`` | Last Sender's Name | Text (string) | +---------------------+-----------------------------------+---------------+ | ``gmaillastsender`` | Last Sender's E-mail Address | Text (string) | +---------------------+-----------------------------------+---------------+ | ``gmailsnippet`` | Snippet of text from conversation | Text (string) | +---------------------+-----------------------------------+---------------+ bugwarrior-1.8.0/bugwarrior/docs/services/jira.rst000066400000000000000000000126031376471246600223420ustar00rootroot00000000000000Jira ==== You can import tasks from your Jira instance using the ``jira`` service name. Additional Requirements ----------------------- Install the following package using ``pip``: * ``jira`` Example Service --------------- Here's an example of a jira project:: [my_issue_tracker] service = jira jira.base_uri = https://bug.tasktools.org jira.username = ralph jira.password = OMG_LULZ .. note:: The ``base_uri`` must not have a trailing slash. .. note:: The `jira.password` may contain an `api token `_. The above example is the minimum required to import issues from Jira. You can also feel free to use any of the configuration options described in :ref:`common_configuration_options` or described in `Service Features`_ below. Service Features ---------------- The following default configuration is used:: jira.import_labels_as_tags = False jira.import_sprints_as_tags = False jira.label_template = {{label}} jira.query = assignee = AND resolution is null jira.verify_ssl = True jira.version = 5 Specify the Query to Use for Gathering Issues +++++++++++++++++++++++++++++++++++++++++++++ By default, the JIRA plugin will include any issues that are assigned to you but do not yet have a resolution set, but you can fine-tune the query used for gathering issues by setting the ``jira.query`` parameter. For example, to select issues assigned to 'ralph' having a status that is not 'closed' and is not 'resolved', you could add the following configuration option:: jira.query = assignee = ralph and status != closed and status != resolved This query needs to be modified accordingly to the literal values of your Jira instance; if the name contains any character, just put it in quotes, e.g. jira.query = assignee = 'firstname.lastname' and status != Closed and status != Resolved and status != Done Jira v4 Support +++++++++++++++ If you happen to be using a very old version of Jira, add the following configuration option to your service configuration:: jira.version = 4 Do Not Verify SSL Certificate +++++++++++++++++++++++++++++ If you want to ignore verifying the SSL certificate, set:: jira.verify_ssl = False Import Labels and Sprints as Tags +++++++++++++++++++++++++++++++++ The Jira issue tracker allows you to attach labels to issues; to use those labels as tags, you can use the ``jira.import_labels_as_tags`` option:: jira.import_labels_as_tags = True You can also import the names of any sprints associated with an issue as tags, by setting the ``jira.import_sprints_as_tags`` option:: jira.import_sprints_as_tags = True If you would like to control how these labels are created, you can specify a template used for converting the Jira label into a Taskwarrior tag. For example, to prefix all incoming labels with the string 'jira_' (perhaps to differentiate them from any existing tags you might have), you could add the following configuration option:: jira.label_template = jira_{{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. Kerberos authentication +++++++++++++++++++++++ If the ``password`` is specified as ``@kerberos``, the service plugin will try to authenticate against server with kerberos. A ticket must be already present on the client (created by running ``kinit`` or any other method). Cookie auth vs. HTTP-Basic auth +++++++++++++++++++++++++++++++ If the ``use_cookies`` option is set to ``True``, the credentials are used for Cookie-based authentication as opposed to HTTP-Basic authenticaton. This only makes sense when Kerberos is not being used (see above). This is useful in situations where HTTP-Basic auth is disabled or disallowed for some reason. When using API token ++++++++++++++++++++ Some hosts only support API tokens to authenticate. If so, ``bugwarrior-pull`` will respond with ``Err: 401 Unauthorized``. Create a token here_. Handle the token like it is a password. .. _here: https://id.atlassian.com/manage-profile/security/api-tokens Provided UDA Fields ------------------- +---------------------+--------------------------------+---------------------+ | Field Name | Description | Type | +=====================+================================+=====================+ | ``jiradescription`` | Description | Text (string) | +---------------------+--------------------------------+---------------------+ | ``jiraid`` | Issue ID | Text (string) | +---------------------+--------------------------------+---------------------+ | ``jirasummary`` | Summary | Text (string) | +---------------------+--------------------------------+---------------------+ | ``jiraurl`` | URL | Text (string) | +---------------------+--------------------------------+---------------------+ | ``jiraestimate`` | Estimate | Decimal (numeric) | +---------------------+--------------------------------+---------------------+ | ``jirasubtasks`` | ,-separated subtasks Issue IDs | Text (string) | +---------------------+--------------------------------+---------------------+ bugwarrior-1.8.0/bugwarrior/docs/services/megaplan.rst000066400000000000000000000023611376471246600232010ustar00rootroot00000000000000Megaplan ======== You can import tasks from your Megaplan instance using the ``megaplan`` service name. Additional Requirements ----------------------- Install the following package using ``pip``: * ``megaplan`` Example Service --------------- Here's an example of a Megaplan target:: [my_issue_tracker] service = megaplan megaplan.hostname = example.megaplan.ru megaplan.login = alice megaplan.password = secret megaplan.project_name = example The above example is the minimum required to import issues from Megaplab. You can also feel free to use any of the configuration options described in :ref:`common_configuration_options`. Provided UDA Fields ------------------- +-------------------+-------------------+-------------------+ | Field Name | Description | Type | +===================+===================+===================+ | ``megaplanid`` | Issue ID | Text (string) | +-------------------+-------------------+-------------------+ | ``megaplantitle`` | Title | Text (string) | +-------------------+-------------------+-------------------+ | ``megaplanurl`` | URL | Text (string) | +-------------------+-------------------+-------------------+ bugwarrior-1.8.0/bugwarrior/docs/services/pagure.rst000066400000000000000000000075751376471246600227140ustar00rootroot00000000000000Pagure ====== You can import tasks from your private or public `pagure `_ 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:: pagure.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) | +-----------------------+---------------------+---------------------+ bugwarrior-1.8.0/bugwarrior/docs/services/phabricator.rst000066400000000000000000000110351376471246600237110ustar00rootroot00000000000000Phabricator =========== You can import Maniphest tasks from your Phabricator instance using the ``phabricator`` service name. This service supports both Maniphest (Tasks) and Differential (Revision). Additional Requirements ----------------------- Install the following package using ``pip``: * ``phabricator`` Example Service --------------- Here's an example of a Phabricator target:: [my_issue_tracker] service = phabricator .. note:: Although this may not look like enough information for us to gather information from Phabricator, but credentials will be gathered from the user's ``~/.arcrc``. To set up an ``~/.arcrc``, install arcanist and run ``arc install-certificate`` The above example is the minimum required to import issues from Phabricator. You can also feel free to use any of the configuration options described in :ref:`common_configuration_options`. Service Features ---------------- Filtering by User and Project ............................. If you have dozens of users and projects, you might want to pull the tasks and code review requests only for the specific ones. If you want to show only the tasks related to a specific user, you just need to add its PHID to the service configuration like this:: phabricator.user_phids = PHID-USER-ab12c3defghi45jkl678 If you want to show only the tasks and diffs related to a specific project or a repository, just add their PHIDs to the service configuration:: phabricator.project_phids = PHID-PROJ-ab12c3defghi45jkl678,PHID-REPO-ab12c3defghi45jkl678 Both ``phabricator.user_phids`` and ``phabricator.project_phids`` accept a comma-separated (no spaces) list of PHIDs. If you specify both, you will get tasks and diffs that match one **or** the other. When working on a Phabricator installations with a huge number of users or projects, it is recommended that you specify ``phabricator.user_phids`` and/or ``phabricator.project_phids``, as the Phabricator API may return a timeout for a query with too many results. If you do not know PHID of a user, project or repository, you can find it out by querying Phabricator Conduit (``https://YOUR_PHABRICATOR_HOST/conduit/``) -- the methods which return the needed info are ``user.query``, ``project.query`` and ``repository.query`` respectively. Selecting a Phabricator Host ............................ If your ``~/.arcrc`` includes credentials for multiple Phabricator instances, it is undefined which one will be used. To make it explicit, you can use:: phabricator.host = https://YOUR_PHABRICATOR_HOST Where ``https://YOUR_PHABRICATOR_HOST`` **must match** the corresponding json key in ``~/.arcrc``, which may include ``/api/`` besides your hostname. Ignoring Some Items ................... By default, any Task or Revision relating to any of the given users or projects will be included. This is likely more than you want! You can ignore some user relationships with the following configuration:: phabricator.ignore_cc = True # ignore CC field phabricator.ignore_author = True # ignore Author field phabricator.ignore_owner = True # ignore Owner field (Tasks only) phabricator.ignore_reviewers = True # ignore Reviewers field (Revisions only) Note that there is no way to filter by the reviewer's response (for example, to exclude Revisions you have already reviewed). Phabricator does not provide the necessary information in the Conduit API. Furthermore, setting `phabricator.only_if_assigned` to something other than False will default to ignoring the CC and Author fields as reported in phabricator. Provided UDA Fields ------------------- +----------------------+--------------------------+----------------------+ | Field Name | Description | Type | +======================+==========================+======================+ | ``phabricatorid`` | Object | Text (string) | +----------------------+--------------------------+----------------------+ | ``phabricatortitle`` | Title | Text (string) | +----------------------+--------------------------+----------------------+ | ``phabricatortype`` | Type (``issue`` for | Text (string) | | | Tasks, ``pull_request`` | | | | for Revisions) | | +----------------------+--------------------------+----------------------+ | ``phabricatorurl`` | URL | Text (string) | +----------------------+--------------------------+----------------------+ bugwarrior-1.8.0/bugwarrior/docs/services/pictures/000077500000000000000000000000001376471246600225175ustar00rootroot00000000000000bugwarrior-1.8.0/bugwarrior/docs/services/pictures/trello_url.png000066400000000000000000000353351376471246600254210ustar00rootroot00000000000000‰PNG  IHDRï=SÏËsBITÛáOàtEXtSoftwareShutterc‚Ð IDATxÚíwx[ÕùÇ_mÉ’%ï½d'Žã‘ØŽØdÇÙ !Œ0Êj¡¥ÐRJùµ@Ã{(«@JÙd8vœ8Nâ½ã½‡,kïuïï#_˲,Ë#!ÐóyîãçZ:º÷ꜣ{¿ç=ïy_šL& ƒÁ`0 ó3„Ž«ƒÁ`0 ƒÁjƒÁ`0 ƒÁ\S˜.>N£ÑpÕ`0 ƒÁ`0?'5D|KKKGG‡L&³X,¸‚0 ƒÁ`0˜Ÿš§ÑhV«õܹsl6;11ÑÏÏÉdâ Â`0 ƒÁ`~j.^¼~~~¸v0ÌσŒÆái4ðñÁµ‚Á`0˜_¬š§Óé½½½</>>ëx ó£Vƒ\nßd²á}­ 0™@¯µŒFÐjA«‹Åþ¢‡Ðé ˆDÀá€@ÞÞÀá€P^^ €ŸŸë ÏXb0 æúTó4M"‘ˆÅb,å1̵€  »:: « úû¡»úú §z{¡· †«~v…ì=Ch(„…Ax8„†BD„„@t4ÄÆ‚—nU ƒÁüdjt:P(Ä5‚Á`¦³ © Z[í[[´·ƒÙ|OÊå7ü/I‚R9 c€¾>è냲2ïClìð‰‰àïÛƒÁ`0×BÍÓh4‚  ® 3%¬Vhn†š¨­µoMM0ÑY|>øù¿¿Ý¿íøûƒ·7øú‡^^ ‡ÞÞÀç› l6ðù¸H@©“ t:ÐhÀdµôzÐhF¸÷8:üä˜Ç”H@"¢¢/†„@R’}KN†¤$»‡ƒÁ`0Ó«æ1 fò45Aq±}+/÷Ô=$bc!<|Øq…r_®ò ¾¾`ÿë! @WH$ÃBýýÐÞíí`2¹øH?ô÷Cn®ý_ f΄ÌLÈÌ„Œ HKÃþ9 ƒÁjƒÁ\s,(.†¼<8w._ÇûœF± .n„/ÊÏNȲXáá.ÞBË(W¢ÖVhl„úzç IBc#46ÂÞ½öAER,ZË–ÁÒ¥„{ƒÁ`& M&““ÉÌÏÏ_¼x1^‹Á`\CPRgÎ@~>œ?:ݘ%£¢ìŽ%‰‰œ ³gOÀæVcmmP[ uuPSuuP[;æj ’’`ùrX¶ –/ŸØ¤ƒÁ`°šÇjƒÁ¸Æ`€ÂB8|€Þ^×eD"HN†Å‹aÑ"˜?‚ƒqµ¹Æj…†(-…ÒR(,„Š °Ù\c0 + 6m‚-[ !WƒÁ`°šÇ`0D«…o¿… 7×u¼ÈØX»!yñbˆ‰Á64()ü|È˃K—\›í““a˸ã˜=WƒÁ`°šÇ`0n!ÈχÏ?‡o¿uáK99°|9,_QQ¸¶¦½.\€ü|8}Š‹ œ ÌŸ;wÂm·¾Kc0 æ*©y©TZ^^ÞÜܬP(l60 __ß3f¤¥¥âêÆ`®_ärxÿ}øè#èìt~+%6m‚Í›!3èt\UWþ~8r‚Ó§'F8زþøGÈÊÂõ„Á`0˜iSó6›íäÉ“uuuÑÑÑ"‘ˆN§AJ¥²½½½¬¬,!!!''‡‰ó¢c0×mmðÖ[ðÉ'ÎÆø”ع¶m±WÒOƒ^§NÁ—_ÂáÃÎ0—,LJ ðø ƒÁ`°šŸªš·Ùlû÷ïg±XË–-3™LƒÁb±$ 4Åbñx<‡“ŸŸo6›o¿ývœ£jê¨5“É. FV "‘ˆÍbáûÅ·øäÐÖ×3Ÿy†ûý÷`µ¿;vÀÝwCZÚDh0´:Ðéto&z½¾_"Q(•&“™Åbúûù½½Ç*O’ä L\Ç{ìbîÑét2¹\­Ö¨5.‡#z‹„€€æùAL&SGg§J¥¶6‘Hzµ2j+ðÕWðÅpñâˆ×gφݻaëÖkÜs0 ó‹RóÇŽS«ÕK—.U(ííí Z­–ʘÈb±bccçÌ™ãããsöìY‘H´nݺÉ]ë‰Î«+àá¹rÜÿåfûê›*•ÊßÏoû¶](8_Xå FûÕ·s¹?Y]ñùž/ ))q~F†‡oaÆmq…RÙÞѱ11¢Éå5àÕWm»w3,¾djjÇ­·*W¯ ›ÄQsóòš[ZÑþ;vðù^î míWz\…ÊI˜5kAf†ËÞk³Ù>þÏg?sæò¥7Lô"¥ƒƒ•U­mm£ß …sç¤ÄÏœ9®ÑÁjµ^*.®«¿BŒtp‰ŽZ”-¸zÙ¯jjà7`ïÞKfW®„·Þ‚ääIÜ+®C,KM]N®^Ï$YQU~¾¾Ñxñ ƒ™¦ê÷"•Jëëë·oß.•J«ªªz{{³²²ÒÓÓÑÃL«Õ–••J¥ÒeË–egg8p ##cr>ô‡Û~ø¸öC¸?é·ÿËj^®P¨T*ˆÃÿ$ɶöv ý ¥<$ùÎûÀ·ïpVóc¿…·Åe2ùåâðõñ™Œš?qx:;‡ëš5ðøã†E‹NìÝå)I–I¨(«ÕÚÞ1ìsßÒÖ:Ç•¾¤(¯¨,-/¾1™\.W¯Ó$ WÚ;:¶nÞ$š>k7I’Å¥¥å•Ž/Òi4bÈú R« ÎVÕÔnX›ãF‘ëõúCG¡Fq¢½£³¿_²rÅŠ«%C““áÓOáùçáŸÿ„?t ¹¹–¿û¼ô8üäǽW\§jÞjEÝ;%)é¨æз›‡Õ<ƒ™N5?¡ÉeGÊÊÊRRRt:]{{»D"ùÃþà(Á 7Ü0þüwß}·¦¦fîܹsæÌ)++ËÉɹfßðŠ¢¾j°RkÑn‹ÛîÃñq=&1H zóµíüà³}¯ó6£lŠbqŒË}ýýF£b (Š’²r½AŸ’”‹Ý VŒÛâÀfƒçžƒçž£B¦ FEs>xß͘¹ŒDé1]]V«ér«ÕÚÒ2ŽšŸ9£´¼ÜÛ[0k–8&Æ×Çl6›L./¼P4 •ÆÓgÎlÛ²eÒ7(§d^þÙæ–ôoDxXJr²Ÿ¯/ŸÏ·X,*µº£³³ªºÆb±(•ÊïÚ°n­Kg!³ÅrxHʇ…†¦ÎÄ`0JeuMmcS“Ñd:züø–MC®^Üý°0xùeøë_áå—áÍ7Ál«þùO((€o¾¸87=G¡TÊdr‹Õ'³ÙìkߟFco_ŸÅb BŽÁ`0˜©0ÕåS---AAA:®³³sÕªU.-Á\.wÆ }}}&“)((¨eèizmØß¸÷W§v<”¿Ì88V™*Yå'nùÍ™{Šú.ü ´]køúøŒõ ~„;Dolj~âoßõüîʪjÜïfj~¼÷©V¯†]»”7Š|ÎîÜ™÷ÚkÞ“u~s¾´¶¢ëŒŸ9¤RF림P(ܶuËŽ[nIOM¥¾ƒÁ ܲic€¿? ʺº»§åò*+«”§Óé9«WmX·.*2R Ðh46›‘ž~ûm·€Á`8u:×긜`ˆË—‹•*$ÎNظ~]dD›Íf0þþË—Þ°(;Ë;[àòãÓ‰¯/¼ô”—Ú5öWÊË!#rÓsšš[róò Î7?I”ÉOåžÉ/8×/‘à_7ƒÁüôj^¡Pp8‹Å¢V«gÍš5V±¸¸8ƒÁ€žš …×û¤Q*• ¥bcÇv³ik€Ð‡kìßâžÒ×K—B^úϲrå×ÿøÇ•ÅKÄq±Ór‹¥£³ bÅbjRé{7ޱê”N§§¥ÎEûÓ¢æ EIYÚ_½rELt´Ëb\gýÚ??_P©ÕÈ#µFS[_þ~~‹²³G_|rRbâìÙ V««jj®EIL„'àý÷í>6J%lÛ{÷N[ÏÁ`0Ì/[ÍÛl6›ÍF„Õjuã¢Íf³­V+FCåq½OšÖ¶v´3–·ŒD2 7àçæ)‹™t‹{„Á[¶@}=ƒÏ(´X¬QII‰kV®ðUŸ$I^*..8w¾½£S­Ñøˆ„±bñÒ%KÒÓRG[dû%’ü‚sáa‹.ÔëõÇNœllllmkg³Ùi©soÚº™{I’¼xùò™¼³í #!>>gõêÙ ³Üªyw-®Ñhªkjú%½Þ@’$Ÿï:#.ŽÃáŒ(÷ðÃP\ ÀbÁ¾}°}{ëwß»ïH£éíë“Ë4:Ü!2à …Bäk.ÇÔ_iÉd*•jrQwÂ>ì÷žr|«ÕJõO–\ŠcbÚÚÛI’lmkOœ@½ÕÙÕL#nlƒ7—ˉ‰ilj’Éåft M«ÍÖÖÖÞÝÓ­Õê¬V+Ç Œ‰ñÏ“ª·¯¯³³K¡TŒF—ë#ÅÄD‡Û;dZ\¸«WCe%Øl‘û?¯ìbEF¢&¶X, M •Jí?®¦fGÌŒ¸X'‹ŒÙlnniéîéÕétL&ÓÇÇ'N, ëòúû%-m­Z­V¯7ðx<¡·wTddxxõ{ilj6›ÍJ•Òþuz{mÖaãNhH°¿¿¿ãMfsSsswOV§³˜-</8((.Vì¾GM¢zѪ ¥R©Ñj-f‹@ @cÞÞÎ}O«Óµ·wxyñbÅb›ÍÖØÜ,‘ (U*¡0..62"bD&ÉööŽ®î.µZc±Z½xùÔÓ”‘*«ª{ëÕW§ëYe0^|åµ#Ç;½~ðÈ‘°ÐЧÿöäD#á$ùù—{?úäSGkkUuõ‘ãÇ_{ó­¾ù:,t¸Ý»{zî¾ÿ´ÀC½jÅ __߆†ÆË%%}ýýe¿º÷¾Ï>þÈQýSv»Wß|‹Åb­^¹"aÖ,‚ Î/¬ª©Q(üîaƒÁ@’äÜ9)é©©¾¾¾UÕ5§Ïœ±Úl¯¾ñæ‚Ì —îã¶8j©ÀÀ@«Óéer¹Åb)8^¯×ÏKOؽÐ(ú¶ÛàÞ{=<ìH)ßyêt.š„Y·fÍh=ÔÞÑüXÄC åp8&“©¥e2jž$ɪšZ ÑhN§I Z«éáG¢†¥r‡u>­Ų·‡ûûûQƒR—ƒ" ‹ˆ§M20ÐÓÛk6›ë*å¦õë|xJå¡#G‘°æó½ü|||h­¦·¯/"|äÄ`p0ìÙË–IÆ• Mq0ŒÐÐh4(ÃW`` cS2Æ'ÝÝ=?žŸ?º&AXh(ßËËbµ ʤR‚ jëê”Jåúuké#gÉ&W½*¥’N§ …Booa#¤ƒƒÝ==$I¢ñüÂì,§«""7/D"‘Ÿ¯¯Õj9’éëï?rì8úQDD„ÇDEñx^f³I¥RwtuáÅc æºPóV›å?Eÿ¼{Íý¼IÉ6ƒþsòŸ¯‡}ÎbLÆ_âóÕû¶ÅmgчŸ=¿ŸûÈœ} :‹öÕÒ—þœöFäžÚšÏ\úûË¥/Àç«÷ƉfŒ:Ô^Èí>½áÐjøSÚã÷$ÞçæÔ¥üáÅ…¯ ó?¢°ïü'níÓ÷lýîÓºïM¼Ÿzëµ²—m¤•Ëà]¾µ<ÞgØœy{üÏf½PГ?¢%˜Ì ¡0ücùã"&ôöv²`ŽÑlv?» .].~ðÀÎ;ï¸qËf7ß10 à±GYµb9uJ¥røh/¿*•r8ìgŸ~zõÊèÅwÞñå¾ýo¼ýŽd`à?_ìyø¡§Ø/?üø$åãgÎ|ûרš!IòË}ûß|ç]›Íöô³Ï}ûÕ¾€1ªÂ‰S¹g”OLHxúoO¢ +ýÙF9$ù÷<‹·ë׿<ýOR^=F“i×ó/œ8uZ&—ÿí™]ÿùèƒÑ.7ërÖ<öÈ#ȯvÞuçã}òì¹óz½>~æÌ¿<öÇôÔT{ÑðŸ/ö¼óÞûAìÙ·ÿ©'ÿ:‰_±léÌ#zu[{{ÞÙ‹ÅRRVÎfÃéÓ¨‡ÁK/yxX'kâ©Ó§ ’  Z—³Æ¥3r³ñ(¤J¥\¡ðÜŸÑÔÜŒ ½³®DÞ„Ðëíž÷ÞOÇ2™L///½^o4 ‚@¿Ë‹ßkœKâr¸£Õ¼Á`È;[€´æ¢ììä¤á`¸Z­öø‰“r…¢¿_RRVæ8ïDDî™<£ÑH§Ó祥¥Îãtp1áyà }™™¡—/ÓHÒ™ê¸\îæàrIiyE¬\¾Ìe,³Å’WP@’¤·· gõj*Lç¼´´ÓgòÚÚÛÛ;:»º»);tcS3’ò£{ã¼´4já:#t÷ô=~æÎ3{–;7³Óh`£Pœ<«T©zz{KËÊ2çÍ›bõ@ΚÕAAANcTÉÀÀ‘£Ç¬6[ummzZêèUaÁAA ³²‚‚†oM”£Éd:y:— ¶xa6ZM‘™™ñïO>ÅâƒÁL#“\k!Ìj½Êâ5Øc­ŸÄfñTëUÂ<¹³çD­s”òÎßh,ê]ÏÕ«¯dÿG)‹BßrštØué)9ìZ.-€D¿$G)ðbz­1•!‰nܲm.Â5Íà  Ü®iCΠÀÀ©XÇç¤$¯YµÒQ1Pþ¦õWÎ_¸÷ìÜIIyÄ;nCCˆÑ¾1E«Õîÿï7Àãñ>x÷í ‡\c4í®;n¿÷î¨BP±q±Ùlï¾ÿ>øúú~ð¯wœ¤<8>È‹KK«ª«`NJÊsÏ<íØ\g÷³»æÎI€ªêê‹—/>WFz:%åÑ‘ï¾ë.´??cÞ°”GzþÖ[m²¥¥ur->ÚßC³fÕJ´¹¸ŠŠì¡å.„èhKÑÙÕuòÔi‚$ׯ[ëÒ½Äd2uu÷Œ> õ¯Ëoç•Z}þB2';ÕØdÕ¼aH…{yþ)//ûTž~H‘[†"NŽ›”ÃaS•C½XSWg6› -u®£ÖDèM7 ê­®®qüTsK+r‹Ê̘—ž–:z¨?zH©ÑhjS‡æC 'Z]W®4 ñÏò¥K#îÓét*ŒOcS3õúà =ø¯Ëù%Ǭ…Åtîo¾¾¾[6mDUU]ãè4¹ê€°ÐÐÑÓMÁAA³‡T¸ËHš3ââ‚‚FÜš¸C‹UªkkÑ…¥§¥:Iy ƒ¹ŽÔZR }/•êôS BRXT„,š7o»ÑÇÕr·;v܆äÔ'NzrÀ²Š $7¼ÿ¾q ½§NŸA;÷ýzçhD£Ñ¸÷´ܳ³SË%•JçÔ¡\'", zz{'ÑâcŽò H¥¦öö¡nš0ÑÃvvuŸ8uš ÉÿëÖ²ÇðG«EaTr«ˆð0ô‘–‰D¶±Z­'O¶X,°(;›Zþ1¨v$HÂóO‘9J£s¨‹tÿY³Å‚vØœáÑ`Kk¢¸LªÅåp’Àj³9¦Ô½ÒÐ|>?%)Éó{…‚Z«êªk¹µWhHòÌq„Ï÷B]‹ZÉ ^CmÔØÜ| î·\.U…ÕjE‹’§R½n ‚ôO40?Z Àd2=o2 ƒù Ô<$I„˜ÌFÄt·¡ åyµ’Ök_b6¡KjþÖ™·€…0/>°à·y÷UV0ùo¼#ΠΆÂáMÅæôLINâ:…L2q¡®®)¯¬¶Gé^z×|D"äÞ×ß<øÝSRj4n÷#wKUM50Œì \Èš?}ýÊjòpñù|¤&m„‹á+ŠÎáRŽÛânˆŽ¶gŒWš†fÀ† ̶«»ûÄ©SAøùùnX¿ÎM!$þø|/ÇY@Î61Ñ R«Ñl€fòTîäª>#.Î}´Ï¡Lòú1Æ™*•*ÿludð\j%%ÕÛyC&y'ãîh(›1Ë£>‚D†eÚÇØmÛÔjW«Í†öQºYÏïL*nÕxÓ£A¹±F/ A E½*•zØ’#Fþëç /9v¼«»›˜îÛûXÝ»_20•êuß©Á›càq1_IÇÕ}ƒÁ`¦¦“íj"jióÉܵ ȉÉ0™Ìß|ûÝ(ecµÍ­­Žk¯Ç¢àÜydmõ÷÷¿añ¢éê·ÔRKG J¡7;¦Óé5ZíÚ5«‘?†Íf3šLÀår©¥\ž³ïÍØ?XûH€Çã:}ÄÍÏS8äÅNÍnét:¤ŒE"á„îs(3ùŒª+£É„üUJÊÊœ‚ÞPv°X,f‹ͽø¯^µò\á½^ßÓÛÛÓÛËãñfΈKNLáqZ º·Á ŸJõ:Ò×ßßÕÝ-—+ …Áh´ Í®Lj¥„ç‹40 fŠLÁ6OI6Â:‰$ˆéó?1>»«´Æ¬v`ü-óéš;ïI¼ß‹É€:yÍ›ñEÔóÅ»lq4jò—Ë‹F&“¡XiW5SL_?%ÜG3üÅéSÊJFÙ5…c?˜©€ÓÃø“à “ÉððìÞÞîÄ:»Ñh$¯š rÜweÔ$%ª«–øñGËb2—,ZˆöÏž;ï&ã[k[ª«ÕªP*6ªuŽ&² {ûùÂ=÷À›o<òHÇ »=‚\°’gÇÏœ) Ùl6Fëï—°·á‹ó}ç<9¸^¯Gâ26F<–|Dn6þ~~.CÎMH]ihäpØn6úÔlóQ‘ö àc/VkkkGz"|ì̔׌vÚÛ;Æ?{T$ÈärÇ8ÙŽ ã"zŒ‘ÆÔñ¤ÅÝ£RÛWÜŠ„Bxâ @‘Išš’_xN63cj‹¦ææÊª*—׉lØq±âó3]nisçéþ1Íóå••È©ƒËál\¿~Úû0NÊ“Uté’Ë2Yó3Q¸C™Lvðð”fÁ`ÌŒ‹¡æ‡|¯këêÆ:]o_Ÿtpb¢£¨zæóùèw¡T©Æú b(¬}Œê5´hÛ1J¬ûžÃ;y"ãÐ!ûÿO=ŒïIQ&—1Ü⺪i´¨ÈÈõksn¹iЇ£T*«jj¦·A‡»÷Òäª*««‘”_±léâ… ƒ9ÎTFél6y¦¹¹ ƒ¹nÔÿæ öùÏ¿ø’'ÆBÊÀ¥Ñh=+uΤ ‹.]tz×h4¢lµpëÍÛÞª«¯G›£klvÖ”dªðBÑ‘cã„Ãß²i#RŸ{öíG5Žôöõ}öå^`2Û¶n¹jj~œwOiiZŠ+Û=vì€'Ÿ´+ï²Òà­[ ±Ñ“C±Y¬µkV#E{úLžbd£7·¶‡Ã {xÀd2‘_¯×ŽÛ]WåÂÅ‹Àb±6®_çïY.°IÀç{-ÊÎFû…EE•ÕÕ.ã®ÄDGSq ah¢Æi„ƒ2[m¶ÃG9Mà$™›—‡ ó³œ"Ê@=æLøö[peA§"ü¨5j—Gšf³¹à|áT¢ÓPö{§°ñàVOâ°]](¸–€Ïwœš›\õ¢a›ÍFŒþ¦r…|r_|V¼}wáÒ%ódWÓb0Ì5QóÄÔló×ÊÓ&ÉÏ{ø¥’zu½`²™šUM:˰]-BáÍÀ¾†=—%—H ’èÖvKôÎâ£j°BaNÊm²™^(~öÁ¼ûÀãÿò¢×©·tÝš–¿YñZ¬š:—…°m?üE:‘µ–*¯R©¾ûá Ú¨¥o`0x¬8fL7›Ö6ô”r"N¢‡èÑãÇ«kkÑÔŠd`%¦ñø)ÒIöK$·ïüõ‡ôôö¢'I’J¥2ïlÁ¡#GçHÐäæçŸ-8×××_\ZŠ<.ܼE£ÑþúçÇÐýóã_}s@¥V#ýTwåʽ¿}°´¬²,Xµb¹ãåutvÞùë{Ñæ(³Ølö~ÿÚúÙçv=¿»º¶–zÞkµZÇ8ÙA(¢¼B¡¸ëÞ{ /¡Ø&ƒálÁ¹;ï¾¥‰ýí÷»ñŸ ž´8EYEe{Gê0V«U*•ž8uº¢ª ÉhÇœ—†§žªÌɱÿSY ™™°w¯'×# Q=[,–Oœ¤æv´:rÇDÓÝ^'?Ç)TcSÓ¹¡ÜF³âgªTêæ–Öѵrw4ÆåGÐ&t(~æ ªB.^ºüßßÖÔÖõööiµZéà`sKËù E_}óãˆåèñG[ÄCCBPr"•Z}àû.—”ttvöôö^ihüþàA4_!²Ìwú 8&Y…{ûú=*“Ë‘QÃh4VU×üxÒž¾`ÉâEŽíž0 ý®¥Ré7ß}ßÔÜì¸ò[&—›Ífho‡+à•WÐL—!2òò`Œ¤~Cöì²ò d}·Ùl*µšÏ:cGgç¡#G;»º¨ˆ1$Ij4Ú†Æ&Ç ygÏ^*.îëïwœ÷”ÉNåž#EYñÑ ³±¹Y¯I’Ôét†Q‘‚ZÚÚjëêÐëI*”Êâ’R*ÏCvV–cEM®zQø|½^_pîuO0šLçΟ+¼0¹ŸðŒ3@"‘|÷ý]]hf³Ù¤ƒƒg °òÀ`0ÓË”VÁÚ‚ )0!l×ÐÓ&3x~fЂâKeÒ’ØÏÃ}8¾J“€¼tKùÜ{šIƒó@òƒ¯—¿¬0Éoø6KÀò6ÙLÂüê¢7žû¨ãÑ>¨ù׿k?LöO ã‡$•ƒV¼ o× ‡ý H zó zóŸ„Ç@Àò±EƒÆA“Íþ$~fþ³c9Ù;2n¥R‰ô‡ûd@l6ûæ›¶}¶çKµZ³óÞû½¼¼Ìf³Õj}ìÑGî¸íVÏëó¯ÿY©Tœ/ì—Hž}a7Òå~~¾jµÚd2Àâ… ‘âG¬X¶tïW_kµÚ?þå ôÊ_ûrbvóVƼôüýoÏî~Ñf³½òú¯¼þFxX˜L&3É…ôÔÔÝÏíòÜAãºu½½}üûc8xäÈÁ#Gø^^áaa­¶_" ð÷?~èêh÷ìü•d`à»Ê劇ÿô“Á íé饿Ln»åæ»ïºó*õØ E³Aòµ¯ã Á`ä¬Zå°­½ýâö›åááËöï§  VÃwÂ;ïÀ[oAV–û³DFDd/XPté’Z£9•{fýºµtÊo Žç:£¢¢èt:A-mm fSÒÊ5µuc9Àp8”fx4}ýýÔzh#ùÄÙN1õÓRçŠD³çÌ‹J¥*,*rùÁ¸ØXqLLÞÙ³&“éðÑcëÖ¬ ¹<#;+Ëf#ê¯\1™Lå•N÷õñY—“3:ÙF[µbùñ'¤Òéï¾g±X\G34ò¤ÓéË—ÞàdÑg2k׬>úã µZ­×ëÏäŸEC,:®ÓénK]mÀ§ŸRù:æÌá|õo(„ëh‚‚ƒ¤Réààž}û©ž³ýÆ­hn„Éd®]³úØ'TjµD"Aê™É`°9ƒÁ€nÝYóçSþ*r…¢±©¹¢² }k6[¨‘3F:±0Œ¤Ù³+ªªL&Ó÷‡±X,dâY˜µ edâ'½^þBÑù EÈÌï[)sÞ<§(«“«Þ¹sç´¶µ©5š†Æ¦Î®.___“É,—ËI’ ð÷”É&ñ¦Óh9«V>vL¥R©ÔjTN¿P ƒ¹~ló¶ÉE¨$ˆk· –´ýk, µç!Ršd¨W˜€5"òSóÿñ@Òƒv££Ec!Ì–wßÙ`–ïl“W9X~¼ãHé@±•°ðY‚’¼|kÅüàÉ†Ø öƒ)¿Ÿ!ЧŽÙ£ëFR~¶oÒ¾œož˜÷ž\?2õq¹ÜÑy x"þ~sß}Û·ÝH=&­V«——WÀ}¸Î¯¼¼ë©¿S>Á6›M*4™Ìt:}Nr²Sð“ßÜwï‚ù™Ô¿â¡Å£nÞ€Í7ìýìÓ/BI×{z{‘”ŒâϽÿÎ?'ºVò{ïùð_ïdÌKG–9^ߨÜÜ×ßïÅ㥧¥ilù ãoOüåµ—^LLH@s]]ÝHÊÏIIyç×ÿò§?Nq¥ïTZœ",,ôæ›¶ÅÅÆÒh4J(ÐétqLÌÍÛnŒˆ}ØÎå+ ¨†œ7àÒ%X´î¿:ÇÉŽ9'%9~æ ÔŠ.› ›ÅŠ'Š?›ÅŠŒˆ@VÒÞÞ¾Ÿö–+ßyûŽ…ÙY££!±X¬XqÌ–MW­X+Þ¸~›Å²Z­­ím£åÚ ‹­ËÉ éb$àóçgdÜtãÖ±â¬s¹ÜÍ7dÍŸ/Àb± ŽÇ`0bÅ1Û·Ý8cä¢[„P(¼ië–ôÔT*ŠŽJ­VÉd±yy·?õ÷€ý Iy’Á¸tÓöü??œà¾Ö¬Zb_ŽzŽ——Óaø! ·Ý¸5c^:•‹×j³éõz’$™LfTd„ãÄÔ¬™ñTè‚ t:=’ò<w~FFÎêU£§n2æ¥'Ξö- A,Ë)"$‹ÅºýÖ[RçÌArŸ’òAÖ­MOK–êår8›6n@¯ ÆÞÞ>™L&àóW,[zÓ[©ô…Ï÷Ú¶uËÜ9s¨ø9¨žrV¯Îr¸ïa0Ì4h]™L†ÌgΜY¼x1Z¾3.Z³êî÷6Ý~ãæAcç$ÎÀÚ÷ý¡Ï:,`‹®Í÷$ìÓõ]QÔ3hŒHï(±Pì2۔ʬª“×j-Ú0~Ø,Ÿ&Ý>w‘Û}záÕð޲ߕ°³N^+5Hm¤5ˆœìŸÂ¢» ¾&ÑK¤†…I¡1«lo±06RéáeÆ/öî#IrvBÂXÉt|÷½L. …·Ýr³Gm§Õ¶´¶é úÀ€À˜˜h¦Çq¬G£T*;:»Ô5Ëó÷÷  r¡™ ˆŽÎ.É€$2""4$ÄQ»y‹B§×·¶µ©T*.‡â&¥”‡hµÚÖ¶6•ZÍáp#ÂÃ܄ϔɺº»µZ­Ð[é3¶/Ó´àI‹Æf³©Tj­NÇår|}|FçÃj4ðì³ðöÛ@ Y,¸åøóŸ!5õçögµÙ4V«eÐ<Wäãã¤;e2YCcSvÖ7.OƒA¥R[¬@à#yE¥VkµZ›àq9¾~~þ•J¥^"á}ù¥èÓOéî.Dfæ¡59’˜Ï{Ž^¯W(•4M ¸Éí Ñh´ZÙbær8\Ï[ pù;5[,zÎd6›L&:Áç{[f³Y¡PX,V/¾—Ϩʾ{“¤F£Q©Õ,&K(z½t¢Õk6›J¥ÍfóóõåN<”+z½žÍfûúø¸É©ŒÁ`0?šßù¯;¶lœ´šßðÈç¿;rÍÔüqTó÷$ÞwÍΫÕjÑÊ­ð°0—Õ ‚¸ÒÐ>#—va~¦ŒÛâÓyØÆFxì1pЍ½f <ô¬_Ó 3=\¹Ÿ~ }ŽÑÃÃáŵ[·vvwO{ÏÁ`0ÌõÏäýæ\oºÅÇŸ;™¸¼ ‹HÀÅY¯=¨d€šv Nw_ó kñé ¹¹ðâ‹›kñäI8yaÇعÒÓq£üô(ðÕWðÅpqdˆ§ÐPøÃàá‡Ïà[ƒÁ`5?è4Æ-‹ïú:wÖ0™cž÷­K~E§1p`0?1+WÂÊ•PZ ¯½ l"•ÂÛoÃÛoCr2ÜtlÞŒeýOÀà = ±cà”&bölxì1¸óNpHkŠÁ`0˜ÿi5?ÑÔw,:;{ÆÊ¤ˆt ažhtÆfp¼9>,:ö Ä`®æÍƒýûa÷nøäسgxQlM ÔÔÀ®] ›6Á–-°t)VW—¦&8t‚ÂBpŠ›ÎáÀÆpÏ=°v-\µuØ ƒùYªù ŒÁòfûp™^9™d~tƒEg3Ø1ƒ¹ž‹áùçáÙgáÂسöí*ÔOW¼÷¼÷ðxž‹êU°d VöÓCo/ÂéÓpê´µ¹(˜¿úÜs EÁ`0 fJjž4ƒÍb`ã:ó‹ƒN‡Å‹añbxýu8~‚cÇ€Ê2f0@a!ÂË/Ÿ‹Á¢E™ ™™c¥+¸Àbª*(.†K— ?\fÈb0 ;6o†-[†ƒŠb0 3-jƒÁüòàæ›áæ›Áj…óçáÐ!8z‡ ètöU³±æÏ‡ÌLHO‡”,îG`6Õ+PY ÅÅP\ `4º.)ÂÊ•°il܈-ñ ƒ{„J‡“››ëy„Êÿ5úô}yݹ° 8Ë“®Ì/–Þ^È˃¼<Èχ–w%ƒ‚ 9!) ’’`öìÿ!}o2As3ÔÖBm-ÔÕAM 47ÛW5pZ²–-ƒeË`Þ<`à ƒÁjƒÁ\mºº  ._¶› †qÊ‹D ±±g߉…ðp˜¾d=×’‰:;¡¥Z[¡µÕ¾ÓÓ1ÎggÌ€ÌLÈÈ€… !#˜x¦ƒÁ`0SSóyyy ,Ä» fX­PScWö••P_?¼‚v\üý!4ÂÃ!$Äþ7, üýÁÏÏþ׳ôŸÓA€Lr9Èå “Áà twƒD]] ‘Ø÷-E§ƒX ))0ož}™6`0 fÕ<›Í®¬¬ ‹Å¸R0ÌT!Iho·»š ­© 4šIdzËz¤ì€Ã‘¸\àñ@$²Gld³Ïþ P h4Ã^.Vëðeèt`2R F#  RÉZ-¨Õvù®PLò‚ ˆŠö2BŽF^^¸_`0 æj©y&“)“Ézzz±³ ƒ¹* ØÝQ¨­­ úûÁlþ©€ˆˆpvŠŽŽÀ‹Á`0˜k¨æ€ÍfWTTx{{GFFÖô æ!‘@?twC?ôô@o/H$vÿd#wÊ„z-¡æüü BC!,̾…‡Ch(ޏÁ`0˜ëEÍÓét:^UUE£ÑÂÂÂÇUÀ`0×Ã}J¯§)4…‚®T‚ÑHÓéh:˜L4µšf4ÒL&šRi/j2ÑãÒ4°ÙH`x)ƒAz{£]’LJ‰€Ë%¹\R(‡äóI>Ÿôó#||H__˜`žl ƒÁ`~25=‹Å’J¥===*•Êê&žƒÁ`0 ƒ¹®Ô<‚Éd2 :ZR†Á`0 ƒÁ`®W\D8¶Z­Ø*Á`0 ƒÁ\ÿ`<ƒÁ`0 ósåÿ·R½sðÉébIEND®B`‚bugwarrior-1.8.0/bugwarrior/docs/services/pivotaltracker.rst000066400000000000000000000242121376471246600244460ustar00rootroot00000000000000Pivotal Tracker =============== You can import tasks from your Pivotal Tracker account using the ``pivotaltracker`` service name. Example Service --------------- Here's an example of a Pivotal Tracker target:: [my_issue_tracker] service = pivotaltracker pivotaltracker.user_d = 123456 pivotaltracker.account_ids = 123456 pivotaltracker.token = The above example is the minimum required to import stories from Pivotal Tracker. You can also feel free to use any of the configuration options described in :ref:`common_configuration_options` or described in `Service Features`_ below. .. describe:: pivotaltracker.user_id Your Pivotal Tracker user account. You can get your user_id by going to https://www.pivotaltracker.com/services/v5/me. It's the id field in the JSON response. .. describe:: pivotaltracker.token Pivotal Tracker offers API keys for accounts to access resources through their API. You will need to provide ``pivotaltracker.token`` to allow bugwarrior to pull stories. .. describe:: pivotaltracker.account_ids Pivotal Tracker account ids to specify which accounts to pull stories. .. describe:: pivotaltracker.version Pivotal Tracker api version to access. The options are ``v5`` and ``edge``. Default is ``v5``. .. describe:: pivotaltracker.host Pivotal Tracker host, default is ``https://www.pivotaltracker.com/services``. .. describe:: pivotaltracker.excude_projects The list of projects to exclude. If omitted, bugwarrior will use all projects the authenticated user is a member of. This must be the projects id, In your browser, navigate to the project you want to pull stories from and look at the URL, it should be something like https://www.pivotaltracker.com/n/projects/xxxxxxxx: copy the part after /b/projects/ in the pivotaltracker.exclude_projects field. .. describe:: pivotaltracker.excude_stories The list of stories to exclude. If omitted, bugwarrior will use all stories the authenticated user is assigned to. This must be the story id and Pivotal Tracker provides an easy way to get that id. Navigate in your browser to the story you wish to exlcude and copy the id next to 'ID' in the story. *Do not include the #* .. describe:: pivotaltracker.excude_tags If set, pull all stories except for stories with those tags listed. .. describe:: pivotaltracker.import_blockers A boolean that indicates whether to include blockers when listed in a story. .. describe:: pivotaltracker.blocker_template Template used to convert Pivotal Trcker story blockers to a template defined before being pushed to UDA. See :ref:`field_templates` for more details regarding how templates are processed. The default value is ``Description: {{description}} State: {{resolved}}\n``. .. describe:: pivotaltracker.import_labels_as_tags A boolean that indicates whether the Pivotal Tracker labels should be imported as tags in taskwarrior. (Defaults to false.) .. describe:: pivotaltracker.label_template Template used to convert Pivotal Tracker labels to taskwarrior tags. See :ref:`field_templates` for more details regarding how templates are processed. The default value is ``{{label|replace(' ', '_')}}``. .. describe:: pivotaltracker.annotation_template Template used to convert Pivotal Tracker story tasks to a template defined before being added as task annotations. See :ref:`field_templates` for more details regarding how templates are processed. The default value is ``status: {{complete}} - {{description}}``. .. note:: Using ``annotations_templates`` will break so do not use it. Service Features ---------------- Exclude Certain Projects ++++++++++++++++++++++++ If you happen to be working with a large number of projects, you may want to pull stories from only a subset of your projects. To do that, you can use the ``pivotaltracker.exclude_projects`` option. For example, if you have a particularly noisy project, you can instead choose to import all stories except for the project listed using the ``pivotaltracker.exclude_projects`` configuration option. In this example, ``noisy_project`` is the project you would *not* like stories created for:: pivotaltracker.exclude_projects = noisy_project Exclude Certain Stories +++++++++++++++++++++++ If you want bugwarrior to not track specific stories you can ignore those stories and ensure bugwarrior only tracks the stories you want. To do this, you need to set:: pivotaltracker.exclude_stories = 123456 For example, if you have stories #123 and #344, you do not wish to pull anymore you can add them like so:: pivotaltracker.exclude_stories = 123,344 Import Labels as Tags +++++++++++++++++++++ Pivotal Tracker allows you to attach labels to stories; to use those labels as tags, you can use the ``pivotaltracker.import_labels_as_tags`` option:: pivotaltracker.import_labels_as_tags = True Also, if you would like to control how these labels are created, you can specify a template used for converting the Pivotal Tracker label into a Taskwarrior tag. For example, to prefix all incoming labels with the string `pivotal_` (perhaps to differentiate them from any existing tags you might have), you could add the following configuration option:: pivotaltracker.label_template = pivotal_{{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. Get involved stories ++++++++++++++++++++ By default, stories from all projects assigned to ``pivotaltracker.user_id`` are tracked. To turn this off, set:: pivotaltracker.only_if_assigned = False Instead of fetching stories on ``pivotaltracker.user_id``'s assigned stories, you may instead get those that are not assigned to ``pivotaltracker.user_id``. This includes all stories in all projects the user has access to. To pull stories, use:: pivotaltracker.also_unassigned = True To only pull stories where ``{{user_id}}`` is the requestor of the story, use:: pivotaltracker.only_if_author = True Queries +++++++ Pivotal Traker provides a decent search feature in their API. If you want to write your own query, as described at https://www.pivotaltracker.com/help/articles/advanced_search/ you will need to use:: pivotaltracker.query = mywork:1234 .. note:: Search is limited by project and will be used in each project to determine what is pulled. To disable the pre-defined query described above and synchronize only the issues matched by a query, set:: pivotaltracker.query = .. note:: Setting a custom query will pull everything that is returned from the result. Be sure you are aware of what your query is doing before having burwarrior pull. Story Tasks +++++++++++ Pivotal Tracker provides the ability to add tasks to stories. Stories pulled in by bugwarrior will create an annotation for each "subtask" provided in the story. To turn this off, set:: pivotaltracker.annotation_comments = False Also, if you would like to control how these blockers are created, you can specify a template used for converting the story blocker into a more reasonable format. For example, the default template:: Completed: {{complete}} - {{description}} Which will result in the following output:: Completed: False - Do a thing and get rewarded. add the following configuration option:: pivotaltracker.annotation_template = {{description}} #{{id}} S{{complete}} In addition to the context variable listed above, you also have access to all fields on the Taskwarrior task and all fields of the blocking object as shown here https://www.pivotaltracker.com/help/api/rest/v5#Story_Tasks. Story Blocker +++++++++++++ Pivotal Tracker allows you assign blockers to stories. To include blockers in the stories pulled by bugwarrior, set:: pivotaltracker.import_blockers = True Also, if you would like to control how these blockers are created, you can specify a template used for converting the story blocker into a more reasonable format. For example, the default template:: Description: {{description}} Resolved: {{resolved}}\n Which will result in the following output:: Description: You cant do this stoy yet! Resovled: False add the following configuration option:: pivotaltracker.blocker_template = {{description}} #{{id}} S{{resolved}} In addition to the context variable listed above, you also have access to all fields on the Taskwarrior task and all fields of the blocking object as shown here https://www.pivotaltracker.com/help/api/rest/v5#Blockers. Provided UDA Fields ------------------- +----------------------------+-------------------+-----------------+ | Field Name | Description | Type | +============================+===================+=================+ | ``pivotaldescription`` | Story Description | Text (string) | +----------------------------+-------------------+-----------------+ | ``pivotalstorytype`` | Story Type | Text (string) | | | (story, issue)| | +----------------------------+-------------------+-----------------+ | ``pivotalrequesters`` | Story Requested By| Text (string) | +----------------------------+-------------------+-----------------+ | ``pivotalid`` | Story ID | Numeric | +----------------------------+-------------------+-----------------+ | ``pivotalestimate`` | Story Estimate | Text (string) | +----------------------------+-------------------+-----------------+ | ``pivotalblockers`` | Story Blockers | Text (string) | +----------------------------+-------------------+-----------------+ | ``pivotalcreated`` | Story Created | Date (date) | +----------------------------+-------------------+-----------------+ | ``pivotalupdated`` | Story Updated | Date (date) | +----------------------------+-------------------+-----------------+ | ``pivotalclosed`` | Story Closed | Date (date) | +----------------------------+-------------------+-----------------+bugwarrior-1.8.0/bugwarrior/docs/services/redmine.rst000066400000000000000000000025361376471246600230440ustar00rootroot00000000000000Redmine ======= You can import tasks from your Redmine instance using the ``redmine`` service name. Only first 100 issues are imported at the moment. Example Service --------------- Here's an example of a Redmine target:: [my_issue_tracker] service = redmine redmine.url = http://redmine.example.org/ redmine.key = c0c4c014cafebabe redmine.user_id = 7 redmine.project_name = redmine redmine.issue_limit = 100 You can also feel free to use any of the configuration options described in :ref:`common_configuration_options`. There are also `redmine.login`/`redmine.password` settings if your instance is behind basic auth. If you want to ignore verifying the SSL certificate, set:: redmine.verify_ssl = False Provided UDA Fields ------------------- +--------------------+--------------------+--------------------+ | Field Name | Description | Type | +====================+====================+====================+ | ``redmineid`` | ID | Text (string) | +--------------------+--------------------+--------------------+ | ``redminesubject`` | Subject | Text (string) | +--------------------+--------------------+--------------------+ | ``redmineurl`` | URL | Text (string) | +--------------------+--------------------+--------------------+ bugwarrior-1.8.0/bugwarrior/docs/services/taiga.rst000066400000000000000000000030601376471246600224770ustar00rootroot00000000000000Taiga ===== You can import tasks from a Taiga instance using the ``taiga`` service name. Example Service --------------- Here's an example of a taiga project:: [my_issue_tracker] service = taiga taiga.base_uri = http://taiga.fedorainfracloud.org taiga.auth_token = ayJ1c4VyX2F1dGhlbnQpY2F0aW9uX2lmIjo1fQ:2a2LPT:qscLbfQC_jyejQsICET5KgYNPLM The above example is the minimum required to import issues from Taiga. You can also feel free to use any of the configuration options described in :ref:`common_configuration_options`. Service Features ---------------- By default, userstories from taiga are added in taskwarrior. If you like to include taiga tasks as well, set the config option:: taiga.include_tasks = True Provided UDA Fields ------------------- +---------------------+---------------------+---------------------+ | Field Name | Description | Type | +=====================+=====================+=====================+ | ``taigaid`` | Issue ID | Text (string) | +---------------------+---------------------+---------------------+ | ``taigasummary`` | Summary | Text (string) | +---------------------+---------------------+---------------------+ | ``taigaurl`` | URL | Text (string) | +---------------------+---------------------+---------------------+ The Taiga service provides a limited set of UDAs. If you have need for some other values not present here, please file a request (there's lots of metadata in there that we could expose). bugwarrior-1.8.0/bugwarrior/docs/services/teamlab.rst000066400000000000000000000030031376471246600230140ustar00rootroot00000000000000Teamlab ======= You can import tasks from your Teamlab instance using the ``teamlab`` service name. Example Service --------------- Here's an example of a Teamlab target:: [my_issue_tracker] service = teamlab teamlab.hostname = teamlab.example.com teamlab.login = alice teamlab.password = secret teamlab.project_name = example_teamlab The above example is the minimum required to import issues from Teamlab. You can also feel free to use any of the configuration options described in :ref:`common_configuration_options`. Provided UDA Fields ------------------- +---------------------------+---------------------------+---------------------------+ | Field Name | Description | Type | +===========================+===========================+===========================+ | ``teamlabid`` | ID | Text (string) | +---------------------------+---------------------------+---------------------------+ | ``teamlabprojectownerid`` | ProjectOwner ID | Text (string) | +---------------------------+---------------------------+---------------------------+ | ``teamlabtitle`` | Title | Text (string) | +---------------------------+---------------------------+---------------------------+ | ``teamlaburl`` | URL | Text (string) | +---------------------------+---------------------------+---------------------------+ bugwarrior-1.8.0/bugwarrior/docs/services/teamwork_projects.rst000066400000000000000000000035041376471246600251570ustar00rootroot00000000000000Teamworks Project ================= You can import tasks from Teamwork Projects using the ``teamwork_projects`` service name. Example Service --------------- Here's an example of a Teamwork Projects target:: [my_issue_tracker] service = teamwork_projects teamwork_projects.token = teamwork_projects.host = https://test.teamwork_projects.com You can also feel free to use any of the configuration options described in :ref:`common_configuration_options`. Provided UDA Fields ------------------- +-------------------------------+--------------------------+---------------------------+ | Field Name | Description | Type | +===============================+==========================+===========================+ | ``teamwork_url`` | URL | Text (string) | +-------------------------------+--------------------------+---------------------------+ | ``teamwork_title`` | Title | Text (string) | +-------------------------------+--------------------------+---------------------------+ | ``teamwork_description_long`` | Desciption | Text (string) | +-------------------------------+--------------------------+---------------------------+ | ``teamwork_project_id`` | Project ID | Number (numeric) | +-------------------------------+--------------------------+---------------------------+ | ``teamwork_id`` | ID | Number (numeric) | +-------------------------------+--------------------------+---------------------------+ | ``teamwork_status`` | Open/Closed Status | Text (string | +-------------------------------+--------------------------+---------------------------+ bugwarrior-1.8.0/bugwarrior/docs/services/trac.rst000066400000000000000000000034011376471246600223420ustar00rootroot00000000000000Trac ==== You can import tasks from your Trac instance using the ``trac`` service name. Additional Dependencies ----------------------- Install packages needed for Trac support with:: pip install bugwarrior[trac] Example Service --------------- Here's an example of a Trac target:: [my_issue_tracker] service = trac trac.base_uri = fedorahosted.org/moksha trac.scheme = https trac.project_template = moksha.{{traccomponent|lower}} By default, this service uses the XML-RPC Trac plugin, which must be installed on the Trac instance. If this is not available, the service can use Trac's built-in CSV support, but in this mode it cannot add annotations based on ticket comments. To enable this mode, add ``trac.no_xmlrpc = true``. If your trac instance requires authentication to perform the query, add:: trac.username = ralph trac.password = OMG_LULZ The above example is the minimum required to import issues from Trac. You can also feel free to use any of the configuration options described in :ref:`common_configuration_options`. Service Features ---------------- Provided UDA Fields ------------------- +-------------------+-----------------+-----------------+ | Field Name | Description | Type | +===================+=================+=================+ | ``tracnumber`` | Number | Text (string) | +-------------------+-----------------+-----------------+ | ``tracsummary`` | Summary | Text (string) | +-------------------+-----------------+-----------------+ | ``tracurl`` | URL | Text (string) | +-------------------+-----------------+-----------------+ | ``traccomponent`` | Component | Text (string) | +-------------------+-----------------+-----------------+ bugwarrior-1.8.0/bugwarrior/docs/services/trello.rst000066400000000000000000000134061376471246600227200ustar00rootroot00000000000000Trello ====== You can import tasks from Trello cards using the ``trello`` service name. Options ------- .. describe:: trello.api_key Your Trello API key, available from https://trello.com/app-key .. describe:: trello.token Trello token, see below for how to get it. .. describe:: trello.include_boards The list of board to include. If omitted, bugwarrior will use all boards the authenticated user is a member of. This can be either the board ids of the board "short links". The latter is the easiest option as it is part of the board URL: in your browser, navigate to the board you want to pull cards from and look at the URL, it should be something like ``https://trello.com/b/xxxxxxxx/myboard``: copy the part between ``/b/`` and the next ``/`` in the ``trello.include_boards`` field. .. image:: pictures/trello_url.png :height: 1cm .. describe:: trello.include_lists If set, only pull cards from lists whose name is present in ``trello.include_lists``. .. describe:: trello.exclude_lists If set, skip cards from lists whose name is present in ``trello.exclude_lists``. .. describe:: trello.import_labels_as_tags A boolean that indicates whether the Trello labels should be imported as tags in taskwarrior. (Defaults to false.) .. describe:: trello.label_template Template used to convert Trello labels to taskwarrior tags. See :ref:`field_templates` for more details regarding how templates are processed. The default value is ``{{label|replace(' ', '_')}}``. Example Service --------------- Here's an example of a Trello target:: [my_project] service = trello trello.api_key = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx trello.token = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx The above example is the minimum required to import tasks from Trello. This will import every card from all the user's boards. Here's an example with more options:: [my_project] service = trello trello.api_key = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx trello.token = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx trello.include_boards = AaBbCcDd, WwXxYyZz trello.include_lists = Todo, Doing trello.exclude_lists = Done trello.only_if_assigned = someuser trello.import_labels_as_tags = true In this case, ``bugwarrior`` will only import cards from the specified boards if they belong to the right lists.. Feel free to use any of the configuration options described in :ref:`common_configuration_options` or described in `Service Features`_ below. .. HINT: Getting your API key and access token To get your API key, go to https://trello.com/app-key and copy the given key (this is your ``trello.api_key``). Next, go to https://trello.com/1/connect?key=TRELLO_API_KEY&name=bugwarrior&response_type=token&scope=read,write&expiration=never replacing ``TRELLO_API_KEY`` by the key you got on the last step. Copy the given toke (this is your ``trello.token``). Service Features ---------------- Include and Exclude Certain Lists +++++++++++++++++++++++++++++++++ You may want to pull cards from only a subset of the open lists in your board. To do that, you can use the ``trello.include_lists`` and ``trello.exclude_lists`` options. For example, if you would like to only pull-in cards from your ``Todo`` and ``Doing`` lists, you could add this line to your service configuration:: trello.include_lists = Todo, Doing Import Labels as Tags +++++++++++++++++++++ Trello allows you to attach labels to cards; to use those labels as tags, you can use the ``trello.import_labels_as_tags`` option:: trello.import_labels_as_tags = True Also, if you would like to control how these labels are created, you can specify a template used for converting the trello label into a Taskwarrior tag. For example, to prefix all incoming labels with the string 'trello_' (perhaps to differentiate them from any existing tags you might have), you could add the following configuration option:: trello.label_template = trello_{{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. The default value is ``{{label|upper|replace(' ', '_')}}``. Provided UDA Fields ------------------- +-----------------------+-----------------------+---------------------+ | Field Name | Description | Type | +=======================+=======================+=====================+ | ``trelloboard`` | Board name | Text (string) | +-----------------------+-----------------------+---------------------+ | ``trellocard`` | Card name | Text (string) | +-----------------------+-----------------------+---------------------+ | ``trellocardid`` | Card ID | Text (string) | +-----------------------+-----------------------+---------------------+ | ``trellolist`` | List name | Text (string) | +-----------------------+-----------------------+---------------------+ | ``trelloshortlink`` | Short Link | Text (string) | +-----------------------+-----------------------+---------------------+ | ``trelloshorturl`` | Short URL | Text (string) | +-----------------------+-----------------------+---------------------+ | ``trellourl`` | Full URL | Text (string) | +-----------------------+-----------------------+---------------------+ | ``trellocardidshort`` | Short card ID | Number (numeric) | +-----------------------+-----------------------+---------------------+ | ``trellodescription`` | Description | Text (string) | +-----------------------+-----------------------+---------------------+ bugwarrior-1.8.0/bugwarrior/docs/services/versionone.rst000066400000000000000000000114241376471246600236040ustar00rootroot00000000000000VersionOne ========== You can import tasks from VersionOne using the ``versionone`` service name. Additional Requirements ----------------------- Install the following package using ``pip``: * ``v1pysdk-unofficial`` Example Service --------------- Here's an example of a VersionOne project:: [my_issue_tracker] service = versionone versionone.base_uri = https://www3.v1host.com/MyVersionOneInstance/ versionone.usermame = somebody versionone.password = hunter5 The above example is the minimum required to import issues from VersionOne. 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:: This plugin does not infer a project name from any attribute of the version one Task or Story; it is recommended that you set the project name to use for imported tasks by either using the below `Set a Global Project Name`_ feature, or, if you require more flexibility, setting the ``project_template`` configuration option (see :ref:`field_templates`). Service Features ---------------- Restrict Task Imports to a Specific Timebox (Sprint) ++++++++++++++++++++++++++++++++++++++++++++++++++++ You can restrict imported tasks to a specific Timebox (VersionOne's internal generic name for a Sprint) -- in this example named 'Sprint 2014-09-22' -- by using the ``versionone.timebox_name`` option; for example:: versionone.timebox_name = Sprint 2014-09-22 Set a Global Project Name +++++++++++++++++++++++++ By default, this importer does not set a project name on imported tasks. Although you can gain more flexibility by using :ref:`field_templates` to generate a project name, if all you need is to set a predictable project name, you can use the ``versionone.project_name`` option; in this example, to add imported tasks to the project 'important_project':: versionone.project_name = important_project Set the Timezone Used for Due Dates +++++++++++++++++++++++++++++++++++ You can configure the timezone used for setting your tasks' due dates by setting the ``versionone.timezone`` option. By default, your local timezone will be used. For example:: versionone.timezone = America/Los_Angeles Provided UDA Fields ------------------- +-----------------------------------+-----------------------+---------------+ | Field Name | Description | Type | +===================================+=======================+===============+ | ``versiononetaskname`` | Task Name | Text (string) | +-----------------------------------+-----------------------+---------------+ | ``versiononetaskoid`` | Task Object ID | Text (string) | +-----------------------------------+-----------------------+---------------+ | ``versiononestoryoid`` | Story Object ID | Text (string) | +-----------------------------------+-----------------------+---------------+ | ``versiononestoryname`` | Story Name | Text (string) | +-----------------------------------+-----------------------+---------------+ | ``versiononetaskreference`` | Task Reference | Text (string) | +-----------------------------------+-----------------------+---------------+ | ``versiononetaskdetailestimate`` | Task Detail Estimate | Text (string) | +-----------------------------------+-----------------------+---------------+ | ``versiononetaskestimate`` | Task Estimate | Text (string) | +-----------------------------------+-----------------------+---------------+ | ``versiononetaskdescrption`` | Task Description | Text (string) | +-----------------------------------+-----------------------+---------------+ | ``versiononetasktodo`` | Task To Do | Text (string) | +-----------------------------------+-----------------------+---------------+ | ``versiononestorydetailestimate`` | Story Detail Estimate | Text (string) | +-----------------------------------+-----------------------+---------------+ | ``versiononestoryurl`` | Story URL | Text (string) | +-----------------------------------+-----------------------+---------------+ | ``versiononetaskurl`` | Task URL | Text (string) | +-----------------------------------+-----------------------+---------------+ | ``versiononestoryestimate`` | Story Estimate | Text (string) | +-----------------------------------+-----------------------+---------------+ | ``versiononestorynumber`` | Story Number | Text (string) | +-----------------------------------+-----------------------+---------------+ | ``versiononestorydescription`` | Story Description | Text (string) | +-----------------------------------+-----------------------+---------------+ bugwarrior-1.8.0/bugwarrior/docs/services/youtrack.rst000066400000000000000000000065371376471246600232670ustar00rootroot00000000000000YouTrack ======== You can import tasks from your YouTrack instance using the ``youtrack`` service name. Example Service --------------- Here's an example of a YouTrack target:: [my_issue_tracker] service = youtrack youtrack.host = youtrack.example.com youtrack.login = turing youtrack.password = 3n1Gm@ The above example is the minimum required to import issues from YouTrack. You can also feel free to use any of the configuration options described in :ref:`common_configuration_options` or described in `Service Features`_ below. Service Features ---------------- Customize the YouTrack Connection +++++++++++++++++++++++++++++++++ The ``youtrack.host`` field is used to construct a URL for the YouTrack server. It defaults to a secure connection scheme (HTTPS) on the standard port (443). To connect on a different port, set:: youtrack.port = 8443 If your YouTrack instance is only available over HTTP, set:: youtrack.use_https = False If you want to ignore verifying the SSL certificate, set:: youtrack.verify_ssl = False For YouTrack InCloud instances set:: youtrack.incloud_instance = True Specify the Query to Use for Gathering Issues +++++++++++++++++++++++++++++++++++++++++++++ The default option selects unresolved issues assigned to the login user:: youtrack.query = for:me #Unresolved Reference the `YouTrack Search Query Grammar `_ for additional examples. Queries are capped at 100 max results by default, but may be adjusted to meet your needs:: youtrack.query_limit = 100 Import Issue Tags +++++++++++++++++ The YouTrack issue tracker allows you to tag issues. To apply these tags in Taskwarrior, set:: youtrack.import_tags = True If you would like to control how these tags are formatted, you can specify a template used for converting the YouTrack tag into a Taskwarrior tag. For example, to prefix all incoming tags with the string 'yt\_' (perhaps to differentiate them from any existing tags you might have), you could add the following configuration option:: youtrack.tag_template = yt_{{tag|lower}} In addition to the context variable ``{{tag}}``, 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 | +===========================+======================+=====================+ | ``youtrackissue`` | PROJECT-ISSUE# | Text (string) | +---------------------------+----------------------+---------------------+ | ``youtracksummary`` | Summary | Text (string) | +---------------------------+----------------------+---------------------+ | ``youtrackurl`` | URL | Text (string) | +---------------------------+----------------------+---------------------+ | ``youtrackproject`` | Project short name | Text (string) | +---------------------------+----------------------+---------------------+ | ``youtracknumber`` | Project issue number | Numeric | +---------------------------+----------------------+---------------------+ bugwarrior-1.8.0/bugwarrior/docs/using.rst000066400000000000000000000044251376471246600207220ustar00rootroot00000000000000How to use ========== Just run ``bugwarrior-pull``. Cron ---- It's ideal to create a cron task like:: */15 * * * * /usr/bin/bugwarrior-pull Bugwarrior can emit desktop notifications when it adds or completes issues to and from your local ``~/.task/`` db. If your ``bugwarriorrc`` file has notifications turned on, you'll also need to tell cron which display to use by adding the following to your crontab:: DISPLAY=:0 */15 * * * * /usr/bin/bugwarrior-pull systemd timer ------------- If you would prefer to use a systemd timer to run ``bugwarrior-pull`` on a schedule, you can create the following two files:: $ cat ~/.config/systemd/user/bugwarrior-pull.service [Unit] Description=bugwarrior-pull [Service] Environment="DISPLAY=:0" ExecStart=/usr/bin/bugwarrior-pull Type=oneshot [Install] WantedBy=default.target $ cat ~/.config/systemd/user/bugwarrior-pull.timer [Unit] Description=Run bugwarrior-pull hourly and on boot [Timer] OnBootSec=15min OnUnitActiveSec=1h [Install] WantedBy=timers.target Once those files are in place, you can start and enable the timer:: $ systemctl --user enable bugwarrior-pull.timer $ systemctl --user start bugwarrior-pull.timer Exporting a list of UDAs ------------------------ Most services define a set of UDAs in which bugwarrior store extra information about the incoming ticket. Usually, this includes things like the title of the ticket and its URL, but some services provide an extensive amount of metadata. See each service's documentation for more information. For using this data in reports, it is recommended that you add these UDA definitions to your ``taskrc`` file. You can generate your list of UDA definitions by running the following command:: bugwarrior-uda You can add those lines verbatim to your ``taskrc`` file if you would like Taskwarrior to know the human-readable name and data type for the defined UDAs. .. note:: Not adding those lines to your ``taskrc`` file will have no negative effects aside from Taskwarrior not knowing the human-readable name for the field, but depending on what version of Taskwarrior you are using, it may prevent you from changing the values of those fields or using them in filter expressions. bugwarrior-1.8.0/bugwarrior/notifications.py000066400000000000000000000071311376471246600213330ustar00rootroot00000000000000from future import standard_library standard_library.install_aliases() import datetime import os import urllib.request, urllib.parse, urllib.error import warnings from bugwarrior.config import asbool cache_dir = os.path.expanduser(os.getenv('XDG_CACHE_HOME', "~/.cache") + "/bugwarrior") logo_path = cache_dir + "/logo.png" logo_url = "https://upload.wikimedia.org/wikipedia/" + \ "en/5/59/Taskwarrior_logo.png" def _cache_logo(): if os.path.exists(logo_path): return if not os.path.isdir(cache_dir): os.makedirs(cache_dir) urllib.request.urlretrieve(logo_url, logo_path) def _get_metadata(issue): due = '' tags = '' priority = '' metadata = '' project = '' if 'project' in issue: project = "Project: " + issue['project'] # if 'due' in issue: # due = "Due: " + datetime.datetime.fromtimestamp( # int(issue['due'])).strftime('%Y-%m-%d') if 'tags' in issue: tags = "Tags: " + ', '.join(issue['tags']) if 'priority' in issue: priority = "Priority: " + issue['priority'] if project != '': metadata += "\n" + project if priority != '': metadata += "\n" + priority if due != '': metadata += "\n" + due if tags != '': metadata += "\n" + tags return metadata def send_notification(issue, op, conf): notify_backend = conf.get('notifications', 'backend') if notify_backend == 'pynotify': warnings.warn("pynotify is deprecated. Use backend=gobject. " "See https://github.com/ralphbean/bugwarrior/issues/336") notify_backend = 'gobject' # Notifications for growlnotify on Mac OS X if notify_backend == 'growlnotify': import gntp.notifier growl = gntp.notifier.GrowlNotifier( applicationName="Bugwarrior", notifications=["New Updates", "New Messages"], defaultNotifications=["New Messages"], ) growl.register() if op == 'bw_finished': growl.notify( noteType="New Messages", title="Bugwarrior", description="Finished querying for new issues.\n%s" % issue['description'], sticky=asbool(conf.get( 'notifications', 'finished_querying_sticky', 'True')), icon="https://upload.wikimedia.org/wikipedia/" "en/5/59/Taskwarrior_logo.png", priority=1, ) return message = "%s task: %s" % (op, issue['description']) metadata = _get_metadata(issue) if metadata is not None: message += metadata growl.notify( noteType="New Messages", title="Bugwarrior", description=message, sticky=asbool(conf.get( 'notifications', 'task_crud_sticky', 'True')), icon="https://upload.wikimedia.org/wikipedia/" "en/5/59/Taskwarrior_logo.png", priority=1, ) return elif notify_backend == 'gobject': _cache_logo() import gi gi.require_version('Notify', '0.7') from gi.repository import Notify Notify.init("bugwarrior") if op == 'bw finished': message = "Finished querying for new issues.\n%s" %\ issue['description'] else: message = "%s task: %s" % (op, issue['description']) metadata = _get_metadata(issue) if metadata is not None: message += metadata Notify.Notification.new("Bugwarrior", message, logo_path).show() bugwarrior-1.8.0/bugwarrior/services/000077500000000000000000000000001376471246600177315ustar00rootroot00000000000000bugwarrior-1.8.0/bugwarrior/services/__init__.py000066400000000000000000000462521376471246600220530ustar00rootroot00000000000000from __future__ import unicode_literals from builtins import str from builtins import object import copy import multiprocessing import time from pkg_resources import iter_entry_points from dateutil.parser import parse as parse_date from dateutil.tz import tzlocal from jinja2 import Template import pytz import six from taskw.task import Task from bugwarrior.config import asbool, asint, aslist, die, get_service_password, ServiceConfig from bugwarrior.db import MARKUP, URLShortener import logging log = logging.getLogger(__name__) # Sentinels for process completion status SERVICE_FINISHED_OK = 0 SERVICE_FINISHED_ERROR = 1 # Used by `parse_date` as a timezone when you would like a naive # date string to be parsed as if it were in your local timezone LOCAL_TIMEZONE = 'LOCAL_TIMEZONE' def get_service(service_name): epoint = iter_entry_points(group='bugwarrior.service', name=service_name) try: epoint = next(epoint) except StopIteration: return None return epoint.load() class IssueService(object): """ Abstract base class for each service """ # Which class should this service instantiate for holding these issues? ISSUE_CLASS = None # What prefix should we use for this service's configuration values CONFIG_PREFIX = '' def __init__(self, main_config, main_section, target): self.config = ServiceConfig(self.CONFIG_PREFIX, main_config, target) self.main_section = main_section self.main_config = main_config self.target = target self.desc_len = self._get_config_or_default('description_length', 35, asint); self.anno_len = self._get_config_or_default('annotation_length', 45, asint); self.inline_links = self._get_config_or_default('inline_links', True, asbool); self.annotation_links = self._get_config_or_default('annotation_links', not self.inline_links, asbool) self.annotation_comments = self._get_config_or_default('annotation_comments', True, asbool) self.annotation_newlines = self._get_config_or_default('annotation_newlines', False, asbool) self.shorten = self._get_config_or_default('shorten', False, asbool) self.default_priority = self.config.get('default_priority', 'M') self.add_tags = [] for raw_option in aslist(self.config.get('add_tags', '')): option = raw_option.strip(' +;') if option: self.add_tags.append(option) log.info("Working on [%s]", self.target) def _get_config_or_default(self, key, default, as_type=lambda x: x): """Return a main config value, or default if it does not exist.""" if self.main_config.has_option(self.main_section, key): return as_type(self.main_config.get(self.main_section, key)) return default def get_templates(self): """ Get any defined templates for configuration values. Users can override the value of any Taskwarrior field using this feature on a per-key basis. The key should be the name of the field to you would like to configure the value of, followed by '_template', and the value should be a Jinja template generating the field's value. As context variables, all fields on the taskwarrior record are available. For example, to prefix the returned project name for tickets returned by a service with 'workproject_', you could add an entry reading: project_template = workproject_{{project}} Or, if you'd simply like to override the returned project name for all tickets incoming from a specific service, you could add an entry like: project_template = myprojectname The above would cause all issues to recieve a project name of 'myprojectname', regardless of what the project name of the generated issue was. """ templates = {} for key in six.iterkeys(Task.FIELDS): template_key = '%s_template' % key if template_key in self.config: templates[key] = self.config.get(template_key) return templates def get_password(self, key, login='nousername'): password = self.config.get(key) keyring_service = self.get_keyring_service(self.config) if not password or password.startswith("@oracle:"): password = get_service_password( keyring_service, login, oracle=password, interactive=self.config.interactive) return password def get_service_metadata(self): return {} def get_issue_for_record(self, record, extra=None): origin = { 'annotation_length': self.anno_len, 'default_priority': self.default_priority, 'description_length': self.desc_len, 'templates': self.get_templates(), 'target': self.target, 'shorten': self.shorten, 'inline_links': self.inline_links, 'add_tags': self.add_tags, } origin.update(self.get_service_metadata()) return self.ISSUE_CLASS(record, origin=origin, extra=extra) def build_annotations(self, annotations, url): final = [] if self.annotation_links: final.append(url) if self.annotation_comments: for author, message in annotations: message = message.strip() if not message or not author: continue if not self.annotation_newlines: message = message.replace('\n', '').replace('\r', '') if self.anno_len: message = '%s%s' % ( message[:self.anno_len], '...' if len(message) > self.anno_len else '' ) final.append('@%s - %s' % (author, message)) return final @classmethod def validate_config(cls, service_config, target): """ Validate generic options for a particular target """ if service_config.has_option(target, 'only_if_assigned'): die("[%s] has an 'only_if_assigned' option. Should be " "'%s.only_if_assigned'." % (target, cls.CONFIG_PREFIX)) if service_config.has_option(target, 'also_unassigned'): die("[%s] has an 'also_unassigned' option. Should be " "'%s.also_unassigned'." % (target, cls.CONFIG_PREFIX)) if service_config.has_option(target, 'default_priority'): die("[%s] has a 'default_priority' option. Should be " "'%s.default_priority'." % (target, cls.CONFIG_PREFIX)) if service_config.has_option(target, 'add_tags'): die("[%s] has an 'add_tags' option. Should be " "'%s.add_tags'." % (target, cls.CONFIG_PREFIX)) def include(self, issue): """ Return true if the issue in question should be included """ only_if_assigned = self.config.get('only_if_assigned', None) if only_if_assigned: owner = self.get_owner(issue) include_owners = [only_if_assigned] if self.config.get('also_unassigned', None, asbool): include_owners.append(None) return owner in include_owners only_if_author = self.config.get('only_if_author', None) if only_if_author: return self.get_author(issue) == only_if_author return True def get_owner(self, issue): """ Override this for filtering on tickets """ raise NotImplementedError() def get_author(self, issue): """ Override this for filtering on tickets """ raise NotImplementedError() def issues(self): """ Returns a list of dicts representing issues from a remote service. This is the main place to begin if you are implementing a new service for bugwarrior. Override this to gather issues for each service. Each item in the list should be a dict that looks something like this: { "description": "Some description of the issue", "project": "some_project", "priority": "H", "annotations": [ "This is an annotation", "This is another annotation", ] } The description can be 'anything' but must be consistent and unique for issues you're pulling from a remote service. You can and should use the ``.description(...)`` method to help format your descriptions. The project should be a string and may be anything you like. The priority should be one of "H", "M", or "L". """ raise NotImplementedError() @staticmethod def get_keyring_service(service_config): """ Given the keyring service name for this service. """ raise NotImplementedError @six.python_2_unicode_compatible class Issue(object): # Set to a dictionary mapping UDA short names with type and long name. # # Example:: # # { # 'project_id': { # 'type': 'string', # 'label': 'Project ID', # }, # 'ticket_number': { # 'type': 'number', # 'label': 'Ticket Number', # }, # } # # Note: For best results, dictionary keys should be unique! UDAS = {} # Should be a tuple of field names (can be UDA names) that are usable for # uniquely identifying an issue in the foreign system. UNIQUE_KEY = [] # Should be a dictionary of value-to-level mappings between the foreign # system and the string values 'H', 'M' or 'L'. PRIORITY_MAP = {} def __init__(self, foreign_record, origin=None, extra=None): self._foreign_record = foreign_record self._origin = origin if origin else {} self._extra = extra if extra else {} def update_extra(self, extra): self._extra.update(extra) def to_taskwarrior(self): """ Transform a foreign record into a taskwarrior dictionary.""" raise NotImplementedError() def get_default_description(self): """ Return the old-style verbose description from bugwarrior. This is useful for two purposes: * Finding and linking historically-created records. * Allowing people to keep using the historical description for taskwarrior. """ raise NotImplementedError() def get_added_tags(self): added_tags = [] for tag in self.origin['add_tags']: tag = Template(tag).render(self.get_template_context()) if tag: added_tags.append(tag) return added_tags def get_taskwarrior_record(self, refined=True): if not getattr(self, '_taskwarrior_record', None): self._taskwarrior_record = self.to_taskwarrior() record = copy.deepcopy(self._taskwarrior_record) if refined: record = self.refine_record(record) if not 'tags' in record: record['tags'] = [] if refined: record['tags'].extend(self.get_added_tags()) return record def get_priority(self): return self.PRIORITY_MAP.get( self.record.get('priority'), self.origin['default_priority'] ) def get_processed_url(self, url): """ Returns a URL with conditional processing. If the following config key are set: - [general]shorten returns a shortened URL; otherwise returns the URL unaltered. """ if self.origin['shorten']: return URLShortener().shorten(url) return url def parse_date(self, date, timezone='UTC'): """ Parse a date string into a datetime object. :param `date`: A time string parseable by `dateutil.parser.parse` :param `timezone`: The string timezone name (from `pytz.all_timezones`) to use as a default should the parsed time string not include timezone information. """ if date: date = parse_date(date) if not date.tzinfo: if timezone == LOCAL_TIMEZONE: tzinfo = tzlocal() else: tzinfo = pytz.timezone(timezone) date = date.replace(tzinfo=tzinfo) return date return None def build_default_description( self, title='', url='', number='', cls="issue" ): cls_markup = { 'issue': 'Is', 'pull_request': 'PR', 'merge_request': 'MR', 'todo': '', 'task': '', 'subtask': 'Subtask #', 'feature': 'Feature', 'bug': 'Bug', 'story': 'Story', 'release': 'Release', 'chore': 'Chore', } url_separator = ' .. ' url = url if self.origin['inline_links'] else '' desc_len = self.origin['description_length'] return u"%s%s#%s - %s%s%s" % ( MARKUP, cls_markup[cls], number, title[:desc_len] if desc_len else title, url_separator if url else '', url, ) def _get_unique_identifier(self): record = self.get_taskwarrior_record() return dict([ (key, record[key],) for key in self.UNIQUE_KEY ]) def get_template_context(self): context = ( self.get_taskwarrior_record(refined=False).copy() ) context.update(self.extra) context.update({ 'description': self.get_default_description(), }) return context def refine_record(self, record): for field in six.iterkeys(Task.FIELDS): if field in self.origin['templates']: template = Template(self.origin['templates'][field]) record[field] = template.render(self.get_template_context()) elif hasattr(self, 'get_default_%s' % field): record[field] = getattr(self, 'get_default_%s' % field)() return record def __iter__(self): record = self.get_taskwarrior_record() for key in six.iterkeys(record): yield key def keys(self): return list(self.__iter__()) def iterkeys(self): return self.__iter__() def items(self): record = self.get_taskwarrior_record() return list(six.iteritems(record)) def iteritems(self): record = self.get_taskwarrior_record() for item in six.iteritems(record): yield item def update(self, *args): raise AttributeError( "You cannot set attributes on issues." ) def get(self, attribute, default=None): try: return self[attribute] except KeyError: return default def __getitem__(self, attribute): record = self.get_taskwarrior_record() return record[attribute] def __setitem__(self, attribute, value): raise AttributeError( "You cannot set attributes on issues." ) def __delitem__(self, attribute): raise AttributeError( "You cannot delete attributes from issues." ) @property def record(self): return self._foreign_record @property def extra(self): return self._extra @property def origin(self): return self._origin def __str__(self): return '%s: %s' % ( self.origin['target'], self.get_taskwarrior_record()['description'] ) def __repr__(self): return '<%s>' % str(self) class ServiceClient(object): """ Abstract class responsible for making requests to service API's. """ @staticmethod def json_response(response): # If we didn't get good results, just bail. if response.status_code != 200: raise IOError( "Non-200 status code %r; %r; %r" % ( response.status_code, response.url, response.text, )) if callable(response.json): # Newer python-requests return response.json() else: # Older python-requests return response.json def _aggregate_issues(conf, main_section, target, queue, service_name): """ This worker function is separated out from the main :func:`aggregate_issues` func only so that we can use multiprocessing on it for speed reasons. """ start = time.time() try: service = get_service(service_name)(conf, main_section, target) issue_count = 0 for issue in service.issues(): queue.put(issue) issue_count += 1 except SystemExit as e: log.critical(str(e)) queue.put((SERVICE_FINISHED_ERROR, (target, e))) except BaseException as e: if hasattr(e, 'request') and e.request: # Exceptions raised by requests library have the HTTP request # object stored as attribute. The request can have hooks attached # to it, and we need to remove them, as there can be unpickleable # methods. There is no one left to call these hooks anyway. e.request.hooks = {} log.exception("Worker for [%s] failed: %s" % (target, e)) queue.put((SERVICE_FINISHED_ERROR, (target, e))) else: queue.put((SERVICE_FINISHED_OK, (target, issue_count, ))) finally: duration = time.time() - start log.info("Done with [%s] in %fs" % (target, duration)) def aggregate_issues(conf, main_section, debug): """ Return all issues from every target. """ log.info("Starting to aggregate remote issues.") # Create and call service objects for every target in the config targets = aslist(conf.get(main_section, 'targets')) queue = multiprocessing.Queue() log.info("Spawning %i workers." % len(targets)) processes = [] if debug: for target in targets: _aggregate_issues( conf, main_section, target, queue, conf.get(target, 'service') ) else: for target in targets: proc = multiprocessing.Process( target=_aggregate_issues, args=(conf, main_section, target, queue, conf.get(target, 'service')) ) proc.start() processes.append(proc) # Sleep for 1 second here to try and avoid a race condition where # all N workers start up and ask the gpg-agent process for # information at the same time. This causes gpg-agent to fumble # and tell some of our workers some incomplete things. time.sleep(1) currently_running = len(targets) while currently_running > 0: issue = queue.get(True) if isinstance(issue, tuple): completion_type, args = issue if completion_type == SERVICE_FINISHED_ERROR: target, e = args log.info("Terminating workers") for process in processes: process.terminate() raise RuntimeError( "critical error in target '{}'".format(target)) currently_running -= 1 continue yield issue log.info("Done aggregating remote issues.") bugwarrior-1.8.0/bugwarrior/services/activecollab.py000066400000000000000000000205071376471246600227370ustar00rootroot00000000000000from builtins import object import re import pypandoc from pyac.library import activeCollab from bugwarrior.services import IssueService, Issue from bugwarrior.config import die import logging log = logging.getLogger(__name__) class ActiveCollabClient(object): def __init__(self, url, key, user_id): self.url = url self.key = key self.user_id = int(user_id) self.activecollabtivecollab = activeCollab( key=key, url=url, user_id=user_id ) class ActiveCollabIssue(Issue): BODY = 'acbody' NAME = 'acname' PERMALINK = 'acpermalink' TASK_ID = 'actaskid' FOREIGN_ID = 'acid' PROJECT_ID = 'acprojectid' PROJECT_NAME = 'acprojectname' TYPE = 'actype' CREATED_ON = 'accreatedon' CREATED_BY_NAME = 'accreatedbyname' ESTIMATED_TIME = 'acestimatedtime' TRACKED_TIME = 'actrackedtime' MILESTONE = 'acmilestone' LABEL = 'aclabel' UDAS = { BODY: { 'type': 'string', 'label': 'ActiveCollab Body' }, NAME: { 'type': 'string', 'label': 'ActiveCollab Name' }, PERMALINK: { 'type': 'string', 'label': 'ActiveCollab Permalink' }, TASK_ID: { 'type': 'numeric', 'label': 'ActiveCollab Task ID' }, FOREIGN_ID: { 'type': 'numeric', 'label': 'ActiveCollab ID', }, PROJECT_ID: { 'type': 'numeric', 'label': 'ActiveCollab Project ID' }, PROJECT_NAME: { 'type': 'string', 'label': 'ActiveCollab Project Name' }, TYPE: { 'type': 'string', 'label': 'ActiveCollab Task Type' }, CREATED_ON: { 'type': 'date', 'label': 'ActiveCollab Created On' }, CREATED_BY_NAME: { 'type': 'string', 'label': 'ActiveCollab Created By' }, ESTIMATED_TIME: { 'type': 'numeric', 'label': 'ActiveCollab Estimated Time' }, TRACKED_TIME: { 'type': 'numeric', 'label': 'ActiveCollab Tracked Time' }, MILESTONE: { 'type': 'string', 'label': 'ActiveCollab Milestone' }, LABEL: { 'type': 'string', 'label': 'ActiveCollab Label' } } UNIQUE_KEY = (FOREIGN_ID, ) def to_taskwarrior(self): record = { 'project': re.sub(r'\W+', '-', self.record['project']).lower(), 'priority': self.get_priority(), 'annotations': self.extra.get('annotations', []), self.NAME: self.record.get('name', ''), self.BODY: pypandoc.convert_text(self.record.get('body'), 'md', format='html').rstrip(), self.PERMALINK: self.record['permalink'], self.TASK_ID: int(self.record.get('task_id')), self.PROJECT_NAME: self.record['project'], self.PROJECT_ID: int(self.record['project_id']), self.FOREIGN_ID: int(self.record['id']), self.TYPE: self.record.get('type', 'subtask').lower(), self.CREATED_BY_NAME: self.record['created_by_name'], self.MILESTONE: self.record['milestone'], self.ESTIMATED_TIME: self.record.get('estimated_time', 0), self.TRACKED_TIME: self.record.get('tracked_time', 0), self.LABEL: self.record.get('label'), } if self.TYPE == 'subtask': # Store the parent task ID for subtasks record['actaskid'] = int(self.record['task_id']) if isinstance(self.record.get('due_on'), dict): record['due'] = self.parse_date( self.record.get('due_on')['formatted_date'] ) if isinstance(self.record.get('created_on'), dict): record[self.CREATED_ON] = self.parse_date( self.record.get('created_on')['formatted_date'] ) return record def get_annotations(self): return self.extra.get('annotations', []) def get_priority(self): value = self.record.get('priority') if value > 0: return 'H' elif value < 0: return 'L' else: return 'M' def get_default_description(self): return self.build_default_description( title=( self.record.get('name') if self.record.get('name') else self.record.get('body') ), url=self.get_processed_url(self.record['permalink']), number=self.record['id'], cls=self.record.get('type', 'subtask').lower(), ) class ActiveCollabService(IssueService): ISSUE_CLASS = ActiveCollabIssue CONFIG_PREFIX = 'activecollab' def __init__(self, *args, **kw): super(ActiveCollabService, self).__init__(*args, **kw) self.url = self.config.get('url').rstrip('/') self.key = self.config.get('key') self.user_id = int(self.config.get('user_id')) self.client = ActiveCollabClient( self.url, self.key, self.user_id ) self.activecollab = activeCollab(url=self.url, key=self.key, user_id=self.user_id) @classmethod def validate_config(cls, service_config, target): for k in ('url', 'key', 'user_id'): if k not in service_config: die("[%s] has no 'activecollab.%s'" % (target, k)) IssueService.validate_config(service_config, target) def _comments(self, issue): comments = self.activecollab.get_comments( issue['project_id'], issue['task_id'] ) comments_formatted = [] if comments is not None: for comment in comments: comments_formatted.append( dict(user=comment['created_by']['display_name'], body=comment['body'])) return comments_formatted def get_owner(self, issue): if issue['assignee_id']: return issue['assignee_id'] def annotations(self, issue, issue_obj): if 'type' not in issue: # Subtask return [] comments = self._comments(issue) if comments is None: return [] return self.build_annotations( (( c['user'], pypandoc.convert_text(c['body'], 'md', format='html').rstrip() ) for c in comments), issue_obj.get_processed_url(issue_obj.record['permalink']), ) def issues(self): data = self.activecollab.get_my_tasks() label_data = self.activecollab.get_assignment_labels() labels = dict() for item in label_data: labels[item['id']] = re.sub(r'\W+', '_', item['name']) task_count = 0 issues = [] for key, record in data.items(): for task_id, task in record['assignments'].items(): task_count = task_count + 1 # Add tasks if task['assignee_id'] == self.user_id: task['label'] = labels.get(task['label_id']) issues.append(task) if 'subtasks' in task: for subtask_id, subtask in task['subtasks'].items(): # Add subtasks task_count = task_count + 1 if subtask['assignee_id'] is self.user_id: # Add some data from the parent task subtask['label'] = labels.get(subtask['label_id']) subtask['project_id'] = task['project_id'] subtask['project'] = task['project'] subtask['task_id'] = task['task_id'] subtask['milestone'] = task['milestone'] issues.append(subtask) log.debug(" Found %i total", task_count) log.debug(" Pruned down to %i", len(issues)) for issue in issues: issue_obj = self.get_issue_for_record(issue) extra = { 'annotations': self.annotations(issue, issue_obj) } issue_obj.update_extra(extra) yield issue_obj bugwarrior-1.8.0/bugwarrior/services/activecollab2.py000066400000000000000000000153061376471246600230220ustar00rootroot00000000000000import itertools import time import six import requests from bugwarrior.services import IssueService, Issue, ServiceClient from bugwarrior.config import die import logging log = logging.getLogger(__name__) class ActiveCollab2Client(ServiceClient): def __init__(self, url, key, user_id, projects, target): self.url = url self.key = key self.user_id = user_id self.projects = projects self.target = target def get_task_dict(self, project, key, task): assigned_task = { 'project': project } if task[u'type'] == 'Ticket': # Load Ticket data # @todo Implement threading here. ticket_data = self.call_api( "/projects/" + six.text_type(task[u'project_id']) + "/tickets/" + six.text_type(task[u'ticket_id'])) assignees = ticket_data[u'assignees'] for assignee in assignees: if ( (assignee[u'is_owner'] is True) and (assignee[u'user_id'] == int(self.user_id)) ): assigned_task.update(ticket_data) return assigned_task elif task[u'type'] == 'Task': # Load Task data assigned_task.update(task) return assigned_task def get_issue_generator(self, user_id, project_id, project_name): """ Approach: 1. Get user ID from bugwarriorrc file 2. Get list of tickets from /user-tasks for a given project 3. For each ticket/task returned from #2, get ticket/task info and check if logged-in user is primary (look at `is_owner` and `user_id`) """ user_tasks_data = self.call_api( "/projects/" + six.text_type(project_id) + "/user-tasks") for key, task in enumerate(user_tasks_data): assigned_task = self.get_task_dict(project_id, key, task) if assigned_task: log.debug( " Adding '" + assigned_task['description'] + "' to task list.") yield assigned_task def call_api(self, uri): url = self.url.rstrip("/") params = { 'token': self.key, 'path_info': uri, 'format': 'json'} return self.json_response(requests.get(url, params=params)) class ActiveCollab2Issue(Issue): BODY = 'ac2body' NAME = 'ac2name' PERMALINK = 'ac2permalink' TICKET_ID = 'ac2ticketid' PROJECT_ID = 'ac2projectid' TYPE = 'ac2type' CREATED_ON = 'ac2createdon' CREATED_BY_ID = 'ac2createdbyid' UDAS = { BODY: { 'type': 'string', 'label': 'ActiveCollab2 Body' }, NAME: { 'type': 'string', 'label': 'ActiveCollab2 Name' }, PERMALINK: { 'type': 'string', 'label': 'ActiveCollab2 Permalink' }, TICKET_ID: { 'type': 'string', 'label': 'ActiveCollab2 Ticket ID' }, PROJECT_ID: { 'type': 'string', 'label': 'ActiveCollab2 Project ID' }, TYPE: { 'type': 'string', 'label': 'ActiveCollab2 Task Type' }, CREATED_ON: { 'type': 'date', 'label': 'ActiveCollab2 Created On' }, CREATED_BY_ID: { 'type': 'string', 'label': 'ActiveCollab2 Created By' }, } UNIQUE_KEY = (PERMALINK, ) PRIORITY_MAP = { -2: 'L', -1: 'L', 0: 'M', 1: 'H', 2: 'H', } def to_taskwarrior(self): record = { 'project': self.record['project'], 'priority': self.get_priority(), 'due': self.parse_date(self.record.get('due_on')), self.PERMALINK: self.record['permalink'], self.TICKET_ID: self.record['ticket_id'], self.PROJECT_ID: self.record['project_id'], self.TYPE: self.record['type'], self.CREATED_ON: self.parse_date(self.record.get('created_on')), self.CREATED_BY_ID: self.record['created_by_id'], self.BODY: self.record.get('body'), self.NAME: self.record.get('name'), } return record def get_default_description(self): record_type = self.record['type'].lower() record_type = 'issue' if record_type == 'ticket' else record_type return self.build_default_description( title=( self.record['name'] if self.record['name'] else self.record['body'] ), url=self.get_processed_url(self.record['permalink']), number=self.record['ticket_id'], cls=record_type, ) class ActiveCollab2Service(IssueService): ISSUE_CLASS = ActiveCollab2Issue CONFIG_PREFIX = 'activecollab2' def __init__(self, *args, **kw): super(ActiveCollab2Service, self).__init__(*args, **kw) self.url = self.config.get('url').rstrip('/') self.key = self.config.get('key') self.user_id = self.config.get('user_id') projects_raw = self.config.get('projects') projects_list = projects_raw.split(',') projects = [] for k, v in enumerate(projects_list): project_data = v.strip().split(":") project = dict([(project_data[0], project_data[1])]) projects.append(project) self.projects = projects self.client = ActiveCollab2Client( self.url, self.key, self.user_id, self.projects, self.target ) @classmethod def validate_config(cls, service_config, target): for k in ( 'url', 'key', 'projects', 'user_id' ): if k not in service_config: die("[%s] has no 'activecollab2.%s'" % (target, k)) super(ActiveCollab2Service, cls).validate_config(service_config, target) def issues(self): # Loop through each project start = time.time() issue_generators = [] projects = self.projects for project in projects: for project_id, project_name in project.items(): log.debug( " Getting tasks for #" + project_id + " " + project_name + '"') issue_generators.append( self.client.get_issue_generator( self.user_id, project_id, project_name ) ) log.debug(" Elapsed Time: %s" % (time.time() - start)) for record in itertools.chain(*issue_generators): yield self.get_issue_for_record(record) bugwarrior-1.8.0/bugwarrior/services/bitbucket.py000066400000000000000000000211771376471246600222670ustar00rootroot00000000000000from __future__ import unicode_literals from builtins import filter import requests from bugwarrior.services import IssueService, Issue, ServiceClient from bugwarrior.config import asbool, aslist, die import logging log = logging.getLogger(__name__) class BitbucketIssue(Issue): TITLE = 'bitbuckettitle' URL = 'bitbucketurl' FOREIGN_ID = 'bitbucketid' UDAS = { TITLE: { 'type': 'string', 'label': 'Bitbucket Title', }, URL: { 'type': 'string', 'label': 'Bitbucket URL', }, FOREIGN_ID: { 'type': 'numeric', 'label': 'Bitbucket Issue ID', } } UNIQUE_KEY = (URL, ) PRIORITY_MAP = { 'trivial': 'L', 'minor': 'L', 'major': 'M', 'critical': 'H', 'blocker': 'H', } def to_taskwarrior(self): return { 'project': self.extra['project'], 'priority': self.get_priority(), 'annotations': self.extra['annotations'], self.URL: self.extra['url'], self.FOREIGN_ID: self.record['id'], self.TITLE: self.record['title'], } def get_default_description(self): return self.build_default_description( title=self.record['title'], url=self.get_processed_url(self.extra['url']), number=self.record['id'], cls='issue' ) class BitbucketService(IssueService, ServiceClient): ISSUE_CLASS = BitbucketIssue CONFIG_PREFIX = 'bitbucket' BASE_API2 = 'https://api.bitbucket.org/2.0' BASE_URL = 'https://bitbucket.org/' def __init__(self, *args, **kw): super(BitbucketService, self).__init__(*args, **kw) key = self.config.get('key') secret = self.config.get('secret') auth = {'oauth': (key, secret)} refresh_token = self.config.data.get('bitbucket_refresh_token') if not refresh_token: login = self.config.get('login') password = self.get_password('password', login) auth['basic'] = (login, password) if key and secret: if refresh_token: response = requests.post( self.BASE_URL + 'site/oauth2/access_token', data={'grant_type': 'refresh_token', 'refresh_token': refresh_token}, auth=auth['oauth']).json() else: response = requests.post( self.BASE_URL + 'site/oauth2/access_token', data={'grant_type': 'password', 'username': login, 'password': password}, auth=auth['oauth']).json() self.config.data.set('bitbucket_refresh_token', response['refresh_token']) auth['token'] = response['access_token'] self.requests_kwargs = {} if 'token' in auth: self.requests_kwargs['headers'] = { 'Authorization': 'Bearer ' + auth['token']} elif 'basic' in auth: self.requests_kwargs['auth'] = auth['basic'] self.exclude_repos = self.config.get('exclude_repos', [], aslist) self.include_repos = self.config.get('include_repos', [], aslist) self.filter_merge_requests = self.config.get( 'filter_merge_requests', default=False, to_type=asbool ) self.project_owner_prefix = self.config.get( 'project_owner_prefix', default=False, to_type=asbool ) @staticmethod def get_keyring_service(service_config): login = service_config.get('login') username = service_config.get('username') return "bitbucket://%s@bitbucket.org/%s" % (login, username) def filter_repos(self, repo_tag): repo = repo_tag.split('/').pop() 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 get_data(self, url): """ Perform a request to the fully qualified url and return json. """ return self.json_response(requests.get(url, **self.requests_kwargs)) def get_collection(self, url): """ Pages through an object collection from the bitbucket API. Returns an iterator that lazily goes through all the 'values' of all the pages in the collection. """ url = self.BASE_API2 + url while url is not None: response = self.get_data(url) for value in response['values']: yield value url = response.get('next', None) @classmethod def validate_config(cls, service_config, target): if 'username' not in service_config: die("[%s] has no 'username'" % target) if 'login' not in service_config: die("[%s] has no 'login'" % target) IssueService.validate_config(service_config, target) def fetch_issues(self, tag): response = self.get_collection('/repositories/%s/issues/' % (tag)) return [(tag, issue) for issue in response] def fetch_pull_requests(self, tag): response = self.get_collection('/repositories/%s/pullrequests/' % tag) return [(tag, issue) for issue in response] def get_annotations(self, tag, issue, issue_obj, url): response = self.get_collection( '/repositories/%s/pullrequests/%i/comments' % (tag, issue['id']) ) return self.build_annotations( (( comment['user']['username'], comment['content']['raw'], ) for comment in response), issue_obj.get_processed_url(url) ) def get_owner(self, issue): _, issue = issue assignee = issue.get('assignee', None) if assignee is not None: return assignee.get('username', None) def issues(self): user = self.config.get('username') response = self.get_collection('/repositories/' + user + '/') repo_tags = list(filter(self.filter_repos, [ repo['full_name'] for repo in response if repo.get('has_issues') ])) issues = sum([self.fetch_issues(repo) for repo in repo_tags], []) log.debug(" Found %i total.", len(issues)) closed = ['resolved', 'duplicate', 'wontfix', 'invalid', 'closed'] try: issues = [tup for tup in issues if tup[1]['status'] not in closed] except KeyError: # Undocumented API change. issues = [tup for tup in issues if tup[1]['state'] not in closed] issues = list(filter(self.include, issues)) log.debug(" Pruned down to %i", len(issues)) for tag, issue in issues: issue_obj = self.get_issue_for_record(issue) tagParts = tag.split('/') projectName = tagParts[1] if self.project_owner_prefix: projectName = tagParts[0] + "." + projectName url = issue['links']['html']['href'] extras = { 'project': projectName, 'url': url, 'annotations': self.get_annotations(tag, issue, issue_obj, url) } issue_obj.update_extra(extras) yield issue_obj if not self.filter_merge_requests: pull_requests = sum( [self.fetch_pull_requests(repo) for repo in repo_tags], []) log.debug(" Found %i total.", len(pull_requests)) closed = ['rejected', 'fulfilled'] not_resolved = lambda tup: tup[1]['state'] not in closed pull_requests = list(filter(not_resolved, pull_requests)) pull_requests = list(filter(self.include, pull_requests)) log.debug(" Pruned down to %i", len(pull_requests)) for tag, issue in pull_requests: issue_obj = self.get_issue_for_record(issue) tagParts = tag.split('/') projectName = tagParts[1] if self.project_owner_prefix: projectName = tagParts[0] + "." + projectName url = self.BASE_URL + '/'.join( issue['links']['html']['href'].split('/')[3:] ).replace('pullrequests', 'pullrequest') extras = { 'project': projectName, 'url': url, 'annotations': self.get_annotations( tag, issue, issue_obj, url) } issue_obj.update_extra(extras) yield issue_obj bugwarrior-1.8.0/bugwarrior/services/bts.py000066400000000000000000000157711376471246600211060ustar00rootroot00000000000000from builtins import str import debianbts import requests from bugwarrior.config import die, asbool from bugwarrior.services import Issue, IssueService, ServiceClient import logging log = logging.getLogger(__name__) UDD_BUGS_SEARCH = "https://udd.debian.org/bugs/" class BTSIssue(Issue): SUBJECT = 'btssubject' URL = 'btsurl' NUMBER = 'btsnumber' PACKAGE = 'btspackage' SOURCE = 'btssource' FORWARDED = 'btsforwarded' STATUS = 'btsstatus' UDAS = { SUBJECT: { 'type': 'string', 'label': 'Debian BTS Subject', }, URL: { 'type': 'string', 'label': 'Debian BTS URL', }, NUMBER: { 'type': 'numeric', 'label': 'Debian BTS Number', }, PACKAGE: { 'type': 'string', 'label': 'Debian BTS Package', }, SOURCE: { 'type': 'string', 'label': 'Debian BTS Source Package', }, FORWARDED: { 'type': 'string', 'label': 'Debian BTS Forwarded URL', }, STATUS: { 'type': 'string', 'label': 'Debian BTS Status', } } UNIQUE_KEY = (URL, ) PRIORITY_MAP = { 'wishlist': 'L', 'minor': 'L', 'normal': 'M', 'important': 'M', 'serious': 'H', 'grave': 'H', 'critical': 'H', } def to_taskwarrior(self): return { 'priority': self.get_priority(), 'annotations': self.extra.get('annotations', []), self.URL: self.record['url'], self.SUBJECT: self.record['subject'], self.NUMBER: self.record['number'], self.PACKAGE: self.record['package'], self.SOURCE: self.record['source'], self.FORWARDED: self.record['forwarded'], self.STATUS: self.record['status'], } def get_default_description(self): return self.build_default_description( title=self.record['subject'], url=self.get_processed_url(self.record['url']), number=self.record['number'], cls='issue' ) def get_priority(self): return self.PRIORITY_MAP.get( self.record.get('severity'), self.origin['default_priority'] ) class BTSService(IssueService, ServiceClient): ISSUE_CLASS = BTSIssue CONFIG_PREFIX = 'bts' def __init__(self, *args, **kw): super(BTSService, self).__init__(*args, **kw) self.email = self.config.get('email', default=None) self.packages = self.config.get('packages', default=None) self.udd = self.config.get( 'udd', default=False, to_type=asbool) self.udd_ignore_sponsor = self.config.get( 'udd_ignore_sponsor', default=True, to_type=asbool) self.ignore_pkg = self.config.get('ignore_pkg', default=None) self.ignore_src = self.config.get('ignore_src', default=None) self.ignore_pending = self.config.get( 'ignore_pending', default=True, to_type=asbool) @classmethod def validate_config(cls, service_config, target): if ('udd' in service_config and asbool(service_config.get('udd')) and 'email' not in service_config): die("[%s] has no 'bts.email' but UDD search was requested" % (target,)) if 'packages' not in service_config and 'email' not in service_config: die("[%s] has neither 'bts.email' or 'bts.packages'" % (target,)) if ('udd_ignore_sponsor' in service_config and (not asbool(service_config.get('udd')))): die("[%s] defines settings for UDD search without enabling" " UDD search" % (target,)) IssueService.validate_config(service_config, target) def _record_for_bug(self, bug): return {'number': bug.bug_num, 'url': 'https://bugs.debian.org/' + str(bug.bug_num), 'package': bug.package, 'subject': bug.subject, 'severity': bug.severity, 'source': bug.source, 'forwarded': bug.forwarded, 'status': bug.pending, } def _get_udd_bugs(self): request_params = { 'format': 'json', 'dmd': 1, 'email1': self.email, } if self.udd_ignore_sponsor: request_params['nosponsor1'] = "on" resp = requests.get(UDD_BUGS_SEARCH, request_params) return self.json_response(resp) def annotations(self, issue, issue_obj): return self.build_annotations( [], issue_obj.get_processed_url(issue['url']) ) def issues(self): # Initialise empty list of bug numbers collected_bugs = [] # Search BTS for bugs owned by email address if self.email: owned_bugs = debianbts.get_bugs(owner=self.email, status="open") collected_bugs.extend(owned_bugs) # Search BTS for bugs related to specified packages if self.packages: packages = self.packages.split(",") for pkg in packages: pkg_bugs = debianbts.get_bugs(package=pkg, status="open") for bug in pkg_bugs: if bug not in collected_bugs: collected_bugs.append(bug) # Search UDD bugs search for bugs belonging to packages that # are maintained by the email address if self.udd: udd_bugs = self._get_udd_bugs() for bug in udd_bugs: if bug not in collected_bugs: collected_bugs.append(bug['id']) issues = [self._record_for_bug(bug) for bug in debianbts.get_status(collected_bugs)] log.debug(" Found %i total.", len(issues)) if self.ignore_pkg: ignore_pkg = self.ignore_pkg.split(",") for pkg in ignore_pkg: issues = [issue for issue in issues if not issue['package'] == pkg] if self.ignore_src: ignore_src = self.ignore_src.split(",") for src in ignore_src: issues = [issue for issue in issues if not issue['source'] == src] if self.ignore_pending: issues = [issue for issue in issues if not issue['status'] == 'pending-fixed'] issues = [issue for issue in issues if not (issue['status'] == 'done' or issue['status'] == 'fixed')] log.debug(" Pruned down to %i.", len(issues)) for issue in issues: issue_obj = self.get_issue_for_record(issue) extra = { 'annotations': self.annotations(issue, issue_obj) } issue_obj.update_extra(extra) yield issue_obj bugwarrior-1.8.0/bugwarrior/services/bz.py000066400000000000000000000253451376471246600207270ustar00rootroot00000000000000import bugzilla import time import pytz import datetime import six from bugwarrior.config import die, asbool, aslist from bugwarrior.services import IssueService, Issue import logging log = logging.getLogger(__name__) class BugzillaIssue(Issue): URL = 'bugzillaurl' SUMMARY = 'bugzillasummary' BUG_ID = 'bugzillabugid' STATUS = 'bugzillastatus' NEEDINFO = 'bugzillaneedinfo' PRODUCT = 'bugzillaproduct' COMPONENT = 'bugzillacomponent' ASSIGNED_ON = 'bugzillaassignedon' UDAS = { URL: { 'type': 'string', 'label': 'Bugzilla URL', }, SUMMARY: { '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', }, PRODUCT: { 'type': 'string', 'label': 'Bugzilla Product', }, COMPONENT: { 'type': 'string', 'label': 'Bugzilla Component', }, ASSIGNED_ON: { 'type': 'date', 'label': 'Bugzilla Assigned On', }, } UNIQUE_KEY = (URL, ) PRIORITY_MAP = { 'unspecified': 'M', 'low': 'L', 'medium': 'M', 'high': 'H', 'urgent': 'H', } def to_taskwarrior(self): task = { 'project': self.record['component'], 'priority': self.get_priority(), 'annotations': self.extra.get('annotations', []), self.URL: self.extra['url'], self.SUMMARY: self.record['summary'], self.BUG_ID: self.record['id'], self.STATUS: self.record['status'], self.PRODUCT: self.record['product'], self.COMPONENT: self.record['component'], } if self.extra.get('needinfo_since', None) is not None: task[self.NEEDINFO] = self.parse_date(self.extra.get('needinfo_since')) if self.extra.get('assigned_on', None) is not None: task[self.ASSIGNED_ON] = self.parse_date(self.extra.get('assigned_on')) return task def get_default_description(self): return self.build_default_description( title=self.record['summary'], url=self.get_processed_url(self.extra['url']), number=self.record['id'], cls='issue', ) _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' COLUMN_LIST = [ 'id', 'status', 'summary', 'priority', 'product', 'component', 'flags', 'longdescs', 'assigned_to', ] def __init__(self, *args, **kw): super(BugzillaService, self).__init__(*args, **kw) self.base_uri = self.config.get('base_uri') self.username = self.config.get('username') self.ignore_cc = self.config.get('ignore_cc', default=False, to_type=lambda x: x == "True") self.query_url = self.config.get('query_url', default=None) self.include_needinfos = self.config.get( 'include_needinfos', False, to_type=lambda x: x == "True") self.open_statuses = self.config.get('open_statuses', _open_statuses, to_type=aslist) log.debug(" filtering on statuses: %r", self.open_statuses) # So more modern bugzilla's require that we specify # query_format=advanced along with the xmlrpc request. # https://bugzilla.redhat.com/show_bug.cgi?id=825370 # ...but older bugzilla's don't know anything about that argument. # Here we make it possible for the user to specify whether they want # to pass that argument or not. self.advanced = asbool(self.config.get('advanced', 'no')) force_rest_kwargs = {} if asbool(self.config.get("force_rest", "no")): force_rest_kwargs = {"force_rest": True} url = 'https://%s' % self.base_uri if self.config.get('api_key'): api_key = self.get_password('api_key') try: self.bz = bugzilla.Bugzilla(url=url, api_key=api_key, **force_rest_kwargs) except TypeError: raise Exception("Bugzilla API keys require python-bugzilla>=2.1.0") else: password = self.get_password('password', self.username) self.bz = bugzilla.Bugzilla(url=url, **force_rest_kwargs) self.bz.login(self.username, password) @staticmethod def get_keyring_service(service_config): username = service_config.get('username') base_uri = service_config.get('base_uri') return "bugzilla://%s@%s" % (username, base_uri) @classmethod def validate_config(cls, service_config, target): req = ['username', 'base_uri'] for option in req: if option not in service_config: die("[%s] has no 'bugzilla.%s'" % (target, option)) if 'password' not in service_config and 'api_key' not in service_config: die("[%s] has neither 'bugzilla.password' nor 'bugzilla.api_key'" % (target,)) super(BugzillaService, cls).validate_config(service_config, target) def get_owner(self, issue): return issue['assigned_to'] def annotations(self, tag, issue, issue_obj): base_url = "https://%s/show_bug.cgi?id=" % self.base_uri long_url = base_url + six.text_type(issue['id']) url = issue_obj.get_processed_url(long_url) if 'comments' in issue: comments = issue.get('comments', []) return self.build_annotations( (( c['author'].split('@')[0], c['text'], ) for c in comments), url ) else: # Backwards compatibility (old python-bugzilla/bugzilla instances) # This block handles a million different contingencies that have to # do with different version of python-bugzilla and different # version of bugzilla itself. :( comments = issue.get('longdescs', []) def _parse_author(obj): if isinstance(obj, dict): return obj['login_name'].split('@')[0] else: return obj def _parse_body(obj): return obj.get('text', obj.get('body')) return self.build_annotations( (( _parse_author(c['author']), _parse_body(c) ) for c in comments), url ) def issues(self): email = self.username # TODO -- doing something with blockedby would be nice. if self.query_url: query = self.bz.url_to_query(self.query_url) query['column_list'] = self.COLUMN_LIST else: query = dict( column_list=self.COLUMN_LIST, bug_status=self.open_statuses, email1=email, emailreporter1=1, emailassigned_to1=1, emailqa_contact1=1, emailtype1="substring", ) if not self.ignore_cc: query['emailcc1'] = 1 if self.advanced: # Required for new bugzilla # https://bugzilla.redhat.com/show_bug.cgi?id=825370 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( ((col, _get_bug_attr(bug, col)) for col in self.COLUMN_LIST) ) for bug in bugs ] bugs = filter(self.include, bugs) issues = [(self.target, bug) for bug in bugs] log.debug(" Found %i total.", len(issues)) # Build a url for each issue base_url = "https://%s/show_bug.cgi?id=" % (self.base_uri) for tag, issue in issues: issue_obj = self.get_issue_for_record(issue) extra = { 'url': base_url + six.text_type(issue['id']), 'annotations': self.annotations(tag, issue, issue_obj), } needinfos = [f for f in issue['flags'] if ( f['name'] == 'needinfo' and f['status'] == '?' and f.get('requestee', self.username) == self.username )] 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).isoformat() if issue['status'] == 'ASSIGNED': extra['assigned_on'] = self._get_assigned_date(issue) else: extra['assigned_on'] = None issue_obj.update_extra(extra) yield issue_obj def _get_assigned_date(self, issue): assigned_date = None bug = self.bz.getbug(issue['id']) history = bug.get_history_raw()['bugs'][0]['history'] # this is already in chronological order, so the last change is the one we want for h in reversed(history): for change in h['changes']: if change['field_name'] == 'status' and change['added'] == 'ASSIGNED': assigned_date = h['when'] # messy conversion :( # TODO: create method that's used here and in needinfos time conv above assigned_date_datetime = datetime.datetime.fromtimestamp( time.mktime(assigned_date.timetuple())) assigned_date_str = pytz.UTC.localize(assigned_date_datetime).isoformat() return assigned_date_str def _get_bug_attr(bug, attr): """Default longdescs/flags case to [] since they may not be present.""" if attr in ("longdescs", "flags"): return getattr(bug, attr, []) return getattr(bug, attr) bugwarrior-1.8.0/bugwarrior/services/gerrit.py000066400000000000000000000116401376471246600216010ustar00rootroot00000000000000from __future__ import absolute_import import json import os import requests from bugwarrior.config import die from bugwarrior.services import IssueService, Issue, ServiceClient class GerritIssue(Issue): SUMMARY = 'gerritsummary' URL = 'gerriturl' FOREIGN_ID = 'gerritid' BRANCH = 'gerritbranch' TOPIC = 'gerrittopic' UDAS = { SUMMARY: { 'type': 'string', 'label': 'Gerrit Summary' }, URL: { 'type': 'string', 'label': 'Gerrit URL', }, FOREIGN_ID: { 'type': 'numeric', 'label': 'Gerrit Change ID' }, BRANCH: { 'type': 'string', 'label': 'Gerrit Branch', }, TOPIC: { 'type': 'string', 'label': 'Gerrit Topic', }, } UNIQUE_KEY = (URL, ) def to_taskwarrior(self): return { 'project': self.record['project'], 'annotations': self.extra['annotations'], self.URL: self.extra['url'], 'priority': self.origin['default_priority'], 'tags': [], self.FOREIGN_ID: self.record['_number'], self.SUMMARY: self.record['subject'], self.BRANCH: self.record['branch'], self.TOPIC: self.record.get('topic', 'notopic'), } def get_default_description(self): return self.build_default_description( title=self.record['subject'], url=self.get_processed_url(self.extra['url']), number=self.record['_number'], cls='pull_request', ) class GerritService(IssueService, ServiceClient): ISSUE_CLASS = GerritIssue CONFIG_PREFIX = 'gerrit' def __init__(self, *args, **kw): super(GerritService, self).__init__(*args, **kw) self.url = self.config.get('base_uri').strip('/') self.username = self.config.get('username') self.password = self.get_password('password', self.username) self.ssl_ca_path = self.config.get('ssl_ca_path', None) self.session = requests.session() self.session.headers.update({ 'Accept': 'application/json', 'Accept-Encoding': 'gzip', }) self.query_string = self.config.get( 'query', 'is:open+is:reviewer' ) + '&o=MESSAGES&o=DETAILED_ACCOUNTS' if self.ssl_ca_path: self.session.verify = os.path.expanduser(self.ssl_ca_path) # uses digest authentication if supported by the server, fallback to basic # gerrithub.io supports only basic response = self.session.head(self.url + '/a/') if 'digest' in response.headers.get('www-authenticate', '').lower(): self.session.auth = requests.auth.HTTPDigestAuth( self.username, self.password) else: self.session.auth = requests.auth.HTTPBasicAuth( self.username, self.password) @staticmethod def get_keyring_service(service_config): base_uri = service_config.get('base_uri') return "gerrit://%s" % base_uri def get_service_metadata(self): return { 'url': self.url, } @classmethod def validate_config(cls, service_config, target): for option in ('username', 'password', 'base_uri'): if option not in service_config: die("[%s] has no 'gerrit.%s'" % (target, option)) IssueService.validate_config(service_config, target) def issues(self): # Construct the whole url by hand here, because otherwise requests will # percent-encode the ':' characters, which gerrit doesn't like. url = self.url + '/a/changes/?q=' + self.query_string response = self.session.get(url) response.raise_for_status() # The response has some ")]}'" garbage prefixed. body = response.text[4:] changes = json.loads(body) for change in changes: extra = { 'url': self.build_url(change), 'annotations': self.annotations(change), } yield self.get_issue_for_record(change, extra) def build_url(self, change): return '%s/#/c/%i/' % (self.url, change['_number']) def annotations(self, change): entries = [] for item in change['messages']: for key in ['name', 'username', 'email']: if key in item['author']: username = item['author'][key] break else: username = item['author']['_account_id'] # Gerrit messages are really messy message = item['message']\ .lstrip('Patch Set ')\ .lstrip("%s:" % item['_revision_number'])\ .strip()\ .replace('\n', ' ') entries.append((username, message,)) return self.build_annotations(entries, self.build_url(change)) bugwarrior-1.8.0/bugwarrior/services/github.py000066400000000000000000000374211376471246600215740ustar00rootroot00000000000000from builtins import filter import re import six import sys from urllib.parse import urlparse import requests from six.moves.urllib.parse import quote_plus from jinja2 import Template from bugwarrior.config import asbool, aslist, die from bugwarrior.services import IssueService, Issue, ServiceClient import logging log = logging.getLogger(__name__) class GithubClient(ServiceClient): def __init__(self, host, auth): self.host = host self.auth = auth self.session = requests.Session() if 'token' in self.auth: authorization = 'token ' + self.auth['token'] self.session.headers['Authorization'] = authorization def _api_url(self, path, **context): """ Build the full url to the API endpoint """ if self.host == 'github.com': baseurl = "https://api.github.com" else: baseurl = "https://{}/api/v3".format(self.host) return baseurl + path.format(**context) def get_repos(self, username): user_repos = self._getter(self._api_url("/user/repos?per_page=100")) public_repos = self._getter(self._api_url( "/users/{username}/repos?per_page=100", username=username)) return user_repos + public_repos def get_query(self, query): """Run a generic issue/PR query""" url = self._api_url( "/search/issues?q={query}&per_page=100", query=query) return self._getter(url, subkey='items') def get_issues(self, username, repo): url = self._api_url( "/repos/{username}/{repo}/issues?per_page=100", username=username, repo=repo) return self._getter(url) def get_directly_assigned_issues(self): """ Returns all issues assigned to authenticated user. This will return all issues assigned to the authenticated user regardless of whether the user owns the repositories in which the issues exist. """ url = self._api_url("/user/issues?per_page=100") return self._getter(url) def get_comments(self, username, repo, number): url = self._api_url( "/repos/{username}/{repo}/issues/{number}/comments?per_page=100", username=username, repo=repo, number=number) return self._getter(url) def get_pulls(self, username, repo): url = self._api_url( "/repos/{username}/{repo}/pulls?per_page=100", username=username, repo=repo) return self._getter(url) def _getter(self, url, subkey=None): """ Pagination utility. Obnoxious. """ kwargs = {} if 'basic' in self.auth: kwargs['auth'] = self.auth['basic'] results = [] link = dict(next=url) while 'next' in link: response = self.session.get(link['next'], **kwargs) # Warn about the mis-leading 404 error code. See: # https://github.com/ralphbean/bugwarrior/issues/374 if response.status_code == 404 and 'token' in self.auth: log.warn("A '404' from github may indicate an auth " "failure. Make sure both that your token is correct " "and that it has 'public_repo' and not 'public " "access' rights.") json_res = self.json_response(response) if subkey is not None: json_res = json_res[subkey] results += json_res link = self._link_field_to_dict(response.headers.get('link', None)) return results @staticmethod def _link_field_to_dict(field): """ Utility for ripping apart github's Link header field. It's kind of ugly. """ if not field: return dict() return dict([ ( part.split('; ')[1][5:-1], part.split('; ')[0][1:-1], ) for part in field.split(', ') ]) class GithubIssue(Issue): TITLE = 'githubtitle' BODY = 'githubbody' CREATED_AT = 'githubcreatedon' UPDATED_AT = 'githubupdatedat' CLOSED_AT = 'githubclosedon' MILESTONE = 'githubmilestone' URL = 'githuburl' REPO = 'githubrepo' TYPE = 'githubtype' NUMBER = 'githubnumber' USER = 'githubuser' NAMESPACE = 'githubnamespace' STATE = 'githubstate' UDAS = { TITLE: { 'type': 'string', 'label': 'Github Title', }, BODY: { 'type': 'string', 'label': 'Github Body', }, CREATED_AT: { 'type': 'date', 'label': 'Github Created', }, UPDATED_AT: { 'type': 'date', 'label': 'Github Updated', }, CLOSED_AT: { 'type': 'date', 'label': 'GitHub Closed', }, MILESTONE: { 'type': 'string', 'label': 'Github Milestone', }, REPO: { 'type': 'string', 'label': 'Github Repo Slug', }, URL: { 'type': 'string', 'label': 'Github URL', }, TYPE: { 'type': 'string', 'label': 'Github Type', }, NUMBER: { 'type': 'numeric', 'label': 'Github Issue/PR #', }, USER: { 'type': 'string', 'label': 'Github User', }, NAMESPACE: { 'type': 'string', 'label': 'Github Namespace', }, STATE: { 'type': 'string', 'label': 'GitHub State', } } UNIQUE_KEY = (URL, TYPE,) def _normalize_label_to_tag(self, label): return re.sub(r'[^a-zA-Z0-9]', '_', label) def to_taskwarrior(self): milestone = self.record['milestone'] if milestone: milestone = milestone['title'] created = self.parse_date(self.record.get('created_at')) updated = self.parse_date(self.record.get('updated_at')) closed = self.parse_date(self.record.get('closed_at')) return { 'project': self.extra['project'], 'priority': self.origin['default_priority'], 'annotations': self.extra.get('annotations', []), 'tags': self.get_tags(), 'entry': created, 'end': closed, self.URL: self.record['html_url'], self.REPO: self.record['repo'], self.TYPE: self.extra['type'], self.USER: self.record['user']['login'], self.TITLE: self.record['title'], self.BODY: self.extra['body'], self.MILESTONE: milestone, self.NUMBER: self.record['number'], self.CREATED_AT: created, self.UPDATED_AT: updated, self.CLOSED_AT: closed, self.NAMESPACE: self.extra['namespace'], self.STATE: self.record.get('state', '') } def get_tags(self): tags = [] if not self.origin['import_labels_as_tags']: return tags context = self.record.copy() label_template = Template(self.origin['label_template']) for label_dict in self.record.get('labels', []): context.update({ 'label': self._normalize_label_to_tag(label_dict['name']) }) tags.append( label_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['number'], cls=self.extra['type'], ) class GithubService(IssueService): ISSUE_CLASS = GithubIssue CONFIG_PREFIX = 'github' def __init__(self, *args, **kw): super(GithubService, self).__init__(*args, **kw) self.host = self.config.get('host', 'github.com') self.login = self.config.get('login') auth = {} token = self.config.get('token') if 'token' in self.config: token = self.get_password('token', self.login) auth['token'] = token else: password = self.get_password('password', self.login) auth['basic'] = (self.login, password) self.client = GithubClient(self.host, auth) self.exclude_repos = self.config.get('exclude_repos', [], aslist) self.include_repos = self.config.get('include_repos', [], aslist) self.username = self.config.get('username') self.filter_pull_requests = self.config.get( 'filter_pull_requests', default=False, to_type=asbool ) self.exclude_pull_requests = self.config.get( 'exclude_pull_requests', default=False, to_type=asbool ) self.involved_issues = self.config.get( 'involved_issues', default=False, to_type=asbool ) self.import_labels_as_tags = self.config.get( 'import_labels_as_tags', default=False, to_type=asbool ) self.label_template = self.config.get( 'label_template', default='{{label}}', to_type=six.text_type ) self.project_owner_prefix = self.config.get( 'project_owner_prefix', default=False, to_type=asbool ) self.query = self.config.get( 'query', default='involves:{user} state:open'.format( user=self.username) if self.involved_issues else '', to_type=six.text_type ) @staticmethod def get_keyring_service(service_config): login = service_config.get('login') username = service_config.get('username') host = service_config.get('host', default='github.com') return "github://{login}@{host}/{username}".format( login=login, username=username, host=host) def get_service_metadata(self): return { 'import_labels_as_tags': self.import_labels_as_tags, 'label_template': self.label_template, } def get_owned_repo_issues(self, tag): """ Grab all the issues """ issues = {} for issue in self.client.get_issues(*tag.split('/')): issues[issue['url']] = (tag, issue) return issues def get_query(self, query): """ Grab all issues matching a github query """ issues = {} for issue in self.client.get_query(query): url = issue['html_url'] try: repo = self.get_repository_from_issue(issue) except ValueError as e: log.critical(e) else: issues[url] = (repo, issue) return issues def get_directly_assigned_issues(self): issues = {} for issue in self.client.get_directly_assigned_issues(): repos = self.get_repository_from_issue(issue) issues[issue['url']] = (repos, issue) return issues @classmethod def get_repository_from_issue(cls, issue): if 'repo' in issue: return issue['repo'] if 'repos_url' in issue: url = issue['repos_url'] elif 'repository_url' in issue: url = issue['repository_url'] else: raise ValueError("Issue has no repository url" + str(issue)) tag = re.match('.*/([^/]*/[^/]*)$', url) if tag is None: raise ValueError("Unrecognized URL: {}.".format(url)) return tag.group(1) def _comments(self, tag, number): user, repo = tag.split('/') return self.client.get_comments(user, repo, number) def annotations(self, tag, issue, issue_obj): url = issue['html_url'] annotations = [] if self.annotation_comments: comments = self._comments(tag, issue['number']) log.debug(" got comments for %s", issue['html_url']) annotations = (( c['user']['login'], c['body'], ) for c in comments) return self.build_annotations( annotations, issue_obj.get_processed_url(url) ) def body(self, issue): body = issue['body'] if body: body = body.replace('\r\n', '\n') max_length = self.config.get('body_length', default=sys.maxsize, to_type=int) body = body[:max_length] return body def _reqs(self, tag): """ Grab all the pull requests """ return [ (tag, i) for i in self.client.get_pulls(*tag.split('/')) ] def get_owner(self, issue): if issue[1]['assignee']: return issue[1]['assignee']['login'] def filter_issues(self, issue): repo, _ = issue return self.filter_repo_name(repo.split('/')[-3]) def filter_repos(self, repo): if repo['owner']['login'] != self.username: return False return self.filter_repo_name(repo['name']) def filter_repo_name(self, name): if self.exclude_repos: if name in self.exclude_repos: return False if self.include_repos: if name in self.include_repos: return True else: return False return True def include(self, issue): if 'pull_request' in issue[1]: if self.exclude_pull_requests: return False if not self.filter_pull_requests: return True return super(GithubService, self).include(issue) def issues(self): issues = {} if self.query: issues.update(self.get_query(self.query)) if self.config.get('include_user_repos', True, asbool): # Only query for all repos if an explicit # include_repos list is not specified. if self.include_repos: repos = self.include_repos else: all_repos = self.client.get_repos(self.username) repos = filter(self.filter_repos, all_repos) repos = [repo['name'] for repo in repos] for repo in repos: issues.update( self.get_owned_repo_issues( self.username + "/" + repo) ) if self.config.get('include_user_issues', True, asbool): issues.update( filter(self.filter_issues, self.get_directly_assigned_issues().items()) ) log.debug(" Found %i issues.", len(issues)) issues = list(filter(self.include, issues.values())) log.debug(" Pruned down to %i issues.", len(issues)) for tag, issue in issues: # Stuff this value into the upstream dict for: # https://github.com/ralphbean/bugwarrior/issues/159 issue['repo'] = tag issue_obj = self.get_issue_for_record(issue) tagParts = tag.split('/') projectName = tagParts[1] if self.project_owner_prefix: projectName = tagParts[0]+"."+projectName extra = { 'project': projectName, 'type': 'pull_request' if 'pull_request' in issue else 'issue', 'annotations': self.annotations(tag, issue, issue_obj), 'body': self.body(issue), 'namespace': self.username, } issue_obj.update_extra(extra) yield issue_obj @classmethod def validate_config(cls, service_config, target): if 'login' not in service_config: die("[%s] has no 'github.login'" % target) if 'token' not in service_config and 'password' not in service_config: die("[%s] has no 'github.token' or 'github.password'" % target) if 'username' not in service_config: die("[%s] has no 'github.username'" % target) super(GithubService, cls).validate_config(service_config, target) bugwarrior-1.8.0/bugwarrior/services/gitlab.py000066400000000000000000000446321376471246600215560ustar00rootroot00000000000000# coding: utf-8 from future import standard_library standard_library.install_aliases() from builtins import map from builtins import filter try: from urllib import quote, urlencode # Python 2.X except ImportError: from urllib.parse import quote, urlencode # Python 3+ from six.moves.configparser import NoOptionError import re import requests import six from jinja2 import Template from bugwarrior.config import asbool, aslist, die from bugwarrior.services import IssueService, Issue, ServiceClient import logging log = logging.getLogger(__name__) class GitlabIssue(Issue): TITLE = 'gitlabtitle' DESCRIPTION = 'gitlabdescription' CREATED_AT = 'gitlabcreatedon' UPDATED_AT = 'gitlabupdatedat' DUEDATE = 'gitlabduedate' MILESTONE = 'gitlabmilestone' URL = 'gitlaburl' REPO = 'gitlabrepo' TYPE = 'gitlabtype' NUMBER = 'gitlabnumber' STATE = 'gitlabstate' UPVOTES = 'gitlabupvotes' DOWNVOTES = 'gitlabdownvotes' WORK_IN_PROGRESS = 'gitlabwip' AUTHOR = 'gitlabauthor' ASSIGNEE = 'gitlabassignee' NAMESPACE = 'gitlabnamespace' WEIGHT = 'gitlabweight' UDAS = { TITLE: { 'type': 'string', 'label': 'Gitlab Title', }, DESCRIPTION: { 'type': 'string', 'label': 'Gitlab Description', }, CREATED_AT: { 'type': 'date', 'label': 'Gitlab Created', }, UPDATED_AT: { 'type': 'date', 'label': 'Gitlab Updated', }, DUEDATE: { 'type': 'date', 'label': 'Gitlab Due Date', }, MILESTONE: { 'type': 'string', 'label': 'Gitlab Milestone', }, URL: { 'type': 'string', 'label': 'Gitlab URL', }, REPO: { 'type': 'string', 'label': 'Gitlab Repo Slug', }, TYPE: { 'type': 'string', 'label': 'Gitlab Type', }, NUMBER: { 'type': 'string', 'label': 'Gitlab Issue/MR #', }, STATE: { 'type': 'string', 'label': 'Gitlab Issue/MR State', }, UPVOTES: { 'type': 'numeric', 'label': 'Gitlab Upvotes', }, DOWNVOTES: { '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', }, NAMESPACE: { 'type': 'string', 'label': 'Gitlab Namespace', }, WEIGHT: { 'type': 'numeric', 'label': 'Gitlab Weight', }, } UNIQUE_KEY = (REPO, TYPE, NUMBER,) def _normalize_label_to_tag(self, label): return re.sub(r'[^a-zA-Z0-9]', '_', label) def to_taskwarrior(self): author = self.record['author'] milestone = self.record.get('milestone') created = self.record['created_at'] updated = self.record.get('updated_at') state = self.record['state'] upvotes = self.record.get('upvotes', 0) downvotes = self.record.get('downvotes', 0) work_in_progress = int(asbool(self.record.get('work_in_progress', 0))) assignee = self.record.get('assignee') duedate = self.record.get('due_date') weight = self.record.get('weight') number = ( self.record['id'] if self.extra['type'] == 'todo' else self.record['iid']) priority = ( self.origin['default_priority'] if self.extra['type'] == 'issue' else 'H') title = ( 'Todo from %s for %s' % (author['name'], self.extra['project']) if self.extra['type'] == 'todo' else self.record['title']) description = ( self.record['body'] if self.extra['type'] == 'todo' else self.record['description']) if milestone and ( self.extra['type'] == 'issue' or (self.extra['type'] == 'merge_request' and duedate is None)): duedate = milestone['due_date'] if milestone: milestone = milestone['title'] if created: created = self.parse_date(created).replace(microsecond=0) if updated: updated = self.parse_date(updated).replace(microsecond=0) if duedate: duedate = self.parse_date(duedate) if author: author = author['username'] if assignee: assignee = assignee['username'] self.title = title return { 'project': self.extra['project'], 'priority': priority, 'annotations': self.extra.get('annotations', []), 'tags': self.get_tags(), 'due': duedate, 'entry': created, self.URL: self.extra['issue_url'], self.REPO: self.extra['project'], self.TYPE: self.extra['type'], self.TITLE: title, self.DESCRIPTION: description, self.MILESTONE: milestone, self.NUMBER: str(number), self.CREATED_AT: created, self.UPDATED_AT: updated, self.DUEDATE: duedate, self.STATE: state, self.UPVOTES: upvotes, self.DOWNVOTES: downvotes, self.WORK_IN_PROGRESS: work_in_progress, self.AUTHOR: author, self.ASSIGNEE: assignee, self.NAMESPACE: self.extra['namespace'], self.WEIGHT: weight, } def get_tags(self): tags = [] if not self.origin['import_labels_as_tags']: return tags context = self.record.copy() label_template = Template(self.origin['label_template']) for label in self.record.get('labels', []): context.update({ 'label': self._normalize_label_to_tag(label) }) tags.append( label_template.render(context) ) return tags def get_default_description(self): return self.build_default_description( title=self.title, url=self.get_processed_url(self.extra['issue_url']), number=self.record.get('iid', ''), cls=self.extra['type'], ) class GitlabService(IssueService, ServiceClient): ISSUE_CLASS = GitlabIssue CONFIG_PREFIX = 'gitlab' def __init__(self, *args, **kw): super(GitlabService, self).__init__(*args, **kw) host = self.config.get( 'host', default='gitlab.com', to_type=six.text_type) self.login = self.config.get('login') token = self.get_password('token', self.login) self.auth = (host, token) if self.config.get('use_https', default=True, to_type=asbool): self.scheme = 'https' else: self.scheme = 'http' self.verify_ssl = self.config.get( 'verify_ssl', default=True, to_type=asbool ) self.membership = self.config.get('membership', False) self.owned = self.config.get('owned', False) self.exclude_repos = self.config.get('exclude_repos', [], aslist) self.include_repos = self.config.get('include_repos', [], aslist) self.exclude_regex = self.config.get('exclude_regex', None) self.include_regex = self.config.get('include_regex', None) self.include_repos = list(map(self.add_default_namespace, self.include_repos)) self.exclude_repos = list(map(self.add_default_namespace, self.exclude_repos)) self.include_regex = re.compile(self.include_regex) if self.include_regex else None self.exclude_regex = re.compile(self.exclude_regex) if self.exclude_regex else None self.import_labels_as_tags = self.config.get( 'import_labels_as_tags', default=False, to_type=asbool ) self.label_template = self.config.get( 'label_template', default='{{label}}', to_type=six.text_type ) self.filter_merge_requests = self.config.get( 'filter_merge_requests', default=False, to_type=asbool ) self.include_todos = self.config.get( 'include_todos', default=False, to_type=asbool ) self.include_all_todos = self.config.get( 'include_all_todos', default=True, to_type=asbool ) self.project_owner_prefix = self.config.get( 'project_owner_prefix', default=False, to_type=asbool ) def add_default_namespace(self, repo): """ Add a default namespace to a repository name. If the name already contains a namespace, it will be returned unchanged: e.g. "foo/bar" → "foo/bar" otherwise, the loggin will be prepended as namespace: e.g. "bar" → "/bar" """ if not repo.startswith('id:') and repo.find('/') < 0: return self.login + '/' + repo else: return repo @staticmethod def get_keyring_service(service_config): login = service_config.get('login') host = service_config.get('host', default='gitlab.com') return "gitlab://%s@%s" % (login, host) def get_service_metadata(self): return { 'import_labels_as_tags': self.import_labels_as_tags, '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 get_author(self, issue): if issue[1]['author'] != None and issue[1]['author']['username']: return issue[1]['author']['username'] def filter_repos(self, repo): if self.exclude_repos: if repo['path_with_namespace'] in self.exclude_repos or "id:%d" % repo['id'] in self.exclude_repos: return False if self.exclude_regex: if self.exclude_regex.match(repo['path_with_namespace']): return False # fallback if no filter is set is_included = True if self.include_repos: if repo['path_with_namespace'] in self.include_repos or "id:%d" % repo['id'] in self.include_repos: return True else: is_included = False if self.include_regex: if self.include_regex.match(repo['path_with_namespace']): return True else: is_included = False return is_included def _get_notes(self, rid, issue_type, issueid): tmpl = '{scheme}://{host}/api/v4/projects/%d/%s/%d/notes' % (rid, issue_type, issueid) return self._fetch_paged(tmpl) def annotations(self, repo, url, issue_type, issue, issue_obj): annotations = [] if self.annotation_comments: notes = self._get_notes(repo['id'], issue_type, issue['iid']) annotations = (( n['author']['username'], n['body'] ) for n in notes) return self.build_annotations( annotations, issue_obj.get_processed_url(url) ) def _fetch(self, tmpl, **kwargs): url = tmpl.format(scheme=self.scheme, host=self.auth[0]) headers = {'PRIVATE-TOKEN': self.auth[1]} if not self.verify_ssl: requests.packages.urllib3.disable_warnings() response = requests.get(url, headers=headers, verify=self.verify_ssl, **kwargs) return self.json_response(response) def _fetch_paged(self, tmpl): params = { 'page': 1, 'per_page': 100, } 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 return full def get_repo_issues(self, rid): tmpl = '{scheme}://{host}/api/v4/projects/%d/issues?state=opened' % rid issues = {} try: repo_issues = self._fetch_paged(tmpl) except IOError: # Projects may have issues disabled. return {} for issue in repo_issues: issues[issue['id']] = (rid, issue) return issues def get_repo_merge_requests(self, rid): tmpl = '{scheme}://{host}/api/v4/projects/%d/merge_requests?state=opened' % rid issues = {} try: repo_merge_requests = self._fetch_paged(tmpl) except IOError: # Projects may have merge requests disabled. return {} for issue in repo_merge_requests: issues[issue['id']] = (rid, issue) return issues def get_todos(self): tmpl = '{scheme}://{host}/api/v4/todos?state=pending' todos = [] try: fetched_todos = self._fetch_paged(tmpl) except IOError: # Older gitlab versions do not have todo items. return {} for todo in fetched_todos: todos.append((todo.get('project'), todo)) return todos def include_todo(self, repos): ids = list(r['id'] for r in repos) def include_todo(todo): project, todo = todo return project is None or project['id'] in ids return include_todo def _get_issue_objs(self, issues, issue_type, repo_map): type_plural = issue_type + 's' for rid, issue in issues: repo = repo_map[rid] issue['repo'] = repo['path'] projectName = repo['path'] if self.project_owner_prefix: projectName = repo['namespace']['path'] + "." + projectName issue_obj = self.get_issue_for_record(issue) issue_url = '%s/%s/%d' % (repo['web_url'], type_plural, issue['iid']) extra = { 'issue_url': issue_url, 'project': repo['path'], 'namespace': repo['namespace']['full_path'], 'type': issue_type, 'annotations': self.annotations(repo, issue_url, type_plural, issue, issue_obj) } issue_obj.update_extra(extra) yield issue_obj def issues(self): tmpl = '{scheme}://{host}/api/v4/projects' all_repos = [] if self.include_repos and not self.include_regex: for repo in self.include_repos: if repo.startswith("id:"): repo = repo[3:] indiv_tmpl = tmpl + '/' + quote(repo, '') + '?simple=true' item = self._fetch(indiv_tmpl) if not item: break all_repos.append(item) else: querystring = { 'simple': True } if self.membership: querystring['membership'] = True if self.owned: querystring['owned'] = True all_repos = self._fetch_paged(tmpl + '?' + urlencode(querystring)) repos = list(filter(self.filter_repos, all_repos)) repo_map = {} issues = {} for repo in repos: rid = repo['id'] repo_map[rid] = repo issues.update( self.get_repo_issues(rid) ) log.debug(" Found %i issues.", len(issues)) issues = list(filter(self.include, issues.values())) log.debug(" Pruned down to %i issues.", len(issues)) for issue in self._get_issue_objs(issues, 'issue', repo_map): yield issue if not self.filter_merge_requests: merge_requests = {} for repo in repos: rid = repo['id'] merge_requests.update( self.get_repo_merge_requests(rid) ) log.debug(" Found %i merge requests.", len(merge_requests)) merge_requests = list(filter(self.include, merge_requests.values())) log.debug(" Pruned down to %i merge requests.", len(merge_requests)) for issue in self._get_issue_objs(merge_requests, 'merge_request', repo_map): yield issue if self.include_todos: todos = self.get_todos() log.debug(" Found %i todo items.", len(todos)) if not self.include_all_todos: todos = list(filter(self.include_todo(repos), todos)) log.debug(" Pruned down to %i todos.", len(todos)) for project, todo in todos: if project is not None: repo = project else: repo = { 'path': 'the instance', } todo['repo'] = repo['path'] todo_obj = self.get_issue_for_record(todo) todo_url = todo['target_url'] projectName = repo['path'] if self.project_owner_prefix: projectName = repo['namespace']['path'] + "." + projectName extra = { 'issue_url': todo_url, 'project': projectName, 'namespace': "todo", 'type': 'todo', 'annotations': [], } todo_obj.update_extra(extra) yield todo_obj @classmethod def validate_config(cls, service_config, target): if 'host' not in service_config: die("[%s] has no 'gitlab.host'" % target) if 'login' not in service_config: die("[%s] has no 'gitlab.login'" % target) if 'token' not in service_config: die("[%s] has no 'gitlab.token'" % target) super(GitlabService, cls).validate_config(service_config, target) bugwarrior-1.8.0/bugwarrior/services/gmail.py000066400000000000000000000207431376471246600214020ustar00rootroot00000000000000import email import logging import multiprocessing import os import pickle import re import time import googleapiclient.discovery from google.auth.transport.requests import Request from google_auth_oauthlib.flow import InstalledAppFlow from bugwarrior.services import IssueService, Issue log = logging.getLogger(__name__) class GmailIssue(Issue): THREAD_ID = 'gmailthreadid' SUBJECT = 'gmailsubject' URL = 'gmailurl' LAST_SENDER = 'gmaillastsender' LAST_SENDER_ADDR = 'gmaillastsenderaddr' LAST_MESSAGE_ID = 'gmaillastmessageid' SNIPPET = 'gmailsnippet' LABELS = 'gmaillabels' UNIQUE_KEY = (THREAD_ID,) UDAS = { THREAD_ID: { 'type': 'string', 'label': 'GMail Thread Id', }, SUBJECT: { 'type': 'string', 'label': 'GMail Subject', }, URL: { 'type': 'string', 'label': 'GMail URL', }, LAST_SENDER: { 'type': 'string', 'label': 'GMail last sender name', }, LAST_SENDER_ADDR: { 'type': 'string', 'label': 'GMail last sender address', }, LAST_MESSAGE_ID: { 'type': 'string', 'label': 'Last RFC2822 Message-ID', }, SNIPPET: { 'type': 'string', 'label': 'GMail snippet', }, LABELS: { 'type': 'string', 'label': 'GMail labels', }, } EXCLUDE_LABELS = [ 'IMPORTANT', 'CATEGORY_PERSONAL', 'CATEGORY_PROMOTIONS', 'CATEGORY_UPDATES', 'CATEGORY_FORUMS', 'SENT'] def to_taskwarrior(self): return { 'annotations': self.get_annotations(), 'entry': self.get_entry(), 'tags': [label for label in self.extra['labels'] if label not in self.EXCLUDE_LABELS], 'priority': self.origin['default_priority'], self.THREAD_ID: self.record['id'], self.SUBJECT: self.extra['subject'], self.URL: self.extra['url'], self.LAST_SENDER: self.extra['last_sender_name'], self.LAST_SENDER_ADDR: self.extra['last_sender_address'], self.LAST_MESSAGE_ID: self.extra['last_message_id'], self.SNIPPET: self.extra['snippet'], self.LABELS: " ".join(sorted(self.extra['labels'])), } def get_default_description(self): return self.build_default_description( title=self.extra['subject'], url=self.get_processed_url(self.extra['url']), number=self.record['id'], cls='issue', ) def get_annotations(self): return self.extra.get('annotations', []) def get_entry(self): date_string = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(int(self.extra['internal_date']) / 1000)) return self.parse_date(date_string) class GmailService(IssueService): APPLICATION_NAME = 'Bugwarrior Gmail Service' SCOPES = ['https://www.googleapis.com/auth/gmail.readonly'] DEFAULT_CLIENT_SECRET_PATH = '~/.gmail_client_secret.json' ISSUE_CLASS = GmailIssue CONFIG_PREFIX = 'gmail' AUTHENTICATION_LOCK = multiprocessing.Lock() def __init__(self, *args, **kw): super(GmailService, self).__init__(*args, **kw) self.query = self.config.get('query', 'label:Starred') self.login_name = self.config.get('login_name', 'me') self.client_secret_path = self.get_config_path( 'client_secret_path', self.DEFAULT_CLIENT_SECRET_PATH) credentials_name = clean_filename(self.login_name if self.login_name != 'me' else self.target) self.credentials_path = os.path.join( self.config.data.path, 'gmail_credentials_%s.pickle' % (credentials_name,)) self.gmail_api = self.build_api() def get_config_path(self, varname, default_path=None): return os.path.expanduser(self.config.get(varname, default_path)) def build_api(self): credentials = self.get_credentials() return googleapiclient.discovery.build('gmail', 'v1', credentials=credentials, cache_discovery=False) def get_credentials(self): """Gets valid user credentials from storage. If nothing has been stored, or if the stored credentials are invalid, the OAuth2 flow is completed to obtain the new credentials. Returns: Credentials, the obtained credential. """ with self.AUTHENTICATION_LOCK: log.info('Starting authentication for %s', self.target) credentials = None # The self.credentials_path file stores the user's access and refresh # tokens as a pickle, and is created automatically when the # authorization flow completes for the first time. if os.path.exists(self.credentials_path): with open(self.credentials_path, 'rb') as token: credentials = pickle.load(token) # If there are no (valid) credentials available, let the user log in. if not credentials or not credentials.valid: log.info("No valid login. Starting OAUTH flow.") if credentials and credentials.expired and credentials.refresh_token: credentials.refresh(Request()) else: flow = InstalledAppFlow.from_client_secrets_file( self.client_secret_path, self.SCOPES) credentials = flow.run_local_server(port=0) # Save the credentials for the next run with open(self.credentials_path, 'wb') as token: pickle.dump(credentials, token) log.info('Storing credentials to %r', self.credentials_path) return credentials def get_labels(self): result = self.gmail_api.users().labels().list(userId=self.login_name).execute() return {label['id']: label['name'] for label in result['labels']} def get_threads(self): thread_service = self.gmail_api.users().threads() result = thread_service.list(userId=self.login_name, q=self.query).execute() return [ thread_service.get(userId='me', id=thread['id']).execute() for thread in result.get('threads', [])] def annotations(self, issue): sender = issue.extra['last_sender_name'] subj = issue.extra['subject'] issue_url = issue.get_processed_url(issue.extra['url']) return self.build_annotations([(sender, subj)], issue_url) def issues(self): labels = self.get_labels() for thread in self.get_threads(): issue = self.get_issue_for_record(thread, thread_extras(thread, labels)) extra = { 'annotations': self.annotations(issue), } issue.update_extra(extra) yield issue def thread_extras(thread, labels): name, address = thread_last_sender(thread) last_message_id = thread_last_message_id(thread) return { 'internal_date': thread_timestamp(thread), 'labels': [labels[label_id] for label_id in thread_labels(thread)], 'last_sender_address': address, 'last_sender_name': name, 'last_message_id': last_message_id, 'snippet': thread_snippet(thread), 'subject': thread_subject(thread), 'url': thread_url(thread), } def thread_labels(thread): return {label for message in thread['messages'] for label in message['labelIds']} def thread_subject(thread): return message_header(thread['messages'][0], 'Subject') def thread_last_sender(thread): from_header = message_header(thread['messages'][-1], 'From') name, address = email.utils.parseaddr(from_header) return name if name else address, address def thread_last_message_id(thread): message_id_header = message_header(thread['messages'][-1], 'Message-ID') if not message_id_header or message_id_header == '': return '' return message_id_header[1:-1] # remove the enclosing < >. def thread_timestamp(thread): return thread['messages'][-1]['internalDate'] def thread_snippet(thread): return thread['messages'][-1]['snippet'] def thread_url(thread): return "https://mail.google.com/mail/u/0/#all/%s" % (thread['id'],) def message_header(message, header_name): for item in message['payload']['headers']: if item['name'] == header_name: return item['value'] def clean_filename(name): return re.sub(r'[^A-Za-z0-9_]+', '_', name) bugwarrior-1.8.0/bugwarrior/services/jira.py000066400000000000000000000305071376471246600212350ustar00rootroot00000000000000from __future__ import absolute_import from builtins import str import six from jinja2 import Template from jira.client import JIRA as BaseJIRA from requests.cookies import RequestsCookieJar from dateutil.tz.tz import tzutc from bugwarrior.config import asbool, die from bugwarrior.services import IssueService, Issue import logging log = logging.getLogger(__name__) # The below `ObliviousCookieJar` and `JIRA` classes are MIT Licensed. # They were taken from this wonderful commit by @GaretJax # https://github.com/GaretJax/lancet/commit/f175cb2ec9a2135fb78188cf0b9f621b51d88977 # Prevents Jira web client being logged out when API call is made. class ObliviousCookieJar(RequestsCookieJar): def set_cookie(self, *args, **kwargs): """Simply ignore any request to set a cookie.""" pass def copy(self): """Make sure to return an instance of the correct class on copying.""" return ObliviousCookieJar() class JIRA(BaseJIRA): def _create_http_basic_session(self, *args, **kwargs): super(JIRA, self)._create_http_basic_session(*args, **kwargs) # XXX: JIRA logs the web user out if we send the session cookies we get # back from the first request in any subsequent requests. As we don't # need cookies when accessing the API anyway, just ignore all of them. self._session.cookies = ObliviousCookieJar() def close(self): self._session.close() def _parse_sprint_string(sprint): """ Parse the big ugly sprint string stored by JIRA. They look like: com.atlassian.greenhopper.service.sprint.Sprint@4c9c41a5[id=2322,rapid ViewId=1173,state=ACTIVE,name=Sprint 1,startDate=2016-09-06T16:08:07.4 55Z,endDate=2016-09-23T16:08:00.000Z,completeDate=,sequence=2322] """ entries = sprint[sprint.index('[')+1:sprint.index(']')].split('=') fields = sum((entry.rsplit(',', 1) for entry in entries), []) return dict(zip(fields[::2], fields[1::2])) class JiraIssue(Issue): ISSUE_TYPE = 'jiraissuetype' SUMMARY = 'jirasummary' URL = 'jiraurl' FOREIGN_ID = 'jiraid' DESCRIPTION = 'jiradescription' ESTIMATE = 'jiraestimate' FIX_VERSION = 'jirafixversion' CREATED_AT = 'jiracreatedts' STATUS = 'jirastatus' SUBTASKS = 'jirasubtasks' UDAS = { ISSUE_TYPE: { 'type': 'string', 'label': 'Issue Type' }, SUMMARY: { 'type': 'string', 'label': 'Jira Summary' }, URL: { 'type': 'string', 'label': 'Jira URL', }, DESCRIPTION: { 'type': 'string', 'label': 'Jira Description', }, FOREIGN_ID: { 'type': 'string', 'label': 'Jira Issue ID' }, ESTIMATE: { 'type': 'numeric', 'label': 'Estimate' }, FIX_VERSION: { 'type': 'string', 'label': 'Fix Version' }, CREATED_AT: { 'type': 'date', 'label': 'Created At' }, STATUS: { 'type': 'string', 'label': "Jira Status" }, SUBTASKS: { 'type': 'string', 'label': "Jira Subtasks" }, } UNIQUE_KEY = (URL, ) PRIORITY_MAP = { 'Highest': 'H', 'High': 'H', 'Medium': 'M', 'Low': 'L', 'Lowest': 'L', 'Trivial': 'L', 'Minor': 'L', 'Major': 'M', 'Critical': 'H', 'Blocker': 'H', } def to_taskwarrior(self): return { 'project': self.get_project(), 'priority': self.get_priority(), 'annotations': self.get_annotations(), 'tags': self.get_tags(), 'due': self.get_due(), 'entry': self.get_entry(), self.ISSUE_TYPE: self.get_issue_type(), self.URL: self.get_url(), self.FOREIGN_ID: self.record['key'], self.DESCRIPTION: self.record.get('fields', {}).get('description'), self.SUMMARY: self.get_summary(), self.ESTIMATE: self.get_estimate(), self.FIX_VERSION: self.get_fix_version(), self.STATUS: self.get_status(), self.SUBTASKS: self.get_subtasks(), } def get_entry(self): created_at = self.record['fields']['created'] # Convert timestamp to an offset-aware datetime date = self.parse_date(created_at).astimezone(tzutc()).replace(microsecond=0) return date def get_tags(self): return self._get_tags_from_labels() + self._get_tags_from_sprints() def get_due(self): # If the duedate is explicitly set on the issue, then use that. if self.record['fields'].get('duedate'): return self.parse_date(self.record['fields']['duedate']) # Otherwise, if the issue is in a sprint, use the end date of that sprint. sprints = self.__get_sprints() for sprint in filter(lambda e: e.get('state', '').lower() != 'closed', sprints): endDate = sprint['endDate'] if endDate != '': return self.parse_date(endDate) def _get_tags_from_sprints(self): tags = [] if not self.origin['import_sprints_as_tags']: return tags context = self.record.copy() label_template = Template(self.origin['label_template']) sprints = self.__get_sprints() for sprint in sprints: # Extract the name and render it into a label context.update({'label': sprint['name'].replace(' ', '')}) tags.append(label_template.render(context)) return tags def __get_sprints(self): fields = self.record.get('fields', {}) sprints = sum([ fields.get(key) or [] for key in self.origin['sprint_field_names'] ], []) for sprint in sprints: if isinstance(sprint, dict): yield sprint else: # Backward compatibility for oder Jira versions where # python-jira is not able to parse the sprint and returns a # string yield _parse_sprint_string(sprint) def _get_tags_from_labels(self): tags = [] if not self.origin['import_labels_as_tags']: return tags context = self.record.copy() label_template = Template(self.origin['label_template']) for label in self.record.get('fields', {}).get('labels', []): context.update({'label': label}) tags.append(label_template.render(context)) return tags def get_annotations(self): return self.extra.get('annotations', []) def get_project(self): return self.record['key'].rsplit('-', 1)[0] def get_number(self): return self.record['key'].rsplit('-', 1)[1] def get_url(self): return self.origin['url'] + '/browse/' + self.record['key'] def get_summary(self): if self.extra.get('jira_version') == 4: 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') try: value = value['name'] except (TypeError, ): value = str(value) # priority.name format: "1 - Critical" map_key = value.strip().split()[-1] return self.PRIORITY_MAP.get(map_key, self.origin['default_priority']) def get_default_description(self): return self.build_default_description( title=self.get_summary(), url=self.get_processed_url(self.get_url()), number=self.get_number(), cls='issue', ) def get_fix_version(self): try: return self.record['fields'].get('fixVersions', [{}])[0].get('name') except (IndexError, KeyError, AttributeError, TypeError): return None def get_status(self): return self.record['fields']['status']['name'] def get_subtasks(self): return ','.join(task['key'] for task in self.record['fields']['subtasks']) def get_issue_type(self): return self.record['fields']['issuetype']['name'] class JiraService(IssueService): ISSUE_CLASS = JiraIssue CONFIG_PREFIX = 'jira' def __init__(self, *args, **kw): super(JiraService, self).__init__(*args, **kw) self.username = self.config.get('username') self.url = self.config.get('base_uri') password = self.get_password('password', self.username) default_query = 'assignee="' + \ self.username.replace("@", "\\u0040") + \ '" AND resolution is null' self.query = self.config.get('query', default_query) self.use_cookies = self.config.get( 'use_cookies', default=False, to_type=asbool ) if password == '@kerberos': auth = dict(kerberos=True) else: if self.use_cookies: auth = dict(auth=(self.username, password)) else: auth = dict(basic_auth=(self.username, password)) self.jira = JIRA( options={ 'server': self.config.get('base_uri'), 'rest_api_version': 'latest', 'verify': self.config.get('verify_ssl', default=True, to_type=asbool), }, **auth ) self.import_labels_as_tags = self.config.get( 'import_labels_as_tags', default=False, to_type=asbool ) self.import_sprints_as_tags = self.config.get( 'import_sprints_as_tags', default=False, to_type=asbool ) self.label_template = self.config.get( 'label_template', default='{{label}}', to_type=six.text_type ) self.sprint_field_names = [] if self.import_sprints_as_tags: field_names = [field for field in self.jira.fields() if field['name'] == 'Sprint'] if len(field_names) < 1: log.warn("No sprint custom field found. Ignoring sprints.") self.import_sprints_as_tags = False else: log.info("Found %i distinct sprint fields." % len(field_names)) self.sprint_field_names = [field['id'] for field in field_names] @staticmethod def get_keyring_service(service_config): username = service_config.get('username') base_uri = service_config.get('base_uri') return "jira://%s@%s" % (username, base_uri) def get_service_metadata(self): return { 'url': self.url, 'import_labels_as_tags': self.import_labels_as_tags, 'import_sprints_as_tags': self.import_sprints_as_tags, 'sprint_field_names': self.sprint_field_names, 'label_template': self.label_template, } @classmethod def validate_config(cls, service_config, target): for option in ('username', 'password', 'base_uri'): if option not in service_config: die("[%s] has no 'jira.%s'" % (target, option)) IssueService.validate_config(service_config, target) def annotations(self, issue, issue_obj): comments = self.jira.comments(issue.key) or [] return self.build_annotations( (( comment.author.displayName, comment.body ) for comment in comments), issue_obj.get_processed_url(issue_obj.get_url()) ) def issues(self): cases = self.jira.search_issues(self.query, maxResults=None) jira_version = 5 if self.config.has_option(self.target, 'jira.version'): jira_version = self.config.getint(self.target, 'jira.version') for case in cases: issue = self.get_issue_for_record(case.raw) extra = { 'jira_version': jira_version, } if jira_version > 4: extra.update({ 'annotations': self.annotations(case, issue) }) issue.update_extra(extra) yield issue bugwarrior-1.8.0/bugwarrior/services/mplan.py000066400000000000000000000057541376471246600214250ustar00rootroot00000000000000from __future__ import absolute_import import megaplan from bugwarrior.config import die from bugwarrior.services import IssueService, Issue import logging log = logging.getLogger(__name__) class MegaplanIssue(Issue): URL = 'megaplanurl' FOREIGN_ID = 'megaplanid' TITLE = 'megaplantitle' UDAS = { TITLE: { 'type': 'string', 'label': 'Megaplan Title', }, URL: { 'type': 'string', 'label': 'Megaplan URL', }, FOREIGN_ID: { 'type': 'string', 'label': 'Megaplan Issue ID' } } UNIQUE_KEY = (URL, ) def to_taskwarrior(self): return { 'project': self.get_project(), 'priority': self.get_priority(), self.FOREIGN_ID: self.record['Id'], self.URL: self.get_issue_url(), self.TITLE: self.get_issue_title(), } def get_project(self): return self.origin['project_name'] def get_default_description(self): return self.build_default_description( title=self.get_issue_title(), url=self.get_processed_url(self.get_issue_url()), number=self.record['Id'], cls='issue', ) def get_issue_url(self): return "https://%s/task/%d/card/" % ( self.origin['hostname'], self.record["Id"] ) def get_issue_title(self): parts = self.record["Name"].split("|") return parts[-1].strip() def get_issue_id(self): if self.record["Id"] > 1000000: return self.record["Id"] - 1000000 return self.record["Id"] class MegaplanService(IssueService): ISSUE_CLASS = MegaplanIssue CONFIG_PREFIX = 'megaplan' def __init__(self, *args, **kw): super(MegaplanService, self).__init__(*args, **kw) self.hostname = self.config.get('hostname') _login = self.config.get('login') _password = self.get_password('password', _login) self.client = megaplan.Client(self.hostname) self.client.authenticate(_login, _password) self.project_name = self.config.get('project_name', self.hostname) @staticmethod def get_keyring_service(service_config): login = service_config.get('login') hostname = service_config.get('hostname') return "megaplan://%s@%s" % (login, hostname) def get_service_metadata(self): return { 'project_name': self.project_name, 'hostname': self.hostname, } @classmethod def validate_config(cls, service_config, target): for k in ('login', 'password', 'hostname'): if k not in service_config: die("[%s] has no 'mplan.%s'" % (target, k)) IssueService.validate_config(service_config, target) def issues(self): issues = self.client.get_actual_tasks() log.debug(" Found %i total.", len(issues)) for issue in issues: yield self.get_issue_for_record(issue) bugwarrior-1.8.0/bugwarrior/services/pagure.py000066400000000000000000000151771376471246600216010ustar00rootroot00000000000000from builtins import filter import re import six import datetime import pytz import requests from jinja2 import Template from bugwarrior.config import asbool, aslist, die from bugwarrior.services import IssueService, Issue import logging log = logging.getLogger(__name__) 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.session = requests.Session() self.tag = self.config.get('tag') self.repo = self.config.get('repo') self.base_url = self.config.get('base_url') self.exclude_repos = self.config.get('exclude_repos', [], aslist) self.include_repos = self.config.get('include_repos', [], aslist) self.import_tags = self.config.get( 'import_tags', default=False, to_type=asbool ) self.tag_template = self.config.get( '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 = self.session.get(url, params=dict(status='Open')) if not bool(response): error = response.json() code = error['error_code'] if code == 'ETRACKERDISABLED': return [] else: raise IOError('Failed to talk to %r %r' % (url, error)) 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 = self.session.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.debug(" Found %i issues.", len(issues)) issues = list(filter(self.include, issues)) log.debug(" Pruned down to %i 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, service_config, target): if 'tag' not in service_config and 'repo' not in service_config: die("[%s] has no 'pagure.tag' or 'pagure.repo'" % target) if 'base_url' not in service_config: die("[%s] has no 'pagure.base_url'" % target) super(PagureService, cls).validate_config(service_config, target) bugwarrior-1.8.0/bugwarrior/services/phab.py000066400000000000000000000207351376471246600212240ustar00rootroot00000000000000from builtins import str import six from bugwarrior.config import aslist from bugwarrior.services import IssueService, Issue # This comes from PyPI import phabricator import logging log = logging.getLogger(__name__) class PhabricatorIssue(Issue): TITLE = 'phabricatortitle' URL = 'phabricatorurl' TYPE = 'phabricatortype' OBJECT_NAME = 'phabricatorid' UDAS = { TITLE: { 'type': 'string', 'label': 'Phabricator Title', }, URL: { 'type': 'string', 'label': 'Phabricator URL', }, TYPE: { 'type': 'string', 'label': 'Phabricator Type', }, OBJECT_NAME: { 'type': 'string', 'label': 'Phabricator Object', }, } UNIQUE_KEY = (URL, ) PRIORITY_MAP = { 'Needs Triage': None, 'Unbreak Now!': 'H', 'High': 'H', 'Normal': 'M', 'Low': 'L', 'Wishlist': 'L', } def to_taskwarrior(self): return { 'project': self.extra['project'], 'priority': self.priority, 'annotations': self.extra.get('annotations', []), self.URL: self.record['uri'], self.TYPE: self.extra['type'], self.TITLE: self.record['title'], self.OBJECT_NAME: self.record['uri'].split('/')[-1], } def get_default_description(self): return self.build_default_description( title=self.record['title'], url=self.get_processed_url(self.record['uri']), number=self.record['uri'].split('/')[-1], cls=self.extra['type'], ) @property def priority(self): return self.PRIORITY_MAP.get(self.record.get('priority')) \ or self.origin['default_priority'] class PhabricatorService(IssueService): ISSUE_CLASS = PhabricatorIssue CONFIG_PREFIX = 'phabricator' def __init__(self, *args, **kw): super(PhabricatorService, self).__init__(*args, **kw) self.host = self.config.get("host", None) # These read login credentials from ~/.arcrc if self.host is not None: self.api = phabricator.Phabricator(host=self.host) else: self.api = phabricator.Phabricator() self.shown_user_phids = ( self.config.get("user_phids", None, aslist)) self.shown_project_phids = ( self.config.get("project_phids", None, aslist)) only_if_assigned = self.config.get('only_if_assigned', default=False, to_type=lambda x: x not in [False, "False", ""]) self.ignore_cc = self.config.get('ignore_cc', default=only_if_assigned, to_type=lambda x: x == "True") self.ignore_author = self.config.get('ignore_author', default=only_if_assigned, to_type=lambda x: x == "True") self.ignore_owner = self.config.get('ignore_owner', default=False, to_type=lambda x: x == "True") self.ignore_reviewers = self.config.get('ignore_reviewers', default=False, to_type=lambda x: x == "True") def tasks(self): # If self.shown_user_phids or self.shown_project_phids is set, retrict API calls to user_phids or project_phids # to avoid time out with Phabricator installations with huge userbase try: if (self.shown_user_phids is not None) or (self.shown_project_phids is not None): if self.shown_user_phids is not None: tasks_owner = self.api.maniphest.query(status='status-open', ownerPHIDs=self.shown_user_phids) tasks_cc = self.api.maniphest.query(status='status-open', ccPHIDs=self.shown_user_phids) tasks_author = self.api.maniphest.query(status='status-open', authorPHIDs=self.shown_user_phids) tasks = list(tasks_owner.items()) + list(tasks_cc.items()) + list(tasks_author.items()) # Delete duplicates seen = set() tasks = [item for item in tasks if str(item[1]) not in seen and not seen.add(str(item[1]))] if self.shown_project_phids is not None: tasks = self.api.maniphest.query(status='status-open', projectPHIDs=self.shown_project_phids) tasks = tasks.items() else: tasks = self.api.maniphest.query(status='status-open') tasks = tasks.items() except phabricator.APIError as err: log.warn("Could not read tasks from Maniphest: %s" % err) return log.info("Found %i tasks" % len(tasks)) for phid, task in tasks: project = self.target # a sensible default this_task_matches = False if self.shown_user_phids is None and self.shown_project_phids is None: this_task_matches = True if self.shown_user_phids is not None: # Checking whether authorPHID, ccPHIDs, ownerPHID # are intersecting with self.shown_user_phids task_relevant_to = set() if not self.ignore_cc: task_relevant_to.update(task['ccPHIDs']) if not self.ignore_owner: task_relevant_to.add(task['ownerPHID']) if not self.ignore_author: task_relevant_to.add(task['authorPHID']) if len(task_relevant_to.intersection(self.shown_user_phids)) > 0: this_task_matches = True if self.shown_project_phids is not None: # Checking whether projectPHIDs # is intersecting with self.shown_project_phids task_relevant_to = set(task['projectPHIDs']) if len(task_relevant_to.intersection(self.shown_project_phids)) > 0: this_task_matches = True if not this_task_matches: continue extra = { 'project': project, 'type': 'issue', #'annotations': self.annotations(phid, issue) } yield self.get_issue_for_record(task, extra) def revisions(self): try: diffs = self.api.differential.query(status='status-open') except phabricator.APIError as err: log.warn("Could not read revisions from Differential: %s" % err) return diffs = list(diffs) log.info("Found %i differentials" % len(diffs)) for diff in diffs: project = self.target # a sensible default this_diff_matches = False if self.shown_user_phids is None and self.shown_project_phids is None: this_diff_matches = True if self.shown_user_phids is not None: # Checking whether authorPHID, ccPHIDs, reviewers # are intersecting with self.shown_user_phids diff_relevant_to = set() if not self.ignore_reviewers: diff_relevant_to.update(list(diff['reviewers'])) if not self.ignore_cc: diff_relevant_to.update(diff['ccs']) if not self.ignore_author: diff_relevant_to.add(diff['authorPHID']) if len(diff_relevant_to.intersection(self.shown_user_phids)) > 0: this_diff_matches = True if self.shown_project_phids is not None: # Checking whether projectPHIDs # is intersecting with self.shown_project_phids phabricator_projects = [] try: phabricator_projects = diff['phabricator:projects'] except KeyError: pass diff_relevant_to = set(phabricator_projects + [diff['repositoryPHID']]) if len(diff_relevant_to.intersection(self.shown_project_phids)) > 0: this_diff_matches = True if not this_diff_matches: continue extra = { 'project': project, 'type': 'pull_request', #'annotations': self.annotations(phid, issue) } yield self.get_issue_for_record(diff, extra) def issues(self): for issue in self.tasks(): yield issue for issue in self.revisions(): yield issue bugwarrior-1.8.0/bugwarrior/services/pivotaltracker.py000066400000000000000000000274261376471246600233500ustar00rootroot00000000000000from builtins import filter, map import six import re import operator import requests from jinja2 import Template from bugwarrior.config import asbool, aslist, asint, die from bugwarrior.services import IssueService, Issue, ServiceClient import logging log = logging.getLogger(__name__) class PivotalTrackerIssue(Issue): URL = 'pivotalurl' DESCRIPTION = 'pivotaldescription' TYPE = 'pivotalstorytype' PROJECT_ID = 'pivotalprojectid' PROJECT_NAME = 'pivotalprojectname' OWNED_BY = 'pivotalowners' REQUEST_BY = 'pivotalrequesters' FOREIGN_ID = 'pivotalid' ESTIMATE = 'pivotalestimate' BLOCKERS = 'pivotalblockers' CREATED_AT = 'pivotalcreated' UPDATED_AT = 'pivotalupdated' CLOSED_AT = 'pivotalclosed' UDAS = { URL: {'type': 'string', 'label': 'Story URL'}, DESCRIPTION: {'type': 'string', 'label': 'Story Description'}, TYPE: {'type': 'string', 'label': 'Story Type'}, PROJECT_ID: {'type': 'numeric', 'label': 'Project ID'}, PROJECT_NAME: {'type': 'string', 'label': 'Project Name'}, FOREIGN_ID: {'type': 'numeric', 'label': 'Story ID'}, OWNED_BY: {'type': 'string', 'label': 'Story Owned By'}, REQUEST_BY: {'type': 'string', 'label': 'Story Requested By'}, ESTIMATE: {'type': 'numeric', 'label': 'Story Estimate'}, BLOCKERS: {'type': 'string', 'label': 'Story Blockers'}, CREATED_AT: {'type': 'date', 'label': 'Story Created'}, UPDATED_AT: {'type': 'date', 'label': 'Story Updated'}, CLOSED_AT: {'type': 'date', 'label': 'Story Closed'} } UNIQUE_KEY = (URL,) def _normalize_label_to_tag(self, label): return re.sub(r'[^a-zA-Z0-9]', '_', label) def get_owner(self, issue): _, issue = issue return issue.get('pivotalowners') def get_author(self, issue): _, issue = issue return issue.get('pivotalrequesters') def to_taskwarrior(self): description = self.record.get('description') created = self.parse_date(self.record.get('created_at')) modified = self.parse_date(self.record.get('updated_at')) closed = self.parse_date(self.record.get('accepted_at')) return { 'project': self._normalize_label_to_tag(self.extra['project_name']).lower(), 'priority': self.origin['default_priority'], 'annotations': self.extra.get('annotations', []), 'tags': self.get_tags(), self.URL: self.record['url'], self.DESCRIPTION: description, self.TYPE: self.record['story_type'], self.PROJECT_ID: int(self.record['project_id']), self.PROJECT_NAME: self.extra['project_name'], self.FOREIGN_ID: int(self.record['id']), self.OWNED_BY: self.extra['owned_user'], self.REQUEST_BY: self.extra['request_user'], self.ESTIMATE: int(self.record.get('estimate', 0)), self.BLOCKERS: self.extra['blockers'], self.CREATED_AT: created, self.UPDATED_AT: modified, self.CLOSED_AT: closed, } def get_tags(self): tags = [] if not self.origin['import_labels_as_tags']: return tags context = self.record.copy() label_template = Template(self.origin['label_template']) for label in map(operator.itemgetter('name'), self.record.get('labels', [])): context.update({ 'label': self._normalize_label_to_tag(label) }) tags.append( label_template.render(context) ) return tags def get_default_description(self): return self.build_default_description( title=self.record.get('name'), url=self.get_processed_url(self.record.get('url')), number=int(self.record.get('id')), cls=self.record.get('story_type') ) class PivotalTrackerService(IssueService, ServiceClient): ISSUE_CLASS = PivotalTrackerIssue CONFIG_PREFIX = 'pivotaltracker' def __init__(self, *args, **kwargs): super(PivotalTrackerService, self).__init__(*args, **kwargs) self.host=self.config.get('host', 'https://www.pivotaltracker.com/services') self.version = self.config.get('version', 'v5') self.token = self.config.get('token') self.path = "{0}/{1}".format(self.host, self.version) self.session = requests.Session() self.session.headers.update( { 'X-TrackerToken': self.token, 'Content-Type': 'application/json' } ) self.account_ids = self.config.get( 'account_ids', default=[], to_type=aslist) self.user_id = self.config.get('user_id', to_type=asint) self.only_if_assigned = self.config.get( 'only_if_assigned', default=True, to_type=asbool) self.also_unassigned = self.config.get( 'also_unassigned', default=False, to_type=asbool) self.only_if_author = self.config.get( 'only_if_author', default=False, to_type=asbool) self.exclude_stories = self.config.get( 'exclude_stories', default=[], to_type=aslist) self.exclude_projects = self.config.get( 'exclude_projects', default=[], to_type=aslist) self.exclude_tags = self.config.get( 'exclude_tag', default=[], to_type=aslist) self.import_labels_as_tags = self.config.get( 'import_labels_as_tags', default=False, to_type=asbool) self.label_template = self.config.get( 'label_template', default='{{label}}', to_type=six.text_type) self.import_blockers = self.config.get( 'import_blockers', default=True, to_type=asbool) self.blocker_template = self.config.get( 'blocker_template', default='Description: {{description}} Resovled: {{resolved}}\n', to_type=six.text_type) self.annotation_template = self.config.get( 'annotation_template', default='Completed: {{complete}} - {{description}}', to_type=six.text_type) self.query = self.config.get('query', default="", to_type=six.text_type) if not self.query: if self.only_if_assigned and not self.also_unassigned: self.query += "mywork:{user_id}".format(user_id=self.user_id) if self.exclude_stories: self.query += " -id:{stories}".format(stories=",".join(self.exclude_stories)) if self.exclude_tags: self.query += " -label:{labels}".format(labels=",".join(self.exclude_tags)) if self.only_if_author: self.query += " requester:{user_id}".format(user_id=self.user_id) @classmethod def validate_config(cls, service_config, target): required = ['user_id', 'token', 'account_ids'] for item in required: if item not in service_config: die("[{0}] has no 'pivotaltracker.{1}'".format(target, item)) if service_config.get('version', default='v5') not in ['v5', 'edge']: die("[%s] has an invalid 'pivotaltracker.version'" % target) super(PivotalTrackerService, cls).validate_config(service_config, target) def get_service_metadata(self): return { 'import_labels_as_tags': self.import_labels_as_tags, 'label_template': self.label_template, 'annotation_template': self.annotation_template, 'import_blockers': self.import_blockers, 'blocker_template': self.blocker_template } def annotations(self, annotations, story): final_annotations = [] if self.annotation_comments: annotation_template = Template(self.annotation_template) for annotation in annotations: final_annotations.append( ('task', annotation_template.render(annotation)) ) return self.build_annotations( final_annotations, story.get('url') ) def blockers(self, blocker_list): blockers = [] if not self.import_blockers: return blockers blocker_template = Template(self.blocker_template) for blocker in blocker_list: blockers.append( blocker_template.render(blocker) ) return ', '.join(blockers) or None def issues(self): for project in self.get_projects(self.account_ids): project_id = project.get('id') if project_id not in self.exclude_projects: for story in self.get_query(project_id, query=self.query): story_id = story.get('id') tasks = self.get_tasks( project_id, story_id ) blockers = self.get_blockers( project_id, story_id ) extra = { 'project_name': project.get('name'), 'annotations': self.annotations( tasks, story ), 'owned_user': self.get_user_by_id( project_id, story['owner_ids'] ), 'request_user': self.get_user_by_id( project_id, [story['requested_by_id']] ), 'blockers': self.blockers(blockers) } yield self.get_issue_for_record(story, extra) def api_request(self, endpoint, params={}): """ Make a PivotalTracker API request. This takes an absolute urland a list of argumnets and return a GET request with the key and token from the configuration. """ subkey = params.pop('subkey', None) url = "{path}/{endpoint}".format( path=self.path, endpoint=endpoint ) response = self.session.get(url, params=params) json_res = self.json_response(response) if subkey is not None: json_res = json_res[subkey] return json_res def get_projects(self, account_ids): params = { 'account_ids': ','.join(account_ids) } projects = self.api_request( 'projects', params=params) return projects def get_query(self, project_id, **params): params['subkey'] = 'stories' query = self.api_request( "projects/{project_id}/search".format(project_id=project_id), params=params) return query['stories'] def get_tasks(self, project_id, story_id): tasks = self.api_request( "projects/{project_id}/stories/{story_id}/tasks".format( project_id=project_id, story_id=story_id)) return tasks def get_blockers(self, project_id, story_id): blockers = self.api_request( "projects/{project_id}/stories/{story_id}/blockers".format( project_id=project_id, story_id=story_id) ) blocker_results = [] for blocker in blockers: blocker['users'] = self.get_user_by_id( project_id, [blocker['person_id']] ) blocker_results.append(blocker) return blocker_results def get_user_by_id(self, project_id, user_ids): persons = self.api_request( "projects/{project_id}/memberships".format( project_id=project_id)) user_list = filter( lambda x: x.get('id') in user_ids, map(operator.itemgetter('person'), persons) ) return ', '.join(list(map(operator.itemgetter('username'), user_list))) or None bugwarrior-1.8.0/bugwarrior/services/redmine.py000066400000000000000000000213241376471246600217300ustar00rootroot00000000000000import six import requests import re from bugwarrior.config import die, asbool from bugwarrior.services import Issue, IssueService, ServiceClient from taskw import TaskWarriorShellout import logging log = logging.getLogger(__name__) class RedMineClient(ServiceClient): def __init__(self, url, key, auth, issue_limit, verify_ssl): self.url = url self.key = key self.auth = auth self.issue_limit = issue_limit self.verify_ssl = verify_ssl def find_issues(self, issue_limit=100, only_if_assigned=False): args = {} # TODO: if issue_limit is greater than 100, implement pagination to return all issues. # Leave the implementation of this to the unlucky soul with >100 issues assigned to them. if issue_limit is not None: args["limit"] = issue_limit if only_if_assigned: args["assigned_to_id"] = 'me' return self.call_api("/issues.json", args)["issues"] def call_api(self, uri, params): url = self.url.rstrip("/") + uri kwargs = { 'headers': {'X-Redmine-API-Key': self.key}, 'params': params} if self.auth: kwargs['auth'] = self.auth kwargs['verify'] = self.verify_ssl return self.json_response(requests.get(url, **kwargs)) class RedMineIssue(Issue): URL = 'redmineurl' SUBJECT = 'redminesubject' ID = 'redmineid' DESCRIPTION = 'redminedescription' TRACKER = 'redminetracker' STATUS = 'redminestatus' AUTHOR = 'redmineauthor' CATEGORY = 'redminecategory' START_DATE = 'redminestartdate' SPENT_HOURS = 'redminespenthours' ESTIMATED_HOURS = 'redmineestimatedhours' CREATED_ON = 'redminecreatedon' UPDATED_ON = 'redmineupdatedon' DUEDATE = 'redmineduedate' ASSIGNED_TO = 'redmineassignedto' PROJECT_NAME = 'redmineprojectname' UDAS = { URL: { 'type': 'string', 'label': 'Redmine URL', }, SUBJECT: { 'type': 'string', 'label': 'Redmine Subject', }, ID: { 'type': 'numeric', 'label': 'Redmine ID', }, DESCRIPTION: { 'type': 'string', 'label': 'Redmine Description', }, TRACKER: { 'type': 'string', 'label': 'Redmine Tracker', }, STATUS: { 'type': 'string', 'label': 'Redmine Status', }, AUTHOR: { 'type': 'string', 'label': 'Redmine Author', }, CATEGORY: { 'type': 'string', 'label': 'Redmine Category', }, START_DATE: { 'type': 'date', 'label': 'Redmine Start Date', }, SPENT_HOURS: { 'type': 'duration', 'label': 'Redmine Spent Hours', }, ESTIMATED_HOURS: { 'type': 'duration', 'label': 'Redmine Estimated Hours', }, CREATED_ON: { 'type': 'date', 'label': 'Redmine Created On', }, UPDATED_ON: { 'type': 'date', 'label': 'Redmine Updated On', }, DUEDATE: { 'type': 'date', 'label': 'Redmine Due Date' }, ASSIGNED_TO: { 'type': 'string', 'label': 'Redmine Assigned To', }, PROJECT_NAME: { 'type': 'string', 'label': 'Redmine Project', }, } UNIQUE_KEY = (ID, ) PRIORITY_MAP = { 'Low': 'L', 'Normal': 'M', 'High': 'H', 'Urgent': 'H', 'Immediate': 'H', } def to_taskwarrior(self): due_date = self.record.get('due_date') start_date = self.record.get('start_date') updated_on = self.record.get('updated_on') created_on = self.record.get('created_on') spent_hours = self.record.get('spent_hours') estimated_hours = self.record.get('estimated_hours') category = self.record.get('category') assigned_to = self.record.get('assigned_to') if due_date: due_date = self.parse_date(due_date).replace(microsecond=0) if start_date: start_date = self.parse_date(start_date).replace(microsecond=0) if updated_on: updated_on = self.parse_date(updated_on).replace(microsecond=0) if created_on: created_on = self.parse_date(created_on).replace(microsecond=0) if spent_hours: spent_hours = str(spent_hours) + ' hours' spent_hours = self.get_converted_hours(spent_hours) if estimated_hours: estimated_hours = str(estimated_hours) + ' hours' estimated_hours = self.get_converted_hours(estimated_hours) if category: category = category['name'] if assigned_to: assigned_to = assigned_to['name'] return { 'project': self.get_project_name(), 'annotations': self.extra.get('annotations', []), 'priority': self.get_priority(), self.URL: self.get_issue_url(), self.SUBJECT: self.record['subject'], self.ID: self.record['id'], self.DESCRIPTION: self.record.get('description', ''), self.TRACKER: self.record['tracker']['name'], self.STATUS: self.record['status']['name'], self.AUTHOR: self.record['author']['name'], self.PROJECT_NAME: self.record['project']['name'], self.ASSIGNED_TO: assigned_to, self.CATEGORY: category, self.START_DATE: start_date, self.CREATED_ON: created_on, self.UPDATED_ON: updated_on, self.DUEDATE: due_date, self.ESTIMATED_HOURS: estimated_hours, self.SPENT_HOURS: spent_hours, } def get_priority(self): return self.PRIORITY_MAP.get( self.record.get('priority', {}).get('Name'), self.origin['default_priority'] ) def get_issue_url(self): return ( self.origin['url'] + "/issues/" + six.text_type(self.record["id"]) ) def get_converted_hours(self, estimated_hours): tw = TaskWarriorShellout() calc = tw._execute('calc', estimated_hours) return ( calc[0].rstrip() ) def get_project_name(self): if self.origin['project_name']: return self.origin['project_name'] # TODO: It would be nice to use the project slug (if the Redmine # instance supports it), but this would require (1) an API call # to get the list of projects, and then a look up between the # project ID contained in self.record and the list of projects. return re.sub(r'[^a-zA-Z0-9]', '', self.record["project"]["name"]).lower() def get_default_description(self): return self.build_default_description( title=self.record['subject'], url=self.get_processed_url(self.get_issue_url()), number=self.record['id'], cls='issue', ) class RedMineService(IssueService): ISSUE_CLASS = RedMineIssue CONFIG_PREFIX = 'redmine' def __init__(self, *args, **kw): super(RedMineService, self).__init__(*args, **kw) self.url = self.config.get('url').rstrip("/") self.key = self.get_password('key') self.issue_limit = self.config.get('issue_limit') self.verify_ssl = self.config.get( 'verify_ssl', default=True, to_type=asbool ) login = self.config.get('login') if login: password = self.get_password('password', login) auth = (login, password) if (login and password) else None self.client = RedMineClient(self.url, self.key, auth, self.issue_limit, self.verify_ssl) self.project_name = self.config.get('project_name') def get_service_metadata(self): return { 'project_name': self.project_name, 'url': self.url, } @staticmethod def get_keyring_service(service_config): url = service_config.get('url') login = service_config.get('login') return "redmine://%s@%s/" % (login, url) @classmethod def validate_config(cls, service_config, target): for k in ('url', 'key'): if k not in service_config: die("[%s] has no 'redmine.%s'" % (target, k)) IssueService.validate_config(service_config, target) def issues(self): only_if_assigned = self.config.get('only_if_assigned', False) issues = self.client.find_issues(self.issue_limit, only_if_assigned) log.debug(" Found %i total.", len(issues)) for issue in issues: yield self.get_issue_for_record(issue) bugwarrior-1.8.0/bugwarrior/services/taiga.py000066400000000000000000000114241376471246600213720ustar00rootroot00000000000000from __future__ import absolute_import import requests import six from bugwarrior.db import CACHE_REGION as cache from bugwarrior.config import die from bugwarrior.services import IssueService, Issue, ServiceClient import logging log = logging.getLogger(__name__) class TaigaIssue(Issue): SUMMARY = 'taigasummary' URL = 'taigaurl' FOREIGN_ID = 'taigaid' UDAS = { SUMMARY: { 'type': 'string', 'label': 'Taiga Summary' }, URL: { 'type': 'string', 'label': 'Taiga URL', }, FOREIGN_ID: { 'type': 'numeric', 'label': 'Taiga Issue ID' }, } UNIQUE_KEY = (URL, ) def to_taskwarrior(self): return { 'project': self.extra['project'], 'annotations': self.extra['annotations'], self.URL: self.extra['url'], 'priority': self.origin['default_priority'], 'tags': self.get_tags(), self.FOREIGN_ID: self.record['ref'], self.SUMMARY: self.record['subject'], } def get_tags(self): return [x if isinstance(x, six.string_types) else x[0] for x in self.record['tags']] def get_default_description(self): return self.build_default_description( title=self.record['subject'], url=self.get_processed_url(self.extra['url']), number=self.record['ref'], cls='issue', ) class TaigaService(IssueService, ServiceClient): ISSUE_CLASS = TaigaIssue CONFIG_PREFIX = 'taiga' def __init__(self, *args, **kw): super(TaigaService, self).__init__(*args, **kw) self.url = self.config.get('base_uri') self.include_tasks = self.config.get('include_tasks', default=False) self.auth_token = self.get_password('auth_token') self.label_template = self.config.get( 'label_template', default='{{label}}', to_type=six.text_type ) self.session = requests.session() self.session.headers.update({ 'Accept': 'application/json', 'Authorization': 'Bearer %s' % self.auth_token, }) @staticmethod def get_keyring_service(service_config): base_uri = service_config.get('base_uri') return "taiga://%s" % base_uri def get_service_metadata(self): return { 'url': self.url, 'label_template': self.label_template, } @classmethod def validate_config(cls, service_config, target): for option in ('auth_token', 'base_uri'): if option not in service_config: die("[%s] has no 'taiga.%s'" % (target, option)) IssueService.validate_config(service_config, target) def _issues(self, userid, task_type, task_type_plural, task_type_short): log.debug('Getting %s' % task_type_plural) response = self.session.get( self.url + '/api/v1/' + task_type_plural, params={'assigned_to': userid, 'status__is_closed': "false"}) tasks = response.json() for task in tasks: project = self.get_project(task['project']) extra = { 'project': project['slug'], 'annotations': self.annotations(task, project, task_type, task_type_short), 'url': self.build_url(task, project, task_type_short), } yield self.get_issue_for_record(task, extra) def issues(self): url = self.url + '/api/v1/users/me' me = self.session.get(url) data = me.json() # Check for errors and bail if we failed. if '_error_message' in data: raise RuntimeError("{_error_type} {_error_message}".format(**data)) # Otherwise, proceed. userid = data['id'] for issue in self._issues(userid, 'userstory', 'userstories', 'us'): yield issue if self.include_tasks: for issue in self._issues(userid, 'task', 'tasks', 'task'): yield issue @cache.cache_on_arguments() def get_project(self, project_id): url = '%s/api/v1/projects/%i' % (self.url, project_id) return self.json_response(self.session.get(url)) def build_url(self, task, project, task_type): return '%s/project/%s/%s/%i' % (self.url, project['slug'], task_type, task['ref']) def annotations(self, task, project, task_type, task_type_short): url = '%s/api/v1/history/%s/%i' % (self.url, task_type, task['id']) response = self.session.get(url) history = response.json() return self.build_annotations( (( item['user']['username'], item['comment'], ) for item in history if item['comment']), self.build_url(task, project, task_type_short) ) bugwarrior-1.8.0/bugwarrior/services/teamlab.py000066400000000000000000000103621376471246600217120ustar00rootroot00000000000000import six import requests from bugwarrior.config import die from bugwarrior.services import Issue, IssueService, ServiceClient import logging log = logging.getLogger(__name__) class TeamLabClient(ServiceClient): def __init__(self, hostname, verbose=False): self.hostname = hostname self.verbose = verbose self.token = None def authenticate(self, login, password): resp = self.call_api("/api/1.0/authentication.json", post={ "userName": six.text_type(login), "password": six.text_type(password), }) self.token = six.text_type(resp["token"]) def get_task_list(self): resp = self.call_api("/api/1.0/project/task/@self.json") return resp def call_api(self, uri, post=None, params=None): uri = "http://" + self.hostname + uri kwargs = {'params': params} if self.token: kwargs['headers'] = {'Authorization': self.token} response = (requests.post(uri, data=post, **kwargs) if post else requests.get(uri, **kwargs)) return self.json_response(response) class TeamLabIssue(Issue): URL = 'teamlaburl' FOREIGN_ID = 'teamlabid' TITLE = 'teamlabtitle' PROJECTOWNER_ID = 'teamlabprojectownerid' UDAS = { URL: { 'type': 'string', 'label': 'Teamlab URL', }, FOREIGN_ID: { 'type': 'string', 'label': 'Teamlab ID', }, TITLE: { 'type': 'string', 'label': 'Teamlab Title', }, PROJECTOWNER_ID: { 'type': 'string', 'label': 'Teamlab ProjectOwner ID', } } UNIQUE_KEY = (URL, ) def to_taskwarrior(self): return { 'project': self.get_project(), 'priority': self.get_priority(), self.TITLE: self.record['title'], self.FOREIGN_ID: self.record['id'], self.URL: self.get_issue_url(), self.PROJECTOWNER_ID: self.record['projectOwner']['id'], } def get_default_description(self): return self.build_default_description( title=self.record['title'], url=self.get_processed_url(self.get_issue_url()), number=self.record['id'], cls='issue', ) def get_project(self): return self.origin['project_name'] def get_issue_url(self): return "http://%s/products/projects/tasks.aspx?prjID=%d&id=%d" % ( self.origin['hostname'], self.record["projectOwner"]["id"], self.record["id"] ) def get_priority(self): if self.record.get("priority") == 1: return "H" return self.origin['default_priority'] class TeamLabService(IssueService): ISSUE_CLASS = TeamLabIssue CONFIG_PREFIX = 'teamlab' def __init__(self, *args, **kw): super(TeamLabService, self).__init__(*args, **kw) self.hostname = self.config.get('hostname') _login = self.config.get('login') _password = self.get_password('password', _login) self.client = TeamLabClient(self.hostname) self.client.authenticate(_login, _password) self.project_name = self.config.get('project_name', self.hostname) @staticmethod def get_keyring_service(service_config): login = service_config.get('login') hostname = service_config.get('hostname') return "teamlab://%s@%s" % (login, hostname) def get_service_metadata(self): return { 'hostname': self.hostname, 'project_name': self.project_name, } @classmethod def validate_config(cls, service_config, target): for k in ('login', 'password', 'hostname'): if k not in service_config: die("[%s] has no 'teamlab.%s'" % (target, k)) IssueService.validate_config(service_config, target) def issues(self): issues = self.client.get_task_list() log.debug(" Remote has %i total issues.", len(issues)) # Filter out closed tasks. issues = [i for i in issues if i["status"] == 1] log.debug(" Remote has %i active issues.", len(issues)) for issue in issues: yield self.get_issue_for_record(issue) bugwarrior-1.8.0/bugwarrior/services/teamwork_projects.py000066400000000000000000000131701376471246600240470ustar00rootroot00000000000000from builtins import filter import re import six from urllib.parse import urlparse import requests from six.moves.urllib.parse import quote_plus from jinja2 import Template from bugwarrior.config import asbool, aslist, die from bugwarrior.services import IssueService, Issue, ServiceClient import logging log = logging.getLogger(__name__) class TeamworkClient(ServiceClient): def __init__(self, host, token): self.host = host self.token = token def authenticate(self): response = requests.get(self.host + "/authenticate.json", auth=(self.token, "")) return self.json_response(response) def call_api(self, method, endpoint, data=None): response = requests.get(self.host + endpoint, auth=(self.token, ""), params=data) return self.json_response(response) class TeamworkIssue(Issue): URL = 'teamwork_url' TITLE = 'teamwork_title' DESCRIPTION_LONG = 'teamwork_description_long' PROJECT_ID = 'teamwork_project_id' STATUS = 'teamwork_status' ID = 'teamwork_id' UDAS = { URL: { 'type': 'string', 'label': 'Teamwork Url', }, TITLE: { 'type': 'string', 'label': 'Teamwork Title', }, DESCRIPTION_LONG: { 'type': 'string', 'label': 'Teamwork Description Long', }, PROJECT_ID: { 'type': 'numeric', 'label': 'Teamwork Project ID', }, STATUS: { 'type': 'string', 'label': 'Teamwork Status', }, ID: { 'type': 'numeric', 'label': 'Teamwork Task ID', }, } UNIQUE_KEY = (URL, ) PRIORITY_MAP = { "low": "L", "medium": "M", "high": "H" } def get_owner(self, issue): if issue: if self.record.get("responsible-party-ids", ""): if self.user_id in self.record.get("responsible-party-ids", ""): return self.name def get_author(self, issue): if issue: author = self.record["creator-firstname"] + " " + self.record["creator-lastname"] return author def get_task_url(self): return self.extra["host"] + "/#/tasks/" + str(self.record["id"]) def get_default_description(self): return self.build_default_description( title=self.record["content"], url=self.get_task_url(), number=self.record["id"], ) def to_taskwarrior(self): task_url = self.get_task_url() description = self.record.get("content", "") parent_id = self.record["parentTaskId"] status = self.record["status"] due = self.parse_date(self.record.get('due-date')) created = self.parse_date(self.record.get('created-on')) modified = self.parse_date(self.record.get('last-changed-on')) end = "" if str(status) in ["reopened", "new"]: status = "Open" else: end = modified status = "Closed" return { 'project': self.record["project-name"], 'priority': self.get_priority(), 'due': due, 'entry': created, 'end': end, 'modified': modified, 'annotations': self.extra.get('annotations', []), self.URL: task_url, self.TITLE: self.record.get("content", ""), self.DESCRIPTION_LONG: self.record.get("description", ""), self.PROJECT_ID: int(self.record["project-id"]), self.STATUS: status, self.ID: int(self.record["id"]), } class TeamworkService(IssueService): ISSUE_CLASS = TeamworkIssue CONFIG_PREFIX = 'teamwork_projects' def __init__(self, *args, **kwargs): super(TeamworkService, self).__init__(*args, **kwargs) self.host = self.config.get('host', '') self.token = self.config.get('token', '') self.client = TeamworkClient(self.host, self.token) user = self.client.authenticate() self.user_id = user["account"]["userId"] self.name = user["account"]["firstname"] + " " + user["account"]["lastname"] def get_comments(self, issue): if self.annotation_comments: if issue.get("comments-count", 0) > 0: endpoint = "/tasks/{task_id}/comments.json".format(task_id=issue["id"]) comments = self.client.call_api("GET", endpoint) comment_list = [] for comment in comments["comments"]: url = self.host + "/#/tasks/" + str(issue["id"]) author = "{first} {last}".format( first=comment["author-firstname"], last=comment["author-lastname"], ) text = comment["body"] comment_list.append((author, text)) return self.build_annotations(comment_list, None) return [] def issues(self): response = self.client.call_api("GET", "/tasks.json")#, data= { "responsible-party-ids": self.user_id }) for issue in response["todo-items"]: # Determine if issue is need by if following comments, changes or assigned if issue["userFollowingComments"] or issue["userFollowingChanges"]\ or (self.user_id in issue.get("responsible-party-ids", "")): issue_obj = self.get_issue_for_record(issue) extra = { "host": self.host, 'annotations': self.get_comments(issue), } issue_obj.update_extra(extra) yield issue_obj bugwarrior-1.8.0/bugwarrior/services/trac.py000066400000000000000000000135141376471246600212400ustar00rootroot00000000000000from future import standard_library standard_library.install_aliases() from builtins import filter from builtins import map from builtins import range import offtrac import csv import io as StringIO import requests import urllib.request, urllib.parse, urllib.error from bugwarrior.config import die, asbool from bugwarrior.services import Issue, IssueService import logging log = logging.getLogger(__name__) class TracIssue(Issue): SUMMARY = 'tracsummary' URL = 'tracurl' NUMBER = 'tracnumber' COMPONENT = 'traccomponent' UDAS = { SUMMARY: { 'type': 'string', 'label': 'Trac Summary', }, URL: { 'type': 'string', 'label': 'Trac URL', }, NUMBER: { 'type': 'numeric', 'label': 'Trac Number', }, COMPONENT: { 'type': 'string', 'label': 'Trac Component', }, } UNIQUE_KEY = (URL, ) PRIORITY_MAP = { 'trivial': 'L', 'minor': 'L', 'major': 'M', 'critical': 'H', 'blocker': 'H', } def to_taskwarrior(self): return { 'project': self.extra['project'], 'priority': self.get_priority(), 'annotations': self.extra['annotations'], self.URL: self.record['url'], self.SUMMARY: self.record['summary'], self.NUMBER: self.record['number'], self.COMPONENT: self.record['component'], } def get_default_description(self): if 'number' in self.record: number = self.record['number'] else: number = self.record['id'] return self.build_default_description( title=self.record['summary'], url=self.get_processed_url(self.record['url']), number=number, cls='issue' ) def get_priority(self): return self.PRIORITY_MAP.get( self.record.get('priority'), self.origin['default_priority'] ) class TracService(IssueService): ISSUE_CLASS = TracIssue CONFIG_PREFIX = 'trac' def __init__(self, *args, **kw): super(TracService, self).__init__(*args, **kw) base_uri = self.config.get('base_uri') scheme = self.config.get('scheme', default='https') username = self.config.get('username', default=None) if username: password = self.get_password('password', username) auth = urllib.parse.quote_plus('%s:%s' % (username, password)) + '@' else: auth = '' if self.config.get('no_xmlrpc', default=False, to_type=asbool): uri = '%s://%s%s/' % (scheme, auth, base_uri) self.uri = uri self.trac = None else: uri = '%s://%s%s/login/xmlrpc' % (scheme, auth, base_uri) self.trac = offtrac.TracServer(uri) @staticmethod def get_keyring_service(service_config): username = service_config.get('username') base_uri = service_config.get('base_uri') return "https://%s@%s/" % (username, base_uri) @classmethod def validate_config(cls, service_config, target): if 'base_uri' not in service_config: die("[%s] has no 'base_uri'" % target) elif '://' in service_config.get('base_uri'): die("[%s] do not include scheme in 'base_uri'" % target) IssueService.validate_config(service_config, target) def annotations(self, tag, issue, issue_obj): annotations = [] # without offtrac, we can't get issue comments if self.trac is None: return annotations changelog = self.trac.server.ticket.changeLog(issue['number']) for time, author, field, oldvalue, newvalue, permament in changelog: if field == 'comment': annotations.append((author, newvalue, )) url = issue_obj.get_processed_url(issue['url']) return self.build_annotations(annotations, url) def get_owner(self, issue): tag, issue = issue return issue.get('owner', None) or None def issues(self): base_url = "https://" + self.config.get('base_uri') if self.trac: tickets = self.trac.query_tickets('status!=closed&max=0') tickets = list(map(self.trac.get_ticket, tickets)) issues = [(self.target, ticket[3]) for ticket in tickets] for i in range(len(issues)): issues[i][1]['url'] = "%s/ticket/%i" % (base_url, tickets[i][0]) issues[i][1]['number'] = tickets[i][0] else: resp = requests.get( self.uri + 'query', params={ 'status': '!closed', 'max': '0', 'format': 'csv', 'col': ['id', 'summary', 'owner', 'priority', 'component'], }) if resp.status_code != 200: raise RuntimeError("Trac responded with %s" % resp) # strip Trac's bogus BOM text = resp.text[1:].lstrip(u'\ufeff') tickets = list(csv.DictReader(StringIO.StringIO(text.encode('utf-8')))) issues = [(self.target, ticket) for ticket in tickets] for i in range(len(issues)): issues[i][1]['url'] = "%s/ticket/%s" % (base_url, tickets[i]['id']) issues[i][1]['number'] = int(tickets[i]['id']) log.debug(" Found %i total.", len(issues)) issues = list(filter(self.include, issues)) log.debug(" Pruned down to %i", len(issues)) for project, issue in issues: issue_obj = self.get_issue_for_record(issue) extra = { 'annotations': self.annotations(project, issue, issue_obj), 'project': project, } issue_obj.update_extra(extra) yield issue_obj bugwarrior-1.8.0/bugwarrior/services/trello.py000066400000000000000000000177061376471246600216170ustar00rootroot00000000000000""" Trello service Pulls trello cards as tasks. Trello API documentation available at https://developers.trello.com/ """ from __future__ import unicode_literals from future import standard_library standard_library.install_aliases() from six.moves.configparser import NoOptionError from jinja2 import Template import requests from bugwarrior.services import IssueService, Issue, ServiceClient from bugwarrior.config import die, asbool, aslist DEFAULT_LABEL_TEMPLATE = "{{label|replace(' ', '_')}}" class TrelloIssue(Issue): NAME = 'trellocard' CARDID = 'trellocardid' SHORTCARDID = 'trellocardidshort' DESCRIPTION = 'trellodescription' BOARD = 'trelloboard' LIST = 'trellolist' SHORTLINK = 'trelloshortlink' SHORTURL = 'trelloshorturl' URL = 'trellourl' UDAS = { NAME: {'type': 'string', 'label': 'Trello card name'}, CARDID: {'type': 'string', 'label': 'Trello card ID'}, SHORTCARDID: {'type': 'numeric', 'label': 'Trello short card ID'}, DESCRIPTION: {'type': 'string', 'label': 'Trello description'}, BOARD: {'type': 'string', 'label': 'Trello board name'}, LIST: {'type': 'string', 'label': 'Trello list name'}, SHORTLINK: {'type': 'string', 'label': 'Trello shortlink'}, SHORTURL: {'type': 'string', 'label': 'Trello short URL'}, URL: {'type': 'string', 'label': 'Trello URL'}, } UNIQUE_KEY = (CARDID,) def get_default_description(self): """ Return the old-style verbose description from bugwarrior. """ return self.build_default_description( title=self.record['name'], url=self.record['shortUrl'], number=self.record['idShort'], cls='task', ) def get_tags(self, twdict): tmpl = Template( self.origin.get('label_template', DEFAULT_LABEL_TEMPLATE)) return [tmpl.render(twdict, label=label['name']) for label in self.record['labels']] def to_taskwarrior(self): twdict = { 'project': self.extra['boardname'], 'due': self.parse_date(self.record['due']), 'priority': self.origin['default_priority'], self.NAME: self.record['name'], self.CARDID: self.record['id'], self.SHORTCARDID: self.record['idShort'], self.DESCRIPTION: self.record['desc'], self.BOARD: self.extra['boardname'], self.LIST: self.extra['listname'], self.SHORTLINK: self.record['shortLink'], self.SHORTURL: self.record['shortUrl'], self.URL: self.record['url'], 'annotations': self.extra.get('annotations', []), } if self.origin['import_labels_as_tags']: twdict['tags'] = self.get_tags(twdict) return twdict class TrelloService(IssueService, ServiceClient): ISSUE_CLASS = TrelloIssue # What prefix should we use for this service's configuration values CONFIG_PREFIX = 'trello' @classmethod def validate_config(cls, service_config, target): def check_key(opt): """ Check that the given key exist in the configuration """ if opt not in service_config: die("[{}] has no 'trello.{}'".format(target, opt)) super(TrelloService, cls).validate_config(service_config, target) check_key('token') check_key('api_key') @staticmethod def get_keyring_service(service_config): api_key = service_config.get('api_key') return "trello://{api_key}@trello.com".format(api_key=api_key) def get_service_metadata(self): """ Return extra config options to be passed to the TrelloIssue class """ return { 'import_labels_as_tags': self.config.get('import_labels_as_tags', False, asbool), 'label_template': self.config.get('label_template', DEFAULT_LABEL_TEMPLATE), } def issues(self): """ Returns a list of dicts representing issues from a remote service. """ for board in self.get_boards(): for lst in self.get_lists(board['id']): listextra = dict(boardname=board['name'], listname=lst['name']) for card in self.get_cards(lst['id']): issue = self.get_issue_for_record(card, extra=listextra) issue.update_extra({"annotations": self.annotations(card)}) yield issue def annotations(self, card_json): """ A wrapper around get_comments that build the taskwarrior annotations. """ comments = self.get_comments(card_json['id']) annotations = self.build_annotations( ((c['memberCreator']['username'], c['data']['text']) for c in comments), card_json["shortUrl"]) return annotations def get_boards(self): """ Get the list of boards to pull cards from. If the user gave a value to trello.include_boards use that, otherwise ask the Trello API for the user's boards. """ if 'include_boards' in self.config: for boardid in self.config.get('include_boards', to_type=aslist): # Get the board name yield self.api_request( "/1/boards/{id}".format(id=boardid), fields='name') else: boards = self.api_request("/1/members/me/boards", fields='name') for board in boards: yield board def get_lists(self, board): """ Returns a list of the filtered lists for the given board This filters the trello lists according to the configuration values of trello.include_lists and trello.exclude_lists. """ lists = self.api_request( "/1/boards/{board_id}/lists/open".format(board_id=board), fields='name') include_lists = self.config.get('include_lists', to_type=aslist) if include_lists: lists = [l for l in lists if l['name'] in include_lists] exclude_lists = self.config.get('exclude_lists', to_type=aslist) if exclude_lists: lists = [l for l in lists if l['name'] not in exclude_lists] return lists def get_cards(self, list_id): """ Returns an iterator for the cards in a given list, filtered according to configuration values of trello.only_if_assigned and trello.also_unassigned """ params = {'fields': 'name,idShort,shortLink,shortUrl,url,labels,due,desc'} member = self.config.get('only_if_assigned', None) unassigned = self.config.get('also_unassigned', False, asbool) if member is not None: params['members'] = 'true' params['member_fields'] = 'username' cards = self.api_request( "/1/lists/{list_id}/cards/open".format(list_id=list_id), **params) for card in cards: if (member is None or member in [m['username'] for m in card['members']] or (unassigned and not card['members'])): yield card def get_comments(self, card_id): """ Returns an iterator for the comments on a certain card. """ params = {'filter': 'commentCard', 'memberCreator_fields': 'username'} comments = self.api_request( "/1/cards/{card_id}/actions".format(card_id=card_id), **params) for comment in comments: assert comment['type'] == 'commentCard' yield comment def api_request(self, url, **params): """ Make a trello API request. This takes an absolute url (without protocol and host) and a list of argumnets and return a GET request with the key and token from the configuration """ params['key'] = self.config.get('api_key'), params['token'] = self.get_password('token', self.config), url = "https://api.trello.com" + url return self.json_response(requests.get(url, params=params)) bugwarrior-1.8.0/bugwarrior/services/versionone.py000066400000000000000000000214001376471246600224670ustar00rootroot00000000000000from v1pysdk import V1Meta from v1pysdk.none_deref import NoneDeref from six.moves.urllib import parse from bugwarrior.services import IssueService, Issue, LOCAL_TIMEZONE from bugwarrior.config import die class VersionOneIssue(Issue): TASK_NAME = 'versiononetaskname' TASK_DESCRIPTION = 'versiononetaskdescrption' TASK_ESTIMATE = 'versiononetaskestimate' TASK_DETAIL_ESTIMATE = 'versiononetaskdetailestimate' TASK_TO_DO = 'versiononetasktodo' TASK_REFERENCE = 'versiononetaskreference' TASK_URL = 'versiononetaskurl' TASK_OID = 'versiononetaskoid' STORY_NAME = 'versiononestoryname' STORY_DESCRIPTION = 'versiononestorydescription' STORY_ESTIMATE = 'versiononestoryestimate' STORY_DETAIL_ESTIMATE = 'versiononestorydetailestimate' STORY_URL = 'versiononestoryurl' STORY_NUMBER = 'versiononestorynumber' STORY_OID = 'versiononestoryoid' TIMEBOX_BEGIN_DATE = 'versiononetimeboxbegindate' TIMEBOX_END_DATE = 'versiononetimeboxenddate' TIMEBOX_NAME = 'versiononetimeboxname' UDAS = { TASK_NAME: { 'type': 'string', 'label': 'VersionOne Task Name' }, TASK_DESCRIPTION: { 'type': 'string', 'label': 'VersionOne Task Description' }, TASK_ESTIMATE: { 'type': 'string', 'label': 'VersionOne Task Estimate' }, TASK_DETAIL_ESTIMATE: { 'type': 'string', 'label': 'VersionOne Task Detail Estimate', }, TASK_TO_DO: { 'type': 'string', 'label': 'VersionOne Task To Do' }, TASK_REFERENCE: { 'type': 'string', 'label': 'VersionOne Task Reference' }, TASK_URL: { 'type': 'string', 'label': 'VersionOne Task URL' }, TASK_OID: { 'type': 'string', 'label': 'VersionOne Task Object ID' }, STORY_NAME: { 'type': 'string', 'label': 'VersionOne Story Name' }, STORY_DESCRIPTION: { 'type': 'string', 'label': 'VersionOne Story Description' }, STORY_ESTIMATE: { 'type': 'string', 'label': 'VersionOne Story Estimate' }, STORY_DETAIL_ESTIMATE: { 'type': 'string', 'label': 'VersionOne Story Detail Estimate' }, STORY_URL: { 'type': 'string', 'label': 'VersionOne Story URL' }, STORY_NUMBER: { 'type': 'string', 'label': 'VersionOne Story Number' }, STORY_OID: { 'type': 'string', 'label': 'VersionOne Story Object ID' }, TIMEBOX_BEGIN_DATE: { 'type': 'string', 'label': 'VersionOne Timebox Begin Date' }, TIMEBOX_END_DATE: { 'type': 'string', 'label': 'VersionOne Timebox End Date' }, TIMEBOX_NAME: { 'type': 'string', 'label': 'VersionOne Timebox Name' } } UNIQUE_KEY = (TASK_URL, ) def to_taskwarrior(self): return { 'project': self.extra['project'], 'priority': self.origin['default_priority'], 'due': self.parse_date( self.record['timebox']['EndDate'], self.origin['timezone'] ), self.TASK_NAME: self.record['task']['Name'], self.TASK_DESCRIPTION: self.record['task']['Description'], self.TASK_ESTIMATE: self.record['task']['Estimate'], self.TASK_DETAIL_ESTIMATE: self.record['task']['DetailEstimate'], self.TASK_TO_DO: self.record['task']['ToDo'], self.TASK_REFERENCE: self.record['task']['Reference'], self.TASK_URL: self.record['task']['url'], self.TASK_OID: self.record['task']['idref'], self.STORY_NAME: self.record['story']['Name'], self.STORY_DESCRIPTION: self.record['story']['Description'], self.STORY_ESTIMATE: self.record['story']['Estimate'], self.STORY_DETAIL_ESTIMATE: self.record['story']['DetailEstimate'], self.STORY_URL: self.record['story']['url'], self.STORY_OID: self.record['story']['idref'], self.STORY_NUMBER: self.record['story']['Number'], self.TIMEBOX_BEGIN_DATE: self.record['timebox']['BeginDate'], self.TIMEBOX_END_DATE: self.record['timebox']['EndDate'], self.TIMEBOX_NAME: self.record['timebox']['Name'], } def get_default_description(self): return self.build_default_description( title=': '.join([ self.record['story']['Name'], self.record['task']['Name'], ]), url=self.record['task']['url'], number=self.record['story']['Number'], cls='task', ) class VersionOneService(IssueService): ISSUE_CLASS = VersionOneIssue CONFIG_PREFIX = 'versionone' TASK_COLLECT_DATA = ( 'Name', 'Description', 'Estimate', 'DetailEstimate', 'ToDo', 'Reference', 'url', 'idref', ) STORY_COLLECT_DATA = ( 'Name', 'Description', 'Estimate', 'DetailEstimate', 'Number', 'url', 'idref', ) TIMEBOX_COLLECT_DATA = ( 'BeginDate', 'EndDate', 'Name', ) def __init__(self, *args, **kw): super(VersionOneService, self).__init__(*args, **kw) parsed_address = parse.urlparse( self.config.get('base_uri') ) self.address = parsed_address.netloc self.instance = parsed_address.path.strip('/') self.username = self.config.get('username') self.password = self.get_password('password', self.username) self.timezone = self.config.get('timezone', default=LOCAL_TIMEZONE) self.project = self.config.get('project_name', default='') self.timebox_name = self.config.get('timebox_name') @staticmethod def get_keyring_service(service_config): parsed_address = parse.urlparse(service_config.get('base_uri')) username = service_config.get('username') return "versionone://%s@%s%s" % ( username, parsed_address.netloc, parsed_address.path ) def get_service_metadata(self): return { 'timezone': self.timezone } @classmethod def validate_config(cls, service_config, target): options = ( 'base_uri', 'username', ) for option in options: if option not in service_config: die("[%s] has no 'versionone.%s'" % (target, option)) IssueService.validate_config(service_config, target) def get_meta(self): if not hasattr(self, '_meta'): self._meta = V1Meta( address=self.address, instance=self.instance, username=self.username, password=self.password ) return self._meta def get_assignments(self, username): meta = self.get_meta() where = { 'IsClosed': False, 'IsCompleted': False, 'IsDead': False, 'IsDeleted': False, 'IsInactive': False, } if self.timebox_name: where['Parent.Timebox.Name'] = self.timebox_name tasks = meta.Task.select( 'Name', 'Parent', 'Description', 'Estimate', 'DetailEstimate', 'ToDo', 'Reference', ).filter( "Owners.Username='{username}'".format(username=self.username) ).where(**where) return tasks def issues(self): for issue in self.get_assignments(self.username): issue_data = { 'task': {}, 'story': {}, 'timebox': {}, } field_maps = ( ('task', issue, self.TASK_COLLECT_DATA, ), ('story', issue.Parent, self.STORY_COLLECT_DATA, ), ('timebox', issue.Parent.Timebox, self.TIMEBOX_COLLECT_DATA, ), ) for key, source, columns in field_maps: for column in columns: value = getattr(source, column, None) # NoneDeref is a special kind of None used by the v1 client if isinstance(value, NoneDeref): value = None issue_data[key][column] = value extras = { 'project': self.project } yield self.get_issue_for_record( issue_data, extras ) bugwarrior-1.8.0/bugwarrior/services/youtrack.py000066400000000000000000000135661376471246600221570ustar00rootroot00000000000000from __future__ import absolute_import import re import six import requests from jinja2 import Template from bugwarrior.config import asbool, die from bugwarrior.services import IssueService, Issue, ServiceClient import logging log = logging.getLogger(__name__) class YoutrackIssue(Issue): ISSUE = 'youtrackissue' SUMMARY = 'youtracksummary' URL = 'youtrackurl' PROJECT = 'youtrackproject' NUMBER = 'youtracknumber' UDAS = { ISSUE: { 'type': 'string', 'label': 'YouTrack Issue' }, SUMMARY: { 'type': 'string', 'label': 'YouTrack Summary', }, URL: { 'type': 'string', 'label': 'YouTrack URL', }, PROJECT: { 'type': 'string', 'label': 'YouTrack Project' }, NUMBER: { 'type': 'string', 'label': 'YouTrack Project Issue Number' }, } UNIQUE_KEY = (URL,) def _get_record_field(self, field_name): for field in self.record['field']: if field['name'] == field_name: return field def _get_record_field_value(self, field_name, field_value='value'): field = self._get_record_field(field_name) if field: return field[field_value] def to_taskwarrior(self): return { 'project': self.get_project(), 'priority': self.get_priority(), 'tags': self.get_tags(), self.ISSUE: self.get_issue(), self.SUMMARY: self.get_issue_summary(), self.URL: self.get_issue_url(), self.PROJECT: self.get_project(), self.NUMBER: self.get_number_in_project(), } def get_issue(self): return self.record['id'] def get_issue_summary(self): return self._get_record_field_value('summary') def get_issue_url(self): return "%s/issue/%s" % ( self.origin['base_url'], self.get_issue() ) def get_project(self): return self._get_record_field_value('projectShortName') def get_number_in_project(self): return int(self._get_record_field_value('numberInProject')) def get_default_description(self): return self.build_default_description( title=self.get_issue_summary(), url=self.get_processed_url(self.get_issue_url()), number=self.get_issue(), cls='issue', ) 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 tag_dict in self.record.get('tag', []): context.update({ 'tag': re.sub(r'[^a-zA-Z0-9]', '_', tag_dict['value']) }) tags.append( tag_template.render(context) ) return tags class YoutrackService(IssueService, ServiceClient): ISSUE_CLASS = YoutrackIssue CONFIG_PREFIX = 'youtrack' def __init__(self, *args, **kw): super(YoutrackService, self).__init__(*args, **kw) self.host = self.config.get('host') if self.config.get('use_https', default=True, to_type=asbool): self.scheme = 'https' self.port = '443' else: self.scheme = 'http' self.port = '80' self.port = self.config.get('port', self.port) self.base_url = '%s://%s:%s' % (self.scheme, self.host, self.port) if self.config.get('incloud_instance', default=False, to_type=asbool): self.base_url += '/youtrack' self.rest_url = self.base_url + '/rest' self.session = requests.Session() self.session.headers['Accept'] = 'application/json' self.verify_ssl = self.config.get('verify_ssl', default=True, to_type=asbool) if not self.verify_ssl: requests.packages.urllib3.disable_warnings() self.session.verify = False login = self.config.get('login') password = self.get_password('password', login) if not self.config.get('anonymous', False): self._login(login, password) self.query = self.config.get('query', default='for:me #Unresolved') self.query_limit = self.config.get('query_limit', default="100") self.import_tags = self.config.get( 'import_tags', default=True, to_type=asbool ) self.tag_template = self.config.get( 'tag_template', default='{{tag|lower}}', to_type=six.text_type ) def _login(self, login, password): params = {'login': login, 'password': password} resp = self.session.post(self.rest_url + "/user/login", params) if resp.status_code != 200: raise RuntimeError("YouTrack responded with %s" % resp) self.session.headers['Cookie'] = resp.headers['set-cookie'] @staticmethod def get_keyring_service(service_config): host = service_config.get('host') login = service_config.get('login') return "youtrack://%s@%s" % (login, host) def get_service_metadata(self): return { 'base_url': self.base_url, 'import_tags': self.import_tags, 'tag_template': self.tag_template, } @classmethod def validate_config(cls, service_config, target): for k in ('login', 'password', 'host'): if k not in service_config: die("[%s] has no 'youtrack.%s'" % (target, k)) IssueService.validate_config(service_config, target) def issues(self): params = {'filter': self.query, 'max': self.query_limit} resp = self.session.get(self.rest_url + '/issue', params=params) issues = self.json_response(resp)['issue'] log.debug(" Found %i total.", len(issues)) for issue in issues: yield self.get_issue_for_record(issue) bugwarrior-1.8.0/setup.py000066400000000000000000000070711376471246600154420ustar00rootroot00000000000000from setuptools import setup, find_packages version = '1.8.0' f = open('bugwarrior/README.rst') long_description = f.read().strip() long_description = long_description.split('split here', 1)[1] f.close() setup(name='bugwarrior', version=version, description="Sync github, bitbucket, and trac issues with taskwarrior", long_description=long_description, classifiers=[ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License (GPL)", "Programming Language :: Python :: 3", "Topic :: Software Development :: Bug Tracking", "Topic :: Utilities", ], keywords='task taskwarrior todo github ', author='Ralph Bean', author_email='ralph.bean@gmail.com', url='http://github.com/ralphbean/bugwarrior', license='GPLv3+', packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), include_package_data=True, zip_safe=False, install_requires=[ "click", "dogpile.cache>=0.5.3", "future", "jinja2>=2.7.2", "lockfile>=0.9.1", "python-dateutil", "pytz", "requests", "six>=1.9.0", "taskw>=0.8", ], extras_require={ "activecollab": ["pypandoc", "pyac>=0.1.5"], "bts": ["PySimpleSOAP", "python-debianbts>=2.6.1"], "bugzilla": ["python-bugzilla>=2.0.0"], "gmail": ["google-api-python-client", "google-auth-oauthlib"], "jira": ["jira>=0.22"], "keyring": ["keyring"], "megaplan": ["megaplan>=1.4"], "phabricator": ["phabricator"], "trac": ["offtrac"], }, tests_require=[ "nose", "responses", "bugwarrior[jira]", "bugwarrior[megaplan]", "bugwarrior[activecollab]", "bugwarrior[bts]", "bugwarrior[gmail]", "bugwarrior[trac]", "bugwarrior[bugzilla]", "bugwarrior[phabricator]" ], test_suite='nose.collector', entry_points=""" [console_scripts] bugwarrior-pull = bugwarrior:pull bugwarrior-vault = bugwarrior:vault bugwarrior-uda = bugwarrior:uda [bugwarrior.service] github=bugwarrior.services.github:GithubService gitlab=bugwarrior.services.gitlab:GitlabService bitbucket=bugwarrior.services.bitbucket:BitbucketService trac=bugwarrior.services.trac:TracService bts=bugwarrior.services.bts:BTSService bugzilla=bugwarrior.services.bz:BugzillaService teamlab=bugwarrior.services.teamlab:TeamLabService redmine=bugwarrior.services.redmine:RedMineService activecollab2=bugwarrior.services.activecollab2:ActiveCollab2Service activecollab=bugwarrior.services.activecollab:ActiveCollabService jira=bugwarrior.services.jira:JiraService megaplan=bugwarrior.services.megaplan:MegaplanService phabricator=bugwarrior.services.phab:PhabricatorService versionone=bugwarrior.services.versionone:VersionOneService pagure=bugwarrior.services.pagure:PagureService taiga=bugwarrior.services.taiga:TaigaService gerrit=bugwarrior.services.gerrit:GerritService trello=bugwarrior.services.trello:TrelloService youtrack=bugwarrior.services.youtrack:YoutrackService gmail=bugwarrior.services.gmail:GmailService teamworks_projects=bugwarrior.services.teamworks_projects:TeamworksService pivotaltracker=bugwarrior.services.pivotaltracker:PivotalTrackerService """, ) bugwarrior-1.8.0/tests/000077500000000000000000000000001376471246600150655ustar00rootroot00000000000000bugwarrior-1.8.0/tests/__init__.py000066400000000000000000000000001376471246600171640ustar00rootroot00000000000000bugwarrior-1.8.0/tests/base.py000066400000000000000000000064261376471246600163610ustar00rootroot00000000000000from builtins import object import shutil import os.path import tempfile import unittest import configparser from unittest import mock import responses from bugwarrior import config from bugwarrior.data import BugwarriorData class AbstractServiceTest(object): """ Ensures that certain test methods are implemented for each service. """ def test_to_taskwarrior(self): """ Test Service.to_taskwarrior(). """ raise NotImplementedError def test_issues(self): """ Test Service.issues(). - When the API is accessed via requests, use the responses library to mock requests. - When the API is accessed via a third party library, substitute a fake implementation class for it. """ raise NotImplementedError class ConfigTest(unittest.TestCase): """ Creates config files, configures the environment, and cleans up afterwards. """ def setUp(self): self.old_environ = os.environ.copy() self.tempdir = tempfile.mkdtemp(prefix='bugwarrior') # Create temporary config files. self.taskrc = os.path.join(self.tempdir, '.taskrc') self.lists_path = os.path.join(self.tempdir, 'lists') os.mkdir(self.lists_path) with open(self.taskrc, 'w+') as fout: fout.write('data.location=%s\n' % self.lists_path) # Configure environment. os.environ['HOME'] = self.tempdir os.environ.pop(config.BUGWARRIORRC, None) os.environ.pop('TASKRC', None) os.environ.pop('XDG_CONFIG_HOME', None) os.environ.pop('XDG_CONFIG_DIRS', None) def tearDown(self): shutil.rmtree(self.tempdir, ignore_errors=True) os.environ = self.old_environ class ServiceTest(ConfigTest): GENERAL_CONFIG = { 'annotation_length': 100, 'description_length': 100, } SERVICE_CONFIG = { } @classmethod def setUpClass(cls): cls.maxDiff = None def get_mock_service( self, service_class, section='unspecified', config_overrides=None, general_overrides=None ): options = { 'general': self.GENERAL_CONFIG.copy(), section: self.SERVICE_CONFIG.copy(), } if config_overrides: options[section].update(config_overrides) if general_overrides: options['general'].update(general_overrides) def has_option(section, name): try: return options[section][name] except KeyError: return False def get_option(section, name): try: return options[section][name] except KeyError: raise configparser.NoOptionError(section, name) def get_int(section, name): return int(get_option(section, name)) config = mock.Mock() config.has_option = mock.Mock(side_effect=has_option) config.get = mock.Mock(side_effect=get_option) config.getint = mock.Mock(side_effect=get_int) config.data = BugwarriorData(self.lists_path) service_instance = service_class(config, 'general', section) return service_instance @staticmethod def add_response(url, **kwargs): responses.add(responses.GET, url, match_querystring=True, **kwargs) bugwarrior-1.8.0/tests/test_activecollab.py000066400000000000000000000113511376471246600211270ustar00rootroot00000000000000from builtins import next from builtins import object import datetime import unittest from unittest import mock import pypandoc import pytz from bugwarrior.services.activecollab import ( ActiveCollabService ) from .base import ServiceTest, AbstractServiceTest class FakeActiveCollabLib(object): def __init__(self, arbitrary_issue): self.arbitrary_issue = arbitrary_issue def get_my_tasks(self): return {'arbitrary_key': {'assignments': { self.arbitrary_issue['task_id']: self.arbitrary_issue}}} def get_assignment_labels(self): return [] def get_comments(self, *args): return [] class TestActiveCollabIssues(AbstractServiceTest, ServiceTest): SERVICE_CONFIG = { 'activecollab.url': 'hello', 'activecollab.key': 'howdy', 'activecollab.user_id': '2', 'activecollab.projects': '1:one, 2:two' } arbitrary_due_on = ( datetime.datetime.now() - datetime.timedelta(hours=1) ).replace(tzinfo=pytz.UTC) arbitrary_created_on = ( datetime.datetime.now() - datetime.timedelta(hours=2) ).replace(tzinfo=pytz.UTC) try: _body = pypandoc.convert_text('

Ticket Body

', 'md', format='html') except OSError: raise unittest.SkipTest('Pandoc is not installed.') arbitrary_issue = { 'priority': 0, 'project': 'something', 'due_on': { 'formatted_date': arbitrary_due_on.isoformat(), }, 'permalink': 'http://wherever/', 'task_id': 10, 'project_name': 'something', 'project_id': 10, 'id': 30, 'type': 'issue', 'created_on': { 'formatted_date': arbitrary_created_on.isoformat(), }, 'created_by_name': 'Tester', 'body': _body.rstrip(), 'name': 'Anonymous', 'milestone': 'Sprint 1', 'estimated_time': 1, 'tracked_time': 10, 'label': 'ON_HOLD', 'assignee_id': 2, 'label_id': 1, } def setUp(self): super(TestActiveCollabIssues, self).setUp() self.maxDiff = None with mock.patch('pyac.library.activeCollab.call_api'): self.service = self.get_mock_service(ActiveCollabService) def get_mock_service(self, *args, **kwargs): service = super(TestActiveCollabIssues, self).get_mock_service( *args, **kwargs) service.activecollab = FakeActiveCollabLib(self.arbitrary_issue) return service def test_to_taskwarrior(self): arbitrary_extra = { 'annotations': ['an annotation'], } issue = self.service.get_issue_for_record( self.arbitrary_issue, arbitrary_extra) expected_output = { 'project': self.arbitrary_issue['project'], 'due': self.arbitrary_due_on, 'priority': 'M', 'annotations': arbitrary_extra['annotations'], issue.PERMALINK: self.arbitrary_issue['permalink'], issue.PROJECT_ID: self.arbitrary_issue['project_id'], issue.PROJECT_NAME: self.arbitrary_issue['project_name'], issue.TYPE: self.arbitrary_issue['type'], issue.CREATED_ON: self.arbitrary_created_on, issue.CREATED_BY_NAME: self.arbitrary_issue['created_by_name'], issue.BODY: self.arbitrary_issue['body'], issue.NAME: self.arbitrary_issue['name'], issue.FOREIGN_ID: self.arbitrary_issue['id'], issue.TASK_ID: self.arbitrary_issue['task_id'], issue.ESTIMATED_TIME: self.arbitrary_issue['estimated_time'], issue.TRACKED_TIME: self.arbitrary_issue['tracked_time'], issue.MILESTONE: self.arbitrary_issue['milestone'], issue.LABEL: self.arbitrary_issue['label'], } actual_output = issue.to_taskwarrior() self.assertEqual(actual_output, expected_output) def test_issues(self): issue = next(self.service.issues()) expected = { 'acbody': u'Ticket Body', 'accreatedbyname': 'Tester', 'accreatedon': self.arbitrary_created_on, 'acestimatedtime': 1, 'acid': 30, 'aclabel': None, 'acmilestone': 'Sprint 1', 'acname': 'Anonymous', 'acpermalink': 'http://wherever/', 'acprojectid': 10, 'acprojectname': 'something', 'actaskid': 10, 'actrackedtime': 10, 'actype': 'issue', 'annotations': [], 'description': '(bw)Is#30 - Anonymous .. http://wherever/', 'due': self.arbitrary_due_on, 'priority': 'M', 'project': 'something', 'tags': []} self.assertEqual(issue.get_taskwarrior_record(), expected) bugwarrior-1.8.0/tests/test_activecollab2.py000066400000000000000000000066061376471246600212200ustar00rootroot00000000000000from builtins import next import re import datetime import pytz import responses from bugwarrior.services.activecollab2 import ActiveCollab2Service from .base import ServiceTest, AbstractServiceTest class TestActiveCollab2Issue(AbstractServiceTest, ServiceTest): SERVICE_CONFIG = { 'activecollab2.url': 'http://hello', 'activecollab2.key': 'howdy', 'activecollab2.user_id': 0, 'activecollab2.projects': '1:one, 2:two' } arbitrary_due_on = ( datetime.datetime.now() - datetime.timedelta(hours=1) ).replace(tzinfo=pytz.UTC) arbitrary_created_on = ( datetime.datetime.now() - datetime.timedelta(hours=2) ).replace(tzinfo=pytz.UTC) arbitrary_issue = { 'project': 'something', 'priority': 2, 'due_on': arbitrary_due_on.isoformat(), 'permalink': 'http://wherever/', 'ticket_id': 10, 'project_id': 20, 'type': 'Ticket', 'created_on': arbitrary_created_on.isoformat(), 'created_by_id': '10', 'body': 'Ticket Body', 'name': 'Anonymous', 'assignees': [ {'user_id': SERVICE_CONFIG['activecollab2.user_id'], 'is_owner': True} ], 'description': 'Further detail.', } def setUp(self): super(TestActiveCollab2Issue, self).setUp() self.service = self.get_mock_service(ActiveCollab2Service) def test_to_taskwarrior(self): issue = self.service.get_issue_for_record(self.arbitrary_issue) expected_output = { 'project': self.arbitrary_issue['project'], 'priority': issue.PRIORITY_MAP[self.arbitrary_issue['priority']], 'due': self.arbitrary_due_on, issue.PERMALINK: self.arbitrary_issue['permalink'], issue.TICKET_ID: self.arbitrary_issue['ticket_id'], issue.PROJECT_ID: self.arbitrary_issue['project_id'], issue.TYPE: self.arbitrary_issue['type'], issue.CREATED_ON: self.arbitrary_created_on, issue.CREATED_BY_ID: self.arbitrary_issue['created_by_id'], issue.BODY: self.arbitrary_issue['body'], issue.NAME: self.arbitrary_issue['name'], } actual_output = issue.to_taskwarrior() self.assertEqual(actual_output, expected_output) @responses.activate def test_issues(self): self.add_response( re.compile( 'http://hello/\?(?=.*token=howdy)(?=.*path_info=\%2Fprojects\%2F[1-2]\%2Fuser-tasks)(?=.*format=json)'), json=[self.arbitrary_issue]) self.add_response( re.compile( 'http://hello/\?(?=.*token=howdy)(?=.*path_info=\%2Fprojects\%2F20\%2Ftickets\%2F10)(?=.*format=json)'), json=self.arbitrary_issue) issue = next(self.service.issues()) expected = { 'ac2body': u'Ticket Body', 'ac2createdbyid': u'10', 'ac2createdon': self.arbitrary_created_on, 'ac2name': u'Anonymous', 'ac2permalink': u'http://wherever/', 'ac2projectid': 20, 'ac2ticketid': 10, 'ac2type': u'Ticket', 'description': u'(bw)Is#10 - Anonymous .. http://wherever/', 'due': self.arbitrary_due_on, 'priority': 'H', 'project': u'something', 'tags': []} self.assertEqual(issue.get_taskwarrior_record(), expected) bugwarrior-1.8.0/tests/test_bitbucket.py000066400000000000000000000127011376471246600204530ustar00rootroot00000000000000from __future__ import unicode_literals import unittest import responses from bugwarrior.services.bitbucket import BitbucketService from .base import ServiceTest, AbstractServiceTest class TestBitbucketIssue(AbstractServiceTest, ServiceTest): SERVICE_CONFIG = { 'bitbucket.login': 'something', 'bitbucket.username': 'somename', 'bitbucket.password': 'something else', } def setUp(self): super(TestBitbucketIssue, self).setUp() self.service = self.get_mock_service(BitbucketService) def test_to_taskwarrior(self): arbitrary_issue = { 'priority': 'trivial', 'id': '100', 'title': 'Some Title', } arbitrary_extra = { 'url': 'http://hello-there.com/', 'project': 'Something', 'annotations': [ 'One', ] } issue = self.service.get_issue_for_record( arbitrary_issue, arbitrary_extra ) expected_output = { 'project': arbitrary_extra['project'], 'priority': issue.PRIORITY_MAP[arbitrary_issue['priority']], 'annotations': arbitrary_extra['annotations'], issue.URL: arbitrary_extra['url'], issue.FOREIGN_ID: arbitrary_issue['id'], issue.TITLE: arbitrary_issue['title'], } actual_output = issue.to_taskwarrior() self.assertEqual(actual_output, expected_output) @responses.activate def test_issues(self): self.add_response( 'https://api.bitbucket.org/2.0/repositories/somename/', json={'values': [{ 'full_name': 'somename/somerepo', 'has_issues': True }]}) self.add_response( 'https://api.bitbucket.org/2.0/repositories/somename/somerepo/issues/', json={'values': [{ 'title': 'Some Bug', 'status': 'open', 'links': {'html': {'href': 'example.com'}}, 'id': 1 }]}) self.add_response( 'https://api.bitbucket.org/2.0/repositories/somename/somerepo/pullrequests/', json={'values': [{ 'title': 'Some Feature', 'state': 'open', 'links': {'html': {'href': 'example.com'}}, 'id': 1 }]}) self.add_response( 'https://api.bitbucket.org/2.0/repositories/somename/somerepo/pullrequests/1/comments', json={'values': [{ 'user': {'username': 'nobody'}, 'content': {'raw': 'Some comment.'} }]}) issue, pr = [i for i in self.service.issues()] expected_issue = { 'annotations': [u'@nobody - Some comment.'], 'bitbucketid': 1, 'bitbuckettitle': u'Some Bug', 'bitbucketurl': u'example.com', 'description': u'(bw)Is#1 - Some Bug .. example.com', 'priority': 'M', 'project': u'somerepo', 'tags': []} self.assertEqual(issue.get_taskwarrior_record(), expected_issue) expected_pr = { 'annotations': [u'@nobody - Some comment.'], 'bitbucketid': 1, 'bitbuckettitle': u'Some Feature', 'bitbucketurl': 'https://bitbucket.org/', 'description': u'(bw)Is#1 - Some Feature .. https://bitbucket.org/', 'priority': 'M', 'project': u'somerepo', 'tags': []} self.assertEqual(pr.get_taskwarrior_record(), expected_pr) def test_get_owner(self): issue = { 'title': 'Foobar', 'assignee': {'username': 'tintin'}, } self.assertEqual(self.service.get_owner(('foo', issue)), 'tintin') def test_get_owner_none(self): issue = { 'title': 'Foobar', 'assignee': None, } self.assertIsNone(self.service.get_owner(('foo', issue))) @unittest.skip('https://github.com/getsentry/responses/issues/156') @responses.activate def test_fetch_issues_pagination(self): self.add_response( 'https://api.bitbucket.org/2.0/repositories/somename/somerepo/issues/', json={ 'values': [{ 'title': 'Some Bug', 'status': 'open', 'links': {'html': {'href': 'example.com'}}, 'id': 1 }], 'next': 'https://api.bitbucket.org/2.0/repositories/somename/somerepo/issues/?page=2', }) self.add_response( 'https://api.bitbucket.org/2.0/repositories/somename/somerepo/issues/?page=2', json={ 'values': [{ 'title': 'Some Other Bug', 'status': 'open', 'links': {'html': {'href': 'example.com'}}, 'id': 2 }], }) issues = list(self.service.fetch_issues('somename/somerepo')) expected = [ ('somename/somerepo', { 'title': 'Some Bug', 'status': 'open', 'links': {'html': {'href': 'example.com'}}, 'id': 1 }), ('somename/somerepo', { 'title': 'Some Other Bug', 'status': 'open', 'links': {'html': {'href': 'example.com'}}, 'id': 2 }), ] self.assertEqual(issues, expected) bugwarrior-1.8.0/tests/test_bts.py000066400000000000000000000053441376471246600172740ustar00rootroot00000000000000from builtins import next from builtins import str from builtins import object from unittest import mock from bugwarrior.services import bts from .base import ServiceTest, AbstractServiceTest class FakeBTSBug(object): bug_num = 810629 package = "wnpp" subject = ("ITP: bugwarrior -- Pull tickets from github, " "bitbucket, bugzilla, jira, trac, and others into " "taskwarrior") severity = "wishlist" source = "" forwarded = "" pending = "pending" class FakeBTSLib(object): def get_bugs(self, *args, **kwargs): return [810629] def get_status(self, bug_num): if bug_num == [810629]: return [FakeBTSBug] class TestBTSService(AbstractServiceTest, ServiceTest): maxDiff = None SERVICE_CONFIG = { 'bts.email': 'irl@debian.org', 'bts.packages': 'bugwarrior', } def setUp(self): super(TestBTSService, self).setUp() self.service = self.get_mock_service(bts.BTSService) def test_to_taskwarrior(self): issue = self.service.get_issue_for_record( self.service._record_for_bug(FakeBTSBug) ) expected_output = { 'priority': issue.PRIORITY_MAP[FakeBTSBug.severity], 'annotations': [], issue.URL: "https://bugs.debian.org/" + str(FakeBTSBug.bug_num), issue.SUBJECT: FakeBTSBug.subject, issue.NUMBER: FakeBTSBug.bug_num, issue.PACKAGE: FakeBTSBug.package, issue.SOURCE: FakeBTSBug.source, issue.FORWARDED: FakeBTSBug.forwarded, issue.STATUS: FakeBTSBug.pending, } actual_output = issue.to_taskwarrior() self.assertEqual(actual_output, expected_output) def test_issues(self): with mock.patch('bugwarrior.services.bts.debianbts', FakeBTSLib()): issue = next(self.service.issues()) expected = { 'annotations': [], 'btsnumber': 810629, 'btsforwarded': '', 'btspackage': 'wnpp', 'btssubject': ('ITP: bugwarrior -- Pull tickets from github, ' 'bitbucket, bugzilla, jira, trac, and others into ' 'taskwarrior'), 'btsurl': 'https://bugs.debian.org/810629', 'btssource': '', 'description': (u'(bw)Is#810629 - ITP: bugwarrior -- Pull tickets' u' from github, bitbucket, bugzilla, jira, trac, ' u'and others into taskwa .. https://bugs.debian.o' u'rg/810629'), 'priority': 'L', 'btsstatus': 'pending', u'tags': []} self.assertEqual(issue.get_taskwarrior_record(), expected) bugwarrior-1.8.0/tests/test_bugzilla.py000066400000000000000000000207751376471246600203220ustar00rootroot00000000000000import datetime from builtins import next from builtins import object from unittest import mock from collections import namedtuple from six.moves import configparser from bugwarrior.services.bz import BugzillaService from .base import ConfigTest, ServiceTest, AbstractServiceTest from bugwarrior.config import ServiceConfig class FakeBugzillaLib(object): def __init__(self, records): self.records = records def query(self, query): return [namedtuple('Record', list(record.keys()))(**record) for record in self.records] class TestBugzillaServiceConfig(ConfigTest): def setUp(self): super(TestBugzillaServiceConfig, self).setUp() self.config = configparser.RawConfigParser() self.config.add_section('general') self.config.add_section('mybz') self.service_config = ServiceConfig( BugzillaService.CONFIG_PREFIX, self.config, 'mybz') @mock.patch('bugwarrior.services.bz.die') def test_validate_config_username_password(self, die): self.config.set('mybz', 'bugzilla.base_uri', 'http://one.com/') self.config.set('mybz', 'bugzilla.username', 'me') self.config.set('mybz', 'bugzilla.password', 'mypas') BugzillaService.validate_config(self.service_config, 'mybz') die.assert_not_called() @mock.patch('bugwarrior.services.bz.die') def test_validate_config_api_key(self, die): self.config.set('mybz', 'bugzilla.base_uri', 'http://one.com/') self.config.set('mybz', 'bugzilla.username', 'me') self.config.set('mybz', 'bugzilla.api_key', '123') BugzillaService.validate_config(self.service_config, 'mybz') die.assert_not_called() @mock.patch('bugwarrior.services.bz.die') def test_validate_config_api_key_no_username(self, die): self.config.set('mybz', 'bugzilla.base_uri', 'http://one.com/') self.config.set('mybz', 'bugzilla.api_key', '123') BugzillaService.validate_config(self.service_config, 'mybz') die.assert_called_with("[mybz] has no 'bugzilla.username'") class TestBugzillaService(AbstractServiceTest, ServiceTest): SERVICE_CONFIG = { 'bugzilla.base_uri': 'http://one.com/', 'bugzilla.username': 'hello', 'bugzilla.password': 'there', } arbitrary_record = { 'product': 'Product', 'component': 'Something', 'priority': 'urgent', 'status': 'NEW', 'summary': 'This is the issue summary', 'id': 1234567, 'flags': [], 'assigned_to': None, } arbitrary_datetime = datetime.datetime.now(tz=datetime.timezone.utc) def setUp(self): super(TestBugzillaService, self).setUp() with mock.patch('bugzilla.Bugzilla'): self.service = self.get_mock_service(BugzillaService) def get_mock_service(self, *args, **kwargs): service = super(TestBugzillaService, self).get_mock_service( *args, **kwargs) service.bz = FakeBugzillaLib([self.arbitrary_record]) service._get_assigned_date = ( lambda issues: self.arbitrary_datetime.isoformat()) return service def test_api_key_supplied(self): with mock.patch('bugzilla.Bugzilla'): self.service = self.get_mock_service( BugzillaService, config_overrides={ 'bugzilla.base_uri': 'http://one.com/', 'bugzilla.username': 'me', 'bugzilla.api_key': '123', }) def test_to_taskwarrior(self): arbitrary_extra = { 'url': 'http://path/to/issue/', 'annotations': [ 'Two', ], } issue = self.service.get_issue_for_record( self.arbitrary_record, arbitrary_extra, ) expected_output = { 'project': self.arbitrary_record['component'], 'priority': issue.PRIORITY_MAP[self.arbitrary_record['priority']], 'annotations': arbitrary_extra['annotations'], issue.STATUS: self.arbitrary_record['status'], issue.URL: arbitrary_extra['url'], issue.SUMMARY: self.arbitrary_record['summary'], issue.BUG_ID: self.arbitrary_record['id'], issue.PRODUCT: self.arbitrary_record['product'], issue.COMPONENT: self.arbitrary_record['component'], } actual_output = issue.to_taskwarrior() self.assertEqual(actual_output, expected_output) def test_issues(self): issue = next(self.service.issues()) expected = { 'annotations': [], 'bugzillabugid': 1234567, 'bugzillastatus': 'NEW', 'bugzillasummary': 'This is the issue summary', 'bugzillaurl': u'https://http://one.com//show_bug.cgi?id=1234567', 'bugzillaproduct': 'Product', 'bugzillacomponent': 'Something', 'description': u'(bw)Is#1234567 - This is the issue summary .. https://http://one.com//show_bug.cgi?id=1234567', 'priority': 'H', 'project': 'Something', 'tags': []} self.assertEqual(issue.get_taskwarrior_record(), expected) def test_only_if_assigned(self): with mock.patch('bugzilla.Bugzilla'): self.service = self.get_mock_service( BugzillaService, config_overrides={ 'bugzilla.only_if_assigned': 'hello', }) assigned_records = [ { 'product': 'Product', 'component': 'Something', 'priority': 'urgent', 'status': 'ASSIGNED', 'summary': 'This is the issue summary', 'id': 1234568, 'flags': [], 'assigned_to': 'hello' }, { 'product': 'Product', 'component': 'Something', 'priority': 'urgent', 'status': 'ASSIGNED', 'summary': 'This is the issue summary', 'id': 1234569, 'flags': [], 'assigned_to': 'somebodyelse' }, ] self.service.bz.records.extend(assigned_records) issues = self.service.issues() expected = { 'annotations': [], 'bugzillaassignedon': self.arbitrary_datetime, 'bugzillabugid': 1234568, 'bugzillastatus': 'ASSIGNED', 'bugzillasummary': 'This is the issue summary', 'bugzillaurl': u'https://http://one.com//show_bug.cgi?id=1234568', 'bugzillaproduct': 'Product', 'bugzillacomponent': 'Something', 'description': u'(bw)Is#1234568 - This is the issue summary .. https://http://one.com//show_bug.cgi?id=1234568', 'priority': 'H', 'project': 'Something', 'tags': []} self.assertEqual(next(issues).get_taskwarrior_record(), expected) # Only one issue is assigned. self.assertRaises(StopIteration, lambda: next(issues)) def test_also_unassigned(self): with mock.patch('bugzilla.Bugzilla'): self.service = self.get_mock_service( BugzillaService, config_overrides={ 'bugzilla.only_if_assigned': 'hello', 'bugzilla.also_unassigned': True, }) assigned_records = [ { 'product': 'Product', 'component': 'Something', 'priority': 'urgent', 'status': 'ASSIGNED', 'summary': 'This is the issue summary', 'id': 1234568, 'flags': [], 'assigned_to': 'hello' }, { 'product': 'Product', 'component': 'Something', 'priority': 'urgent', 'status': 'ASSIGNED', 'summary': 'This is the issue summary', 'id': 1234569, 'flags': [], 'assigned_to': 'somebodyelse' }, ] self.service.bz.records.extend(assigned_records) issues = self.service.issues() self.assertIn(next(issues).get_taskwarrior_record()['bugzillabugid'], [1234567, 1234568]) self.assertIn(next(issues).get_taskwarrior_record()['bugzillabugid'], [1234567, 1234568]) # Only two issues are assigned to the user or unassigned. self.assertRaises(StopIteration, lambda: next(issues)) bugwarrior-1.8.0/tests/test_config.py000066400000000000000000000173331376471246600177520ustar00rootroot00000000000000# coding: utf-8 from __future__ import unicode_literals import os from six.moves import configparser from unittest import TestCase import bugwarrior.config as config from .base import ConfigTest class TestGetConfigPath(ConfigTest): def create(self, path): """ Create an empty file in the temporary directory, return the full path. """ fpath = os.path.join(self.tempdir, path) if not os.path.exists(os.path.dirname(fpath)): os.makedirs(os.path.dirname(fpath)) open(fpath, 'a').close() return fpath def test_default(self): """ If it exists, use the file at $XDG_CONFIG_HOME/bugwarrior/bugwarriorrc """ rc = self.create('.config/bugwarrior/bugwarriorrc') self.assertEqual(config.get_config_path(), rc) def test_legacy(self): """ Falls back on .bugwarriorrc if it exists """ rc = self.create('.bugwarriorrc') self.assertEqual(config.get_config_path(), rc) def test_xdg_first(self): """ If both files above exist, the one in $XDG_CONFIG_HOME takes precedence """ self.create('.bugwarriorrc') rc = self.create('.config/bugwarrior/bugwarriorrc') self.assertEqual(config.get_config_path(), rc) def test_no_file(self): """ If no bugwarriorrc exist anywhere, the path to the prefered one is returned. """ self.assertEqual( config.get_config_path(), os.path.join(self.tempdir, '.config/bugwarrior/bugwarriorrc')) def test_BUGWARRIORRC(self): """ If $BUGWARRIORRC is set, it takes precedence over everything else (even if the file doesn't exist). """ rc = os.path.join(self.tempdir, 'my-bugwarriorc') os.environ['BUGWARRIORRC'] = rc self.create('.bugwarriorrc') self.create('.config/bugwarrior/bugwarriorrc') self.assertEqual(config.get_config_path(), rc) def test_BUGWARRIORRC_empty(self): """ If $BUGWARRIORRC is set but emty, it is not used and the default file is used instead. """ os.environ['BUGWARRIORRC'] = '' rc = self.create('.config/bugwarrior/bugwarriorrc') self.assertEqual(config.get_config_path(), rc) class TestGetDataPath(ConfigTest): def setUp(self): super(TestGetDataPath, self).setUp() self.config = configparser.RawConfigParser() self.config.add_section('general') def assertDataPath(self, expected_datapath): self.assertEqual( expected_datapath, config.get_data_path(self.config, 'general')) def test_TASKDATA(self): """ TASKDATA should be respected, even when taskrc's data.location is set. """ datapath = os.environ['TASKDATA'] = os.path.join(self.tempdir, 'data') self.assertDataPath(datapath) def test_taskrc_datalocation(self): """ When TASKDATA is not set, data.location in taskrc should be respected. """ self.assertTrue('TASKDATA' not in os.environ) self.assertDataPath(self.lists_path) def test_unassigned(self): """ When data path is not assigned, use default location. """ # Empty taskrc. with open(self.taskrc, 'w'): pass self.assertTrue('TASKDATA' not in os.environ) self.assertDataPath(os.path.expanduser('~/.task')) class TestOracleEval(TestCase): def test_echo(self): self.assertEqual(config.oracle_eval("echo fööbår"), "fööbår") class TestBugwarriorConfigParser(TestCase): def setUp(self): self.config = config.BugwarriorConfigParser() self.config.add_section('general') self.config.set('general', 'someint', '4') self.config.set('general', 'somenone', '') self.config.set('general', 'somechar', 'somestring') def test_getint(self): self.assertEqual(self.config.getint('general', 'someint'), 4) def test_getint_none(self): self.assertEqual(self.config.getint('general', 'somenone'), None) def test_getint_valueerror(self): with self.assertRaises(ValueError): self.config.getint('general', 'somechar') class TestServiceConfig(TestCase): def setUp(self): self.target = 'someservice' self.config = config.BugwarriorConfigParser() self.config.add_section(self.target) self.config.set(self.target, 'someprefix.someint', '4') self.config.set(self.target, 'someprefix.somenone', '') self.config.set(self.target, 'someprefix.somechar', 'somestring') self.config.set(self.target, 'someprefix.somebool', 'true') self.service_config = config.ServiceConfig( 'someprefix', self.config, self.target) def test_configparser_proxy(self): """ Methods not defined in ServiceConfig should be proxied to configparser. """ self.assertTrue( self.service_config.has_option(self.target, 'someprefix.someint')) def test__contains__(self): self.assertTrue('someint' in self.service_config) def test_get(self): self.assertEqual(self.service_config.get('someint'), '4') def test_get_default(self): self.assertEqual( self.service_config.get('someoption', default='somedefault'), 'somedefault' ) def test_get_default_none(self): self.assertIsNone(self.service_config.get('someoption')) def test_get_to_type(self): self.assertIs( self.service_config.get('somebool', to_type=config.asbool), True ) class TestLoggingPath(TestCase): def setUp(self): self.config = config.BugwarriorConfigParser(allow_no_value=True) self.config.add_section('general') self.config.set('general', 'log.level', 'INFO') self.config.set('general', 'log.file', None) self.dir = os.getcwd() os.chdir(os.path.expanduser('~')) def test_log_stdout(self): self.assertIsNone(config.fix_logging_path(self.config, 'general')) def test_log_relative_path(self): self.config.set('general', 'log.file', 'bugwarrior.log') self.assertEqual( config.fix_logging_path(self.config, 'general'), 'bugwarrior.log', ) def test_log_absolute_path(self): filename = os.path.join(os.path.expandvars('$HOME'), 'bugwarrior.log') self.config.set('general', 'log.file', filename) self.assertEqual( config.fix_logging_path(self.config, 'general'), 'bugwarrior.log', ) def test_log_userhome(self): self.config.set('general', 'log.file', '~/bugwarrior.log') self.assertEqual( config.fix_logging_path(self.config, 'general'), 'bugwarrior.log', ) def test_log_envvar(self): self.config.set('general', 'log.file', '$HOME/bugwarrior.log') self.assertEqual( config.fix_logging_path(self.config, 'general'), 'bugwarrior.log', ) def tearDown(self): os.chdir(self.dir) class TestCasters(TestCase): def test_asbool(self): self.assertEqual(config.asbool('True'), True) self.assertEqual(config.asbool('False'), False) def test_aslist(self): self.assertEqual( config.aslist('project_bar,project_baz'), ['project_bar', 'project_baz'] ) def test_aslist_jinja(self): self.assertEqual( config.aslist("work, jira, {{jirastatus|lower|replace(' ','_')}}"), ['work', 'jira', "{{jirastatus|lower|replace(' ','_')}}"] ) def test_asint(self): self.assertEqual(config.asint(''), None) self.assertEqual(config.asint('42'), 42) bugwarrior-1.8.0/tests/test_data.py000066400000000000000000000022241376471246600174070ustar00rootroot00000000000000import os import json from six.moves import configparser from bugwarrior import data from .base import ConfigTest class TestData(ConfigTest): def setUp(self): super(TestData, self).setUp() config = configparser.RawConfigParser() config.add_section('general') self.data = data.BugwarriorData(self.lists_path) def assert0600(self): permissions = oct(os.stat(self.data.datafile).st_mode & 0o777) # python2 -> 0600, python3 -> 0o600 self.assertIn(permissions, ['0600', '0o600']) def test_get_set(self): # "touch" data file. with open(self.data.datafile, 'w+') as handle: json.dump({'old': 'stuff'}, handle) self.data.set('key', 'value') self.assertEqual(self.data.get('key'), 'value') self.assertEqual( self.data.get_data(), {'old': 'stuff', 'key': 'value'}) self.assert0600() def test_set_first_time(self): self.data.set('key', 'value') self.assertEqual(self.data.get('key'), 'value') self.assert0600() def test_path_attribute(self): self.assertEqual(self.data.path, self.lists_path) bugwarrior-1.8.0/tests/test_db.py000066400000000000000000000164331376471246600170720ustar00rootroot00000000000000# -*- coding: utf-8 -*- import unittest from six.moves import configparser import taskw.task from bugwarrior import db from .base import ConfigTest class TestMergeLeft(unittest.TestCase): def setUp(self): self.issue_dict = {'annotations': ['testing']} def assertMerged(self, local, remote, **kwargs): db.merge_left('annotations', local, remote, **kwargs) self.assertEqual(local, remote) def test_with_dict(self): self.assertMerged({}, self.issue_dict) def test_with_taskw(self): self.assertMerged(taskw.task.Task({}), self.issue_dict) def test_already_in_sync(self): self.assertMerged(self.issue_dict, self.issue_dict) def test_rough_equality_hamming_false(self): """ When hamming=False, rough equivalents are duplicated. """ remote = {'annotations': ['\n testing \n']} db.merge_left('annotations', self.issue_dict, remote, hamming=False) self.assertEqual(len(self.issue_dict['annotations']), 2) def test_rough_equality_hamming_true(self): """ When hamming=True, rough equivalents are not duplicated. """ remote = {'annotations': ['\n testing \n']} db.merge_left('annotations', self.issue_dict, remote, hamming=True) self.assertEqual(len(self.issue_dict['annotations']), 1) class TestReplaceLeft(unittest.TestCase): def setUp(self): self.issue_dict = {'tags': ['test', 'test2'] } self.remote = { 'tags': ['remote_tag1', 'remote_tag2'] } def assertReplaced(self, local, remote, **kwargs): db.replace_left('tags', local, remote, **kwargs) self.assertEqual(local, remote) def test_with_dict(self): self.assertReplaced({}, self.issue_dict) def test_with_taskw(self): self.assertReplaced(taskw.task.Task({}), self.issue_dict) def test_already_in_sync(self): self.assertReplaced(self.issue_dict, self.issue_dict) def test_replace(self): self.assertReplaced(self.issue_dict, self.remote) def test_replace_with_keeped_item(self): """ When keeped_item is set, all item in this list are keeped """ result = {'tags': ['test', 'remote_tag1', 'remote_tag2'] } print(self.issue_dict) keeped_items = [ 'test' ] db.replace_left('tags', self.issue_dict, self.remote, keeped_items) self.assertEqual(self.issue_dict, result) class TestSynchronize(ConfigTest): def test_synchronize(self): def get_tasks(tw): tasks = tw.load_tasks() # Remove non-deterministic keys. del tasks['pending'][0]['modified'] del tasks['pending'][0]['entry'] del tasks['pending'][0]['uuid'] return tasks config = configparser.RawConfigParser() config.add_section('general') config.set('general', 'targets', 'my_service') config.set('general', 'static_fields', 'project, priority') config.add_section('my_service') config.set('my_service', 'service', 'github') tw = taskw.TaskWarrior(self.taskrc) self.assertEqual(tw.load_tasks(), {'completed': [], 'pending': []}) issue = { 'description': 'Blah blah blah. ☃', 'project': 'sample_project', 'githubtype': 'issue', 'githuburl': 'https://example.com', 'priority': 'M', } # TEST NEW ISSUE AND EXISTING ISSUE. for _ in range(2): # Use an issue generator with two copies of the same issue. # These should be de-duplicated in db.synchronize before # writing out to taskwarrior. # https://github.com/ralphbean/bugwarrior/issues/601 issue_generator = iter((issue, issue,)) db.synchronize(issue_generator, config, 'general') self.assertEqual(get_tasks(tw), { 'completed': [], 'pending': [{ u'project': u'sample_project', u'priority': u'M', u'status': u'pending', u'description': u'Blah blah blah. ☃', u'githuburl': u'https://example.com', u'githubtype': u'issue', u'id': 1, u'urgency': 4.9, }]}) # TEST CHANGED ISSUE. issue['description'] = 'Yada yada yada.' # Change static field issue['project'] = 'other_project' db.synchronize(iter((issue,)), config, 'general') self.assertEqual(get_tasks(tw), { 'completed': [], 'pending': [{ u'priority': u'M', u'project': u'sample_project', u'status': u'pending', u'description': u'Yada yada yada.', u'githuburl': u'https://example.com', u'githubtype': u'issue', u'id': 1, u'urgency': 4.9, }]}) # TEST CLOSED ISSUE. db.synchronize(iter(()), config, 'general') tasks = tw.load_tasks() # Remove non-deterministic keys. del tasks['completed'][0]['modified'] del tasks['completed'][0]['entry'] del tasks['completed'][0]['end'] del tasks['completed'][0]['uuid'] self.assertEqual(tasks, { 'completed': [{ u'project': u'sample_project', u'description': u'Yada yada yada.', u'githubtype': u'issue', u'githuburl': u'https://example.com', u'id': 0, u'priority': u'M', u'status': u'completed', u'urgency': 4.9, }], 'pending': []}) class TestUDAs(ConfigTest): def test_udas(self): config = configparser.RawConfigParser() config.add_section('general') config.set('general', 'targets', 'my_service') config.add_section('my_service') config.set('my_service', 'service', 'github') udas = sorted(list(db.get_defined_udas_as_strings(config, 'general'))) self.assertEqual(udas, [ u'uda.githubbody.label=Github Body', u'uda.githubbody.type=string', u'uda.githubclosedon.label=GitHub Closed', u'uda.githubclosedon.type=date', u'uda.githubcreatedon.label=Github Created', u'uda.githubcreatedon.type=date', u'uda.githubmilestone.label=Github Milestone', u'uda.githubmilestone.type=string', u'uda.githubnamespace.label=Github Namespace', u'uda.githubnamespace.type=string', u'uda.githubnumber.label=Github Issue/PR #', u'uda.githubnumber.type=numeric', u'uda.githubrepo.label=Github Repo Slug', u'uda.githubrepo.type=string', u'uda.githubstate.label=GitHub State', u'uda.githubstate.type=string', u'uda.githubtitle.label=Github Title', u'uda.githubtitle.type=string', u'uda.githubtype.label=Github Type', u'uda.githubtype.type=string', u'uda.githubupdatedat.label=Github Updated', u'uda.githubupdatedat.type=date', u'uda.githuburl.label=Github URL', u'uda.githuburl.type=string', u'uda.githubuser.label=Github User', u'uda.githubuser.type=string', ]) bugwarrior-1.8.0/tests/test_gerrit.py000066400000000000000000000050031376471246600177700ustar00rootroot00000000000000from builtins import next import json import responses from bugwarrior.services.gerrit import GerritService from .base import ServiceTest, AbstractServiceTest class TestGerritIssue(AbstractServiceTest, ServiceTest): SERVICE_CONFIG = { 'gerrit.base_uri': 'https://one.com', 'gerrit.username': 'two', 'gerrit.password': 'three', } record = { 'project': 'nova', '_number': 1, 'branch': 'master', 'topic': 'test-topic', 'subject': 'this is a title', 'messages': [{'author': {'username': 'Iam Author'}, 'message': 'this is a message', '_revision_number': 1}], } def setUp(self): super(TestGerritIssue, self).setUp() responses.add( responses.HEAD, self.SERVICE_CONFIG['gerrit.base_uri'] + '/a/', headers={'www-authenticate': 'digest'}) with responses.mock: self.service = self.get_mock_service(GerritService) def test_to_taskwarrior(self): extra = { 'annotations': [ # TODO - test annotations? ], 'url': 'this is a url', } issue = self.service.get_issue_for_record(self.record, extra) actual = issue.to_taskwarrior() expected = { 'annotations': [], 'priority': 'M', 'project': 'nova', 'gerritid': 1, 'gerritsummary': 'this is a title', 'gerriturl': 'this is a url', 'gerritbranch': 'master', 'gerrittopic': 'test-topic', 'tags': [], } self.assertEqual(actual, expected) @responses.activate def test_issues(self): self.add_response( 'https://one.com/a/changes/?q=is:open+is:reviewer&o=MESSAGES&o=DETAILED_ACCOUNTS', # The response has some ")]}'" garbage prefixed. body=")]}'" + json.dumps([self.record])) issue = next(self.service.issues()) expected = { 'annotations': [u'@Iam Author - is is a message'], 'description': u'(bw)PR#1 - this is a title .. https://one.com/#/c/1/', 'gerritid': 1, 'gerritsummary': u'this is a title', 'gerriturl': 'https://one.com/#/c/1/', 'gerritbranch': 'master', 'gerrittopic': 'test-topic', 'priority': 'M', 'project': u'nova', 'tags': []} self.assertEqual(issue.get_taskwarrior_record(), expected) bugwarrior-1.8.0/tests/test_github.py000066400000000000000000000276131376471246600177710ustar00rootroot00000000000000from builtins import next import datetime from unittest import TestCase from six.moves.configparser import RawConfigParser import pytz import responses from bugwarrior.config import ServiceConfig from bugwarrior.services.github import GithubService, GithubClient from .base import ServiceTest, AbstractServiceTest ARBITRARY_CREATED = ( datetime.datetime.utcnow() - datetime.timedelta(hours=1) ).replace(tzinfo=pytz.UTC, microsecond=0) ARBITRARY_CLOSED = ( datetime.datetime.utcnow() - datetime.timedelta(minutes=30) ).replace(tzinfo=pytz.UTC, microsecond=0) ARBITRARY_UPDATED = datetime.datetime.utcnow().replace( tzinfo=pytz.UTC, microsecond=0) ARBITRARY_ISSUE = { 'title': 'Hallo', 'html_url': 'https://github.com/arbitrary_username/arbitrary_repo/pull/1', 'url': 'https://api.github.com/repos/arbitrary_username/arbitrary_repo/issues/1', 'number': 10, 'body': 'Something', 'user': {'login': 'arbitrary_login'}, 'milestone': {'title': 'alpha'}, 'labels': [{'name': 'bugfix'}], 'created_at': ARBITRARY_CREATED.isoformat(), 'closed_at': ARBITRARY_CLOSED.isoformat(), 'updated_at': ARBITRARY_UPDATED.isoformat(), 'repo': 'arbitrary_username/arbitrary_repo', 'state': 'closed' } ARBITRARY_EXTRA = { 'project': 'one', 'type': 'issue', 'annotations': [], 'body': 'Something', 'namespace': 'arbitrary_username', } class TestGithubIssue(AbstractServiceTest, ServiceTest): maxDiff = None SERVICE_CONFIG = { 'github.login': 'arbitrary_login', 'github.password': 'arbitrary_password', 'github.username': 'arbitrary_username', } def setUp(self): super(TestGithubIssue, self).setUp() self.service = self.get_mock_service(GithubService) def test_normalize_label_to_tag(self): issue = self.service.get_issue_for_record( ARBITRARY_ISSUE, ARBITRARY_EXTRA ) self.assertEqual(issue._normalize_label_to_tag('needs work'), 'needs_work') def test_to_taskwarrior(self): self.service.import_labels_as_tags = True issue = self.service.get_issue_for_record( ARBITRARY_ISSUE, ARBITRARY_EXTRA ) expected_output = { 'project': ARBITRARY_EXTRA['project'], 'priority': self.service.default_priority, 'annotations': [], 'tags': ['bugfix'], 'entry': ARBITRARY_CREATED, 'end': ARBITRARY_CLOSED, issue.URL: ARBITRARY_ISSUE['html_url'], issue.REPO: ARBITRARY_ISSUE['repo'], issue.TYPE: ARBITRARY_EXTRA['type'], issue.TITLE: ARBITRARY_ISSUE['title'], issue.NUMBER: ARBITRARY_ISSUE['number'], issue.UPDATED_AT: ARBITRARY_UPDATED, issue.CREATED_AT: ARBITRARY_CREATED, issue.CLOSED_AT: ARBITRARY_CLOSED, issue.BODY: ARBITRARY_EXTRA['body'], issue.MILESTONE: ARBITRARY_ISSUE['milestone']['title'], issue.USER: ARBITRARY_ISSUE['user']['login'], issue.NAMESPACE: 'arbitrary_username', issue.STATE: 'closed', } actual_output = issue.to_taskwarrior() self.assertEqual(actual_output, expected_output) @responses.activate def test_issues(self): self.add_response( 'https://api.github.com/user/repos?per_page=100', json=[{ 'name': 'some_repo', 'owner': {'login': 'some_username'} }]) self.add_response( 'https://api.github.com/users/arbitrary_username/repos?per_page=100', json=[{ 'name': 'arbitrary_repo', 'owner': {'login': 'arbitrary_username'} }]) self.add_response( 'https://api.github.com/repos/arbitrary_username/arbitrary_repo/issues?per_page=100', json=[ARBITRARY_ISSUE]) self.add_response( 'https://api.github.com/user/issues?per_page=100', json=[ARBITRARY_ISSUE]) self.add_response( 'https://api.github.com/repos/arbitrary_username/arbitrary_repo/issues/10/comments?per_page=100', json=[{ 'user': {'login': 'arbitrary_login'}, 'body': 'Arbitrary comment.' }]) issue = next(self.service.issues()) expected = { 'annotations': [u'@arbitrary_login - Arbitrary comment.'], 'description': u'(bw)Is#10 - Hallo .. https://github.com/arbitrary_username/arbitrary_repo/pull/1', 'entry': ARBITRARY_CREATED, 'end': ARBITRARY_CLOSED, 'githubbody': u'Something', 'githubcreatedon': ARBITRARY_CREATED, 'githubclosedon': ARBITRARY_CLOSED, 'githubmilestone': u'alpha', 'githubnamespace': 'arbitrary_username', 'githubnumber': 10, 'githubrepo': 'arbitrary_username/arbitrary_repo', 'githubtitle': u'Hallo', 'githubtype': 'issue', 'githubupdatedat': ARBITRARY_UPDATED, 'githuburl': u'https://github.com/arbitrary_username/arbitrary_repo/pull/1', 'githubuser': u'arbitrary_login', 'githubstate': u'closed', 'priority': 'M', 'project': 'arbitrary_repo', 'tags': []} self.assertEqual(issue.get_taskwarrior_record(), expected) class TestGithubIssueQuery(AbstractServiceTest, ServiceTest): maxDiff = None SERVICE_CONFIG = { 'github.login': 'arbitrary_login', 'github.password': 'arbitrary_password', 'github.username': 'arbitrary_username', 'github.query': 'is:open reviewer:octocat', 'github.include_user_repos': 'False', 'github.include_user_issues': 'False', } def setUp(self): super(TestGithubIssueQuery, self).setUp() self.service = self.get_mock_service(GithubService) def test_to_taskwarrior(self): pass @responses.activate def test_issues(self): self.add_response( 'https://api.github.com/search/issues?q=is%3Aopen+reviewer%3Aoctocat&per_page=100', json={'items': [ARBITRARY_ISSUE]}) self.add_response( 'https://api.github.com/repos/arbitrary_username/arbitrary_repo/issues/10/comments?per_page=100', json=[{ 'user': {'login': 'arbitrary_login'}, 'body': 'Arbitrary comment.' }]) issue = list(self.service.issues())[0] expected = { 'annotations': [u'@arbitrary_login - Arbitrary comment.'], 'description': u'(bw)Is#10 - Hallo .. https://github.com/arbitrary_username/arbitrary_repo/pull/1', 'entry': ARBITRARY_CREATED, 'end': ARBITRARY_CLOSED, 'githubbody': u'Something', 'githubcreatedon': ARBITRARY_CREATED, 'githubclosedon': ARBITRARY_CLOSED, 'githubmilestone': u'alpha', 'githubnamespace': 'arbitrary_username', 'githubnumber': 10, 'githubrepo': 'arbitrary_username/arbitrary_repo', 'githubtitle': u'Hallo', 'githubtype': 'issue', 'githubupdatedat': ARBITRARY_UPDATED, 'githuburl': u'https://github.com/arbitrary_username/arbitrary_repo/pull/1', 'githubuser': u'arbitrary_login', 'githubstate': u'closed', 'priority': 'M', 'project': 'arbitrary_repo', 'tags': []} self.assertEqual(issue.get_taskwarrior_record(), expected) class TestGithubService(TestCase): def setUp(self): self.config = RawConfigParser() self.config.interactive = False self.config.add_section('general') self.config.add_section('mygithub') self.config.set('mygithub', 'service', 'github') self.config.set('mygithub', 'github.login', 'tintin') self.config.set('mygithub', 'github.username', 'milou') self.config.set('mygithub', 'github.password', 't0ps3cr3t') self.service_config = ServiceConfig( GithubService.CONFIG_PREFIX, self.config, 'mygithub') def test_token_authorization_header(self): self.config.remove_option('mygithub', 'github.password') self.config.set('mygithub', 'github.token', '@oracle:eval:echo 1234567890ABCDEF') service = GithubService(self.config, 'general', 'mygithub') self.assertEqual(service.client.session.headers['Authorization'], "token 1234567890ABCDEF") def test_default_host(self): """ Check that if github.host is not set, we default to github.com """ service = GithubService(self.config, 'general', 'mygithub') self.assertEqual("github.com", service.host) def test_overwrite_host(self): """ Check that if github.host is set, we use its value as host """ self.config.set('mygithub', 'github.host', 'github.example.com') service = GithubService(self.config, 'general', 'mygithub') self.assertEqual("github.example.com", service.host) def test_keyring_service(self): """ Checks that the keyring service name """ keyring_service = GithubService.get_keyring_service(self.service_config) self.assertEqual("github://tintin@github.com/milou", keyring_service) def test_keyring_service_host(self): """ Checks that the keyring key depends on the github host. """ self.config.set('mygithub', 'github.host', 'github.example.com') keyring_service = GithubService.get_keyring_service(self.service_config) self.assertEqual("github://tintin@github.example.com/milou", keyring_service) def test_get_repository_from_issue_url__issue(self): issue = dict(repos_url="https://github.com/foo/bar") repository = GithubService.get_repository_from_issue(issue) self.assertEqual("foo/bar", repository) def test_get_repository_from_issue_url__pull_request(self): issue = dict(repos_url="https://github.com/foo/bar") repository = GithubService.get_repository_from_issue(issue) self.assertEqual("foo/bar", repository) def test_get_repository_from_issue__enterprise_github(self): issue = dict(repos_url="https://github.acme.biz/foo/bar") repository = GithubService.get_repository_from_issue(issue) self.assertEqual("foo/bar", repository) def test_body_no_limit(self): service = GithubService(self.config, 'general', 'mygithub') issue = dict(body="A very short issue body. Fixes #42.") self.assertEqual(issue["body"], service.body(issue)) def test_body_newline_style(self): service = GithubService(self.config, 'general', 'mygithub') issue = dict(body="An\r\nIssue\r\nWith\r\nNewlines") self.assertEqual("An\nIssue\nWith\nNewlines", service.body(issue)) def test_body_length_limit(self): self.config.set('mygithub', 'github.body_length', '5') service = GithubService(self.config, 'general', 'mygithub') issue = dict(body="A very short issue body. Fixes #42.") self.assertEqual(issue["body"][:5], service.body(issue)) class TestGithubClient(TestCase): def test_api_url(self): auth = {'token': 'xxxx'} client = GithubClient('github.com', auth) self.assertEqual( client._api_url('/some/path'), 'https://api.github.com/some/path') def test_api_url_with_context(self): auth = {'token': 'xxxx'} client = GithubClient('github.com', auth) self.assertEqual( client._api_url('/some/path/{foo}', foo='bar'), 'https://api.github.com/some/path/bar') def test_api_url_with_custom_host(self): """ Test generating an API URL with a custom host """ auth = {'token': 'xxxx'} client = GithubClient('github.example.com', auth) self.assertEqual( client._api_url('/some/path'), 'https://github.example.com/api/v3/some/path') bugwarrior-1.8.0/tests/test_gitlab.py000066400000000000000000000262461376471246600177520ustar00rootroot00000000000000from future import standard_library standard_library.install_aliases() from builtins import next from six.moves import configparser import datetime import pytz import responses from bugwarrior.config import ServiceConfig from bugwarrior.services.gitlab import GitlabService from .base import ConfigTest, ServiceTest, AbstractServiceTest class TestGitlabService(ConfigTest): def setUp(self): super(TestGitlabService, self).setUp() self.config = configparser.RawConfigParser() self.config.add_section('general') self.config.add_section('myservice') self.config.set('myservice', 'gitlab.login', 'foobar') self.config.set('myservice', 'gitlab.token', 'XXXXXX') self.service_config = ServiceConfig( GitlabService.CONFIG_PREFIX, self.config, 'myservice') def test_get_keyring_service_default_host(self): self.assertEqual( GitlabService.get_keyring_service(self.service_config), 'gitlab://foobar@gitlab.com') def test_get_keyring_service_custom_host(self): self.config.set('myservice', 'gitlab.host', 'gitlab.example.com') self.assertEqual( GitlabService.get_keyring_service(self.service_config), 'gitlab://foobar@gitlab.example.com') def test_add_default_namespace_to_included_repos(self): self.config.set('myservice', 'gitlab.include_repos', 'baz, banana/tree') service = GitlabService(self.config, 'general', 'myservice') self.assertEqual(service.include_repos, ['foobar/baz', 'banana/tree']) def test_add_default_namespace_to_excluded_repos(self): self.config.set('myservice', 'gitlab.exclude_repos', 'baz, banana/tree') service = GitlabService(self.config, 'general', 'myservice') self.assertEqual(service.exclude_repos, ['foobar/baz', 'banana/tree']) def test_filter_repos_default(self): service = GitlabService(self.config, 'general', 'myservice') repo = {'path_with_namespace': 'foobar/baz'} self.assertTrue(service.filter_repos(repo)) def test_filter_repos_exclude(self): self.config.set('myservice', 'gitlab.exclude_repos', 'foobar/baz') service = GitlabService(self.config, 'general', 'myservice') repo = {'path_with_namespace': 'foobar/baz', 'id': 1234} self.assertFalse(service.filter_repos(repo)) def test_filter_repos_exclude_id(self): self.config.set('myservice', 'gitlab.exclude_repos', 'id:1234') service = GitlabService(self.config, 'general', 'myservice') repo = {'path_with_namespace': 'foobar/baz', 'id': 1234} self.assertFalse(service.filter_repos(repo)) def test_filter_repos_include(self): self.config.set('myservice', 'gitlab.include_repos', 'foobar/baz') service = GitlabService(self.config, 'general', 'myservice') repo = {'path_with_namespace': 'foobar/baz', 'id': 1234} self.assertTrue(service.filter_repos(repo)) def test_filter_repos_include_id(self): self.config.set('myservice', 'gitlab.include_repos', 'id:1234') service = GitlabService(self.config, 'general', 'myservice') repo = {'path_with_namespace': 'foobar/baz', 'id': 1234} self.assertTrue(service.filter_repos(repo)) class TestGitlabIssue(AbstractServiceTest, ServiceTest): maxDiff = None SERVICE_CONFIG = { 'gitlab.host': 'gitlab.example.com', 'gitlab.login': 'arbitrary_login', 'gitlab.token': 'arbitrary_token', } def setUp(self): super(TestGitlabIssue, self).setUp() self.service = self.get_mock_service(GitlabService) self.arbitrary_created = ( datetime.datetime.utcnow() - datetime.timedelta(hours=1) ).replace(tzinfo=pytz.UTC, microsecond=0) self.arbitrary_updated = datetime.datetime.utcnow().replace( tzinfo=pytz.UTC, microsecond=0) self.arbitrary_duedate = ( datetime.datetime.combine(datetime.date.today(), datetime.datetime.min.time()) ).replace(tzinfo=pytz.UTC) self.arbitrary_issue = { "id": 42, "iid": 3, "project_id": 8, "title": "Add user settings", "description": "", "labels": [ "feature" ], "milestone": { "id": 1, "title": "v1.0", "description": "", "due_date": self.arbitrary_duedate.date().isoformat(), "state": "closed", "updated_at": "2012-07-04T13:42:48Z", "created_at": "2012-07-04T13:42:48Z" }, "assignee": { "id": 2, "username": "jack_smith", "email": "jack@example.com", "name": "Jack Smith", "state": "active", "created_at": "2012-05-23T08:01:01Z" }, "author": { "id": 1, "username": "john_smith", "email": "john@example.com", "name": "John Smith", "state": "active", "created_at": "2012-05-23T08:00:58Z" }, "state": "opened", "updated_at": self.arbitrary_updated.isoformat(), "created_at": self.arbitrary_created.isoformat(), "weight": 3, "work_in_progress": "true" } self.arbitrary_extra = { 'issue_url': 'https://gitlab.example.com/arbitrary_username/project/issues/3', 'project': 'project', 'namespace': 'arbitrary_namespace', 'type': 'issue', 'annotations': [], } def test_normalize_label_to_tag(self): issue = self.service.get_issue_for_record( self.arbitrary_issue, self.arbitrary_extra ) self.assertEqual(issue._normalize_label_to_tag('needs work'), 'needs_work') def test_to_taskwarrior(self): self.service.import_labels_as_tags = True issue = self.service.get_issue_for_record( self.arbitrary_issue, self.arbitrary_extra ) expected_output = { 'project': self.arbitrary_extra['project'], 'priority': self.service.default_priority, 'annotations': [], 'tags': [u'feature'], 'due': self.arbitrary_duedate.replace(microsecond=0), 'entry': self.arbitrary_created.replace(microsecond=0), issue.URL: self.arbitrary_extra['issue_url'], issue.REPO: 'project', issue.STATE: self.arbitrary_issue['state'], issue.TYPE: self.arbitrary_extra['type'], issue.TITLE: self.arbitrary_issue['title'], issue.NUMBER: str(self.arbitrary_issue['iid']), issue.UPDATED_AT: self.arbitrary_updated.replace(microsecond=0), issue.CREATED_AT: self.arbitrary_created.replace(microsecond=0), issue.DUEDATE: self.arbitrary_duedate, issue.DESCRIPTION: self.arbitrary_issue['description'], issue.MILESTONE: self.arbitrary_issue['milestone']['title'], issue.UPVOTES: 0, issue.DOWNVOTES: 0, issue.WORK_IN_PROGRESS: 1, issue.AUTHOR: 'john_smith', issue.ASSIGNEE: 'jack_smith', issue.NAMESPACE: 'arbitrary_namespace', issue.WEIGHT: 3, } actual_output = issue.to_taskwarrior() self.assertEqual(actual_output, expected_output) def test_work_in_progress(self): self.arbitrary_issue['work_in_progress'] = 'false' self.service.import_labels_as_tags = True issue = self.service.get_issue_for_record( self.arbitrary_issue, self.arbitrary_extra ) expected_output = { 'project': self.arbitrary_extra['project'], 'priority': self.service.default_priority, 'annotations': [], 'tags': [u'feature'], 'due': self.arbitrary_duedate.replace(microsecond=0), 'entry': self.arbitrary_created.replace(microsecond=0), issue.URL: self.arbitrary_extra['issue_url'], issue.REPO: 'project', issue.STATE: self.arbitrary_issue['state'], issue.TYPE: self.arbitrary_extra['type'], issue.TITLE: self.arbitrary_issue['title'], issue.NUMBER: str(self.arbitrary_issue['iid']), issue.UPDATED_AT: self.arbitrary_updated.replace(microsecond=0), issue.CREATED_AT: self.arbitrary_created.replace(microsecond=0), issue.DUEDATE: self.arbitrary_duedate, issue.DESCRIPTION: self.arbitrary_issue['description'], issue.MILESTONE: self.arbitrary_issue['milestone']['title'], issue.UPVOTES: 0, issue.DOWNVOTES: 0, issue.WORK_IN_PROGRESS: 0, issue.AUTHOR: 'john_smith', issue.ASSIGNEE: 'jack_smith', issue.NAMESPACE: 'arbitrary_namespace', issue.WEIGHT: 3, } actual_output = issue.to_taskwarrior() self.assertEqual(actual_output, expected_output) @responses.activate def test_issues(self): self.add_response( 'https://gitlab.example.com/api/v4/projects?simple=True&per_page=100&page=1', json=[{ 'id': 1, 'path': 'arbitrary_username/project', 'web_url': 'example.com', "namespace": { "full_path": "arbitrary_username" } }]) self.add_response( 'https://gitlab.example.com/api/v4/projects/1/issues?state=opened&per_page=100&page=1', json=[self.arbitrary_issue]) self.add_response( 'https://gitlab.example.com/api/v4/projects/1/issues/3/notes?per_page=100&page=1', json=[{ 'author': {'username': 'john_smith'}, 'body': 'Some comment.' }]) issue = next(self.service.issues()) expected = { 'annotations': [u'@john_smith - Some comment.'], 'description': u'(bw)Is#3 - Add user settings .. example.com/issues/3', 'due': self.arbitrary_duedate, 'entry': self.arbitrary_created, 'gitlabassignee': u'jack_smith', 'gitlabauthor': u'john_smith', 'gitlabcreatedon': self.arbitrary_created, 'gitlabdescription': u'', 'gitlabdownvotes': 0, 'gitlabmilestone': u'v1.0', 'gitlabnamespace': u'arbitrary_username', 'gitlabnumber': '3', 'gitlabrepo': u'arbitrary_username/project', 'gitlabstate': u'opened', 'gitlabtitle': u'Add user settings', 'gitlabtype': 'issue', 'gitlabupdatedat': self.arbitrary_updated, 'gitlabduedate': self.arbitrary_duedate, 'gitlabupvotes': 0, 'gitlaburl': u'example.com/issues/3', 'gitlabwip': 1, 'gitlabweight': 3, 'priority': 'M', 'project': u'arbitrary_username/project', 'tags': []} self.assertEqual(issue.get_taskwarrior_record(), expected) bugwarrior-1.8.0/tests/test_gmail.py000066400000000000000000000166671376471246600176070ustar00rootroot00000000000000import os.path import pickle from copy import copy from datetime import datetime, timedelta from unittest import mock from unittest.mock import patch from dateutil.tz import tzutc from google.oauth2.credentials import Credentials from six.moves import configparser import bugwarrior.services.gmail as gmail from bugwarrior.config import ServiceConfig from bugwarrior.services.gmail import GmailService from .base import AbstractServiceTest, ConfigTest, ServiceTest TEST_CREDENTIAL = { "token": "itsatokeneveryone", "refresh_token": "itsarefreshtokeneveryone", "token_uri": "https://oauth2.googleapis.com/token", "client_id": "example.apps.googleusercontent.com", "client_secret": "itsasecrettoeveryone", "scopes": ["https://www.googleapis.com/auth/gmail.readonly"], } class TestGmailService(ConfigTest): def setUp(self): super(TestGmailService, self).setUp() self.config = configparser.RawConfigParser() self.config.add_section("general") self.config.add_section("myservice") mock_data = mock.Mock() mock_data.path = self.tempdir self.config.data = mock_data self.service_config = ServiceConfig(GmailService.CONFIG_PREFIX, self.config, "myservice") def test_get_credentials_exists_and_valid(self): mock_api = mock.Mock() gmail.GmailService.build_api = mock_api service = GmailService(self.config, "general", "myservice") expected = Credentials(**copy(TEST_CREDENTIAL)) self.assertEqual(expected.valid, True) with open(service.credentials_path, "wb") as token: pickle.dump(expected, token) self.assertEqual(service.get_credentials().to_json(), expected.to_json()) def test_get_credentials_with_refresh(self): mock_api = mock.Mock() gmail.GmailService.build_api = mock_api service = GmailService(self.config, "general", "myservice") expired_credential = Credentials(**copy(TEST_CREDENTIAL)) expired_credential.expiry = datetime.now() self.assertEqual(expired_credential.valid, False) with open(service.credentials_path, "wb") as token: pickle.dump(expired_credential, token) with patch("google.oauth2._client.refresh_grant") as mock_refresh_grant: access_token = "newaccesstoken" refresh_token = "newrefreshtoken" expiry = datetime.now() + timedelta(hours=24) grant_response = {"id_token": "idtoken"} mock_refresh_grant.return_value = access_token, refresh_token, expiry, grant_response refreshed_credential = service.get_credentials() self.assertEqual(refreshed_credential.valid, True) TEST_THREAD = { "messages": [ { "payload": { "headers": [ {"name": "From", "value": "Foo Bar "}, {"name": "Subject", "value": "Regarding Bugwarrior"}, {"name": "To", "value": "ct@example.com"}, { "name": "Message-ID", "value": "", }, ], "parts": [{}], }, "snippet": "Bugwarrior is great", "internalDate": 1546722467000, "threadId": "1234", "labelIds": ["IMPORTANT", "Label_1", "Label_43", "CATEGORY_PERSONAL"], "id": "9999", } ], "id": "1234", } TEST_LABELS = [ {"id": "IMPORTANT", "name": "IMPORTANT"}, {"id": "CATEGORY_PERSONAL", "name": "CATEGORY_PERSONAL"}, {"id": "Label_1", "name": "sticky"}, {"id": "Label_43", "name": "postit"}, ] class TestGmailIssue(AbstractServiceTest, ServiceTest): SERVICE_CONFIG = { 'gmail.add_tags': 'added', 'gmail.login_name': 'test@example.com', } def setUp(self): super(TestGmailIssue, self).setUp() mock_api = mock.Mock() mock_api().users().labels().list().execute.return_value = {'labels': TEST_LABELS} mock_api().users().threads().list().execute.return_value = {'threads': [{'id': TEST_THREAD['id']}]} mock_api().users().threads().get().execute.return_value = TEST_THREAD gmail.GmailService.build_api = mock_api self.service = self.get_mock_service(gmail.GmailService, section='test_section') def test_config_paths(self): credentials_path = os.path.join( self.service.config.data.path, 'gmail_credentials_test_example_com.pickle') self.assertEqual(self.service.credentials_path, credentials_path) def test_to_taskwarrior(self): thread = TEST_THREAD issue = self.service.get_issue_for_record( thread, gmail.thread_extras(thread, self.service.get_labels())) expected = { 'annotations': [], 'entry': datetime(2019, 1, 5, 21, 7, 47, tzinfo=tzutc()), 'gmailthreadid': '1234', 'gmaillastmessageid': 'CMCRSF+6r=x5JtW4wlRYR5qdfRq+iAtSoec5NqrHvRpvVgHbHdg@mail.gmail.com', 'gmailsnippet': 'Bugwarrior is great', 'gmaillastsender': 'Foo Bar', 'tags': {'postit', 'sticky'}, 'gmailsubject': 'Regarding Bugwarrior', 'gmailurl': 'https://mail.google.com/mail/u/0/#all/1234', 'gmaillabels': 'CATEGORY_PERSONAL IMPORTANT postit sticky', 'priority': u'M', 'gmaillastsenderaddr': 'foobar@example.com'} taskwarrior = issue.to_taskwarrior() taskwarrior['tags'] = set(taskwarrior['tags']) self.assertEqual(taskwarrior, expected) def test_issues(self): issue = next(self.service.issues()) expected = { 'annotations': ['@Foo Bar - Regarding Bugwarrior'], 'entry': datetime(2019, 1, 5, 21, 7, 47, tzinfo=tzutc()), 'gmailthreadid': '1234', 'gmaillastmessageid': 'CMCRSF+6r=x5JtW4wlRYR5qdfRq+iAtSoec5NqrHvRpvVgHbHdg@mail.gmail.com', 'gmailsnippet': 'Bugwarrior is great', 'gmaillastsender': 'Foo Bar', 'description': u'(bw)Is#1234 - Regarding Bugwarrior .. https://mail.google.com/mail/u/0/#all/1234', 'priority': u'M', 'tags': {'added', 'postit', 'sticky'}, 'gmailsubject': 'Regarding Bugwarrior', 'gmailurl': 'https://mail.google.com/mail/u/0/#all/1234', 'gmaillabels': 'CATEGORY_PERSONAL IMPORTANT postit sticky', 'gmaillastsenderaddr': 'foobar@example.com'} taskwarrior = issue.get_taskwarrior_record() taskwarrior['tags'] = set(taskwarrior['tags']) self.assertEqual(taskwarrior, expected) def test_last_sender(self): test_thread = { 'messages': [ { 'payload': { 'headers': [ {'name': 'From', 'value': 'Xyz ,sequence=2322]'] def setUp(self): super(TestJiraIssue, self).setUp() with mock.patch('jira.client.JIRA._get_json'): self.service = self.get_mock_service(JiraService) def get_mock_service(self, *args, **kwargs): service = super(TestJiraIssue, self).get_mock_service(*args, **kwargs) service.jira = FakeJiraClient(self.arbitrary_record) service.sprint_field_names = ['Sprint'] service.import_sprints_as_tags = True return service def test_to_taskwarrior(self): arbitrary_url = 'http://one' arbitrary_extra = { 'jira_version': 5, 'annotations': ['an annotation'], } issue = self.service.get_issue_for_record( self.arbitrary_record, arbitrary_extra ) expected_output = { 'project': self.arbitrary_project, 'priority': ( issue.PRIORITY_MAP[self.arbitrary_record['fields']['priority']] ), 'annotations': arbitrary_extra['annotations'], 'due': None, 'tags': [], 'entry': datetime.datetime(2016, 6, 6, 13, 7, 8, tzinfo=tzutc()), 'jirafixversion': '1.2.3', 'jiraissuetype': 'Epic', 'jirastatus': 'Open', 'jirasubtasks': 'DONUT-11,DONUT-12', issue.URL: arbitrary_url, issue.FOREIGN_ID: self.arbitrary_record['key'], issue.SUMMARY: self.arbitrary_summary, issue.DESCRIPTION: None, issue.ESTIMATE: self.arbitrary_estimation / 60 / 60 } def get_url(*args): return arbitrary_url with mock.patch.object(issue, 'get_url', side_effect=get_url): actual_output = issue.to_taskwarrior() self.assertEqual(actual_output, expected_output) def test_to_taskwarrior_sprint_with_goal(self): record_with_goal = self.arbitrary_record.copy() record_with_goal['fields'] = self.arbitrary_record_with_due['fields'].copy() record_with_goal['fields']['Sprint'] = [ 'com.atlassian.greenhopper.service.sprint.Sprint@4c9c41a5[id=2322,rapidViewId=1173,\ state=ACTIVE,name=Sprint 1,goal=Do foo, bar, baz,startDate=2016-09-06T16:08:07.4\ 55Z,endDate=2016-09-23T16:08:00.000Z,completeDate=,sequence=2322]' ] arbitrary_url = 'http://one' arbitrary_extra = { 'jira_version': 5, 'annotations': ['an annotation'], } issue = self.service.get_issue_for_record( record_with_goal, arbitrary_extra ) expected_output = { 'project': self.arbitrary_project, 'priority': ( issue.PRIORITY_MAP[record_with_goal['fields']['priority']] ), 'annotations': arbitrary_extra['annotations'], 'due': datetime.datetime(2016, 9, 23, 16, 8, tzinfo=tzutc()), 'tags': ['Sprint1'], 'entry': datetime.datetime(2016, 6, 6, 13, 7, 8, tzinfo=tzutc()), 'jirafixversion': '1.2.3', 'jiraissuetype': 'Epic', 'jirastatus': 'Open', 'jirasubtasks': 'DONUT-11,DONUT-12', issue.URL: arbitrary_url, issue.FOREIGN_ID: record_with_goal['key'], issue.SUMMARY: self.arbitrary_summary, issue.DESCRIPTION: None, issue.ESTIMATE: self.arbitrary_estimation / 60 / 60 } def get_url(*args): return arbitrary_url with mock.patch.object(issue, 'get_url', side_effect=get_url): actual_output = issue.to_taskwarrior() self.assertEqual(actual_output, expected_output) def test_issues(self): issue = next(self.service.issues()) expected = { 'annotations': [], 'due': None, 'description': '(bw)Is#10 - lkjaldsfjaldf .. two/browse/DONUT-10', 'entry': datetime.datetime(2016, 6, 6, 13, 7, 8, tzinfo=tzutc()), 'jiradescription': None, 'jiraestimate': 1, 'jirafixversion': '1.2.3', 'jiraid': 'DONUT-10', 'jiraissuetype': 'Epic', 'jirastatus': 'Open', 'jirasummary': 'lkjaldsfjaldf', 'jiraurl': 'two/browse/DONUT-10', 'jirasubtasks': 'DONUT-11,DONUT-12', 'priority': 'H', 'project': 'DONUT', 'tags': []} self.assertEqual(issue.get_taskwarrior_record(), expected) def test_get_due(self): issue = self.service.get_issue_for_record( self.arbitrary_record_with_due ) self.assertEqual(issue.get_due(), datetime.datetime(2016, 9, 23, 16, 8, tzinfo=tzutc())) bugwarrior-1.8.0/tests/test_megaplan.py000066400000000000000000000051561376471246600202710ustar00rootroot00000000000000from builtins import next from builtins import object import unittest from unittest import mock try: from bugwarrior.services.mplan import MegaplanService except SyntaxError: raise unittest.SkipTest( 'Upstream python-megaplan does not support python3 yet.') from .base import ServiceTest, AbstractServiceTest class FakeMegaplanClient(object): def __init__(self, record): self.record = record def get_actual_tasks(self): return [self.record] class TestMegaplanIssue(AbstractServiceTest, ServiceTest): SERVICE_CONFIG = { 'megaplan.hostname': 'something', 'megaplan.login': 'something_else', 'megaplan.password': 'aljlkj', } name_parts = ['one', 'two', 'three'] arbitrary_issue = { 'Id': 10, 'Name': '|'.join(name_parts) } def setUp(self): super(TestMegaplanIssue, self).setUp() with mock.patch('megaplan.Client'): self.service = self.get_mock_service(MegaplanService) def get_mock_service(self, *args, **kwargs): service = super(TestMegaplanIssue, self).get_mock_service( *args, **kwargs) service.client = FakeMegaplanClient(self.arbitrary_issue) return service def test_to_taskwarrior(self): arbitrary_project = 'one' arbitrary_url = 'http://one.com/' issue = self.service.get_issue_for_record(self.arbitrary_issue) expected_output = { 'project': arbitrary_project, 'priority': self.service.default_priority, issue.FOREIGN_ID: self.arbitrary_issue['Id'], issue.URL: arbitrary_url, issue.TITLE: self.name_parts[-1] } def get_url(*args): return arbitrary_url def get_project(*args): return arbitrary_project with mock.patch.multiple( issue, get_project=mock.DEFAULT, get_issue_url=mock.DEFAULT ) as mocked: mocked['get_project'].side_effect = get_project mocked['get_issue_url'].side_effect = get_url actual_output = issue.to_taskwarrior() self.assertEqual(actual_output, expected_output) def test_issues(self): issue = next(self.service.issues()) expected = { 'description': '(bw)Is#10 - three .. https://something/task/10/card/', 'megaplanid': 10, 'megaplantitle': 'three', 'megaplanurl': 'https://something/task/10/card/', 'priority': 'M', 'project': 'something', 'tags': []} self.assertEqual(issue.get_taskwarrior_record(), expected) bugwarrior-1.8.0/tests/test_phab.py000066400000000000000000000037461376471246600174220ustar00rootroot00000000000000import datetime import unittest import pytz from bugwarrior.services.phab import PhabricatorService from .base import ServiceTest, AbstractServiceTest class TestPhabricatorIssue(AbstractServiceTest, ServiceTest): maxDiff = None SERVICE_CONFIG = { 'phabricator.host': 'phabricator.example.com', } def setUp(self): super(TestPhabricatorIssue, self).setUp() self.service = self.get_mock_service(PhabricatorService) self.arbitrary_created = ( datetime.datetime.utcnow() - datetime.timedelta(hours=1) ).replace(tzinfo=pytz.UTC, microsecond=0) self.arbitrary_updated = datetime.datetime.utcnow().replace( tzinfo=pytz.UTC, microsecond=0) self.arbitrary_duedate = ( datetime.datetime.combine(datetime.date.today(), datetime.datetime.min.time()) ).replace(tzinfo=pytz.UTC) self.arbitrary_issue = { "id": 42, "uri": "https://phabricator.example.com/arbitrary_username/project/issues/3", "title": "A phine phabricator issue", } self.arbitrary_extra = { 'type': 'issue', 'project': 'PHROJECT', 'annotations': [], } def test_to_taskwarrior(self): self.service.import_labels_as_tags = True issue = self.service.get_issue_for_record( self.arbitrary_issue, self.arbitrary_extra ) expected_output = { issue.URL: self.arbitrary_issue['uri'], issue.TYPE: self.arbitrary_extra['type'], issue.TITLE: self.arbitrary_issue['title'], issue.OBJECT_NAME: '3', 'project': 'PHROJECT', 'priority': 'M', 'annotations': [], } actual_output = issue.to_taskwarrior() self.assertEqual(actual_output, expected_output) @unittest.skip('The phabricator library is hard to mock.') def test_issues(self): pass bugwarrior-1.8.0/tests/test_pivotaltracker.py000066400000000000000000000260371376471246600215400ustar00rootroot00000000000000import datetime from dateutil.tz import tzutc from six.moves import configparser from unittest import mock import responses from .base import ServiceTest, AbstractServiceTest, ConfigTest from bugwarrior.config import ServiceConfig from bugwarrior.services.pivotaltracker import PivotalTrackerService PROJECT = { 'account_id': 100, 'atom_enabled': True, 'automatic_planning': True, 'bugs_and_chores_are_estimatable': False, 'created_at': '2019-05-14T12:00:05Z', 'current_iteration_number': 15, 'description': 'Expeditionary Battle Planetoid', 'enable_following': True, 'enable_incoming_emails': True, 'enable_tasks': True, 'has_google_domain': False, 'id': 99, 'initial_velocity': 10, 'iteration_length': 1, 'kind': 'project', 'name': 'Death Star', 'number_of_done_iterations_to_show': 4, 'point_scale': '0,1,2,3', 'point_scale_is_custom': False, 'profile_content': "This is a machine of war such as the universe has never known. It's colossal, the size of a class-four moon. And it possesses firepower unequaled in the history of warfare.", 'project_type': 'private', 'public': False, 'start_date': '2019-01-28', 'start_time': '2019-05-14T12:00:10Z', 'time_zone': { 'kind': 'time_zone', 'olson_name': 'America/Los_Angeles', 'offset': '-07:00' }, 'updated_at': '2019-05-14T12:00:10Z', 'velocity_averaged_over': 3, 'version': 66, 'week_start_day': 'Monday' } STORY = { 'project': PROJECT, 'kind': 'story', 'id': 561, 'created_at': '2019-05-14T12:00:00Z', 'updated_at': '2019-05-14T12:00:00Z', 'accepted_at': '2019-05-14T12:00:00Z', 'story_type': 'story', 'estimate': 3, 'name': 'Tractor beam loses power intermittently', 'description': 'All your base are belong to us', 'current_state': 'unstarted', 'requested_by_id': 106, 'url': 'http://localhost/story/show/561', 'project_id': 99, 'owner_ids': [ 106 ], 'labels': [ { 'kind': 'label', 'id': 5101, 'project_id': 99, 'name': 'look sir metal', 'created_at': '2019-05-14T12:00:05Z', 'updated_at': '2019-05-14T12:00:05Z' }, ] } USER = [ { 'created_at': '2019-05-14T12:00:00Z', 'favorite': False, 'id': 16200, 'kind': 'project_membership', 'person': { 'kind': 'person', 'id': 106, 'name': 'Galen Marek', 'email': 'marek@sith.mil', 'initials': 'GM', 'username': 'starkiller' }, 'project_color': 'b800bb', 'project_id': 99, 'role': 'member', 'updated_at': '2019-05-14T12:00:00Z', 'wants_comment_notification_emails': True, 'will_receive_mention_notifications_or_emails': True } ] TASKS = [ { 'kind': 'task', 'id': 5, 'story_id': 561, 'description': 'Port 0', 'complete': False, 'position': 1, 'created_at': '2019-05-14T12:00:00Z', 'updated_at': '2019-05-14T12:00:00Z' }, { 'kind': 'task', 'id': 6, 'story_id': 561, 'description': 'Port 90', 'complete': False, 'position': 2, 'created_at': '2019-05-14T12:00:00Z', 'updated_at': '2019-05-14T12:00:00Z' } ] BLOCKERS = [ { 'kind': 'blocker', 'id': 1100, 'story_id': 561, 'person_id': 106, 'description': 'Set weapons to stun', 'resolved': False, 'created_at': '2019-05-14T12:00:00Z', 'updated_at': '2019-05-14T12:00:00Z' } ] QUERY = { "epics": { "epics": [ ], "total_hits": 0 }, "query": "mywork:106", "stories": { "stories": [ STORY ], "total_points": 0, "total_points_completed": 0, "total_hits": 1, "total_hits_with_done": 0 } } EXTRA = { 'request_user': [ 'request_user' ], 'owned_user': [ 'owned_user' ], 'annotations': TASKS, 'blockers': BLOCKERS, 'project_name': PROJECT['name'] } class TestPivotalTrackerServiceConfig(ConfigTest): def setUp(self): super(TestPivotalTrackerServiceConfig, self).setUp() self.config = configparser.RawConfigParser() self.config.add_section('general') self.config.add_section('pivotal') self.service_config = ServiceConfig( PivotalTrackerService.CONFIG_PREFIX, self.config, 'pivotal') @mock.patch('bugwarrior.services.pivotaltracker.die') def test_validate_config(self, die): self.config.set('pivotal', 'pivotaltracker.account_ids', [12345]) self.config.set('pivotal', 'pivotaltracker.user_id', '12345') self.config.set('pivotal', 'pivotaltracker.token', '12345') PivotalTrackerService.validate_config(self.service_config, 'pivotal') die.assert_not_called() @mock.patch('bugwarrior.services.pivotaltracker.die') def test_validate_config_no_account_ids(self, die): self.config.set('pivotal', 'pivotaltracker.token', '123') self.config.set('pivotal', 'pivotaltracker.user_id', '12345') PivotalTrackerService.validate_config(self.service_config, 'pivotal') die.assert_called_with("[pivotal] has no 'pivotaltracker.account_ids'") @mock.patch('bugwarrior.services.pivotaltracker.die') def test_validate_config_no_user_id(self, die): self.config.set('pivotal', 'pivotaltracker.account_ids', [12345]) self.config.set('pivotal', 'pivotaltracker.token', '123') PivotalTrackerService.validate_config(self.service_config, 'pivotal') die.assert_called_with("[pivotal] has no 'pivotaltracker.user_id'") @mock.patch('bugwarrior.services.pivotaltracker.die') def test_validate_config_token(self, die): self.config.set('pivotal', 'pivotaltracker.account_ids', [12345]) self.config.set('pivotal', 'pivotaltracker.user_id', '12345') PivotalTrackerService.validate_config(self.service_config, 'pivotal') die.assert_called_with("[pivotal] has no 'pivotaltracker.token'") @mock.patch('bugwarrior.services.pivotaltracker.die') def test_validate_config_invalid_endpoint(self, die): self.config.set('pivotal', 'pivotaltracker.account_ids', [12345]) self.config.set('pivotal', 'pivotaltracker.token', '123') self.config.set('pivotal', 'pivotaltracker.user_id', '12345') self.config.set('pivotal', 'pivotaltracker.version', 'v1') PivotalTrackerService.validate_config(self.service_config, 'pivotal') die.assert_called_with("[pivotal] has an invalid 'pivotaltracker.version'") class TestPivotalTrackerIssue(AbstractServiceTest, ServiceTest): SERVICE_CONFIG = { 'pivotaltracker.token': '123456', 'pivotaltracker.user_id': 106, 'pivotaltracker.account_ids': '100', 'pivotaltracker.annotation_comments': True, 'pivotaltracker.import_labels_as_tags': True, 'pivotaltracker.import_blockers': True } def setUp(self): super(TestPivotalTrackerIssue, self).setUp() self.service = self.get_mock_service(PivotalTrackerService) responses.add(responses.GET, 'https://www.pivotaltracker.com/services/v5/projects?account_ids=100', json=[PROJECT]) responses.add(responses.GET, 'https://www.pivotaltracker.com/services/v5/projects/99/search?query=mywork:106', json=QUERY) responses.add(responses.GET, 'https://www.pivotaltracker.com/services/v5/projects/99/stories/561/tasks', json=TASKS) responses.add(responses.GET, 'https://www.pivotaltracker.com/services/v5/projects/99/stories/561/blockers', json=BLOCKERS) responses.add(responses.GET, 'https://www.pivotaltracker.com/services/v5/projects/99/memberships', json=USER) def test_normalize_label_to_tag(self): story = self.service.get_issue_for_record(STORY, EXTRA) self.assertEqual(story._normalize_label_to_tag('needs work'), 'needs_work') def test_to_taskwarrior(self): story = self.service.get_issue_for_record( STORY, EXTRA ) expected_output = { 'annotations': [ { 'complete': False, 'created_at': '2019-05-14T12:00:00Z', 'description': 'Port 0', 'id': 5, 'kind': 'task', 'position': 1, 'story_id': 561, 'updated_at': '2019-05-14T12:00:00Z' }, { 'complete': False, 'created_at': '2019-05-14T12:00:00Z', 'description': 'Port 90', 'id': 6, 'kind': 'task', 'position': 2, 'story_id': 561, 'updated_at': '2019-05-14T12:00:00Z' } ], 'pivotalclosed': datetime.datetime(2019, 5, 14, 12, 0, tzinfo=tzutc()), 'pivotalcreated': datetime.datetime(2019, 5, 14, 12, 0, tzinfo=tzutc()), 'pivotalupdated': datetime.datetime(2019, 5, 14, 12, 0, tzinfo=tzutc()), 'pivotalurl': 'http://localhost/story/show/561', 'pivotalblockers': [ { 'created_at': '2019-05-14T12:00:00Z', 'description': 'Set weapons to stun', 'id': 1100, 'kind': 'blocker', 'person_id': 106, 'resolved': False, 'story_id': 561, 'updated_at': '2019-05-14T12:00:00Z' } ], 'pivotaldescription': 'All your base are belong to us', 'pivotalestimate': 3, 'pivotalid': 561, 'pivotalowners': ['owned_user'], 'pivotalprojectid': 99, 'pivotalprojectname': 'Death Star', 'pivotalrequesters': ['request_user'], 'pivotalstorytype': 'story', 'priority': 'M', 'project': 'death_star', 'tags': [ 'look_sir_metal' ] } actual_output = story.to_taskwarrior() self.assertEqual(actual_output, expected_output) @responses.activate def test_issues(self): story = next(self.service.issues()) story_date = datetime.datetime(2019, 5, 14, 12, 0, tzinfo=tzutc()) expected ={ 'annotations': [ '@task - Completed: False - Port 0', '@task - Completed: False - Port 90' ], 'description': '(bw)Story#561 - Tractor beam loses power intermittently .. ' 'http://localhost/story/show/561', 'pivotalclosed': story_date, 'pivotalcreated': story_date, 'pivotalupdated': story_date, 'pivotalurl': 'http://localhost/story/show/561', 'pivotalblockers': 'Description: Set weapons to stun Resovled: False', 'pivotaldescription': 'All your base are belong to us', 'pivotalestimate': 3, 'pivotalid': 561, 'pivotalowners': 'starkiller', 'pivotalprojectid': 99, 'pivotalprojectname': 'Death Star', 'pivotalrequesters': 'starkiller', 'pivotalstorytype': 'story', 'priority': 'M', 'project': 'death_star', 'tags': ['look_sir_metal'] } self.assertEqual(story.get_taskwarrior_record(), expected) bugwarrior-1.8.0/tests/test_redmine.py000066400000000000000000000103011376471246600201140ustar00rootroot00000000000000from builtins import next import datetime from unittest import mock import dateutil import responses from bugwarrior.services.redmine import RedMineService from .base import ServiceTest, AbstractServiceTest class TestRedmineIssue(AbstractServiceTest, ServiceTest): maxDiff = None SERVICE_CONFIG = { 'redmine.url': 'https://something', 'redmine.key': 'something_else', 'redmine.issue_limit': '100', } arbitrary_created = datetime.datetime.utcnow().replace( tzinfo=dateutil.tz.tz.tzutc(), microsecond=0) - datetime.timedelta(1) arbitrary_updated = datetime.datetime.utcnow().replace( tzinfo=dateutil.tz.tz.tzutc(), microsecond=0) arbitrary_issue = { "assigned_to": { "id": 35546, "name": "Adam Coddington" }, "author": { "id": 35546, "name": "Adam Coddington" }, "created_on": arbitrary_created.isoformat(), "due_on": "2016-12-30T16:40:29Z", "description": "This is a test issue.", "done_ratio": 0, "id": 363901, "priority": { "id": 4, "name": "Normal" }, "project": { "id": 27375, "name": "Boiled Cabbage - Yum" }, "status": { "id": 1, "name": "New" }, "subject": "Biscuits", "tracker": { "id": 4, "name": "Task" }, "updated_on": arbitrary_updated.isoformat(), } def setUp(self): super(TestRedmineIssue, self).setUp() self.service = self.get_mock_service(RedMineService) def test_to_taskwarrior(self): arbitrary_url = 'http://lkjlj.com' issue = self.service.get_issue_for_record(self.arbitrary_issue) expected_output = { 'annotations': [], 'project': issue.get_project_name(), 'priority': self.service.default_priority, issue.DUEDATE: None, issue.ASSIGNED_TO: self.arbitrary_issue['assigned_to']['name'], issue.AUTHOR: self.arbitrary_issue['author']['name'], issue.CATEGORY: None, issue.DESCRIPTION: self.arbitrary_issue['description'], issue.ESTIMATED_HOURS: None, issue.STATUS: 'New', issue.URL: arbitrary_url, issue.SUBJECT: self.arbitrary_issue['subject'], issue.TRACKER: u'Task', issue.CREATED_ON: self.arbitrary_created, issue.UPDATED_ON: self.arbitrary_updated, issue.ID: self.arbitrary_issue['id'], issue.PROJECT_NAME: 'Boiled Cabbage - Yum', issue.SPENT_HOURS: None, issue.START_DATE: None, } def get_url(*args): return arbitrary_url with mock.patch.object(issue, 'get_issue_url', side_effect=get_url): actual_output = issue.to_taskwarrior() self.assertEqual(actual_output, expected_output) @responses.activate def test_issues(self): self.add_response( 'https://something/issues.json?limit=100', json={'issues': [self.arbitrary_issue]}) issue = next(self.service.issues()) expected = { 'annotations': [], issue.DUEDATE: None, 'description': u'(bw)Is#363901 - Biscuits .. https://something/issues/363901', 'priority': 'M', 'project': u'boiledcabbageyum', 'redmineid': 363901, 'redmineprojectname': 'Boiled Cabbage - Yum', issue.SPENT_HOURS: None, issue.START_DATE: None, 'redmineassignedto': 'Adam Coddington', 'redmineauthor': 'Adam Coddington', issue.CATEGORY: None, issue.DESCRIPTION: self.arbitrary_issue['description'], issue.ESTIMATED_HOURS: None, issue.STATUS: 'New', 'redminesubject': u'Biscuits', 'redminetracker': u'Task', issue.CREATED_ON: self.arbitrary_created, issue.UPDATED_ON: self.arbitrary_updated, 'redmineurl': u'https://something/issues/363901', 'tags': []} self.assertEqual(issue.get_taskwarrior_record(), expected) bugwarrior-1.8.0/tests/test_service.py000066400000000000000000000055121376471246600201410ustar00rootroot00000000000000import unittest from bugwarrior import config, services LONG_MESSAGE = """\ Some message that is over 100 characters. This message is so long it's going to fill up your floppy disk taskwarrior backup. Actually it's not that long.""".replace('\n', ' ') class TestIssueService(unittest.TestCase): def setUp(self): super(TestIssueService, self).setUp() self.config = config.BugwarriorConfigParser() self.config.add_section('general') def test_build_annotations_default(self): service = services.IssueService(self.config, 'general', 'test') annotations = service.build_annotations( (('some_author', LONG_MESSAGE),), 'example.com') self.assertEqual(annotations, [ u'@some_author - Some message that is over 100 characters. Thi...' ]) def test_build_annotations_limited(self): self.config.set('general', 'annotation_length', '20') service = services.IssueService(self.config, 'general', 'test') annotations = service.build_annotations( (('some_author', LONG_MESSAGE),), 'example.com') self.assertEqual( annotations, [u'@some_author - Some message that is...']) def test_build_annotations_limitless(self): self.config.set('general', 'annotation_length', '') service = services.IssueService(self.config, 'general', 'test') annotations = service.build_annotations( (('some_author', LONG_MESSAGE),), 'example.com') self.assertEqual(annotations, [ u'@some_author - {message}'.format(message=LONG_MESSAGE)]) class TestIssue(unittest.TestCase): def setUp(self): super(TestIssue, self).setUp() self.config = config.BugwarriorConfigParser() self.config.add_section('general') def makeIssue(self): service = services.IssueService(self.config, 'general', 'test') service.ISSUE_CLASS = services.Issue return service.get_issue_for_record(None) def test_build_default_description_default(self): issue = self.makeIssue() description = issue.build_default_description(LONG_MESSAGE) self.assertEqual( description, u'(bw)Is# - Some message that is over 100 chara') def test_build_default_description_limited(self): self.config.set('general', 'description_length', '20') issue = self.makeIssue() description = issue.build_default_description(LONG_MESSAGE) self.assertEqual( description, u'(bw)Is# - Some message that is') def test_build_default_description_limitless(self): self.config.set('general', 'description_length', '') issue = self.makeIssue() description = issue.build_default_description(LONG_MESSAGE) self.assertEqual( description, u'(bw)Is# - {message}'.format(message=LONG_MESSAGE)) bugwarrior-1.8.0/tests/test_string_compat.py000066400000000000000000000015521376471246600213520ustar00rootroot00000000000000import unittest import six from unittest.mock import MagicMock from bugwarrior.services import Issue class StringCompatTest(unittest.TestCase): """This class implements method to test correct and compatible implementation of __str__ and __repr__ methods""" def test_issue_str(self): "check Issue class" record = {} origin = {'target': 'target', 'default_priority': 'prio', 'templates': 'templates', 'add_tags': []} issue = Issue(record, origin) issue.to_taskwarrior = MagicMock(return_value=record) issue.get_default_description = MagicMock(return_value='description') self.assertIsInstance(str(issue), six.string_types) self.assertIsInstance(issue.__repr__(), six.string_types) self.assertTrue(six.PY3 or hasattr(issue, '__unicode__')) bugwarrior-1.8.0/tests/test_taiga.py000066400000000000000000000052231376471246600175650ustar00rootroot00000000000000from builtins import next import responses from bugwarrior.services.taiga import TaigaService from .base import ServiceTest, AbstractServiceTest class TestTaigaIssue(AbstractServiceTest, ServiceTest): SERVICE_CONFIG = { 'taiga.base_uri': 'https://one', 'taiga.auth_token': 'two', } record = { 'id': 400, 'project': 4, 'ref': 40, 'subject': 'this is a title', 'tags': [ 'single', [ 'bugwarrior', None ], [ 'task', '#c0ffee' ] ], } def setUp(self): super(TestTaigaIssue, self).setUp() self.service = self.get_mock_service(TaigaService) def test_to_taskwarrior(self): extra = { 'project': 'awesome', 'annotations': [ # TODO - test annotations? ], 'url': 'this is a url', } issue = self.service.get_issue_for_record(self.record, extra) actual = issue.to_taskwarrior() expected = { 'annotations': [], 'priority': 'M', 'project': 'awesome', 'tags': ['single', 'bugwarrior', 'task'], 'taigaid': 40, 'taigasummary': 'this is a title', 'taigaurl': 'this is a url', } self.assertEqual(actual, expected) @responses.activate def test_issues(self): userid = 1 self.add_response( 'https://one/api/v1/users/me', json={'id': userid}) self.add_response( 'https://one/api/v1/userstories?status__is_closed=false&assigned_to={0}'.format( userid), json=[self.record]) self.add_response( 'https://one/api/v1/projects/{0}'.format(self.record['project']), json={'slug': 'something'}) self.add_response( 'https://one/api/v1/history/userstory/{0}'.format( self.record['id']), json=[{'user': {'username': 'you'}, 'comment': 'Blah blah blah!'}]) issue = next(self.service.issues()) expected = { 'annotations': [u'@you - Blah blah blah!'], 'description': u'(bw)Is#40 - this is a title .. https://one/project/something/us/40', 'priority': u'M', 'project': u'something', 'tags': [u'single', u'bugwarrior', u'task'], 'taigaid': 40, 'taigasummary': u'this is a title', 'taigaurl': u'https://one/project/something/us/40'} self.assertEqual(issue.get_taskwarrior_record(), expected) bugwarrior-1.8.0/tests/test_teamlab.py000066400000000000000000000045001376471246600201020ustar00rootroot00000000000000from builtins import next from unittest import mock import responses from bugwarrior.services.teamlab import TeamLabService from .base import ServiceTest, AbstractServiceTest class TestTeamlabIssue(AbstractServiceTest, ServiceTest): SERVICE_CONFIG = { 'teamlab.hostname': 'something', 'teamlab.login': 'alkjdsf', 'teamlab.password': 'lkjklj', 'teamlab.project_name': 'abcdef', } arbitrary_issue = { 'title': 'Hello', 'id': 10, 'projectOwner': { 'id': 140, }, 'status': 1, } def setUp(self): super(TestTeamlabIssue, self).setUp() with mock.patch( 'bugwarrior.services.teamlab.TeamLabClient.authenticate' ): self.service = self.get_mock_service(TeamLabService) def test_to_taskwarrior(self): arbitrary_url = 'http://galkjsdflkj.com/' issue = self.service.get_issue_for_record(self.arbitrary_issue) expected_output = { 'project': self.SERVICE_CONFIG['teamlab.project_name'], 'priority': self.service.default_priority, issue.TITLE: self.arbitrary_issue['title'], issue.FOREIGN_ID: self.arbitrary_issue['id'], issue.URL: arbitrary_url, issue.PROJECTOWNER_ID: self.arbitrary_issue['projectOwner']['id'] } def get_url(*args): return arbitrary_url with mock.patch.object(issue, 'get_issue_url', side_effect=get_url): actual_output = issue.to_taskwarrior() self.assertEqual(actual_output, expected_output) @responses.activate def test_issues(self): self.add_response( 'http://something/api/1.0/project/task/@self.json', json=[self.arbitrary_issue]) issue = next(self.service.issues()) expected = { 'description': u'(bw)Is#10 - Hello .. http://something/products/projects/tasks.aspx?prjID=140&id=10', 'priority': 'M', 'project': 'abcdef', 'tags': [], 'teamlabid': 10, 'teamlabprojectownerid': 140, 'teamlabtitle': u'Hello', 'teamlaburl': 'http://something/products/projects/tasks.aspx?prjID=140&id=10'} self.assertEqual(issue.get_taskwarrior_record(), expected) bugwarrior-1.8.0/tests/test_teamwork_projects.py000066400000000000000000000131541376471246600222440ustar00rootroot00000000000000from .base import ServiceTest, AbstractServiceTest from bugwarrior.services.teamwork_projects import TeamworkService, TeamworkClient import responses import datetime from dateutil.tz import tzutc class TestTeamworkIssue(AbstractServiceTest, ServiceTest): SERVICE_CONFIG = { 'teamwork_projects.host': 'https://test.teamwork_projects.com', 'teamwork_projects.token': 'arbitrary_token', } @responses.activate def setUp(self): super(TestTeamworkIssue, self).setUp() self.add_response( 'https://test.teamwork_projects.com/authenticate.json', json={ 'account': { 'userId': 5, 'firstname': 'Greg', 'lastname': 'McCoy' } } ) self.service = self.get_mock_service(TeamworkService) self.arbitrary_issue = { "todo-items": [{ "id": 5, "comments-count": 2, "description": "This issue is meant for testing", "content": "This is a test issue", "project-id": 1, "project-name": "Test Project", "status": "new", "company-name": "Test Company", "company-id": 1, "creator-id": 1, "creator-firstname": "Greg", "creator-lastname": "McCoy", "updater-id": 0, "updater-firstname": "", "updater-lastname": "", "completed": False, "start-date": "", "due-date": "2019-12-12T10:06:31Z", "created-on": "2018-12-12T10:06:31Z", "last-changed-on": "2019-01-16T11:00:44Z", "priority": "high", "parentTaskId": "", "userFollowingComments": True, "userFollowingChanges": True, "DLM": 0, "responsible-party-ids": ["5"] }] } self.arbitrary_extra = { "host": "https://test.teamwork_projects.com", "annotations": [("Greg McCoy", "Test comment"), ("Bob Test", "testing")] } self.arbitrary_comments = { "comments": [ { "project-id": "999", "author-lastname": "User", "datetime": "2014-03-31T13:03:29Z", "author_id": "999", "id": "999", "company-name": "Test Company", "last-changed-on": "", "company-id": "999", "project-name": "demo", "body": "A test comment", "commentNo": "1", "author-firstname": "Demo", "comment-link": "tasks/436523?c=93", "author-id": "999" } ] } @responses.activate def test_to_taskwarrior(self): issue = self.service.get_issue_for_record(self.arbitrary_issue["todo-items"][0], self.arbitrary_extra) data = self.arbitrary_issue["todo-items"][0] expected_data = { 'project': data["project-name"], 'priority': "H", 'due': datetime.datetime(2019, 12, 12, 10, 6, 31, tzinfo=tzutc()), 'entry': datetime.datetime(2018, 12, 12, 10, 6, 31, tzinfo=tzutc()), 'end': "", 'modified': datetime.datetime(2019, 1, 16, 11, 0, 44, tzinfo=tzutc()), 'annotations': self.arbitrary_extra.get("annotations", ""), issue.URL: "https://test.teamwork_projects.com/#/tasks/5", issue.TITLE: data["content"], issue.DESCRIPTION_LONG: data["description"], issue.PROJECT_ID: int(data["project-id"]), issue.STATUS: "Open", issue.ID: int(data["id"]), "annotations": [('Greg McCoy', 'Test comment'), ('Bob Test', 'testing')], } actual_output = issue.to_taskwarrior() self.assertEqual(actual_output, expected_data) @responses.activate def test_issues(self): self.add_response( 'https://test.teamwork_projects.com/tasks/5/comments.json', json=self.arbitrary_comments ) self.add_response( 'https://test.teamwork_projects.com/tasks.json', json=self.arbitrary_issue ) issue = next(self.service.issues()) data = self.arbitrary_issue["todo-items"][0] expected_data = { 'project': data["project-name"], 'priority': "H", 'due': datetime.datetime(2019, 12, 12, 10, 6, 31, tzinfo=tzutc()), 'entry': datetime.datetime(2018, 12, 12, 10, 6, 31, tzinfo=tzutc()), 'end': "", 'modified': datetime.datetime(2019, 1, 16, 11, 0, 44, tzinfo=tzutc()), 'annotations': self.arbitrary_extra.get("annotations", ""), 'description': '(bw)Is#5 - This is a test issue .. https://test.teamwork_projects.com/#/tasks/5', issue.URL: "https://test.teamwork_projects.com/#/tasks/5", issue.TITLE: data["content"], issue.DESCRIPTION_LONG: data["description"], issue.PROJECT_ID: int(data["project-id"]), issue.STATUS: "Open", issue.ID: int(data["id"]), "annotations": ['@Demo User - A test comment'], "tags": [], } issue.user_id = "5" issue.name = "Greg McCoy" self.assertEqual(issue.get_taskwarrior_record(), expected_data) self.assertEqual(issue.get_owner(issue), "Greg McCoy") self.assertEqual(issue.get_author(issue), "Greg McCoy") bugwarrior-1.8.0/tests/test_templates.py000066400000000000000000000060121376471246600204730ustar00rootroot00000000000000from bugwarrior.services import Issue from .base import ServiceTest class TestTemplates(ServiceTest): def setUp(self): super(TestTemplates, self).setUp() self.arbitrary_default_description = 'Construct Library on Terminus' self.arbitrary_issue = { 'project': 'end_of_empire', 'priority': 'H', } def get_issue( self, templates=None, issue=None, description=None, add_tags=None ): templates = {} if templates is None else templates origin = { 'annotation_length': 100, # Arbitrary 'default_priority': 'H', # Arbitrary 'description_length': 100, # Arbitrary 'templates': templates, 'shorten': False, # Arbitrary 'add_tags': add_tags if add_tags else [], } issue = Issue({}, origin) issue.to_taskwarrior = lambda: ( self.arbitrary_issue if description is None else description ) issue.get_default_description = lambda: ( self.arbitrary_default_description if description is None else description ) return issue def test_default_taskwarrior_record(self): issue = self.get_issue({}) record = issue.get_taskwarrior_record() expected_record = self.arbitrary_issue.copy() expected_record.update({ 'description': self.arbitrary_default_description, 'tags': [], }) self.assertEqual(record, expected_record) def test_override_description(self): description_template = "{{ priority }} - {{ description }}" issue = self.get_issue({ 'description': description_template }) record = issue.get_taskwarrior_record() expected_record = self.arbitrary_issue.copy() expected_record.update({ 'description': '%s - %s' % ( self.arbitrary_issue['priority'], self.arbitrary_default_description, ), 'tags': [], }) self.assertEqual(record, expected_record) def test_override_project(self): project_template = "wat_{{ project|upper }}" issue = self.get_issue({ 'project': project_template }) record = issue.get_taskwarrior_record() expected_record = self.arbitrary_issue.copy() expected_record.update({ 'description': self.arbitrary_default_description, 'project': 'wat_%s' % self.arbitrary_issue['project'].upper(), 'tags': [], }) self.assertEqual(record, expected_record) def test_tag_templates(self): issue = self.get_issue(add_tags=['one', '{{ project }}']) record = issue.get_taskwarrior_record() expected_record = self.arbitrary_issue.copy() expected_record.update({ 'description': self.arbitrary_default_description, 'tags': ['one', self.arbitrary_issue['project']] }) self.assertEqual(record, expected_record) bugwarrior-1.8.0/tests/test_trac.py000066400000000000000000000053721376471246600174360ustar00rootroot00000000000000from builtins import next from builtins import object from bugwarrior.services.trac import TracService from .base import ServiceTest, AbstractServiceTest class FakeTracTicket(object): @staticmethod def changeLog(issuenumber): return [] class FakeTracServer(object): ticket = FakeTracTicket() class FakeTracLib(object): server = FakeTracServer() def __init__(self, record): self.record = record @staticmethod def query_tickets(query): return ['something'] def get_ticket(self, ticket): return (1, None, None, self.record) class TestTracIssue(AbstractServiceTest, ServiceTest): SERVICE_CONFIG = { 'trac.base_uri': 'http://ljlkajsdfl.com', 'trac.username': 'something', 'trac.password': 'somepwd', } arbitrary_issue = { 'url': 'http://some/url.com/', 'summary': 'Some Summary', 'number': 204, 'priority': 'critical', 'component': 'testcomponent', } def setUp(self): super(TestTracIssue, self).setUp() self.service = self.get_mock_service(TracService) def get_mock_service(self, *args, **kwargs): service = super(TestTracIssue, self).get_mock_service(*args, **kwargs) service.trac = FakeTracLib(self.arbitrary_issue) return service def test_to_taskwarrior(self): arbitrary_extra = { 'annotations': [ 'alpha', 'beta', ], 'project': 'some project', } issue = self.service.get_issue_for_record( self.arbitrary_issue, arbitrary_extra, ) expected_output = { 'project': arbitrary_extra['project'], 'priority': issue.PRIORITY_MAP[self.arbitrary_issue['priority']], 'annotations': arbitrary_extra['annotations'], issue.URL: self.arbitrary_issue['url'], issue.SUMMARY: self.arbitrary_issue['summary'], issue.NUMBER: self.arbitrary_issue['number'], issue.COMPONENT: self.arbitrary_issue['component'], } actual_output = issue.to_taskwarrior() self.assertEqual(actual_output, expected_output) def test_issues(self): issue = next(self.service.issues()) expected = { 'annotations': [], 'description': '(bw)Is#1 - Some Summary .. https://http://ljlkajsdfl.com/ticket/1', 'priority': 'H', 'project': 'unspecified', 'tags': [], 'tracnumber': 1, 'tracsummary': 'Some Summary', 'tracurl': 'https://http://ljlkajsdfl.com/ticket/1', 'traccomponent': 'testcomponent'} self.assertEqual(issue.get_taskwarrior_record(), expected) bugwarrior-1.8.0/tests/test_trello.py000066400000000000000000000230341376471246600200010ustar00rootroot00000000000000from __future__ import unicode_literals, print_function from future import standard_library standard_library.install_aliases() from builtins import next from unittest.mock import patch from six.moves import configparser import responses from dateutil.parser import parse as parse_date from dateutil.tz import tzlocal from bugwarrior.config import ServiceConfig from bugwarrior.services.trello import TrelloService, TrelloIssue from .base import ConfigTest, ServiceTest class TestTrelloIssue(ServiceTest): JSON = { "due": "2018-12-02T12:59:00.000Z", "id": "542bbb6583d705eb05bbe491", "idShort": 42, "name": "So long, and thanks for all the fish!", "shortLink": "AAaaBBbb", "shortUrl": "https://trello.com/c/AAaaBBbb", "url": "https://trello.com/c/AAaBBbb/42-so-long", "labels": [{'name': "foo"}, {"name": "bar"}], "desc": "some description", } def setUp(self): super(TestTrelloIssue, self).setUp() origin = dict(inline_links=True, description_length=31, import_labels_as_tags=True, default_priority='M') extra = {'boardname': 'Hyperspatial express route', 'listname': 'Something'} self.issue = TrelloIssue(self.JSON, origin, extra) def test_default_description(self): """ Test the generated description """ expected_desc = "(bw)#42 - So long, and thanks for all the" \ " .. https://trello.com/c/AAaaBBbb" self.assertEqual(expected_desc, self.issue.get_default_description()) def test_to_taskwarrior__project(self): """ By default, the project is the board name """ expected_project = "Hyperspatial express route" self.assertEqual(expected_project, self.issue.to_taskwarrior().get('project', None)) class TestTrelloService(ConfigTest): BOARD = {'id': 'B04RD', 'name': 'My Board'} CARD1 = {'id': 'C4RD', 'name': 'Card 1', 'members': [{'username': 'tintin'}], 'due': '2018-12-02T12:59:00.000Z', 'idShort': 1, 'shortLink': 'abcd', 'shortUrl': 'https://trello.com/c/AAaaBBbb', 'desc': 'some description', 'url': 'https://trello.com/c/AAaBBbb/42-so-long'} CARD2 = {'id': 'kard', 'name': 'Card 2', 'members': [{'username': 'mario'}]} CARD3 = {'id': 'K4rD', 'name': 'Card 3', 'members': []} LIST1 = {'id': 'L15T', 'name': 'List 1'} LIST2 = {'id': 'ZZZZ', 'name': 'List 2'} COMMENT1 = { "type": "commentCard", "data": { "text": "Preums" }, "memberCreator": { "username": "luidgi" } } COMMENT2 = { "type": "commentCard", "data": { "text": "Deuz" }, "memberCreator": { "username": "mario" } } def setUp(self): super(TestTrelloService, self).setUp() self.config = configparser.RawConfigParser() self.config.add_section('general') self.config.add_section('mytrello') self.config.set('mytrello', 'trello.api_key', 'XXXX') self.config.set('mytrello', 'trello.token', 'YYYY') self.service_config = ServiceConfig( TrelloService.CONFIG_PREFIX, self.config, 'mytrello') responses.add(responses.GET, 'https://api.trello.com/1/lists/L15T/cards/open', json=[self.CARD1, self.CARD2, self.CARD3]) responses.add(responses.GET, 'https://api.trello.com/1/boards/B04RD/lists/open', json=[self.LIST1, self.LIST2]) responses.add(responses.GET, 'https://api.trello.com/1/boards/F00', json={'id': 'F00', 'name': 'Foo Board'}) responses.add(responses.GET, 'https://api.trello.com/1/boards/B4R', json={'id': 'B4R', 'name': 'Bar Board'}) responses.add(responses.GET, 'https://api.trello.com/1/members/me/boards', json=[self.BOARD]) responses.add(responses.GET, 'https://api.trello.com/1/cards/C4RD/actions', json=[self.COMMENT1, self.COMMENT2]) @responses.activate def test_get_boards_config(self): self.config.set('mytrello', 'trello.include_boards', 'F00, B4R') service = TrelloService(self.config, 'general', 'mytrello') boards = service.get_boards() self.assertEqual(list(boards), [{'id': 'F00', 'name': 'Foo Board'}, {'id': 'B4R', 'name': 'Bar Board'}]) @responses.activate def test_get_boards_api(self): service = TrelloService(self.config, 'general', 'mytrello') boards = service.get_boards() self.assertEqual(list(boards), [self.BOARD]) @responses.activate def test_get_lists(self): service = TrelloService(self.config, 'general', 'mytrello') lists = service.get_lists('B04RD') self.assertEqual(list(lists), [self.LIST1, self.LIST2]) @responses.activate def test_get_lists_include(self): self.config.set('mytrello', 'trello.include_lists', 'List 1') service = TrelloService(self.config, 'general', 'mytrello') lists = service.get_lists('B04RD') self.assertEqual(list(lists), [self.LIST1]) @responses.activate def test_get_lists_exclude(self): self.config.set('mytrello', 'trello.exclude_lists', 'List 1') service = TrelloService(self.config, 'general', 'mytrello') lists = service.get_lists('B04RD') self.assertEqual(list(lists), [self.LIST2]) @responses.activate def test_get_cards(self): service = TrelloService(self.config, 'general', 'mytrello') cards = service.get_cards('L15T') self.assertEqual(list(cards), [self.CARD1, self.CARD2, self.CARD3]) @responses.activate def test_get_cards_assigned(self): self.config.set('mytrello', 'trello.only_if_assigned', 'tintin') service = TrelloService(self.config, 'general', 'mytrello') cards = service.get_cards('L15T') self.assertEqual(list(cards), [self.CARD1]) @responses.activate def test_get_cards_assigned_unassigned(self): self.config.set('mytrello', 'trello.only_if_assigned', 'tintin') self.config.set('mytrello', 'trello.also_unassigned', 'true') service = TrelloService(self.config, 'general', 'mytrello') cards = service.get_cards('L15T') self.assertEqual(list(cards), [self.CARD1, self.CARD3]) @responses.activate def test_get_comments(self): service = TrelloService(self.config, 'general', 'mytrello') comments = service.get_comments('C4RD') self.assertEqual(list(comments), [self.COMMENT1, self.COMMENT2]) @responses.activate def test_annotations(self): service = TrelloService(self.config, 'general', 'mytrello') annotations = service.annotations(self.CARD1) self.assertEqual( list(annotations), ["@luidgi - Preums", "@mario - Deuz"]) @responses.activate def test_annotations_with_link(self): self.config.set('general', 'annotation_links', 'true') service = TrelloService(self.config, 'general', 'mytrello') annotations = service.annotations(self.CARD1) self.assertEqual( list(annotations), ["https://trello.com/c/AAaaBBbb", "@luidgi - Preums", "@mario - Deuz"]) @responses.activate def test_issues(self): self.config.set('mytrello', 'trello.include_lists', 'List 1') self.config.set('mytrello', 'trello.only_if_assigned', 'tintin') service = TrelloService(self.config, 'general', 'mytrello') issues = service.issues() expected = { 'due': parse_date('2018-12-02T12:59:00.000Z'), 'description': u'(bw)#1 - Card 1 .. https://trello.com/c/AAaaBBbb', 'priority': 'M', 'project': 'My Board', 'trelloboard': 'My Board', 'trellolist': 'List 1', 'trellocard': 'Card 1', 'trellocardid': 'C4RD', 'trellocardidshort': 1, 'trellodescription': 'some description', 'trelloshortlink': 'abcd', 'trelloshorturl': 'https://trello.com/c/AAaaBBbb', 'trellourl': 'https://trello.com/c/AAaBBbb/42-so-long', 'annotations': [ "@luidgi - Preums", "@mario - Deuz" ], 'tags': []} actual = next(issues).get_taskwarrior_record() self.assertEqual(expected, actual) maxDiff = None @patch('bugwarrior.services.trello.die') def test_validate_config(self, die): TrelloService.validate_config(self.service_config, 'mytrello') die.assert_not_called() @patch('bugwarrior.services.trello.die') def test_valid_config_no_access_token(self, die): self.config.remove_option('mytrello', 'trello.token') TrelloService.validate_config(self.service_config, 'mytrello') die.assert_called_with("[mytrello] has no 'trello.token'") @patch('bugwarrior.services.trello.die') def test_valid_config_no_api_key(self, die): self.config.remove_option('mytrello', 'trello.api_key') TrelloService.validate_config(self.service_config, 'mytrello') die.assert_called_with("[mytrello] has no 'trello.api_key'") def test_keyring_service(self): """ Checks that the keyring service name """ keyring_service = TrelloService.get_keyring_service(self.service_config) self.assertEqual("trello://XXXX@trello.com", keyring_service) bugwarrior-1.8.0/tests/test_youtrak.py000066400000000000000000000070131376471246600201750ustar00rootroot00000000000000from future import standard_library standard_library.install_aliases() from builtins import next from six.moves import configparser import responses from bugwarrior.services import ServiceConfig from bugwarrior.services.youtrack import YoutrackService from .base import ConfigTest, ServiceTest, AbstractServiceTest class TestYoutrackService(ConfigTest): def setUp(self): super(TestYoutrackService, self).setUp() self.config = configparser.RawConfigParser() self.config.add_section('general') self.config.add_section('myservice') self.config.set('myservice', 'youtrack.login', 'foobar') self.config.set('myservice', 'youtrack.password', 'XXXXXX') def test_get_keyring_service(self): self.config.set('myservice', 'youtrack.host', 'youtrack.example.com') service_config = ServiceConfig( YoutrackService.CONFIG_PREFIX, self.config, 'myservice') self.assertEqual( YoutrackService.get_keyring_service(service_config), 'youtrack://foobar@youtrack.example.com') class TestYoutrackIssue(AbstractServiceTest, ServiceTest): maxDiff = None SERVICE_CONFIG = { 'youtrack.host': 'youtrack.example.com', 'youtrack.login': 'arbitrary_login', 'youtrack.password': 'arbitrary_password', 'youtrack.anonymous': True, } arbitrary_issue = { "id": "TEST-1", "field": [ { "name": "projectShortName", "value": "TEST" }, { "name": "numberInProject", "value": "1" }, { "name": "summary", "value": "Hello World" }, ], "tag": [ { "value": "bug", }, { "value": "New Feature", } ] } arbitrary_extra = { } def setUp(self): super(TestYoutrackIssue, self).setUp() self.service = self.get_mock_service(YoutrackService) def test_to_taskwarrior(self): self.service.import_tags = True issue = self.service.get_issue_for_record(self.arbitrary_issue, self.arbitrary_extra) expected_output = { 'project': 'TEST', 'priority': self.service.default_priority, 'tags': [u'bug', u'new_feature'], issue.ISSUE: 'TEST-1', issue.SUMMARY: 'Hello World', issue.URL: 'https://youtrack.example.com:443/issue/TEST-1', issue.PROJECT: 'TEST', issue.NUMBER: 1, } actual_output = issue.to_taskwarrior() self.assertEqual(actual_output, expected_output) @responses.activate def test_issues(self): self.add_response( 'https://youtrack.example.com:443/rest/issue?filter=for%3Ame+%23Unresolved&max=100', json={'issue': [self.arbitrary_issue]}) issue = next(self.service.issues()) expected = { 'description': u'(bw)Is#TEST-1 - Hello World .. https://youtrack.example.com:443/issue/TEST-1', 'project': 'TEST', 'priority': self.service.default_priority, 'tags': [u'bug', u'new_feature'], 'youtrackissue': 'TEST-1', 'youtracksummary': 'Hello World', 'youtrackurl': 'https://youtrack.example.com:443/issue/TEST-1', 'youtrackproject': 'TEST', 'youtracknumber': 1, } self.assertEqual(issue.get_taskwarrior_record(), expected) bugwarrior-1.8.0/tox.ini000066400000000000000000000005421376471246600152370ustar00rootroot00000000000000[tox] envlist = py36-jira{1010,200},py37-jira200 [testenv] commands = python setup.py test setenv = XDG_CACHE_HOME={envtmpdir}/ deps = pysimplesoap jira107: jira==1.0.7 jira1010: jira==1.0.10 jira200: jira>=2.0.0 [flake8] max-line-length = 100 exclude = ./.tox ./.git ./bugwarrior/docs/conf.py ./build ./.env