diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b8a95bb --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +# generated from manifests external_dependencies +github3.py +markdown diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..c30a4a4 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,2 @@ +odoo-addon-vcp_management @ git+https://github.com/OCA/version-control-platform.git@refs/pull/11/head#subdirectory=vcp_management +odoo-addon-vcp_git @ git+https://github.com/OCA/version-control-platform.git@refs/pull/12/head#subdirectory=vcp_git diff --git a/vcp_github/README.rst b/vcp_github/README.rst new file mode 100644 index 0000000..ecf860e --- /dev/null +++ b/vcp_github/README.rst @@ -0,0 +1,83 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +========== +Vcp Github +========== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:c11ed718a96213c61adaf41d94d05c5ae2965b6a676231d6d4fac3a6ebf3264b + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fversion--control--platform-lightgray.png?logo=github + :target: https://github.com/OCA/version-control-platform/tree/19.0/vcp_github + :alt: OCA/version-control-platform +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/version-control-platform-19-0/version-control-platform-19-0-vcp_github + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/version-control-platform&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module facilitates the integration between Github and Version +Control Platform. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Dixmit + +Contributors +------------ + +- `Dixmit `__ + + - Enric Tobella + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/version-control-platform `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/vcp_github/__init__.py b/vcp_github/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/vcp_github/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/vcp_github/__manifest__.py b/vcp_github/__manifest__.py new file mode 100644 index 0000000..ee41152 --- /dev/null +++ b/vcp_github/__manifest__.py @@ -0,0 +1,23 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Vcp Github", + "summary": """Integrate Version Control Platform with Github""", + "version": "19.0.1.0.0", + "license": "AGPL-3", + "author": "Dixmit,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/version-control-platform", + "depends": [ + "vcp_git", + ], + "external_dependencies": { + "python": ["github3.py", "markdown"], + }, + "data": [ + "data/data.xml", + ], + "demo": [ + "demo/demo_vcp_platform.xml", + ], +} diff --git a/vcp_github/data/data.xml b/vcp_github/data/data.xml new file mode 100644 index 0000000..47915bb --- /dev/null +++ b/vcp_github/data/data.xml @@ -0,0 +1,12 @@ + + + + GitHub + github + git + + + GitHub + + + diff --git a/vcp_github/demo/demo_vcp_platform.xml b/vcp_github/demo/demo_vcp_platform.xml new file mode 100644 index 0000000..984dc6c --- /dev/null +++ b/vcp_github/demo/demo_vcp_platform.xml @@ -0,0 +1,9 @@ + + + + OCA + + ^\d+\.\d+$ + + + diff --git a/vcp_github/i18n/it.po b/vcp_github/i18n/it.po new file mode 100644 index 0000000..b26bac8 --- /dev/null +++ b/vcp_github/i18n/it.po @@ -0,0 +1,62 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * vcp_github +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2026-04-08 12:45+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.15.2\n" + +#. module: vcp_github +#. odoo-python +#: code:addons/vcp_github/models/vcp_platform.py:0 +msgid "" +"No github clients configured. Please enter at least a Github Personal Access" +" Token. You can check more information at " +"https://docs.github.com/en/authentication/keeping-your-account-and-data-" +"secure/managing-your-personal-access-tokens" +msgstr "" +"Nessun client GitHub configurato. Inserire almeno un token di accesso " +"personale di GitHub. Si possono trovare maggiori informazioni su https://" +"docs.github.com/en/authentication/keeping-your-account-and-data-secure/" +"managing-your-personal-access-tokens" + +#. module: vcp_github +#. odoo-python +#: code:addons/vcp_github/models/vcp_host.py:0 +msgid "Only one GitHub Host type is allowed." +msgstr "È consentito solo un tipo host GitHub." + +#. module: vcp_github +#: model:ir.model,name:vcp_github.model_vcp_organization +msgid "Organization" +msgstr "Organizzazione" + +#. module: vcp_github +#: model:ir.model,name:vcp_github.model_vcp_repository +msgid "Repository" +msgstr "Repository" + +#. module: vcp_github +#: model:ir.model,name:vcp_github.model_vcp_user +msgid "User" +msgstr "Utente" + +#. module: vcp_github +#: model:ir.model,name:vcp_github.model_vcp_host +msgid "VCP Host" +msgstr "Host VCP" + +#. module: vcp_github +#: model:ir.model,name:vcp_github.model_vcp_platform +msgid "VCP Platform" +msgstr "Piattaforma VCP" diff --git a/vcp_github/i18n/vcp_github.pot b/vcp_github/i18n/vcp_github.pot new file mode 100644 index 0000000..87b0648 --- /dev/null +++ b/vcp_github/i18n/vcp_github.pot @@ -0,0 +1,55 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * vcp_github +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: vcp_github +#. odoo-python +#: code:addons/vcp_github/models/vcp_platform.py:0 +msgid "" +"No github clients configured. Please enter at least a Github Personal Access" +" Token. You can check more information at " +"https://docs.github.com/en/authentication/keeping-your-account-and-data-" +"secure/managing-your-personal-access-tokens" +msgstr "" + +#. module: vcp_github +#. odoo-python +#: code:addons/vcp_github/models/vcp_host.py:0 +msgid "Only one GitHub Host type is allowed." +msgstr "" + +#. module: vcp_github +#: model:ir.model,name:vcp_github.model_vcp_organization +msgid "Organization" +msgstr "" + +#. module: vcp_github +#: model:ir.model,name:vcp_github.model_vcp_repository +msgid "Repository" +msgstr "" + +#. module: vcp_github +#: model:ir.model,name:vcp_github.model_vcp_user +msgid "User" +msgstr "" + +#. module: vcp_github +#: model:ir.model,name:vcp_github.model_vcp_host +msgid "VCP Host" +msgstr "" + +#. module: vcp_github +#: model:ir.model,name:vcp_github.model_vcp_platform +msgid "VCP Platform" +msgstr "" diff --git a/vcp_github/models/__init__.py b/vcp_github/models/__init__.py new file mode 100644 index 0000000..c6c3d79 --- /dev/null +++ b/vcp_github/models/__init__.py @@ -0,0 +1,5 @@ +from . import vcp_host +from . import vcp_platform +from . import vcp_repository +from . import vcp_user +from . import vcp_organization diff --git a/vcp_github/models/vcp_host.py b/vcp_github/models/vcp_host.py new file mode 100644 index 0000000..5b3331b --- /dev/null +++ b/vcp_github/models/vcp_host.py @@ -0,0 +1,21 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, models +from odoo.exceptions import ValidationError + + +class VcpHost(models.Model): + _inherit = "vcp.host" + + @api.constrains("type_id") + def _check_kind_github(self): + for record in self.filtered(lambda r: r.type_id.code == "github"): + platforms = self.search( + [("id", "!=", record.id), ("type_id.code", "=", "github")], + limit=1, + ) + if platforms: + raise ValidationError( + self.env._("Only one GitHub Host type is allowed.") + ) diff --git a/vcp_github/models/vcp_organization.py b/vcp_github/models/vcp_organization.py new file mode 100644 index 0000000..be5339a --- /dev/null +++ b/vcp_github/models/vcp_organization.py @@ -0,0 +1,15 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +from odoo import models + + +class VcpOrganization(models.Model): + _inherit = "vcp.organization" + + def _get_contributor_url(self): + result = super()._get_contributor_url() + if not result and self.host_id.type_id.code == "github": + return f"https://github.com/{self.external_id}" + return result diff --git a/vcp_github/models/vcp_platform.py b/vcp_github/models/vcp_platform.py new file mode 100644 index 0000000..22d95e0 --- /dev/null +++ b/vcp_github/models/vcp_platform.py @@ -0,0 +1,102 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import base64 +from datetime import datetime + +import github3 +import markdown +import requests +from pytz import UTC + +from odoo import fields, models +from odoo.exceptions import ValidationError + + +class VcpPlatform(models.Model): + _inherit = "vcp.platform" + + def _get_git_url_github(self, repository): + return f"https://github.com/{self.name}/{repository.name}" + + def _get_github_clients(self): + git = [] + for key in self.key_ids: + git.append(github3.login(token=key.name)) + return git + + def _update_information_github(self): + self.ensure_one() + clients = self._get_github_clients() + if not clients: + raise ValidationError( + self.env._( + "No github clients configured. " + "Please enter at least a Github Personal Access Token. " + "You can check more information at " + "https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens" + ) + ) + org = clients[0].organization(self.name) + self.short_description = org.name + self.description = org.description + if org.avatar_url: + response = requests.get(org.avatar_url, timeout=10) + response.raise_for_status() + self.image_1920 = base64.b64encode(response.content) + repos = org.repositories() + for repo in repos: + if repo.fork and not self.fetch_repository_fork: + continue + if repo.archived and not self.fetch_repository_archived: + continue + self._update_github_repository(repo) + self.last_update = fields.Datetime.now() + + def _parse_github_date(self, date): + if not date: + return False + return UTC.normalize( + datetime.fromisoformat(date.replace("Z", "+00:00")) + ).replace(tzinfo=None) + + def _parse_github_markdown(self, text): + return markdown.markdown(text, extensions=["fenced_code"]) + + def _update_github_repository(self, repo): + vals = { + "created_at": self._parse_github_date(repo.created_at), + "last_commit_date": self._parse_github_date(repo.pushed_at), + "stargazers_count": repo.stargazers_count, + "fork_count": repo.forks_count, + "is_fork": repo.fork, + "active": not repo.archived, + "watchers_count": repo.watchers_count, + "description": repo.description, + } + repository = ( + self.env["vcp.repository"] + .with_context(active_test=False) + .search( + [ + ("name", "=", repo.name), + ("platform_id", "=", self.id), + ], + limit=1, + ) + ) + if not repository: + repository = ( + self.env["vcp.repository"] + .sudo() + .create( + { + "name": repo.name, + "platform_id": self.id, + "from_date": vals.get("created_at"), + **vals, + } + ) + ) + else: + repository.sudo().write(vals) diff --git a/vcp_github/models/vcp_repository.py b/vcp_github/models/vcp_repository.py new file mode 100644 index 0000000..2654c8e --- /dev/null +++ b/vcp_github/models/vcp_repository.py @@ -0,0 +1,247 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import logging +import re +from datetime import datetime, timedelta + +import github3 +from github3 import pulls +from pytz import UTC + +from odoo import fields, models +from odoo.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + + +class VcpRepository(models.Model): + _inherit = "vcp.repository" + + def _get_repository_url(self): + result = super()._get_repository_url() + if not result and self.platform_id.host_id.type_id.code == "github": + return f"https://github.com/{self.platform_id.name}/{self.name}" + return result + + def _update_branches_github(self): + self.ensure_one() + client = self.platform_id._get_github_clients()[0] + try: + repo = client.repository(self.platform_id.name, self.name) + original_branches = self.branch_ids + existing_branches = {b.branch_id.name: b for b in self.branch_ids} + found_branches = self.env["vcp.repository.branch"] + for branch in repo.branches(): + branch_pattern = ( + self.fetch_branch_pattern + or self.platform_id.fetch_repository_branch_pattern + or False + ) + if branch_pattern and not re.match(branch_pattern, branch.name): + continue + if branch.name in existing_branches: + existing_branches[branch.name].sudo().write( + {"last_commit": branch.commit.sha} + ) + found_branches |= existing_branches[branch.name] + else: + self.env["vcp.repository.branch"].sudo().create( + { + "repository_id": self.id, + "branch_id": self.platform_id._get_branch(branch.name), + "last_commit": branch.commit.sha, + } + ) + (original_branches - found_branches).sudo().unlink() + except github3.exceptions.ForbiddenError as e: + _logger.error(e) + rate = client.rate_limit() + reset = fields.Datetime.to_string( + datetime.utcfromtimestamp(rate["resources"]["core"]["reset"]) + ) + raise ValidationError(self.env._("Reset on %(reset)s", reset=reset)) from e + + def _parse_github_pr(self, pr, client): + self.ensure_one() + origin_data = pr.as_dict() + comments_url = pr.comments_url + comments_req = client.session.get(comments_url) + comments = comments_req.json() + while comments_req.links.get("next"): + comments_url = comments_req.links["next"]["url"] + comments_req = client.session.get(comments_url) + comments += comments_req.json() + reviews_url = pr.reviews().url + reviews_req = client.session.get(reviews_url) + reviews = reviews_req.json() + while reviews_req.links.get("next"): + reviews_url = reviews_req.links["next"]["url"] + reviews_req = client.session.get(reviews_url) + reviews += reviews_req.json() + + branch_pattern = ( + self.fetch_branch_pattern + or self.platform_id.fetch_repository_branch_pattern + or False + ) + if branch_pattern and not re.match(branch_pattern, pr.base.ref): + branch_id = False + else: + branch_id = self.platform_id._get_branch(pr.base.ref) + return ( + str(pr.id), + { + "user_id": self.platform_id.host_id._get_user(pr.user.login), + "repository_id": self.id, + "branch_id": branch_id, + "organization_id": self.platform_id.host_id._get_organization( + pr.head.repo[0] + ), + "url": pr.html_url, + "state": pr.state, + "name": pr.title, + "is_merged": any(label["name"] == "merged 🎉" for label in pr.labels) + or pr.is_merged(), + "is_draft": pr.draft, + "created_at": self.platform_id._parse_github_date( + origin_data["created_at"] + ), + "closed_at": self.platform_id._parse_github_date( + origin_data["closed_at"] + ), + "number": pr.number, + "updated_at": self.platform_id._parse_github_date( + origin_data["updated_at"] + ), + "label_ids": [fields.Command.clear()] + + [ + fields.Command.link( + self.env["vcp.request.label"]._get_label(label["name"]) + ) + for label in origin_data["labels"] + ], + "commits": origin_data["commits"], + "total_comments": origin_data["comments"], + "review_comments": origin_data["review_comments"], + "additions": origin_data["additions"], + "deletions": origin_data["deletions"], + }, + [ + { + "id": str(c["id"]), + "user_id": c.get("user") + and self.platform_id.host_id._get_user(c["user"].get("login")), + "body": self.platform_id._parse_github_markdown(c["body"]), + "created_at": self.platform_id._parse_github_date(c["created_at"]), + "updated_at": self.platform_id._parse_github_date(c["updated_at"]), + } + for c in comments + ], + [ + { + "id": str(r["id"]), + "user_id": r.get("user") + and self.platform_id.host_id._get_user(r["user"].get("login")), + "body": self.platform_id._parse_github_markdown(r["body"]), + "submitted_at": self.platform_id._parse_github_date( + r.get("submitted_at") + ), + "state": r["state"]["keyword"] + if isinstance(r["state"], dict) + else r["state"], + } + for r in reviews + if r.get("submitted_at") + ], + ) + + def _update_information_github( + self, update_interval_days=None, client_for_search=0 + ): + self.ensure_one() + clients = self.platform_id._get_github_clients() + try: + start = UTC.localize(self.from_date) + end = min( + start + + timedelta( + days=update_interval_days or self.platform_id.update_interval_days + ), + UTC.localize(datetime.now()), + ) + start += timedelta( + days=-1 + ) # Add buffer day to avoid missing PRs on boundary dates + i = client_for_search % len(clients) + for pr in clients[i].search_issues( + f"is:pr repo:{self.platform_id.name}/{self.name} " + f"updated:{start.isoformat()}..{end.isoformat()}" + ): + i = (1 + i) % len(clients) + pr_id, pr_data, comments, reviews = self._parse_github_pr( + clients[i]._instance_or_null( + pulls.PullRequest, + clients[i]._json( + pr.issue._get(pr.issue.pull_request_urls.get("url")), 200 + ), + ), + clients[i], + ) + opr = self.env["vcp.request"].search( + [("external_id", "=", pr_id), ("repository_id", "=", self.id)], + limit=1, + ) + if not opr: + opr = ( + self.env["vcp.request"] + .sudo() + .create({"external_id": pr_id, **pr_data}) + ) + else: + opr.sudo().write(pr_data) + for comment in comments: + comment_id = comment.pop("id") + ocomment = self.env["vcp.comment"].search( + [ + ("external_id", "=", comment_id), + ("repository_id", "=", self.id), + ], + limit=1, + ) + if not ocomment: + self.env["vcp.comment"].sudo().create( + { + "external_id": comment_id, + "request_id": opr.id, + **comment, + } + ) + else: + ocomment.sudo().write(comment) + for review in reviews: + review_id = review.pop("id") + oreview = self.env["vcp.review"].search( + [ + ("external_id", "=", review_id), + ("repository_id", "=", self.id), + ], + limit=1, + ) + if not oreview: + self.env["vcp.review"].sudo().create( + { + "external_id": review_id, + "request_id": opr.id, + **review, + } + ) + else: + oreview.sudo().write(review) + self.sudo().from_date = end.replace(tzinfo=None) + except github3.exceptions.ForbiddenError as e: + _logger.error(e) + rate = clients[i].rate_limit() + reset = fields.Datetime.to_string( + datetime.utcfromtimestamp(rate["resources"]["core"]["reset"]) + ) + raise ValidationError(self.env._("Reset on %(reset)s", reset=reset)) from e diff --git a/vcp_github/models/vcp_user.py b/vcp_github/models/vcp_user.py new file mode 100644 index 0000000..f395d2c --- /dev/null +++ b/vcp_github/models/vcp_user.py @@ -0,0 +1,54 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +import logging +from datetime import datetime + +import github3 + +from odoo import fields, models +from odoo.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + + +class VcpUser(models.Model): + _inherit = "vcp.user" + + def _get_contributor_url(self): + result = super()._get_contributor_url() + if not result and self.host_id.type_id.code == "github": + return f"https://github.com/{self.external_id}" + return result + + def _prepare_user_vals(self, user): + return { + "name": user.name or user.login, + "email": user.email, + "avatar_url": user.avatar_url, + "company": user.company, + } + + def _update_information_github(self): + self.ensure_one() + # TODO maybe we should move the api key on the host ? + platform = self.env["vcp.platform"].search( + [("host_id", "=", self.host_id.id)], limit=1 + ) + client = platform._get_github_clients()[0] + try: + user = client.user(self.external_id) + self.write(self._prepare_user_vals(user)) + except github3.exceptions.ForbiddenError as e: + _logger.error(e) + rate = client.rate_limit() + reset = fields.Datetime.to_string( + datetime.utcfromtimestamp(rate["resources"]["core"]["reset"]) + ) + raise ValidationError(self.env._("Reset on %(reset)s", reset=reset)) from e + except github3.exceptions.NotFoundError: + _logger.warning( + "The user %s do not exist anymore, inactive it", self.external_id + ) + self.active = False diff --git a/vcp_github/pyproject.toml b/vcp_github/pyproject.toml new file mode 100644 index 0000000..4231d0c --- /dev/null +++ b/vcp_github/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/vcp_github/readme/CONTRIBUTORS.md b/vcp_github/readme/CONTRIBUTORS.md new file mode 100644 index 0000000..2c066ba --- /dev/null +++ b/vcp_github/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [Dixmit](https://www.dixmit.com) + - Enric Tobella diff --git a/vcp_github/readme/DESCRIPTION.md b/vcp_github/readme/DESCRIPTION.md new file mode 100644 index 0000000..345189d --- /dev/null +++ b/vcp_github/readme/DESCRIPTION.md @@ -0,0 +1 @@ +This module facilitates the integration between Github and Version Control Platform. diff --git a/vcp_github/static/description/icon.png b/vcp_github/static/description/icon.png new file mode 100644 index 0000000..3a0328b Binary files /dev/null and b/vcp_github/static/description/icon.png differ diff --git a/vcp_github/static/description/index.html b/vcp_github/static/description/index.html new file mode 100644 index 0000000..5ab8cc7 --- /dev/null +++ b/vcp_github/static/description/index.html @@ -0,0 +1,433 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Vcp Github

