diff --git a/inlineplz/interfaces/__init__.py b/inlineplz/interfaces/__init__.py index 330d4352..476a6e69 100644 --- a/inlineplz/interfaces/__init__.py +++ b/inlineplz/interfaces/__init__.py @@ -5,6 +5,16 @@ from inlineplz.interfaces.github import GitHubInterface +STASH_SUPPORTED = False +try: + from inlineplz.interfaces.stash import StashInterface + STASH_SUPPORTED = True +except ImportError: + pass + INTERFACES = { - 'github': GitHubInterface + 'github': GitHubInterface, } + +if STASH_SUPPORTED: + INTERFACES['stash'] = StashInterface diff --git a/inlineplz/interfaces/github.py b/inlineplz/interfaces/github.py index 45d4a329..6f01b2e3 100644 --- a/inlineplz/interfaces/github.py +++ b/inlineplz/interfaces/github.py @@ -14,8 +14,9 @@ class GitHubInterface(InterfaceBase): - def __init__(self, owner, repo, pr, token, url=None): + def __init__(self, owner, repo, pr, auth, url=None): self.github = None + token = auth.get('token') # TODO: support non-PR runs try: pr = int(pr) diff --git a/inlineplz/interfaces/stash.py b/inlineplz/interfaces/stash.py new file mode 100644 index 00000000..4ee4cace --- /dev/null +++ b/inlineplz/interfaces/stash.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import + +import stashy +import unidiff + +from inlineplz.interfaces.base import InterfaceBase +from inlineplz.util import git + + +class StashInterface(InterfaceBase): + def __init__(self, project, repo, pr, auth, url=None): + self.stash = None + username = auth.get('username') + password = auth.get('password') + # TODO: support non-PR runs + try: + pr = int(pr) + except ValueError: + return + if not url: + return + else: + self.stash = stashy.connect(url, username=username, password=password, verify=False) + self.pull_request = self.stash.projects[project].repos[repo].pull_requests[pr] + self.commits = [c for c in self.pull_request.commits()] + self.last_sha = self.commits[0]['id'] + self.first_sha = self.commits[-1]['id'] + self.parent_sha = self.commits[-1]['parents'][0]['id'] + self.diff = git.diff(self.parent_sha, self.last_sha) + + def post_messages(self, messages, max_comments): + # TODO: support non-PR runs + if not self.stash: + return + messages_to_post = 0 + messages_posted = 0 + for msg in messages: + if not msg.comments: + continue + msg_position = self.position(msg) + if msg_position: + messages_to_post += 1 + if not self.is_duplicate(msg, msg_position): + self.pull_request.comment( + self.format_message(msg), + srcPath=msg.path, + fileLine=msg_position, + lineType='ADDED', + fileType='TO' + ) + messages_posted += 1 + if max_comments >= 0 and messages_posted > max_comments: + break + return messages_to_post + + def is_duplicate(self, message, position): + for comment in self.pull_request.comments(message.path): + if ('anchor' in comment and + 'line' in comment['anchor'] and + comment['anchor']['line'] == position and + comment['text'].strip() == self.format_message(message).strip()): + return True + return False + + @staticmethod + def format_message(message): + if not message.comments: + return '' + if len(message.comments) > 1: + return ( + '```\n' + + '\n'.join(sorted(list(message.comments))) + + '\n```' + ) + return '`{0}`'.format(list(message.comments)[0].strip()) + + def position(self, message): + """ + Determine where the comment should go + + Skips messages outside of the scope of changes we're looking at + """ + patch = unidiff.PatchSet(self.diff.split('\n')) + for patched_file in patch: + target = patched_file.target_file.lstrip('b/') + if target == message.path: + for hunk_no, hunk in enumerate(patched_file): + for position, hunk_line in enumerate(hunk): + if '+' not in hunk_line.line_type: + continue + if hunk_line.target_line_no == message.line_number: + return hunk_line.target_line_no diff --git a/inlineplz/main.py b/inlineplz/main.py index 7f4b7ad5..9b0b7c3e 100644 --- a/inlineplz/main.py +++ b/inlineplz/main.py @@ -25,6 +25,8 @@ def main(): parser.add_argument('--repo', type=str) parser.add_argument('--repo-slug', type=str) parser.add_argument('--token', type=str) + parser.add_argument('--user', type=str) + parser.add_argument('--password', type=str) parser.add_argument('--interface', type=str, choices=interfaces.INTERFACES) parser.add_argument('--url', type=str) parser.add_argument('--enabled-linters', type=str, nargs='+') @@ -105,6 +107,8 @@ def inline(args): repo: Repository name pr: Pull request ID token: Authentication for repository + user: (If not using token) username for repository + password: (If not using token) password for repository url: Root URL of repository (not your project) Default: https://github.com dryrun: Prints instead of posting comments. zero_exit: If true: always return a 0 exit code. @@ -144,7 +148,7 @@ def inline(args): owner, repo, args.pull_request, - args.token, + dict(token=args.token, username=args.user, password=args.password), args.url ) if my_interface.post_messages(messages, args.max_comments) and not args.zero_exit: diff --git a/setup.py b/setup.py index a60a7635..3e49ef68 100644 --- a/setup.py +++ b/setup.py @@ -13,6 +13,7 @@ requirements = [ 'unidiff', 'github3.py', + #'stashy>=0.3', #TODO: Use this instead of dependency_links for stashy 'xmltodict', 'pyyaml', 'scandir', @@ -34,6 +35,9 @@ packages=find_packages('.', exclude=('tests*', 'testing*')), include_package_data=True, install_requires=requirements, + dependency_links=[ # TODO: Remove dependency_links once stashy is updated on PyPI + 'git+https://github.com/RisingOak/stashy.git#egg=stashy-0.3' + ], license="ISCL", zip_safe=False, keywords='inlineplz',