From d2d0f6e291c2264a5b632691e94d0842b7e21be4 Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Wed, 20 Feb 2019 19:33:34 +0000 Subject: [PATCH] Add task to add PRs to review to todoist --- README.md | 1 + actioner/scheduler/__init__.py | 2 + actioner/scheduler/todoist_assigned_issues.py | 17 +---- actioner/scheduler/todoist_repo_prs.py | 70 +++++++++++++++++++ actioner/utils/github.py | 16 +++++ .../test_todoist_assigned_issues.py | 48 +------------ tests/test_utils/test_github.py | 36 ++++++++++ 7 files changed, 127 insertions(+), 63 deletions(-) create mode 100644 actioner/scheduler/todoist_repo_prs.py create mode 100644 actioner/utils/github.py create mode 100644 tests/test_utils/test_github.py diff --git a/README.md b/README.md index 8548eaf..dc3b62c 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,4 @@ Do things - Healthcheck endpoint - Add assigned issues from certain GitHub repos to Todoist +- Add all unreviewed PRs for certain GitHub repos to Todoist diff --git a/actioner/scheduler/__init__.py b/actioner/scheduler/__init__.py index 15bff66..63a2380 100644 --- a/actioner/scheduler/__init__.py +++ b/actioner/scheduler/__init__.py @@ -3,11 +3,13 @@ import asyncio from apscheduler.schedulers.asyncio import AsyncIOScheduler from .todoist_assigned_issues import todoist_assigned_issues +from .todoist_repo_prs import todoist_repo_prs def create_scheduler(): scheduler = AsyncIOScheduler() scheduler.add_job(todoist_assigned_issues, 'interval', minutes=15) + scheduler.add_job(todoist_repo_prs, 'interval', minutes=15) return scheduler diff --git a/actioner/scheduler/todoist_assigned_issues.py b/actioner/scheduler/todoist_assigned_issues.py index 4ad1033..8f84772 100644 --- a/actioner/scheduler/todoist_assigned_issues.py +++ b/actioner/scheduler/todoist_assigned_issues.py @@ -1,10 +1,10 @@ import logging -from typing import Dict from github import Issue from actioner.clients import github, todoist from actioner.utils import get_todoist_project_from_repo +from actioner.utils.github import get_existing_task, get_issue_link REPOS = frozenset([ 'srobo/tasks', @@ -28,25 +28,10 @@ def get_status_for_issue(issue: Issue) -> int: return max(priorities, default=1) -def get_issue_link(issue: Issue) -> str: - return "[#{id}]({url})".format( - id=issue.number, - url=issue.html_url - ) - - def issue_to_task_name(issue: Issue) -> str: return get_issue_link(issue) + ": " + issue.title -def get_existing_task(tasks: Dict[int, str], issue: Issue): - issue_link = get_issue_link(issue) - for task_id, task_title in tasks.items(): - if task_title.startswith(issue_link): - return task_id - return None - - def todoist_assigned_issues(): me = github.get_user() todoist.projects.sync() diff --git a/actioner/scheduler/todoist_repo_prs.py b/actioner/scheduler/todoist_repo_prs.py new file mode 100644 index 0000000..97a6dd8 --- /dev/null +++ b/actioner/scheduler/todoist_repo_prs.py @@ -0,0 +1,70 @@ +import logging + +from github import PullRequest + +from actioner.clients import github, todoist +from actioner.utils import get_todoist_project_from_repo +from actioner.utils.github import get_existing_task, get_issue_link + +logger = logging.getLogger(__name__) + + +REPOS = frozenset([ + 'srobo/core-team-minutes' +]) + + +def pr_to_task_name(pr: PullRequest) -> str: + return "Review " + get_issue_link(pr) + ": " + pr.title + + +def get_my_review(me, pr: PullRequest): + for review in pr.get_reviews(): + if review.user.login == me.login: + return review + + +def todoist_repo_prs(): + me = github.get_user() + todoist.projects.sync() + todoist.items.sync() + for repo_name in REPOS: + project_id = get_todoist_project_from_repo(repo_name) + existing_tasks = {item['id']: item['content'] for item in todoist.state['items'] if item['project_id'] == project_id} + repo = github.get_repo(repo_name) + for pr in repo.get_pulls(state='all'): + existing_task_id = get_existing_task(existing_tasks, pr) + + if pr.state == 'closed' and existing_task_id: + my_review = get_my_review(me, pr) + if pr.merged and my_review and my_review.state == 'APPROVED': + logger.info("Completing task to review '{}'".format(pr.title)) + todoist.items.complete([existing_task_id]) + else: + logger.info("Deleting task to review '{}'".format(pr.title)) + todoist.items.delete([existing_task_id]) + + elif pr.state == 'open': + if existing_task_id is None: + logger.info("Creating task to review '{}'".format(pr.title)) + existing_task_id = todoist.items.add( + pr_to_task_name(pr), + project_id + )['id'] + + existing_task = todoist.items.get_by_id(existing_task_id) + my_review = get_my_review(me, pr) + if existing_task_id and my_review: + if my_review.commit_id == pr.head.sha and not existing_task['checked']: + logger.info("Completing task to review '{}'".format(pr.title)) + todoist.items.complete([existing_task_id]) + elif existing_task['checked']: + logger.info("Re-opening task to review '{}'".format(pr.title)) + todoist.items.uncomplete([existing_task_id]) + existing_task.update( + content=pr_to_task_name(pr) + ) + if pr.milestone and pr.milestone.due_on: + existing_task.update(date_string=pr.milestone.due_on.strftime("%d/%m/%Y")) + + todoist.commit() diff --git a/actioner/utils/github.py b/actioner/utils/github.py new file mode 100644 index 0000000..758a28b --- /dev/null +++ b/actioner/utils/github.py @@ -0,0 +1,16 @@ +from typing import Dict + + +def get_issue_link(issue_or_pr) -> str: + return "[#{id}]({url})".format( + id=issue_or_pr.number, + url=issue_or_pr.html_url + ) + + +def get_existing_task(tasks: Dict[int, str], issue_or_pr): + issue_link = get_issue_link(issue_or_pr) + for task_id, task_title in tasks.items(): + if issue_link in task_title: + return task_id + return None diff --git a/tests/test_scheduler/test_todoist_assigned_issues.py b/tests/test_scheduler/test_todoist_assigned_issues.py index f6d9838..94635c9 100644 --- a/tests/test_scheduler/test_todoist_assigned_issues.py +++ b/tests/test_scheduler/test_todoist_assigned_issues.py @@ -1,55 +1,9 @@ -from collections import namedtuple - -from actioner.scheduler.todoist_assigned_issues import ( - REPOS, - get_existing_task, - get_issue_link, - issue_to_task_name, -) +from actioner.scheduler.todoist_assigned_issues import REPOS from actioner.utils import get_todoist_project_from_repo from tests import BaseTestCase -FakeIssue = namedtuple('FakeIssue', ['number', 'html_url', 'title']) - - -class IssueTaskNameTestCase(BaseTestCase): - def setUp(self): - super().setUp() - self.issue = FakeIssue(123, 'https://github.com/repo/thing', 'issue title') - - def test_creates_link(self): - self.assertEqual(get_issue_link(self.issue), "[#123](https://github.com/repo/thing)") - self.assertIn(self.issue.html_url, get_issue_link(self.issue)) - - def test_task_name_contains_title(self): - self.assertIn(self.issue.title, issue_to_task_name(self.issue)) - - def test_task_name_contains_link(self): - self.assertIn(get_issue_link(self.issue), issue_to_task_name(self.issue)) - class ConfigurationTestCase(BaseTestCase): def test_repo_is_known(self): for repo in REPOS: self.assertIsNotNone(get_todoist_project_from_repo(repo)) - - -class ExistingTaskTestCase(BaseTestCase): - def setUp(self): - super().setUp() - self.tasks = { - 123: '[#1](url): title', - 456: '[#2](url/2): title 2', - 789: '[#3](url/3): title 3', - } - - def test_finds_existing_repos(self): - self.assertEqual( - get_existing_task(self.tasks, FakeIssue(1, 'url', 'title')), - 123 - ) - - def test_not_existing_repo(self): - self.assertIsNone( - get_existing_task(self.tasks, FakeIssue(123, 'url', 'title')) - ) diff --git a/tests/test_utils/test_github.py b/tests/test_utils/test_github.py new file mode 100644 index 0000000..846ade6 --- /dev/null +++ b/tests/test_utils/test_github.py @@ -0,0 +1,36 @@ +from collections import namedtuple + +from actioner.utils.github import get_existing_task, get_issue_link +from tests import BaseTestCase + +FakeIssue = namedtuple('FakeIssue', ['number', 'html_url', 'title']) + + +class IssueLinkTestCase(BaseTestCase): + def setUp(self): + super().setUp() + self.issue = FakeIssue(123, 'https://github.com/repo/thing', 'issue title') + + def test_creates_link(self): + self.assertEqual(get_issue_link(self.issue), "[#123](https://github.com/repo/thing)") + + +class ExistingTaskTestCase(BaseTestCase): + def setUp(self): + super().setUp() + self.tasks = { + 123: '[#1](url): title', + 456: '[#2](url/2): title 2', + 789: '[#3](url/3): title 3', + } + + def test_finds_existing_repos(self): + self.assertEqual( + get_existing_task(self.tasks, FakeIssue(1, 'url', 'title')), + 123 + ) + + def test_not_existing_repo(self): + self.assertIsNone( + get_existing_task(self.tasks, FakeIssue(123, 'url', 'title')) + )