diff options
Diffstat (limited to 'aux-files/python-bugwarrior/gitea-support.patch')
-rw-r--r-- | aux-files/python-bugwarrior/gitea-support.patch | 1426 |
1 files changed, 1426 insertions, 0 deletions
diff --git a/aux-files/python-bugwarrior/gitea-support.patch b/aux-files/python-bugwarrior/gitea-support.patch new file mode 100644 index 0000000..6c2c331 --- /dev/null +++ b/aux-files/python-bugwarrior/gitea-support.patch @@ -0,0 +1,1426 @@ +From 6d7e50b5d1f7e79a685b08045cd91ea0b24f2154 Mon Sep 17 00:00:00 2001 +From: wamsachel <wamsachel@gmail.com> +Date: Wed, 1 Apr 2020 22:27:52 +0000 +Subject: [PATCH 1/2] (WIP) Adding service support for Gitea + +Imported from https://github.com/GothenburgBitFactory/bugwarrior/pull/720 +--- + bugwarrior/services/gitea.py | 536 +++++++++++++++++++++++++++++++++++ + setup.py | 1 + + 2 files changed, 537 insertions(+) + create mode 100644 bugwarrior/services/gitea.py + +diff --git a/bugwarrior/services/gitea.py b/bugwarrior/services/gitea.py +new file mode 100644 +index 00000000..a4c174c8 +--- /dev/null ++++ b/bugwarrior/services/gitea.py +@@ -0,0 +1,536 @@ ++# coding: utf-8 ++# gitea.py ++"""Bugwarrior service support class for Gitea ++ ++Available classes: ++- GiteaClient(ServiceClient): Constructs Gitea API strings ++- GiteaIssue(Issue): TaskWarrior Interface ++- GiteaService(IssueService): Engine for firing off requests ++ ++Todo: ++ * Add token support ++ * Flesh out more features offered by gitea api ++""" ++from builtins import filter ++import re ++import six ++from urllib.parse import urlparse ++from urllib.parse import quote_plus ++ ++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__) # pylint: disable-msg=C0103 ++ ++ ++class GiteaClient(ServiceClient): ++ """Builds Gitea API strings ++ Args: ++ host (str): remote gitea server ++ auth (dict): authentication credentials ++ ++ Attributes: ++ host (str): remote gitea server ++ auth (dict): authentication credentials ++ session (requests.Session): requests persist settings ++ ++ Publics Functions: ++ - get_repos: ++ - get_query: ++ - get_issues: ++ - get_directly_assigned_issues: ++ - get_comments: ++ - get_pulls: ++ """ ++ 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 """ ++ # TODO add token support ++ if 'basic' in self.auth: ++ (username, password) = self.auth['basic'] ++ baseurl = 'https://{user}:{secret}@{host}/api/v1'.format( ++ host=self.host, ++ user=username, ++ secret=quote_plus(password)) ++ if 'token' in self.auth: ++ baseurl = 'https://{host}/api/v1'.format( ++ host=self.host) ++ return baseurl + path.format(**context) ++ ++ # TODO Modify these for gitea support ++ 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', username=username)) ++ return 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, username): ++ """ 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('/users/{username}/issues', ++ username=username) ++ return self._getter(url) ++ ++ # TODO close to gitea format: /comments/{id} ++ 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://gitea.com/ralphbean/bugwarrior/issues/374 ++ # TODO this is a copy paste from github.py, see what gitea produces ++ if response.status_code == 404 and 'token' in self.auth: ++ log.warning('A \'404\' from gitea 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 ++ ++ # TODO: just copied from github.py ++ @staticmethod ++ def _link_field_to_dict(field): ++ """ Utility for ripping apart gitea'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 GiteaIssue(Issue): ++ TITLE = 'giteatitle' ++ BODY = 'giteabody' ++ CREATED_AT = 'giteacreatedon' ++ UPDATED_AT = 'giteaupdatedat' ++ CLOSED_AT = 'giteaclosedon' ++ MILESTONE = 'giteamilestone' ++ URL = 'giteaurl' ++ REPO = 'gitearepo' ++ TYPE = 'giteatype' ++ NUMBER = 'giteanumber' ++ USER = 'giteauser' ++ NAMESPACE = 'giteanamespace' ++ STATE = 'giteastate' ++ ++ UDAS = { ++ TITLE: { ++ 'type': 'string', ++ 'label': 'Gitea Title', ++ }, ++ BODY: { ++ 'type': 'string', ++ 'label': 'Gitea Body', ++ }, ++ CREATED_AT: { ++ 'type': 'date', ++ 'label': 'Gitea Created', ++ }, ++ UPDATED_AT: { ++ 'type': 'date', ++ 'label': 'Gitea Updated', ++ }, ++ CLOSED_AT: { ++ 'type': 'date', ++ 'label': 'Gitea Closed', ++ }, ++ MILESTONE: { ++ 'type': 'string', ++ 'label': 'Gitea Milestone', ++ }, ++ REPO: { ++ 'type': 'string', ++ 'label': 'Gitea Repo Slug', ++ }, ++ URL: { ++ 'type': 'string', ++ 'label': 'Gitea URL', ++ }, ++ TYPE: { ++ 'type': 'string', ++ 'label': 'Gitea Type', ++ }, ++ NUMBER: { ++ 'type': 'numeric', ++ 'label': 'Gitea Issue/PR #', ++ }, ++ USER: { ++ 'type': 'string', ++ 'label': 'Gitea User', ++ }, ++ NAMESPACE: { ++ 'type': 'string', ++ 'label': 'Gitea Namespace', ++ }, ++ STATE: { ++ 'type': 'string', ++ 'label': 'Gitea State', ++ } ++ } ++ UNIQUE_KEY = (URL, TYPE,) ++ ++ @staticmethod ++ def _normalize_label_to_tag(label): ++ return re.sub(r'[^a-zA-Z0-9]', '_', label) ++ ++ def to_taskwarrior(self): ++ milestone = self.record['milestone'] ++ if milestone: ++ milestone = milestone['title'] ++ ++ body = self.record['body'] ++ if body: ++ body = body.replace('\r\n', '\n') ++ ++ 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['url'], ++ self.REPO: self.record['repo'], ++ self.TYPE: self.extra['type'], ++ self.USER: self.record['user']['login'], ++ self.TITLE: self.record['title'], ++ self.BODY: 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): ++ log.info('In get_default_description') ++ return self.build_default_description( ++ title=self.record['title'], ++ url=self.get_processed_url(self.record['url']), ++ number=self.record['number'], ++ cls=self.extra['type'], ++ ) ++ ++ ++class GiteaService(IssueService): ++ ISSUE_CLASS = GiteaIssue ++ CONFIG_PREFIX = 'gitea' ++ ++ def __init__(self, *args, **kw): ++ super(GiteaService, self).__init__(*args, **kw) ++ ++ self.host = self.config.get('host', 'gitea.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 = GiteaClient(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): ++ #TODO grok this ++ login = service_config.get('login') ++ username = service_config.get('username') ++ host = service_config.get('host', default='gitea.com') ++ return 'gitea://{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 gitea query """ ++ log.info('In get_query') ++ issues = {} ++ for issue in self.client.get_query(query): ++ url = issue['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, username): ++ issues = {} ++ for issue in self.client.get_directly_assigned_issues(self.username): ++ 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): ++ log.info('in Annotations') ++ #log.info(repr(issue)) ++ log.info('body: {}'.format(issue['body'])) ++ url = issue['url'] ++ annotations = [] ++ if self.annotation_comments: ++ comments = self._comments(tag, issue['body']) ++ # log.info(" got comments for %s", issue['url']) ++ annotations = (( ++ c['user']['login'], ++ c['body'], ++ ) for c in comments) ++ annotations_result = self.build_annotations( ++ annotations, ++ issue_obj.get_processed_url(url)) ++ log.info('annotations: {}'.format(annotations_result)) ++ return annotations_result ++ ++ 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(GiteaService, 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: ++ log.info('Found repo: {}'.format(repo)) ++ issues.update( ++ self.get_owned_repo_issues( ++ self.username + '/' + repo) ++ ) ++ issues.update( ++ filter(self.filter_issues, ++ self.get_directly_assigned_issues(self.username).items()) ++ ) ++ ++ log.info(' Found %i issues.', len(issues)) # these were debug logs ++ issues = list(filter(self.include, issues.values())) ++ log.info(' Pruned down to %i issues.', len(issues)) # these were debug logs ++ ++ for tag, issue in issues: ++ # Stuff this value into the upstream dict for: ++ # https://gitea.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': [issue['body']], ++ '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 \'gitea.login\'' % target) ++ ++ if 'token' not in service_config and 'password' not in service_config: ++ die('[%s] has no \'gitea.token\' or \'gitea.password\'' % target) ++ +diff --git a/setup.py b/setup.py +index c761a231..b5306da5 100644 +--- a/setup.py ++++ b/setup.py +@@ -82,6 +82,7 @@ + bugwarrior-vault = bugwarrior:vault + bugwarrior-uda = bugwarrior:uda + [bugwarrior.service] ++ gitea=bugwarrior.services.gitea:GiteaService + github=bugwarrior.services.github:GithubService + gitlab=bugwarrior.services.gitlab:GitlabService + bitbucket=bugwarrior.services.bitbucket:BitbucketService + +From 80cd03d1ff85244f8a2c2beb37eab16af11e1adf Mon Sep 17 00:00:00 2001 +From: msglm <msglm@techchud.xyz> +Date: Tue, 21 May 2024 04:21:35 -0500 +Subject: [PATCH 2/2] Add basic Gitea support + +Builds off PR #720 to add gitea integration to bugwarrior. +--- + bugwarrior/services/gitea.py | 120 +++++++++++++++++++++-------------- + 1 file changed, 74 insertions(+), 46 deletions(-) + +diff --git a/bugwarrior/services/gitea.py b/bugwarrior/services/gitea.py +index a4c174c8..445846ff 100644 +--- a/bugwarrior/services/gitea.py ++++ b/bugwarrior/services/gitea.py +@@ -1,4 +1,4 @@ +-# coding: utf-8 ++#/home/joybuke/Documents/ComputerScience/Projects/Personal/bugwarrior/bugwarrior/services coding: utf-8 + # gitea.py + """Bugwarrior service support class for Gitea + +@@ -12,21 +12,45 @@ + * Flesh out more features offered by gitea api + """ + from builtins import filter ++import logging ++import pathlib + import re + import six ++import sys + from urllib.parse import urlparse + from urllib.parse import quote_plus + + import requests + from six.moves.urllib.parse import quote_plus ++import typing_extensions + from jinja2 import Template + +-from bugwarrior.config import asbool, aslist, die ++from bugwarrior import config + from bugwarrior.services import IssueService, Issue, ServiceClient + +-import logging + log = logging.getLogger(__name__) # pylint: disable-msg=C0103 + ++class GiteaConfig(config.ServiceConfig): ++ service: typing_extensions.Literal['gitea'] ++ ++ host = "gitea.com" ++ login: str ++ token: str ++ login: str ++ username: str ++ password: str ++ exclude_repos = [] ++ include_repos = [] ++ ++ 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: ++ return default ++ + + class GiteaClient(ServiceClient): + """Builds Gitea API strings +@@ -95,9 +119,9 @@ def get_directly_assigned_issues(self, username): + regardless of whether the user owns the repositories in which the + issues exist. + """ +- url = self._api_url('/users/{username}/issues', +- username=username) +- return self._getter(url) ++ url = self._api_url('/repos/issues/search', ++ username=username, assignee=True) ++ return self._getter(url, passedParams={'assigned': True, 'limit': 100}) #TODO: make the limit configurable + + # TODO close to gitea format: /comments/{id} + def get_comments(self, username, repo, number): +@@ -112,7 +136,7 @@ def get_pulls(self, username, repo): + username=username, repo=repo) + return self._getter(url) + +- def _getter(self, url, subkey=None): ++ def _getter(self, url, subkey=None, passedParams={}): + """ Pagination utility. Obnoxious. """ + + kwargs = {} +@@ -123,7 +147,7 @@ def _getter(self, url, subkey=None): + link = dict(next=url) + + while 'next' in link: +- response = self.session.get(link['next'], **kwargs) ++ response = self.session.get(link['next'], params=passedParams, **kwargs) + + # Warn about the mis-leading 404 error code. See: + # https://gitea.com/ralphbean/bugwarrior/issues/374 +@@ -253,14 +277,14 @@ def to_taskwarrior(self): + + return { + 'project': self.extra['project'], +- 'priority': self.origin['default_priority'], ++ 'priority': self.config.default_priority, + 'annotations': self.extra.get('annotations', []), + 'tags': self.get_tags(), + 'entry': created, + 'end': closed, + + self.URL: self.record['url'], +- self.REPO: self.record['repo'], ++ self.REPO: self.record['repository'], + self.TYPE: self.extra['type'], + self.USER: self.record['user']['login'], + self.TITLE: self.record['title'], +@@ -277,11 +301,11 @@ def to_taskwarrior(self): + def get_tags(self): + tags = [] + +- if not self.origin['import_labels_as_tags']: ++ if not self.config.get('import_labels_as_tags'): + return tags + + context = self.record.copy() +- label_template = Template(self.origin['label_template']) ++ label_template = Template(self.config.get('label_template')) + + for label_dict in self.record.get('labels', []): + context.update({ +@@ -305,46 +329,51 @@ def get_default_description(self): + + class GiteaService(IssueService): + ISSUE_CLASS = GiteaIssue ++ CONFIG_SCHEMA = GiteaConfig + CONFIG_PREFIX = 'gitea' + + def __init__(self, *args, **kw): + super(GiteaService, self).__init__(*args, **kw) + +- self.host = self.config.get('host', 'gitea.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) ++ token = self.config.token ++ self.login = self.config.login ++ if hasattr(self.config, 'token'): ++ token = self.get_password('token', login=self.login) + auth['token'] = token +- else: +- password = self.get_password('password', self.login) ++ elif hasattr(self.config.hasattr, 'password'): ++ password = self.get_password('password', login=self.login) + auth['basic'] = (self.login, password) ++ else: ++ #Probably should be called by validate_config, but I don't care to fix that. ++ logging.critical("ERROR! Neither token or password was provided in config!") ++ sys.exit(1) + +- self.client = GiteaClient(self.host, auth) ++ self.client = GiteaClient(host=self.config.host, auth=auth) + +- self.exclude_repos = self.config.get('exclude_repos', [], aslist) +- self.include_repos = self.config.get('include_repos', [], aslist) ++ self.host = self.config.host + +- self.username = self.config.get('username') ++ self.exclude_repos = self.config.exclude_repos ++ self.include_repos = self.config.include_repos ++ ++ self.username = self.config.username + self.filter_pull_requests = self.config.get( +- 'filter_pull_requests', default=False, to_type=asbool ++ 'filter_pull_requests', default=False, to_type=bool + ) + self.exclude_pull_requests = self.config.get( +- 'exclude_pull_requests', default=False, to_type=asbool ++ 'exclude_pull_requests', default=False, to_type=bool + ) + self.involved_issues = self.config.get( +- 'involved_issues', default=False, to_type=asbool ++ 'involved_issues', default=False, to_type=bool + ) + self.import_labels_as_tags = self.config.get( +- 'import_labels_as_tags', default=False, to_type=asbool ++ 'import_labels_as_tags', default=False, to_type=bool + ) + 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 ++ 'project_owner_prefix', default=False, to_type=bool + ) + + self.query = self.config.get( +@@ -357,9 +386,9 @@ def __init__(self, *args, **kw): + @staticmethod + def get_keyring_service(service_config): + #TODO grok this +- login = service_config.get('login') +- username = service_config.get('username') +- host = service_config.get('host', default='gitea.com') ++ login = service_config.login ++ username = service_config.username ++ host = service_config.host + return 'gitea://{login}@{host}/{username}'.format( + login=login, username=username, host=host) + +@@ -399,18 +428,17 @@ def get_directly_assigned_issues(self, username): + + @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'] ++ if 'repository' in issue: ++ url = issueloc=issue["html_url"] + else: + raise ValueError('Issue has no repository url' + str(issue)) ++ ++ #Literal cargo-cult crap, idk if this should be kept + tag = re.match('.*/([^/]*/[^/]*)$', url) + if tag is None: + raise ValueError('Unrecognized URL: {}.'.format(url)) +- return tag.group(1) ++ ++ return url.rsplit("/",2)[0] + + def _comments(self, tag, number): + user, repo = tag.split('/') +@@ -482,7 +510,7 @@ def issues(self): + if self.query: + issues.update(self.get_query(self.query)) + +- if self.config.get('include_user_repos', True, asbool): ++ if self.config.get('include_user_repos', True, bool): + # Only query for all repos if an explicit + # include_repos list is not specified. + if self.include_repos: +@@ -510,13 +538,11 @@ def issues(self): + for tag, issue in issues: + # Stuff this value into the upstream dict for: + # https://gitea.com/ralphbean/bugwarrior/issues/159 +- issue['repo'] = tag ++ projectName = issue['repository']["name"] + + issue_obj = self.get_issue_for_record(issue) +- tagParts = tag.split('/') +- projectName = tagParts[1] + if self.project_owner_prefix: +- projectName = tagParts[0]+'.'+projectName ++ projectName = issue['repository']["owner"] +'.'+projectName + extra = { + 'project': projectName, + 'type': 'pull_request' if 'pull_request' in issue else 'issue', +@@ -529,8 +555,10 @@ def issues(self): + @classmethod + def validate_config(cls, service_config, target): + if 'login' not in service_config: +- die('[%s] has no \'gitea.login\'' % target) ++ log.critical('[%s] has no \'gitea.login\'' % target) ++ sys.exit(1) + + if 'token' not in service_config and 'password' not in service_config: +- die('[%s] has no \'gitea.token\' or \'gitea.password\'' % target) ++ log.critical('[%s] has no \'gitea.token\' or \'gitea.password\'' % target) ++ sys.exit(1) + + +From 81b3fa0b47db93fb83a54c6727f5ee3c408797c5 Mon Sep 17 00:00:00 2001 +From: msglm <msglm@techchud.xyz> +Date: Wed, 22 May 2024 20:59:54 -0500 +Subject: [PATCH 3/4] Add basic documentation + +mish-mash between the github and gitlab documentation with everything I +don't think is supported removed. +--- + bugwarrior/docs/services/gitea.rst | 124 +++++++++++++++++++++++++++++ + 1 file changed, 124 insertions(+) + create mode 100644 bugwarrior/docs/services/gitea.rst + +diff --git a/bugwarrior/docs/services/gitea.rst b/bugwarrior/docs/services/gitea.rst +new file mode 100644 +index 00000000..6ccf2308 +--- /dev/null ++++ b/bugwarrior/docs/services/gitea.rst +@@ -0,0 +1,124 @@ ++Gitea ++====== ++ ++You can import tasks from your Gitea instance using ++the ``gitea`` service name. ++ ++Example Service ++--------------- ++ ++Here's an example of a Gitea target: ++ ++.. config:: ++ ++ [user_gitea] ++ service = gitea ++ gitea.login = ralphbean ++ gitea.username = ralphbean ++ gitea.host = git.bean.com #Note: the lack of https, the service will assume HTTPS by default. ++ gitea.password = @oracle:eval:pass show 'git.bean.com' ++ gitea.token = 0000000000000000000000000000000 ++ ++The above example is the minimum required to import issues from ++Gitea. 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 ``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 ``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): ++ ++.. config:: ++ :fragment: gitea ++ ++ gitea.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 ++``exclude_repos`` configuration option. ++ ++In this example, ``noisy/repository`` is the repository you would ++*not* like issues created for: ++ ++.. config:: ++ :fragment: gitea ++ ++ gitea.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: ++ ++.. config:: ++ :fragment: gitea ++ ++ gitea.login = foo ++ gitea.include_repos = bar ++ ++and: ++ ++.. config:: ++ :fragment: gitea ++ ++ gitea.login = foo ++ gitea.include_repos = foo/bar ++ ++Alternatively, you can use project IDs instead of names by prefixing the ++project id with `id:`: ++ ++.. config:: ++ :fragment: gitea ++ ++ gitea.include_repos = id:1234,id:3141 ++ ++Import Labels as Tags +++++++++++++++++++++++ ++ ++The gitea issue tracker allows you to attach labels to issues; to ++use those labels as tags, you can use the ``import_labels_as_tags`` ++option: ++ ++.. config:: ++ :fragment: gitea ++ ++ gitea.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 gitea label into a Taskwarrior ++tag. ++ ++For example, to prefix all incoming labels with the string 'gitea_' (perhaps ++to differentiate them from any existing tags you might have), you could ++add the following configuration option: ++ ++.. config:: ++ :fragment: gitea ++ ++ gitea.label_template = gitea_{{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 ++------------------- ++ ++.. udas:: bugwarrior.services.gitea.GiteaIssue + +From 1626a36c15013fc42e369cdc9ef98c23e10845c2 Mon Sep 17 00:00:00 2001 +From: msglm <msglm@techchud.xyz> +Date: Wed, 22 May 2024 21:00:20 -0500 +Subject: [PATCH 4/4] Remove Six usage and clean the codebase + +Suggestions from here are implemented +https://github.com/GothenburgBitFactory/bugwarrior/pull/1048#pullrequestreview-2070021239 +--- + bugwarrior/services/gitea.py | 20 ++++---------------- + 1 file changed, 4 insertions(+), 16 deletions(-) + +diff --git a/bugwarrior/services/gitea.py b/bugwarrior/services/gitea.py +index 445846ff..341ec617 100644 +--- a/bugwarrior/services/gitea.py ++++ b/bugwarrior/services/gitea.py +@@ -15,7 +15,6 @@ + import logging + import pathlib + import re +-import six + import sys + from urllib.parse import urlparse + from urllib.parse import quote_plus +@@ -36,11 +35,10 @@ class GiteaConfig(config.ServiceConfig): + host = "gitea.com" + login: str + token: str +- login: str + username: str + password: str +- exclude_repos = [] +- include_repos = [] ++ exclude_repos = config.ConfigList([]) ++ include_repos = config.ConfigList([]) + + def get(self, key, default=None, to_type=None): + try: +@@ -370,7 +368,7 @@ def __init__(self, *args, **kw): + 'import_labels_as_tags', default=False, to_type=bool + ) + self.label_template = self.config.get( +- 'label_template', default='{{label}}', to_type=six.text_type ++ 'label_template', default='{{label}}', to_type=bool + ) + self.project_owner_prefix = self.config.get( + 'project_owner_prefix', default=False, to_type=bool +@@ -380,7 +378,7 @@ def __init__(self, *args, **kw): + 'query', + default='involves:{user} state:open'.format( + user=self.username) if self.involved_issues else '', +- to_type=six.text_type ++ to_type=str + ) + + @staticmethod +@@ -552,13 +550,3 @@ def issues(self): + issue_obj.update_extra(extra) + yield issue_obj + +- @classmethod +- def validate_config(cls, service_config, target): +- if 'login' not in service_config: +- log.critical('[%s] has no \'gitea.login\'' % target) +- sys.exit(1) +- +- if 'token' not in service_config and 'password' not in service_config: +- log.critical('[%s] has no \'gitea.token\' or \'gitea.password\'' % target) +- sys.exit(1) +- + +From 17b725774281e9742b786dbcbcf791f7f3dacf61 Mon Sep 17 00:00:00 2001 +From: msglm <msglm@techchud.xyz> +Date: Thu, 23 May 2024 05:03:39 -0500 +Subject: [PATCH 5/6] Intake Critique and simplify + +Remove user+pass auth, token only now. +Added issue API Querying for writing custom queries +Added include_assigned,created,mentioned, and review_requested issues +config settings +Added ability to limit the number of issues you will query (Gitea limits +the API by default to 50, but I host my own instance so I raised it) +get_tags simplified greatly +--- + bugwarrior/services/gitea.py | 178 ++++++++++++++++++++--------------- + 1 file changed, 102 insertions(+), 76 deletions(-) + +diff --git a/bugwarrior/services/gitea.py b/bugwarrior/services/gitea.py +index 341ec617..28a92e96 100644 +--- a/bugwarrior/services/gitea.py ++++ b/bugwarrior/services/gitea.py +@@ -29,16 +29,28 @@ + + log = logging.getLogger(__name__) # pylint: disable-msg=C0103 + ++#TODO: Document this with docstrings + class GiteaConfig(config.ServiceConfig): + service: typing_extensions.Literal['gitea'] +- + host = "gitea.com" +- login: str + token: str + username: str +- password: str +- exclude_repos = config.ConfigList([]) +- include_repos = config.ConfigList([]) ++ include_assigned_issues: bool = False ++ include_created_issues: bool = False ++ include_mentioned_issues: bool = False ++ include_review_requested_issues: bool = False ++ import_labels_as_tags: bool = True ++ involved_issues: bool = False ++ project_owner_prefix: bool = False ++ include_repos: config.ConfigList = config.ConfigList([]) ++ exclude_repos: config.ConfigList = config.ConfigList([]) ++ label_template = str = '{{label}}' ++ filter_pull_requests: bool = False ++ exclude_pull_requests: bool = False ++ """ ++ The maximum number of issues the API may get from the host ++ """ ++ issue_limit: int = 100 + + def get(self, key, default=None, to_type=None): + try: +@@ -65,7 +77,7 @@ class GiteaClient(ServiceClient): + - get_repos: + - get_query: + - get_issues: +- - get_directly_assigned_issues: ++ - get_special_issues: + - get_comments: + - get_pulls: + """ +@@ -79,16 +91,8 @@ def __init__(self, host, auth): + + def _api_url(self, path, **context): + """ Build the full url to the API endpoint """ +- # TODO add token support +- if 'basic' in self.auth: +- (username, password) = self.auth['basic'] +- baseurl = 'https://{user}:{secret}@{host}/api/v1'.format( +- host=self.host, +- user=username, +- secret=quote_plus(password)) +- if 'token' in self.auth: +- baseurl = 'https://{host}/api/v1'.format( +- host=self.host) ++ baseurl = 'https://{host}/api/v1'.format( ++ host=self.host) + return baseurl + path.format(**context) + + # TODO Modify these for gitea support +@@ -109,17 +113,17 @@ def get_issues(self, username, repo): + '/repos/{username}/{repo}/issues?per_page=100', + username=username, repo=repo) + return self._getter(url) ++ ++ def get_special_issues(self, username, query: str): ++ """ Returns all issues assigned to authenticated user given a specific query. + +- def get_directly_assigned_issues(self, username): +- """ 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. ++ This will return all issues this authenticated user has access to and then ++ filter the issues with the query that the user supplied. + """ +- url = self._api_url('/repos/issues/search', +- username=username, assignee=True) +- return self._getter(url, passedParams={'assigned': True, 'limit': 100}) #TODO: make the limit configurable ++ logging.info("Querying /repos/issues/search with query: " + query) ++ url = self._api_url('/repos/issues/search?{query}', ++ username=username, query=query) ++ return self._getter(url) + + # TODO close to gitea format: /comments/{id} + def get_comments(self, username, repo, number): +@@ -134,7 +138,7 @@ def get_pulls(self, username, repo): + username=username, repo=repo) + return self._getter(url) + +- def _getter(self, url, subkey=None, passedParams={}): ++ def _getter(self, url, subkey=None): + """ Pagination utility. Obnoxious. """ + + kwargs = {} +@@ -145,7 +149,7 @@ def _getter(self, url, subkey=None, passedParams={}): + link = dict(next=url) + + while 'next' in link: +- response = self.session.get(link['next'], params=passedParams, **kwargs) ++ response = self.session.get(link['next'], **kwargs) + + # Warn about the mis-leading 404 error code. See: + # https://gitea.com/ralphbean/bugwarrior/issues/374 +@@ -269,6 +273,9 @@ def to_taskwarrior(self): + if body: + body = body.replace('\r\n', '\n') + ++ if len(body) < 1: ++ body = "No annotation was provided." ++ + 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')) +@@ -295,25 +302,19 @@ def to_taskwarrior(self): + self.NAMESPACE: self.extra['namespace'], + self.STATE: self.record.get('state', '') + } +- + def get_tags(self): +- tags = [] +- +- if not self.config.get('import_labels_as_tags'): +- return tags ++ labels = [label['name'] for label in self.record.get('labels', [])] ++ return self.get_tags_from_labels(labels) + +- context = self.record.copy() +- label_template = Template(self.config.get('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) +- ) ++ def get_default_description(self): ++ log.info('In get_default_description') ++ return self.build_default_description( ++ title=self.record['title'], ++ url=self.get_processed_url(self.record['url']), ++ number=self.record['number'], ++ cls=self.extra['type'], ++ ) + +- return tags + + def get_default_description(self): + log.info('In get_default_description') +@@ -335,44 +336,42 @@ def __init__(self, *args, **kw): + + auth = {} + token = self.config.token +- self.login = self.config.login + if hasattr(self.config, 'token'): +- token = self.get_password('token', login=self.login) ++ token = self.get_password('token', login=self.config.username) + auth['token'] = token +- elif hasattr(self.config.hasattr, 'password'): +- password = self.get_password('password', login=self.login) +- auth['basic'] = (self.login, password) + else: + #Probably should be called by validate_config, but I don't care to fix that. +- logging.critical("ERROR! Neither token or password was provided in config!") ++ logging.critical("ERROR! No token was provided in config!") + sys.exit(1) + ++ #TODO: document these with docstrings + self.client = GiteaClient(host=self.config.host, auth=auth) + + self.host = self.config.host + + self.exclude_repos = self.config.exclude_repos ++ + self.include_repos = self.config.include_repos + + self.username = self.config.username +- self.filter_pull_requests = self.config.get( +- 'filter_pull_requests', default=False, to_type=bool +- ) +- self.exclude_pull_requests = self.config.get( +- 'exclude_pull_requests', default=False, to_type=bool +- ) +- self.involved_issues = self.config.get( +- 'involved_issues', default=False, to_type=bool +- ) +- self.import_labels_as_tags = self.config.get( +- 'import_labels_as_tags', default=False, to_type=bool +- ) +- self.label_template = self.config.get( +- 'label_template', default='{{label}}', to_type=bool +- ) +- self.project_owner_prefix = self.config.get( +- 'project_owner_prefix', default=False, to_type=bool +- ) ++ ++ self.filter_pull_requests = self.config.filter_pull_requests ++ ++ self.exclude_pull_requests = self.config.exclude_pull_requests ++ ++ self.involved_issues = self.config.involved_issues ++ ++ self.project_owner_prefix = self.config.project_owner_prefix ++ ++ self.include_assigned_issues = self.config.include_assigned_issues ++ ++ self.include_created_issues = self.config.include_created_issues ++ ++ self.include_review_requested_issues = self.config.include_review_requested_issues ++ ++ self.import_labels_as_tags = self.config.import_labels_as_tags ++ ++ self.label_template = self.config.label_template + + self.query = self.config.get( + 'query', +@@ -384,11 +383,10 @@ def __init__(self, *args, **kw): + @staticmethod + def get_keyring_service(service_config): + #TODO grok this +- login = service_config.login + username = service_config.username + host = service_config.host +- return 'gitea://{login}@{host}/{username}'.format( +- login=login, username=username, host=host) ++ return 'gitea://{username}@{host}/{username}'.format( ++ username=username, host=host) + + def get_service_metadata(self): + return { +@@ -417,9 +415,9 @@ def get_query(self, query): + issues[url] = (repo, issue) + return issues + +- def get_directly_assigned_issues(self, username): ++ def get_special_issues(self, username, query): + issues = {} +- for issue in self.client.get_directly_assigned_issues(self.username): ++ for issue in self.client.get_special_issues(self.username, query): + repos = self.get_repository_from_issue(issue) + issues[issue['url']] = (repos, issue) + return issues +@@ -524,10 +522,38 @@ def issues(self): + self.get_owned_repo_issues( + self.username + '/' + repo) + ) +- issues.update( +- filter(self.filter_issues, +- self.get_directly_assigned_issues(self.username).items()) +- ) ++ ++ ''' ++ A variable used to represent the attachable HTTP query that can be attached to the /repos/issues/search API end. ++ ++ if httpQuery is set to "review_requested=True?mentioned=True" for example, then the /repos/issues/search API end will be told to search for all issues where a review is requested AND where the user is mentioned. ++ ''' ++ httpQuery = "limit=" + str(self.config.issue_limit) + "&" ++ ++ if self.config.get('include_assigned_issues', True, bool): ++ log.info("assigned was true") ++ issues.update( ++ filter(self.filter_issues, ++ self.get_special_issues(self.username, httpQuery + "assigned=true&").items()) ++ ) ++ if self.config.get('include_created_issues', True, bool): ++ log.info("created was true") ++ issues.update( ++ filter(self.filter_issues, ++ self.get_special_issues(self.username, httpQuery + "created=true&").items()) ++ ) ++ if self.config.get('include_mentioned_issues', True, bool): ++ log.info("mentioned was true") ++ issues.update( ++ filter(self.filter_issues, ++ self.get_special_issues(self.username, httpQuery + "mentioned=true&").items()) ++ ) ++ if self.config.get('include_review_requested_issues', True, bool): ++ log.info("review request was true") ++ issues.update( ++ filter(self.filter_issues, ++ self.get_special_issues(self.username, httpQuery + "review_requested=true&").items()) ++ ) + + log.info(' Found %i issues.', len(issues)) # these were debug logs + issues = list(filter(self.include, issues.values())) + +From 3eb6e743c7ee4c7892525c05d880f5d05d3f8600 Mon Sep 17 00:00:00 2001 +From: msglm <msglm@techchud.xyz> +Date: Thu, 23 May 2024 05:13:33 -0500 +Subject: [PATCH 6/6] Documentation for previous commit + +--- + bugwarrior/docs/services/gitea.rst | 31 +++++++++++++++++++++++++++--- + 1 file changed, 28 insertions(+), 3 deletions(-) + +diff --git a/bugwarrior/docs/services/gitea.rst b/bugwarrior/docs/services/gitea.rst +index 6ccf2308..19e0930a 100644 +--- a/bugwarrior/docs/services/gitea.rst ++++ b/bugwarrior/docs/services/gitea.rst +@@ -13,11 +13,9 @@ Here's an example of a Gitea target: + + [user_gitea] + service = gitea +- gitea.login = ralphbean + gitea.username = ralphbean + gitea.host = git.bean.com #Note: the lack of https, the service will assume HTTPS by default. +- gitea.password = @oracle:eval:pass show 'git.bean.com' +- gitea.token = 0000000000000000000000000000000 ++ gitea.token = @oracle:eval:pass show 'git.bean.com token' + + The above example is the minimum required to import issues from + Gitea. You can also feel free to use any of the +@@ -117,6 +115,33 @@ to all fields on the Taskwarrior task if needed: + See :ref:`field_templates` for more details regarding how templates + are processed. + ++Limit Issues Imported +++++++++++++++++++++++ ++Gitea lets system administrators configure the amount of objects that any given API request will return. ++You may configure the amount to tell Gitea to give to you using the ``issue_limit`` option: ++ ++.. config:: ++ :fragment: gitea ++ ++ gitea.issue_limit = 200 ++ ++Do note, this will not overwrite what the gitea instance limits you to, it merely lets you set the amount of issues you will import. ++ ++ ++Including various types of issues +++++++++++++++++++++++ ++ ++Gitea has metadata attached to each issue, primarily: If you are assigned to an issue, if you created an issue, if an issue mentions you, and if an issue has a review reqest for you. You may set if each of these traits is worth importing by using the various ``include_*_issues`` options: ++ ++.. config:: ++ :fragment: gitea ++ ++ gitea.include_assigned_issues = true ++ gitea.include_created_issues = true ++ gitea.include_mentioned_issues = true ++ gitea.include_review_requested_issues = true ++ ++Each setting will query the API for that trait alone and then add it to your Taskwarrior task list. For example, if you have created issues and mentioned issues off, but assigned issues and review requested issues on: You will only recieve new tasks for the issues you are assigned to do or requested to review, but not for issues you've created or mentioned. Issues that have been assigned to you and created by you would be included though, as these settings merely mark inclusion, not exclusion. + + Provided UDA Fields + ------------------- |