+ +

Beta License: AGPL-3 OCA/version-control-platform Translate me on Weblate Try me on Runboat

+

This module facilitates the integration between Github and Version +Control Platform.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Dixmit
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/version-control-platform project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/vcp_github/tests/__init__.py b/vcp_github/tests/__init__.py new file mode 100644 index 0000000..c63e32e --- /dev/null +++ b/vcp_github/tests/__init__.py @@ -0,0 +1 @@ +from . import test_github diff --git a/vcp_github/tests/test_github.py b/vcp_github/tests/test_github.py new file mode 100644 index 0000000..b467154 --- /dev/null +++ b/vcp_github/tests/test_github.py @@ -0,0 +1,140 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import base64 +from unittest.mock import MagicMock, patch + +from odoo.fields import Command +from odoo.tests.common import TransactionCase + + +class TestGithub(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.platform = cls.env["vcp.platform"].create( + { + "name": "oca", + "host_id": cls.env.ref("vcp_github.vcp_github_host").id, + "key_ids": [ + Command.create({"name": "ghp_exampletoken1234567890abcdef"}) + ], + "default_repository_scheduled_information_update": True, + "scheduled_information_update": True, + } + ) + + def test_update_organization_and_logo(self): + base_url = self.env["ir.config_parameter"].sudo().get_param("web.base.url") + with ( + patch("odoo.addons.vcp_github.models.vcp_platform.github3") as mock_github3, + patch( + "odoo.addons.vcp_github.models.vcp_platform.requests.get" + ) as mock_requests_get, + ): + mock_client = MagicMock() + mock_org = MagicMock() + mock_org.name = "Odoo Community Association" + mock_org.avatar_url = f"{base_url}/logo.png" + mock_requests_get.return_value.content = base64.b64decode( + self.env.company.logo + ) + mock_client.organization.return_value = mock_org + mock_github3.login.return_value = mock_client + self.platform.update_information() + self.assertEqual( + self.platform.short_description, "Odoo Community Association" + ) + mock_github3.login.assert_called_once_with( + token="ghp_exampletoken1234567890abcdef" + ) + mock_client.organization.assert_called_once_with("oca") + + def test_update_organization_with_repository(self): + with patch( + "odoo.addons.vcp_github.models.vcp_platform.github3" + ) as mock_github3: + mock_client = MagicMock() + mock_org = MagicMock() + mock_org.name = "Odoo Community Association" + mock_org.avatar_url = False + mock_repo1 = MagicMock() + mock_repo1.name = "server-tools" + mock_repo1.archived = False + mock_repo1.fork = False + mock_repo1.created_at = "2020-01-01T00:00:00Z" + mock_repo1.pushed_at = "2020-02-01T00:00:00Z" + mock_repo2 = MagicMock() + mock_repo2.name = "server-brand" + mock_repo2.archived = False + mock_repo2.fork = False + mock_repo2.created_at = "2021-01-01T10:00:00Z" + mock_repo2.pushed_at = "2021-02-01T00:00:00Z" + mock_org.repositories.return_value = [mock_repo1, mock_repo2] + mock_client.organization.return_value = mock_org + mock_github3.login.return_value = mock_client + self.platform.update_information() + self.assertEqual(len(self.platform.repository_ids), 2) + repo_names = {repo.name for repo in self.platform.repository_ids} + self.assertSetEqual(repo_names, {"server-tools", "server-brand"}) + mock_github3.login.assert_called_once_with( + token="ghp_exampletoken1234567890abcdef" + ) + mock_client.organization.assert_called_once_with("oca") + return self.platform.repository_ids.filtered(lambda r: r.name == "server-tools") + + def test_update_repository(self): + repository = self.test_update_organization_with_repository() + self.assertFalse(repository.request_ids) + with patch( + "odoo.addons.vcp_github.models.vcp_platform.github3.login" + ) as mock_client: + mock_login = MagicMock() + mock_client.return_value = mock_login + mock_login.session.get.return_value = MagicMock( + links={}, + json=lambda: [], + ) + mock_issue_request = MagicMock( + as_dict=lambda: { + "id": 1, + "user": {"login": "contributor1"}, + "base": {"ref": "main"}, + "head": {"repo": ["oca"]}, + "html_url": "https://github.com/oca/server-tools/pull/1", + "state": "closed", + "title": "Fix issue", + "labels": [{"name": "merged 🎉"}], + "created_at": "2023-01-01T00:00:00Z", + "updated_at": "2023-01-03T00:00:00Z", + "closed_at": "2023-01-02T00:00:00Z", + "pushed_at": "2024-01-02T00:00:00Z", + "commits": 3, + "comments": 2, + "review_comments": 1, + "additions": 150, + "deletions": 50, + "number": 1, + } + ) + mock_issue_request.id = 1 + mock_issue_request.user.login = "contributor1" + mock_issue_request.base.ref = "main" + mock_issue_request.head.repo = ["oca"] + mock_issue_request.html_url = "https://github.com/oca/server-tools/pull/1" + mock_issue_request.state = "closed" + mock_issue_request.title = "Fix issue" + mock_issue_request.labels = [{"name": "merged 🎉"}] + mock_issue_request.is_merged.return_value = False + mock_issue_request.draft = False + mock_issue_request.number = 1 + mock_issue_request.comments_url = ( + "https://api.github.com/repos/oca/server-tools/issues/1/comments" + ) + mock_issue_request.reviews.return_value.url = ( + "https://api.github.com/repos/oca/server-tools/pulls/1/reviews" + ) + mock_login._instance_or_null.return_value = mock_issue_request + mock_login.search_issues.return_value = [mock_issue_request] + repository.update_information() + self.assertTrue(repository.request_ids)